mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-07 11:32:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4595a5e95a | ||
|
|
d4d93a17d2 | ||
|
|
3798025699 | ||
|
|
9065ace05a | ||
|
|
5a06787a26 | ||
|
|
a1d8318563 | ||
|
|
eedf1fe068 | ||
|
|
80c4a67d6a | ||
|
|
5d0cf98e6c | ||
|
|
e3b5464785 | ||
|
|
7c48905ecd | ||
|
|
707e405492 | ||
|
|
340ac7ea6b | ||
|
|
d2fe0ee389 | ||
|
|
4951494a27 | ||
|
|
ac412b1095 | ||
|
|
9e14d06143 | ||
|
|
9c2c8f54fa | ||
|
|
42ba71fbbf | ||
|
|
35148aca91 | ||
|
|
8c7c8b8dab | ||
|
|
2f583aed8f | ||
|
|
c4bbad4dfe | ||
|
|
dc33c1adaf | ||
|
|
c76cacc696 | ||
|
|
8c8586aa23 | ||
|
|
678690274d | ||
|
|
0762586067 | ||
|
|
57a6fb9e3a | ||
|
|
b9951ded37 | ||
|
|
f6c9c3bf1c | ||
|
|
2cf81e0a20 | ||
|
|
c757f5b4e3 | ||
|
|
9fbb68be17 | ||
|
|
b33c547b06 | ||
|
|
b897c3789b | ||
|
|
5879f8b62b | ||
|
|
ea75cdc493 | ||
|
|
2bee29729a | ||
|
|
37176a4a44 | ||
|
|
7603712a88 | ||
|
|
d56da8c112 | ||
|
|
befa6b8050 | ||
|
|
9577b92b55 | ||
|
|
94d91daddb | ||
|
|
0eb5b43b4f | ||
|
|
1bb704b6e0 | ||
|
|
64817ae604 | ||
|
|
fd0b4e0cf0 | ||
|
|
12b85c8ce9 | ||
|
|
a3aa44a716 | ||
|
|
32f66712c8 | ||
|
|
10478b98a5 | ||
|
|
e687ca79b4 | ||
|
|
2edc660e28 | ||
|
|
dce33f9443 | ||
|
|
fac8222bfe | ||
|
|
144e9f1e42 | ||
|
|
e691965558 |
No files matched your search
@@ -7,20 +7,27 @@ outputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Extract FOLLY_COMMIT_HASH from Makefile
|
||||
- name: Extract FOLLY_MK_HASH
|
||||
id: extract-folly-hash
|
||||
shell: bash
|
||||
run: |
|
||||
FOLLY_COMMIT_HASH=$(grep '^FOLLY_COMMIT_HASH' Makefile | awk '{print $3}')
|
||||
echo "hash=$FOLLY_COMMIT_HASH" >> $GITHUB_OUTPUT
|
||||
FOLLY_MK_HASH=$(md5sum folly.mk | cut -d' ' -f1)
|
||||
echo "hash=$FOLLY_MK_HASH" >> $GITHUB_OUTPUT
|
||||
- name: Extract FOLLY_INSTALL_DIR
|
||||
id: extract-folly-install-dir
|
||||
shell: bash
|
||||
run: |
|
||||
FOLLY_INSTALL_DIR=$(cd third-party/folly && python3 build/fbcode_builder/getdeps.py show-inst-dir)
|
||||
echo "dir=$(echo $FOLLY_INSTALL_DIR | sed 's|installed/folly|installed|')" >> $GITHUB_OUTPUT
|
||||
- name: Cache folly build
|
||||
id: cache-folly-build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
# Cache the folly build directory
|
||||
path: /tmp/fbcode_builder_getdeps-Z__wZrocksdbZrocksdbZthird-partyZfollyZbuildZfbcode_builder-root/installed
|
||||
path: ${{ steps.extract-folly-install-dir.outputs.dir }}
|
||||
# Key is based on:
|
||||
# - OS and architecture
|
||||
# - The specific folly commit hash from Makefile
|
||||
# - The container image version to account for different compiler/library versions
|
||||
key: folly-build-${{ runner.os }}-${{ runner.arch }}-${{ steps.extract-folly-hash.outputs.hash }}-ubuntu22.1-v1
|
||||
# - The docker image, which may not always be specified/known
|
||||
# - Hash of folly.mk, which includes the folly repository commit hash
|
||||
# NOTE: this is still only intended for DEBUG folly builds
|
||||
key: folly-build-${{ runner.os }}-${{ runner.arch }}-${{ github.job_container.image }}-${{ steps.extract-folly-hash.outputs.hash }}
|
||||
@@ -5,5 +5,7 @@ runs:
|
||||
- name: Checkout folly sources
|
||||
run: |
|
||||
make checkout_folly
|
||||
echo "GETDEPS_USE_WGET=1" >> "$GITHUB_ENV"
|
||||
shell: bash
|
||||
- name: Install patchelf
|
||||
run: apt-get update -y && apt-get install -y patchelf
|
||||
shell: bash
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
- uses: "./.github/actions/build-folly"
|
||||
- run: LIB_MODE=static USE_CLANG=1 USE_FOLLY=1 COMPILE_WITH_UBSAN=1 COMPILE_WITH_ASAN=1 make -j32 check
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-valgrind:
|
||||
build-linux-cmake-with-folly:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
@@ -68,7 +68,28 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: make V=1 -j32 valgrind_test
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/cache-folly"
|
||||
id: cache-folly
|
||||
- uses: "./.github/actions/build-folly"
|
||||
with:
|
||||
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
|
||||
- run: "(mkdir build && cd build && cmake -DUSE_FOLLY=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 .. && make VERBOSE=1 -j20 && ctest -j20)"
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-release-with-folly:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- run: "DEBUG_LEVEL=0 make -j20 build_folly"
|
||||
- run: "USE_FOLLY=1 LIB_MODE=static DEBUG_LEVEL=0 V=1 make -j20 release"
|
||||
- run: "(mkdir build && cd build && cmake -DUSE_FOLLY=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 -DCMAKE_BUILD_TYPE=Release .. && make VERBOSE=1 -j20 && ctest -j20)"
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-windows-vs2022-avx2:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
@@ -89,6 +110,20 @@ jobs:
|
||||
- run: sudo apt-get update && sudo apt-get install -y build-essential libgflags-dev
|
||||
- run: make V=1 J=4 -j4 check
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-arm-crashtest:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: sudo apt-get update && sudo apt-get install -y build-essential libgflags-dev libsnappy-dev zlib1g-dev libbz2-dev liblz4-dev libzstd-dev
|
||||
- run: sudo mount -o remount,size=16G /dev/shm
|
||||
- run: sudo dd bs=1048576 count=4096 if=/dev/zero of=/swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
|
||||
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=1800 --max_key=2500000' blackbox_crash_test_with_atomic_flush
|
||||
- run: rm -rf /dev/shm/rocksdb.*
|
||||
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=1800 --max_key=2500000' blackbox_crash_test_with_multiops_wc_txn
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-examples:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
@@ -117,7 +152,7 @@ jobs:
|
||||
- name: Build fuzzers
|
||||
run: cd fuzz && make sst_file_writer_fuzzer db_fuzzer db_map_fuzzer
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-cmake-with-folly-lite-no-test:
|
||||
build-linux-cmake-with-folly-lite:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
@@ -128,5 +163,5 @@ jobs:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- run: "(mkdir build && cd build && cmake -DUSE_FOLLY_LITE=1 -DWITH_GFLAGS=1 -DCMAKE_CXX_FLAGS=-DGLOG_USE_GLOG_EXPORT .. && make V=1 -j20)"
|
||||
- run: "(mkdir build && cd build && cmake -DUSE_FOLLY_LITE=1 -DWITH_GFLAGS=1 -DCMAKE_CXX_FLAGS=-DGLOG_USE_GLOG_EXPORT .. && make VERBOSE=1 -j20 && ctest -j20)"
|
||||
- uses: "./.github/actions/post-steps"
|
||||
@@ -92,24 +92,6 @@ jobs:
|
||||
which javac && javac -version
|
||||
mkdir build && cd build && cmake -DJNI=1 -DWITH_GFLAGS=OFF .. -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ -DCMAKE_SYSTEM_NAME=Windows && make -j4 rocksdb rocksdbjni
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-cmake-with-folly:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/cache-folly"
|
||||
id: cache-folly
|
||||
- uses: "./.github/actions/build-folly"
|
||||
with:
|
||||
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
|
||||
- run: "(mkdir build && cd build && cmake -DUSE_FOLLY=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 .. && make V=1 -j20 && ctest -j20)"
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-make-with-folly:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
@@ -157,9 +139,9 @@ jobs:
|
||||
- uses: "./.github/actions/build-folly"
|
||||
with:
|
||||
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
|
||||
- run: "(mkdir build && cd build && cmake -DUSE_COROUTINES=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 .. && make V=1 -j20 && ctest -j20)"
|
||||
- run: "(mkdir build && cd build && cmake -DUSE_COROUTINES=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 .. && make VERBOSE=1 -j20 && ctest -j20)"
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-cmake-with-benchmark:
|
||||
build-linux-cmake-with-benchmark-no-thread-status:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
@@ -169,7 +151,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: mkdir build && cd build && cmake -DWITH_GFLAGS=1 -DWITH_BENCHMARK=1 .. && make V=1 -j20 && ctest -j20
|
||||
- run: mkdir build && cd build && cmake -DWITH_GFLAGS=1 -DWITH_BENCHMARK=1 -DCMAKE_CXX_FLAGS=-DNROCKSDB_THREAD_STATUS .. && make VERBOSE=1 -j20 && ctest -j20
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-encrypted_env-no_compression:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
@@ -372,7 +354,7 @@ jobs:
|
||||
- name: cmake generate project file
|
||||
run: ulimit -S -n `ulimit -H -n` && mkdir build && cd build && cmake -DWITH_GFLAGS=1 ..
|
||||
- name: Build tests
|
||||
run: cd build && make V=1 -j8
|
||||
run: cd build && make VERBOSE=1 -j8
|
||||
- name: Run shard 0 out of 4 test shards
|
||||
run: ulimit -S -n `ulimit -H -n` && cd build && ctest -j8 -I 0,,4
|
||||
if: ${{ matrix.run_sharded_tests == 0 }}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
name: facebook/rocksdb/weekly
|
||||
on:
|
||||
schedule:
|
||||
- cron: 0 9 * * 0
|
||||
workflow_dispatch:
|
||||
permissions: {}
|
||||
jobs:
|
||||
build-linux-valgrind:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
timeout-minutes: 840
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: make V=1 -j20 valgrind_test
|
||||
- uses: "./.github/actions/post-steps"
|
||||
@@ -1,12 +1,14 @@
|
||||
# This file @generated by:
|
||||
#$ python3 buckifier/buckify_rocksdb.py
|
||||
# --> DO NOT EDIT MANUALLY <--
|
||||
# This file is a Facebook-specific integration for buck builds, so can
|
||||
# only be validated by Facebook employees.
|
||||
# This file is a Meta-specific integration for buck builds, so can
|
||||
# only be validated by Meta employees.
|
||||
load("//rocks/buckifier:defs.bzl", "cpp_library_wrapper","rocks_cpp_library_wrapper","cpp_binary_wrapper","cpp_unittest_wrapper","fancy_bench_wrapper","add_c_test_wrapper")
|
||||
load("@fbcode_macros//build_defs:export_files.bzl", "export_file")
|
||||
|
||||
|
||||
oncall("rocksdb_point_of_contact")
|
||||
|
||||
cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"cache/cache.cc",
|
||||
"cache/cache_entry_roles.cc",
|
||||
@@ -5200,12 +5202,6 @@ cpp_unittest_wrapper(name="import_column_family_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="index_builder_test",
|
||||
srcs=["table/block_based/index_builder_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="inlineskiplist_test",
|
||||
srcs=["memtable/inlineskiplist_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
|
||||
+26
-2
@@ -80,6 +80,7 @@ if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE "${default_build_type}" CACHE STRING
|
||||
"Default BUILD_TYPE is ${default_build_type}" FORCE)
|
||||
endif()
|
||||
message(STATUS "CMAKE_BUILD_TYPE is set to ${CMAKE_BUILD_TYPE}")
|
||||
|
||||
find_program(CCACHE_FOUND ccache)
|
||||
if(CCACHE_FOUND)
|
||||
@@ -132,7 +133,9 @@ else()
|
||||
option(WITH_GFLAGS "build with GFlags" ON)
|
||||
endif()
|
||||
set(GFLAGS_LIB)
|
||||
if(WITH_GFLAGS)
|
||||
# Skip all gflags detection and setup when USE_FOLLY or USE_COROUTINES is enabled
|
||||
# since Folly provides its own gflags (USE_COROUTINES automatically sets USE_FOLLY)
|
||||
if(WITH_GFLAGS AND NOT USE_FOLLY AND NOT USE_COROUTINES)
|
||||
# Config with namespace available since gflags 2.2.2
|
||||
option(GFLAGS_USE_TARGET_NAMESPACE "Use gflags import target with namespace." ON)
|
||||
find_package(gflags CONFIG)
|
||||
@@ -151,6 +154,9 @@ else()
|
||||
include_directories(${GFLAGS_INCLUDE_DIR})
|
||||
list(APPEND THIRDPARTY_LIBS ${GFLAGS_LIB})
|
||||
add_definitions(-DGFLAGS=1)
|
||||
elseif(WITH_GFLAGS AND (USE_FOLLY OR USE_COROUTINES))
|
||||
# Still set the DGFLAGS=1 define when using Folly since Folly provides gflags
|
||||
add_definitions(-DGFLAGS=1)
|
||||
endif()
|
||||
|
||||
if(WITH_SNAPPY)
|
||||
@@ -213,6 +219,10 @@ if(MSVC)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zi /nologo /EHsc /GS /Gd /GR /GF /fp:precise /Zc:wchar_t /Zc:forScope /errorReport:queue")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /FC /d2Zi+ /W4 /wd4127 /wd4996 /wd4100 /wd4324")
|
||||
endif()
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DNDEBUG")
|
||||
message(STATUS "Setting /DNDEBUG as CMAKE_BUILD_TYPE is set to ${CMAKE_BUILD_TYPE}")
|
||||
endif()
|
||||
else()
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W -Wextra -Wall -pthread")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsign-compare -Wshadow -Wno-unused-parameter -Wno-unused-variable -Woverloaded-virtual -Wnon-virtual-dtor -Wno-missing-field-initializers -Wno-strict-aliasing -Wno-invalid-offsetof")
|
||||
@@ -642,6 +652,12 @@ if(USE_FOLLY)
|
||||
${FOLLY_INST_PATH}/lib/cmake/folly/folly-targets.cmake)
|
||||
|
||||
include(${FOLLY_INST_PATH}/lib/cmake/folly/folly-config.cmake)
|
||||
|
||||
# Fix gflags library name for debug builds
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath=${GFLAGS_INST_PATH}/lib")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${GFLAGS_INST_PATH}/lib/libgflags_debug.so.2.2")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_compile_definitions(USE_FOLLY FOLLY_NO_CONFIG HAVE_CXX11_ATOMIC)
|
||||
@@ -1082,12 +1098,21 @@ if(USE_FOLLY_LITE)
|
||||
third-party/folly/folly/synchronization/DistributedMutex.cpp
|
||||
third-party/folly/folly/synchronization/ParkingLot.cpp)
|
||||
include_directories(${PROJECT_SOURCE_DIR}/third-party/folly)
|
||||
# Add boost to the include path
|
||||
exec_program(python3 ${PROJECT_SOURCE_DIR}/third-party/folly ARGS
|
||||
build/fbcode_builder/getdeps.py show-source-dir boost OUTPUT_VARIABLE
|
||||
BOOST_SOURCE_PATH)
|
||||
exec_program(ls ARGS -d ${BOOST_SOURCE_PATH}/boost* OUTPUT_VARIABLE
|
||||
BOOST_INCLUDE_DIR)
|
||||
include_directories(${BOOST_INCLUDE_DIR})
|
||||
# Add fmt to the include path
|
||||
exec_program(python3 ${PROJECT_SOURCE_DIR}/third-party/folly ARGS
|
||||
build/fbcode_builder/getdeps.py show-source-dir fmt OUTPUT_VARIABLE
|
||||
FMT_SOURCE_PATH)
|
||||
exec_program(ls ARGS -d ${FMT_SOURCE_PATH}/fmt*/include OUTPUT_VARIABLE
|
||||
FMT_INCLUDE_DIR)
|
||||
include_directories(${FMT_INCLUDE_DIR})
|
||||
|
||||
add_definitions(-DUSE_FOLLY -DFOLLY_NO_CONFIG)
|
||||
list(APPEND THIRDPARTY_LIBS glog)
|
||||
endif()
|
||||
@@ -1453,7 +1478,6 @@ if(WITH_TESTS)
|
||||
table/block_based/block_based_table_reader_test.cc
|
||||
table/block_based/block_test.cc
|
||||
table/block_based/data_block_hash_index_test.cc
|
||||
table/block_based/index_builder_test.cc
|
||||
table/block_based/full_filter_block_test.cc
|
||||
table/block_based/partitioned_filter_block_test.cc
|
||||
table/cleanable_test.cc
|
||||
|
||||
+25
-5
@@ -1,18 +1,38 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 10.8.3 (11/10/2025)
|
||||
### New Features
|
||||
* Add a GetColumnFamilyMetaData API variant in DB to get the SST files intersecting a given key range.
|
||||
## 10.10.1 (02/02/2026)
|
||||
### Bug Fixes
|
||||
* Fix Windows VS 2022 build errors.
|
||||
|
||||
## 10.8.2 (11/07/2025)
|
||||
## 10.10.0 (12/16/2025)
|
||||
### Bug Fixes
|
||||
* Fixed a bug in best-efforts recovery that causes use-after-free crashes when accessing SST files that were cached during the recovery.
|
||||
* Fix resumable compaction incorrectly allowing resumption from a truncated range deletion that is not well handled currently.
|
||||
* Fixed a bug in `PosixRandomFileAccess` IO uring submission queue ownership & management. Fix eliminates the false positive 'Bad cqe data' IO errors in `PosixRandomFileAccess::MultiRead` when interleaved with `PosixRandomFileAccess::ReadAsync` on the same thread.
|
||||
|
||||
## 10.9.0 (11/21/2025)
|
||||
### New Features
|
||||
* Added an auto-tuning feature for DB manifest file size that also (by default) improves the safety of existing configurations in case `max_manifest_file_size` is repeatedly exceeded. The new recommendation is to set `max_manifest_file_size` to something small like 1MB and tune `max_manifest_space_amp_pct` as needed to balance write amp and space amp in the manifest. Refer to comments on those options in `DBOptions` for details. Both options are (now) mutable.
|
||||
* Added a new API to support option migration for multiple column families
|
||||
* Added new option target_file_size_is_upper_bound that makes most compaction output SST files come close to the target file size without exceeding it, rather than commonly exceeding it by some fraction (current behavior). For now the new behavior is off by default, but we expect to enable it by default in the future.
|
||||
* Add a new option allow_trivial_move in CompactionOptions to allow CompactFiles to perform trivial move if possible. By default the flag of allow_trivial_move is false, so it preserve the original behavior.
|
||||
|
||||
## 10.8.1 (10/29/2025)
|
||||
### Public API Changes
|
||||
* To reduce risk of ODR violations or similar, `ROCKSDB_USING_THREAD_STATUS` has been removed from public headers and replaced with static `const bool ThreadStatus::kEnabled`. Some other uses of conditional compilation have been removed from public API headers to reduce risk of ODR violations or other issues.
|
||||
|
||||
### Behavior Changes
|
||||
* PosixWritableFile now repositions the seek pointer to the new end of file after a call to Truncate.
|
||||
* Updated standalone range deletion L0 file compaction behavior to avoid compacting with any newer L0 files (which is expensive and not useful).
|
||||
|
||||
### Bug Fixes
|
||||
* Fix a bug where compaction with range deletion can persist kTypeMaxValid in MANIFEST as file metadata. kTypeMaxValid is not supposed to be persisted and can change as new value types are introduced. This can cause a forward compatibility issue where older versions of RocksDB don't recognize kTypeMaxValid from newer versions. A new placeholder value type kTypeTruncatedRangeDeletionSentinel is also introduced to replace kTypeMaxValid when reading existing SST files' metadata from MANIFEST. This allows us to strengthen some checks to avoid using kTypeMaxValid in the future.
|
||||
* Fixed a bug where `DB::GetSortedWalFiles()` could hang when waiting for a purge operation that found nothing to do (potentially triggered by iterator release, flush, compaction, etc.).
|
||||
* Fixed a bug in MultiScan where `max_sequential_skip_in_iterations` could cause the iterator to seek backward to already-unpinned blocks when the same user key spans multiple data blocks, leading to assertion failures or seg fault.
|
||||
* Fixed a bug for `WAL_ttl_seconds > 0` use cases where the newest archived WAL files could be incorrectly deleted when the system clock moved backwards.
|
||||
|
||||
### Performance Improvements
|
||||
* Added optimization that allowed for the asynchronous prefetching of all data outlined in a multiscan iterator. This optimization was applied to the level iterator, which prefetches all data through each of the block-based iterators.
|
||||
|
||||
## 10.8.0 (10/21/2025)
|
||||
### New Features
|
||||
|
||||
@@ -296,6 +296,28 @@ $(info $(shell $(CC) --version))
|
||||
$(info $(shell $(CXX) --version))
|
||||
endif
|
||||
|
||||
# ccache support
|
||||
# Set USE_CCACHE=1 to enable ccache, or let it auto-detect
|
||||
ifndef USE_CCACHE
|
||||
CCACHE := $(shell which ccache 2>/dev/null)
|
||||
ifneq ($(CCACHE),)
|
||||
USE_CCACHE := 1
|
||||
else
|
||||
USE_CCACHE := 0
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(USE_CCACHE), 1)
|
||||
CCACHE := $(shell which ccache 2>/dev/null)
|
||||
ifneq ($(CCACHE),)
|
||||
$(info Using ccache: $(CCACHE))
|
||||
CC := $(CCACHE) $(CC)
|
||||
CXX := $(CCACHE) $(CXX)
|
||||
else
|
||||
$(warning ccache requested but not found in PATH)
|
||||
endif
|
||||
endif
|
||||
|
||||
missing_make_config_paths := $(shell \
|
||||
grep "\./\S*\|/\S*" -o $(CURDIR)/make_config.mk | \
|
||||
while read path; \
|
||||
@@ -446,83 +468,7 @@ else
|
||||
PLATFORM_CXXFLAGS += -isystem $(GTEST_DIR)
|
||||
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 ($(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 += $(FOLLY_PATH)/lib/libfolly.a $(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 $(FMT_LIB_PATH)/libfmt.a $(GLOG_LIB_PATH)/libglog.so $(GFLAGS_PATH)/lib/libgflags.so.2.2 $(LIBEVENT_PATH)/lib/libevent-2.1.so -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
|
||||
ifneq ($(strip $(BOOST_SOURCE_PATH)),)
|
||||
BOOST_INCLUDE = $(shell (ls -d $(BOOST_SOURCE_PATH)/boost*/))
|
||||
# AIX: pre-defined system headers are surrounded by an extern "C" block
|
||||
ifeq ($(PLATFORM), OS_AIX)
|
||||
PLATFORM_CCFLAGS += -I$(BOOST_INCLUDE)
|
||||
PLATFORM_CXXFLAGS += -I$(BOOST_INCLUDE)
|
||||
else
|
||||
PLATFORM_CCFLAGS += -isystem $(BOOST_INCLUDE)
|
||||
PLATFORM_CXXFLAGS += -isystem $(BOOST_INCLUDE)
|
||||
endif
|
||||
endif # BOOST_SOURCE_PATH
|
||||
# AIX: pre-defined system headers are surrounded by an extern "C" block
|
||||
ifeq ($(PLATFORM), OS_AIX)
|
||||
PLATFORM_CCFLAGS += -I$(FOLLY_DIR)
|
||||
PLATFORM_CXXFLAGS += -I$(FOLLY_DIR)
|
||||
else
|
||||
PLATFORM_CCFLAGS += -isystem $(FOLLY_DIR)
|
||||
PLATFORM_CXXFLAGS += -isystem $(FOLLY_DIR)
|
||||
endif
|
||||
PLATFORM_CCFLAGS += -DUSE_FOLLY -DFOLLY_NO_CONFIG
|
||||
PLATFORM_CXXFLAGS += -DUSE_FOLLY -DFOLLY_NO_CONFIG
|
||||
# TODO: fix linking with fbcode compiler config
|
||||
PLATFORM_LDFLAGS += -lglog
|
||||
endif
|
||||
include folly.mk
|
||||
|
||||
ifdef TEST_CACHE_LINE_SIZE
|
||||
PLATFORM_CCFLAGS += -DTEST_CACHE_LINE_SIZE=$(TEST_CACHE_LINE_SIZE)
|
||||
@@ -683,6 +629,7 @@ am__v_CCH_1 =
|
||||
%.h.pub: %.h # .h.pub not actually created, so re-checked on each invocation
|
||||
$(AM_V_CCH) cd include/ && echo '#include "$(patsubst include/%,%,$<)"' | \
|
||||
$(CXX) -std=$(or $(ROCKSDB_CXX_STANDARD),c++20) -I. -DROCKSDB_NAMESPACE=42 -x c++ -c - -o /dev/null
|
||||
build_tools/check-public-header.sh $<
|
||||
|
||||
check-headers: $(HEADER_OK_FILES)
|
||||
|
||||
@@ -1742,9 +1689,6 @@ block_test: $(OBJ_DIR)/table/block_based/block_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
data_block_hash_index_test: $(OBJ_DIR)/table/block_based/data_block_hash_index_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
index_builder_test: $(OBJ_DIR)/table/block_based/index_builder_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
inlineskiplist_test: $(OBJ_DIR)/memtable/inlineskiplist_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -2498,42 +2442,6 @@ commit_prereq:
|
||||
false # J=$(J) build_tools/precommit_checker.py unit clang_unit release clang_release tsan asan ubsan lite unit_non_shm
|
||||
# $(MAKE) clean && $(MAKE) jclean && $(MAKE) rocksdbjava;
|
||||
|
||||
FOLLY_COMMIT_HASH = e95383b7c8b5b1e46cf47acf2f317d54f93c8268
|
||||
|
||||
# For public CI runs, checkout folly in a way that can build with RocksDB.
|
||||
# This is mostly intended as a test-only simulation of Meta-internal folly
|
||||
# integration.
|
||||
checkout_folly:
|
||||
if [ -e third-party/folly ]; then \
|
||||
cd third-party/folly && ${GIT_COMMAND} fetch origin; \
|
||||
else \
|
||||
cd third-party && ${GIT_COMMAND} clone https://github.com/facebook/folly.git; \
|
||||
fi
|
||||
@# Pin to a particular version for public CI, so that PR authors don't
|
||||
@# need to worry about folly breaking our integration. Update periodically
|
||||
cd third-party/folly && git reset --hard $(FOLLY_COMMIT_HASH)
|
||||
@# Apparently missing include
|
||||
perl -pi -e 's/(#include <atomic>)/$$1\n#include <cstring>/' third-party/folly/folly/lang/Exception.h
|
||||
@# Warning-as-error on memcpy
|
||||
perl -pi -e 's/memcpy.&ptr/memcpy((void*)&ptr/' third-party/folly/folly/lang/Exception.cpp
|
||||
@# const mismatch
|
||||
perl -pi -e 's/: environ/: (const char**)(environ)/' third-party/folly/folly/Subprocess.cpp
|
||||
@# NOTE: boost source will be needed for any build including `USE_FOLLY_LITE` builds as those depend on boost headers
|
||||
cd third-party/folly && $(PYTHON) build/fbcode_builder/getdeps.py fetch boost
|
||||
|
||||
CXX_M_FLAGS = $(filter -m%, $(CXXFLAGS))
|
||||
|
||||
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
|
||||
cd third-party/folly && \
|
||||
CXXFLAGS=" $(CXX_M_FLAGS) -DHAVE_CXX11_ATOMIC " $(PYTHON) build/fbcode_builder/getdeps.py build --no-tests
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build size testing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -135,6 +135,9 @@ def generate_buck(repo_path, deps_map):
|
||||
|
||||
BUCK = TARGETSBuilder("%s/BUCK" % repo_path, extra_argv)
|
||||
|
||||
# Add oncall("rocksdb_point_of_contact") at the top
|
||||
BUCK.add_oncall("rocksdb_point_of_contact")
|
||||
|
||||
# rocksdb_lib
|
||||
BUCK.add_library(
|
||||
"rocksdb_lib",
|
||||
|
||||
@@ -45,6 +45,11 @@ class TARGETSBuilder:
|
||||
self.total_bin = 0
|
||||
self.total_test = 0
|
||||
self.tests_cfg = ""
|
||||
|
||||
def add_oncall(self, oncall):
|
||||
with open(self.path, "ab") as targets_file:
|
||||
targets_file.write(targets_cfg.oncall_template.format(name=oncall).encode("utf-8"))
|
||||
|
||||
|
||||
def add_library(
|
||||
self,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates. 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 the Apache 2.0 License (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
rocksdb_target_header_template = """# This file \100generated by:
|
||||
#$ python3 buckifier/buckify_rocksdb.py{extra_argv}
|
||||
# --> DO NOT EDIT MANUALLY <--
|
||||
# This file is a Facebook-specific integration for buck builds, so can
|
||||
# only be validated by Facebook employees.
|
||||
# This file is a Meta-specific integration for buck builds, so can
|
||||
# only be validated by Meta employees.
|
||||
load("//rocks/buckifier:defs.bzl", "cpp_library_wrapper","rocks_cpp_library_wrapper","cpp_binary_wrapper","cpp_unittest_wrapper","fancy_bench_wrapper","add_c_test_wrapper")
|
||||
load("@fbcode_macros//build_defs:export_files.bzl", "export_file")
|
||||
|
||||
@@ -41,3 +43,8 @@ fancy_bench_wrapper(suite_name="{name}", binary_to_bench_to_metric_list_map={ben
|
||||
export_file_template = """
|
||||
export_file(name = "{name}")
|
||||
"""
|
||||
|
||||
|
||||
oncall_template = """
|
||||
oncall("{name}")
|
||||
"""
|
||||
@@ -311,7 +311,8 @@ EOF
|
||||
EOF
|
||||
then
|
||||
COMMON_FLAGS="$COMMON_FLAGS -DGFLAGS=1"
|
||||
PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -lgflags"
|
||||
# Hack: don't link extra gflags assuming it comes with folly
|
||||
[ "$USE_FOLLY" ] || PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -lgflags"
|
||||
# check if namespace is gflags
|
||||
elif $CXX $PLATFORM_CXXFLAGS -x c++ - -o test.o 2>/dev/null << EOF
|
||||
#include <gflags/gflags.h>
|
||||
@@ -320,7 +321,8 @@ EOF
|
||||
EOF
|
||||
then
|
||||
COMMON_FLAGS="$COMMON_FLAGS -DGFLAGS=1 -DGFLAGS_NAMESPACE=gflags"
|
||||
PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -lgflags"
|
||||
# Hack: don't link extra gflags assuming it comes with folly
|
||||
[ "$USE_FOLLY" ] || PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -lgflags"
|
||||
# check if namespace is google
|
||||
elif $CXX $PLATFORM_CXXFLAGS -x c++ - -o test.o 2>/dev/null << EOF
|
||||
#include <gflags/gflags.h>
|
||||
@@ -772,6 +774,7 @@ fi
|
||||
if [ "$USE_FOLLY_LITE" ]; then
|
||||
if [ "$FOLLY_DIR" ]; then
|
||||
BOOST_SOURCE_PATH=`cd $FOLLY_DIR && $PYTHON build/fbcode_builder/getdeps.py show-source-dir boost`
|
||||
FMT_SOURCE_PATH=`cd $FOLLY_DIR && $PYTHON build/fbcode_builder/getdeps.py show-source-dir fmt`
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -816,6 +819,7 @@ echo "FIND=$FIND" >> "$OUTPUT"
|
||||
echo "WATCH=$WATCH" >> "$OUTPUT"
|
||||
echo "FOLLY_PATH=$FOLLY_PATH" >> "$OUTPUT"
|
||||
echo "BOOST_SOURCE_PATH=$BOOST_SOURCE_PATH" >> "$OUTPUT"
|
||||
echo "FMT_SOURCE_PATH=$FMT_SOURCE_PATH" >> "$OUTPUT"
|
||||
|
||||
# This will enable some related identifiers for the preprocessor
|
||||
if test -n "$JEMALLOC"; then
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved.
|
||||
#
|
||||
# Check for some simple mistakes in public headers (on the command line)
|
||||
# that should prevent commit or push
|
||||
|
||||
BAD=""
|
||||
|
||||
# Look for potential for ODR violations caused by public headers depending on
|
||||
# build parameters that could vary between RocksDB build and application build.
|
||||
# * Cases like LUA, ROCKSDB_NAMESPACE, and ROCKSDB_ASSERT_STATUS_CHECKED are
|
||||
# intentional, hard to avoid. (We expect definitions to change and the user
|
||||
# should also.)
|
||||
# * Cases like _WIN32, OS_WIN, and __cplusplus are essentially ODR-safe.
|
||||
# * Cases like
|
||||
# #ifdef BLAH // ODR-SAFE
|
||||
# #undef BLAH
|
||||
# #endif
|
||||
# that should not cause ODR violations can be exempted with the ODR-SAFE
|
||||
# marker recognized here.
|
||||
|
||||
grep -nHE '^#if' -- "$@" | grep -vE 'ROCKSDB_NAMESPACE|ROCKSDB_ASSERT_STATUS_CHECKED|LUA|_WIN32|OS_WIN|ODR-SAFE|__cplusplus|ROCKSDB_DLL|ROCKSDB_LIBRARY_EXPORTS'
|
||||
if [ "$?" != "1" ]; then
|
||||
echo "^^^^^ #if in public API could cause an ODR violation."
|
||||
echo " Add // ODR-SAFE if verified safe."
|
||||
BAD=1
|
||||
fi
|
||||
|
||||
if [ "$BAD" ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -148,6 +148,70 @@ else
|
||||
echo "Checking format of uncommitted changes..."
|
||||
fi
|
||||
|
||||
# Check for missing copyright in new files
|
||||
echo "Checking for copyright headers in new files..."
|
||||
|
||||
# Get list of new files (added, not just modified)
|
||||
if [ -z "$uncommitted_code" ]; then
|
||||
# Post-commit: check files added since merge base
|
||||
new_files=$(git diff --name-only --diff-filter=A "$FORMAT_UPSTREAM_MERGE_BASE" -- '*.h' '*.cc' '*.py' $EXCLUDE)
|
||||
else
|
||||
# Pre-commit: check staged new files
|
||||
new_files=$(git diff --name-only --diff-filter=A --cached HEAD -- '*.h' '*.cc' '*.py' $EXCLUDE)
|
||||
fi
|
||||
|
||||
if [ -n "$new_files" ]; then
|
||||
files_missing_copyright=""
|
||||
|
||||
for file in $new_files; do
|
||||
if [ -f "$file" ]; then
|
||||
# Check if file is missing copyright
|
||||
# For .py files, check for Python-style comment
|
||||
# For .h and .cc files, check for C++-style comment
|
||||
if [[ "$file" == *.py ]]; then
|
||||
if ! grep -q "Copyright (c) Meta Platforms, Inc. and affiliates" "$file"; then
|
||||
files_missing_copyright="$files_missing_copyright $file"
|
||||
# Add copyright header to Python file
|
||||
temp_file=$(mktemp)
|
||||
{
|
||||
echo "# Copyright (c) Meta Platforms, Inc. and affiliates."
|
||||
echo "# This source code is licensed under both the GPLv2 (found in the COPYING file in the root directory)"
|
||||
echo "# and the Apache 2.0 License (found in the LICENSE.Apache file in the root directory)."
|
||||
echo
|
||||
cat "$file"
|
||||
} > "$temp_file"
|
||||
mv "$temp_file" "$file"
|
||||
echo "Added copyright header to $file"
|
||||
fi
|
||||
elif [[ "$file" == *.h ]] || [[ "$file" == *.cc ]]; then
|
||||
if ! grep -q "Copyright (c) Meta Platforms, Inc. and affiliates" "$file"; then
|
||||
files_missing_copyright="$files_missing_copyright $file"
|
||||
# Add copyright header to C++ file
|
||||
temp_file=$(mktemp)
|
||||
{
|
||||
echo "// Copyright (c) Meta Platforms, Inc. and affiliates. "
|
||||
echo "// This source code is licensed under both the GPLv2 (found in the "
|
||||
echo "// COPYING file in the root directory) and Apache 2.0 License "
|
||||
echo "// (found in the LICENSE.Apache file in the root directory)."
|
||||
echo
|
||||
cat "$file"
|
||||
} > "$temp_file"
|
||||
mv "$temp_file" "$file"
|
||||
echo "Added copyright header to $file"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$files_missing_copyright" ]; then
|
||||
echo "Copyright headers were added to new files."
|
||||
else
|
||||
echo "All new files have copyright headers."
|
||||
fi
|
||||
else
|
||||
echo "No new files to check for copyright headers."
|
||||
fi
|
||||
|
||||
if [ -z "$diffs" ]
|
||||
then
|
||||
echo "Nothing needs to be reformatted!"
|
||||
|
||||
Vendored
+1
-1
@@ -119,7 +119,7 @@ DEFINE_uint32(seed, 0, "Hashing/random seed to use. 0 = choose at random");
|
||||
DEFINE_string(secondary_cache_uri, "",
|
||||
"Full URI for creating a custom secondary cache object");
|
||||
|
||||
DEFINE_string(cache_type, "lru_cache", "Type of block cache.");
|
||||
DEFINE_string(cache_type, "hyper_clock_cache", "Type of block cache.");
|
||||
|
||||
DEFINE_bool(use_jemalloc_no_dump_allocator, false,
|
||||
"Whether to use JemallocNoDumpAllocator");
|
||||
|
||||
Vendored
+101
-124
@@ -1752,26 +1752,6 @@ inline void GetHomeIndexAndShift(uint64_t length_info, uint64_t hash,
|
||||
assert(*home < LengthInfoToUsedLength(length_info));
|
||||
}
|
||||
|
||||
inline int GetShiftFromNextWithShift(uint64_t next_with_shift) {
|
||||
return BitwiseAnd(next_with_shift,
|
||||
AutoHyperClockTable::HandleImpl::kShiftMask);
|
||||
}
|
||||
|
||||
inline size_t GetNextFromNextWithShift(uint64_t next_with_shift) {
|
||||
return static_cast<size_t>(next_with_shift >>
|
||||
AutoHyperClockTable::HandleImpl::kNextShift);
|
||||
}
|
||||
|
||||
inline uint64_t MakeNextWithShift(size_t next, int shift) {
|
||||
return (uint64_t{next} << AutoHyperClockTable::HandleImpl::kNextShift) |
|
||||
static_cast<uint64_t>(shift);
|
||||
}
|
||||
|
||||
inline uint64_t MakeNextWithShiftEnd(size_t head, int shift) {
|
||||
return AutoHyperClockTable::HandleImpl::kNextEndFlags |
|
||||
MakeNextWithShift(head, shift);
|
||||
}
|
||||
|
||||
// Helper function for Lookup
|
||||
inline bool MatchAndRef(const UniqueId64x2* hashed_key, const ClockHandle& h,
|
||||
int shift = 0, size_t home = 0,
|
||||
@@ -1821,36 +1801,39 @@ inline bool MatchAndRef(const UniqueId64x2* hashed_key, const ClockHandle& h,
|
||||
}
|
||||
}
|
||||
|
||||
using NextWithShift = AutoHyperClockTable::HandleImpl::NextWithShift;
|
||||
|
||||
// Assumes a chain rewrite lock prevents concurrent modification of
|
||||
// these chain pointers
|
||||
void UpgradeShiftsOnRange(AutoHyperClockTable::HandleImpl* arr,
|
||||
size_t& frontier, uint64_t stop_before_or_new_tail,
|
||||
int old_shift, int new_shift) {
|
||||
size_t& frontier,
|
||||
NextWithShift stop_before_or_new_tail, int old_shift,
|
||||
int new_shift) {
|
||||
assert(frontier != SIZE_MAX);
|
||||
assert(new_shift == old_shift + 1);
|
||||
(void)old_shift;
|
||||
(void)new_shift;
|
||||
using HandleImpl = AutoHyperClockTable::HandleImpl;
|
||||
for (;;) {
|
||||
uint64_t next_with_shift = arr[frontier].chain_next_with_shift.Load();
|
||||
assert(GetShiftFromNextWithShift(next_with_shift) == old_shift);
|
||||
NextWithShift next_with_shift = arr[frontier].chain_next_with_shift.Load();
|
||||
assert(next_with_shift.GetShift() == old_shift);
|
||||
if (next_with_shift == stop_before_or_new_tail) {
|
||||
// Stopping at entry with pointer matching "stop before"
|
||||
assert(!HandleImpl::IsEnd(next_with_shift));
|
||||
assert(!next_with_shift.IsEnd());
|
||||
return;
|
||||
}
|
||||
if (HandleImpl::IsEnd(next_with_shift)) {
|
||||
if (next_with_shift.IsEnd()) {
|
||||
// Also update tail to new tail
|
||||
assert(HandleImpl::IsEnd(stop_before_or_new_tail));
|
||||
assert(stop_before_or_new_tail.IsEnd());
|
||||
arr[frontier].chain_next_with_shift.Store(stop_before_or_new_tail);
|
||||
// Mark nothing left to upgrade
|
||||
frontier = SIZE_MAX;
|
||||
return;
|
||||
}
|
||||
// Next is another entry to process, so upgrade and advance frontier
|
||||
arr[frontier].chain_next_with_shift.FetchAdd(1U);
|
||||
assert(GetShiftFromNextWithShift(next_with_shift + 1) == new_shift);
|
||||
frontier = GetNextFromNextWithShift(next_with_shift);
|
||||
arr[frontier].chain_next_with_shift.Apply(
|
||||
NextWithShift::Shift::PlusTransformPromiseNoOverflow(1U));
|
||||
assert(next_with_shift.GetShift() + 1 == new_shift);
|
||||
frontier = next_with_shift.GetNext();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1888,19 +1871,19 @@ class AutoHyperClockTable::ChainRewriteLock {
|
||||
// RAII wrap existing lock held (or end)
|
||||
explicit ChainRewriteLock(HandleImpl* h,
|
||||
RelaxedAtomic<uint64_t>& /*yield_count*/,
|
||||
uint64_t already_locked_or_end)
|
||||
NextWithShift already_locked_or_end)
|
||||
: head_ptr_(&h->head_next_with_shift) {
|
||||
saved_head_ = already_locked_or_end;
|
||||
// already locked or end
|
||||
assert(saved_head_ & HandleImpl::kHeadLocked);
|
||||
assert(saved_head_.IsLocked());
|
||||
}
|
||||
|
||||
~ChainRewriteLock() {
|
||||
if (!IsEnd()) {
|
||||
// Release lock
|
||||
uint64_t old = head_ptr_->FetchAnd(~HandleImpl::kHeadLocked);
|
||||
(void)old;
|
||||
assert((old & HandleImpl::kNextEndFlags) == HandleImpl::kHeadLocked);
|
||||
NextWithShift old;
|
||||
head_ptr_->Apply(NextWithShift::LockedFlag::ClearTransform(), &old);
|
||||
assert(old.IsLockedNotEnd());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1910,12 +1893,13 @@ class AutoHyperClockTable::ChainRewriteLock {
|
||||
}
|
||||
|
||||
// Expected current state, assuming no parallel updates.
|
||||
uint64_t GetSavedHead() const { return saved_head_; }
|
||||
NextWithShift GetSavedHead() const { return saved_head_; }
|
||||
|
||||
bool CasUpdate(uint64_t next_with_shift,
|
||||
bool CasUpdate(NextWithShift next_with_shift,
|
||||
RelaxedAtomic<uint64_t>& yield_count) {
|
||||
uint64_t new_head = next_with_shift | HandleImpl::kHeadLocked;
|
||||
uint64_t expected = GetSavedHead();
|
||||
NextWithShift new_head =
|
||||
next_with_shift.With<NextWithShift::LockedFlag>(true);
|
||||
NextWithShift expected = GetSavedHead();
|
||||
bool success = head_ptr_->CasStrong(expected, new_head);
|
||||
if (success) {
|
||||
// Ensure IsEnd() is kept up-to-date, including for dtor
|
||||
@@ -1924,7 +1908,7 @@ class AutoHyperClockTable::ChainRewriteLock {
|
||||
// Parallel update to head, such as Insert()
|
||||
if (IsEnd()) {
|
||||
// Didn't previously hold a lock
|
||||
if (HandleImpl::IsEnd(expected)) {
|
||||
if (expected.IsEnd()) {
|
||||
// Still don't need to
|
||||
saved_head_ = expected;
|
||||
} else {
|
||||
@@ -1933,28 +1917,25 @@ class AutoHyperClockTable::ChainRewriteLock {
|
||||
}
|
||||
} else {
|
||||
// Parallel update must preserve our lock
|
||||
assert((expected & HandleImpl::kNextEndFlags) ==
|
||||
HandleImpl::kHeadLocked);
|
||||
assert(expected.IsLockedNotEnd());
|
||||
saved_head_ = expected;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
bool IsEnd() const { return HandleImpl::IsEnd(saved_head_); }
|
||||
bool IsEnd() const { return saved_head_.IsEnd(); }
|
||||
|
||||
private:
|
||||
void Acquire(RelaxedAtomic<uint64_t>& yield_count) {
|
||||
for (;;) {
|
||||
// Acquire removal lock on the chain
|
||||
uint64_t old_head = head_ptr_->FetchOr(HandleImpl::kHeadLocked);
|
||||
if ((old_head & HandleImpl::kNextEndFlags) != HandleImpl::kHeadLocked) {
|
||||
NextWithShift old_head;
|
||||
head_ptr_->Apply(NextWithShift::LockedFlag::SetTransform(), &old_head,
|
||||
&saved_head_);
|
||||
if (!old_head.IsLockedNotEnd()) {
|
||||
// Either acquired the lock or lock not needed (end)
|
||||
assert((old_head & HandleImpl::kNextEndFlags) == 0 ||
|
||||
(old_head & HandleImpl::kNextEndFlags) ==
|
||||
HandleImpl::kNextEndFlags);
|
||||
|
||||
saved_head_ = old_head | HandleImpl::kHeadLocked;
|
||||
assert(old_head.IsEnd() == old_head.IsLocked());
|
||||
break;
|
||||
}
|
||||
// NOTE: one of the few yield-wait loops, which is rare enough in practice
|
||||
@@ -1965,8 +1946,8 @@ class AutoHyperClockTable::ChainRewriteLock {
|
||||
}
|
||||
}
|
||||
|
||||
AcqRelAtomic<uint64_t>* head_ptr_;
|
||||
uint64_t saved_head_;
|
||||
AcqRelBitFieldsAtomic<NextWithShift>* head_ptr_;
|
||||
NextWithShift saved_head_;
|
||||
};
|
||||
|
||||
AutoHyperClockTable::AutoHyperClockTable(
|
||||
@@ -2021,9 +2002,9 @@ AutoHyperClockTable::AutoHyperClockTable(
|
||||
#endif
|
||||
if (major + i < used_length) {
|
||||
array_[i].head_next_with_shift.StoreRelaxed(
|
||||
MakeNextWithShiftEnd(i, max_shift));
|
||||
NextWithShift::MakeEnd(i, max_shift));
|
||||
array_[major + i].head_next_with_shift.StoreRelaxed(
|
||||
MakeNextWithShiftEnd(major + i, max_shift));
|
||||
NextWithShift::MakeEnd(major + i, max_shift));
|
||||
#ifndef NDEBUG // Extra invariant checking
|
||||
GetHomeIndexAndShift(length_info, i, &home, &shift);
|
||||
assert(home == i);
|
||||
@@ -2034,7 +2015,7 @@ AutoHyperClockTable::AutoHyperClockTable(
|
||||
#endif
|
||||
} else {
|
||||
array_[i].head_next_with_shift.StoreRelaxed(
|
||||
MakeNextWithShiftEnd(i, min_shift));
|
||||
NextWithShift::MakeEnd(i, min_shift));
|
||||
#ifndef NDEBUG // Extra invariant checking
|
||||
GetHomeIndexAndShift(length_info, i, &home, &shift);
|
||||
assert(home == i);
|
||||
@@ -2066,8 +2047,10 @@ AutoHyperClockTable::~AutoHyperClockTable() {
|
||||
// just a reasonable frontier past what we expect to have written.
|
||||
#ifdef MUST_FREE_HEAP_ALLOCATIONS
|
||||
for (size_t i = used_end; i < array_.Count() && i < used_end + 64U; i++) {
|
||||
assert(array_[i].head_next_with_shift.LoadRelaxed() == 0);
|
||||
assert(array_[i].chain_next_with_shift.LoadRelaxed() == 0);
|
||||
assert(array_[i].head_next_with_shift.LoadRelaxed() ==
|
||||
HandleImpl::kUnusedMarker);
|
||||
assert(array_[i].chain_next_with_shift.LoadRelaxed() ==
|
||||
HandleImpl::kUnusedMarker);
|
||||
assert(array_[i].meta.LoadRelaxed() == 0);
|
||||
}
|
||||
#endif // MUST_FREE_HEAP_ALLOCATIONS
|
||||
@@ -2089,11 +2072,9 @@ AutoHyperClockTable::~AutoHyperClockTable() {
|
||||
usage_.FetchSubRelaxed(h.total_charge);
|
||||
occupancy_.FetchSubRelaxed(1U);
|
||||
was_populated[i] = true;
|
||||
if (!HandleImpl::IsEnd(h.chain_next_with_shift.LoadRelaxed())) {
|
||||
assert((h.chain_next_with_shift.LoadRelaxed() &
|
||||
HandleImpl::kHeadLocked) == 0);
|
||||
size_t next =
|
||||
GetNextFromNextWithShift(h.chain_next_with_shift.LoadRelaxed());
|
||||
if (!h.chain_next_with_shift.LoadRelaxed().IsEnd()) {
|
||||
assert(!h.chain_next_with_shift.LoadRelaxed().IsLocked());
|
||||
size_t next = h.chain_next_with_shift.LoadRelaxed().GetNext();
|
||||
assert(!was_pointed_to[next]);
|
||||
was_pointed_to[next] = true;
|
||||
}
|
||||
@@ -2105,9 +2086,8 @@ AutoHyperClockTable::~AutoHyperClockTable() {
|
||||
break;
|
||||
}
|
||||
#ifndef NDEBUG // Extra invariant checking
|
||||
if (!HandleImpl::IsEnd(h.head_next_with_shift.LoadRelaxed())) {
|
||||
size_t next =
|
||||
GetNextFromNextWithShift(h.head_next_with_shift.LoadRelaxed());
|
||||
if (!h.head_next_with_shift.LoadRelaxed().IsEnd()) {
|
||||
size_t next = h.head_next_with_shift.LoadRelaxed().GetNext();
|
||||
assert(!was_pointed_to[next]);
|
||||
was_pointed_to[next] = true;
|
||||
}
|
||||
@@ -2222,10 +2202,10 @@ bool AutoHyperClockTable::Grow(InsertState& state) {
|
||||
// chain rewrite lock has been released.
|
||||
size_t old_old_home = BottomNBits(grow_home, old_shift - 1);
|
||||
for (;;) {
|
||||
uint64_t old_old_head = array_[old_old_home].head_next_with_shift.Load();
|
||||
if (GetShiftFromNextWithShift(old_old_head) >= old_shift) {
|
||||
if ((old_old_head & HandleImpl::kNextEndFlags) !=
|
||||
HandleImpl::kHeadLocked) {
|
||||
NextWithShift old_old_head =
|
||||
array_[old_old_home].head_next_with_shift.Load();
|
||||
if (old_old_head.GetShift() >= old_shift) {
|
||||
if (!old_old_head.IsLockedNotEnd()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2285,8 +2265,7 @@ void AutoHyperClockTable::CatchUpLengthInfoNoWait(
|
||||
if (published_usable_size < known_usable_grow_home) {
|
||||
int old_shift = FloorLog2(next_usable_size - 1);
|
||||
size_t old_home = BottomNBits(published_usable_size, old_shift);
|
||||
int shift = GetShiftFromNextWithShift(
|
||||
array_[old_home].head_next_with_shift.Load());
|
||||
int shift = array_[old_home].head_next_with_shift.Load().GetShift();
|
||||
if (shift <= old_shift) {
|
||||
// Not ready
|
||||
break;
|
||||
@@ -2437,9 +2416,10 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
ChainRewriteLock zero_head_lock(&arr[old_home], yield_count_);
|
||||
|
||||
// Used for locking the one chain below
|
||||
uint64_t saved_one_head;
|
||||
NextWithShift saved_one_head;
|
||||
// One head has not been written to
|
||||
assert(arr[grow_home].head_next_with_shift.Load() == 0);
|
||||
assert(arr[grow_home].head_next_with_shift.Load() ==
|
||||
HandleImpl::kUnusedMarker);
|
||||
|
||||
// old_home will also the head of the new "zero chain" -- all entries in the
|
||||
// "from" chain whose next hash bit is 0. grow_home will be head of the new
|
||||
@@ -2461,7 +2441,7 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
assert(cur == SIZE_MAX);
|
||||
assert(chain_frontier_first == -1);
|
||||
|
||||
uint64_t next_with_shift = zero_head_lock.GetSavedHead();
|
||||
NextWithShift next_with_shift = zero_head_lock.GetSavedHead();
|
||||
|
||||
// Find a single representative for each target chain, or scan the whole
|
||||
// chain if some target chain has no representative.
|
||||
@@ -2474,16 +2454,16 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
assert((cur == SIZE_MAX) == (zero_chain_frontier == SIZE_MAX &&
|
||||
one_chain_frontier == SIZE_MAX));
|
||||
|
||||
assert(GetShiftFromNextWithShift(next_with_shift) == old_shift);
|
||||
assert(next_with_shift.GetShift() == old_shift);
|
||||
|
||||
// Check for end of original chain
|
||||
if (HandleImpl::IsEnd(next_with_shift)) {
|
||||
if (next_with_shift.IsEnd()) {
|
||||
cur = SIZE_MAX;
|
||||
break;
|
||||
}
|
||||
|
||||
// next_with_shift is not End
|
||||
cur = GetNextFromNextWithShift(next_with_shift);
|
||||
cur = next_with_shift.GetNext();
|
||||
|
||||
if (BottomNBits(arr[cur].hashed_key[1], new_shift) == old_home) {
|
||||
// Entry for zero chain
|
||||
@@ -2522,10 +2502,10 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
(zero_chain_frontier == SIZE_MAX && one_chain_frontier == SIZE_MAX));
|
||||
|
||||
// Always update one chain's head first (safe), and mark it as locked
|
||||
saved_one_head = HandleImpl::kHeadLocked |
|
||||
(one_chain_frontier != SIZE_MAX
|
||||
? MakeNextWithShift(one_chain_frontier, new_shift)
|
||||
: MakeNextWithShiftEnd(grow_home, new_shift));
|
||||
saved_one_head = one_chain_frontier != SIZE_MAX
|
||||
? NextWithShift::Make(one_chain_frontier, new_shift)
|
||||
: NextWithShift::MakeEnd(grow_home, new_shift);
|
||||
saved_one_head.Set<NextWithShift::LockedFlag>(true);
|
||||
arr[grow_home].head_next_with_shift.Store(saved_one_head);
|
||||
|
||||
// Make sure length_info_ hasn't been updated too early, as we're about
|
||||
@@ -2535,8 +2515,8 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
// Try to set zero's head.
|
||||
if (zero_head_lock.CasUpdate(
|
||||
zero_chain_frontier != SIZE_MAX
|
||||
? MakeNextWithShift(zero_chain_frontier, new_shift)
|
||||
: MakeNextWithShiftEnd(old_home, new_shift),
|
||||
? NextWithShift::Make(zero_chain_frontier, new_shift)
|
||||
: NextWithShift::MakeEnd(old_home, new_shift),
|
||||
yield_count_)) {
|
||||
// Both heads successfully updated to new shift
|
||||
break;
|
||||
@@ -2570,10 +2550,10 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
size_t& other_frontier = chain_frontier_first != 0
|
||||
? /*&*/ zero_chain_frontier
|
||||
: /*&*/ one_chain_frontier;
|
||||
uint64_t stop_before_or_new_tail =
|
||||
NextWithShift stop_before_or_new_tail =
|
||||
other_frontier != SIZE_MAX
|
||||
? /*stop before*/ MakeNextWithShift(other_frontier, old_shift)
|
||||
: /*new tail*/ MakeNextWithShiftEnd(
|
||||
? /*stop before*/ NextWithShift::Make(other_frontier, old_shift)
|
||||
: /*new tail*/ NextWithShift::MakeEnd(
|
||||
chain_frontier_first == 0 ? old_home : grow_home, new_shift);
|
||||
UpgradeShiftsOnRange(arr, first_frontier, stop_before_or_new_tail,
|
||||
old_shift, new_shift);
|
||||
@@ -2599,20 +2579,19 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
? /*&*/ zero_chain_frontier
|
||||
: /*&*/ one_chain_frontier;
|
||||
assert(cur != first_frontier);
|
||||
assert(GetNextFromNextWithShift(
|
||||
arr[first_frontier].chain_next_with_shift.Load()) ==
|
||||
assert(arr[first_frontier].chain_next_with_shift.Load().GetNext() ==
|
||||
other_frontier);
|
||||
|
||||
uint64_t next_with_shift = arr[cur].chain_next_with_shift.Load();
|
||||
NextWithShift next_with_shift = arr[cur].chain_next_with_shift.Load();
|
||||
|
||||
// Check for end of original chain
|
||||
if (HandleImpl::IsEnd(next_with_shift)) {
|
||||
if (next_with_shift.IsEnd()) {
|
||||
// Can set upgraded tail on first chain
|
||||
uint64_t first_new_tail = MakeNextWithShiftEnd(
|
||||
NextWithShift first_new_tail = NextWithShift::MakeEnd(
|
||||
chain_frontier_first == 0 ? old_home : grow_home, new_shift);
|
||||
arr[first_frontier].chain_next_with_shift.Store(first_new_tail);
|
||||
// And upgrade remainder of other chain
|
||||
uint64_t other_new_tail = MakeNextWithShiftEnd(
|
||||
NextWithShift other_new_tail = NextWithShift::MakeEnd(
|
||||
chain_frontier_first != 0 ? old_home : grow_home, new_shift);
|
||||
UpgradeShiftsOnRange(arr, other_frontier, other_new_tail, old_shift,
|
||||
new_shift);
|
||||
@@ -2621,7 +2600,7 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
}
|
||||
|
||||
// next_with_shift is not End
|
||||
cur = GetNextFromNextWithShift(next_with_shift);
|
||||
cur = next_with_shift.GetNext();
|
||||
|
||||
int target_chain;
|
||||
if (BottomNBits(arr[cur].hashed_key[1], new_shift) == old_home) {
|
||||
@@ -2634,7 +2613,7 @@ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
|
||||
}
|
||||
if (target_chain == chain_frontier_first) {
|
||||
// Found next entry to skip to on the first chain
|
||||
uint64_t skip_to = MakeNextWithShift(cur, new_shift);
|
||||
NextWithShift skip_to = NextWithShift::Make(cur, new_shift);
|
||||
arr[first_frontier].chain_next_with_shift.Store(skip_to);
|
||||
first_frontier = cur;
|
||||
// Upgrade other chain up to entry before that one
|
||||
@@ -2675,17 +2654,17 @@ void AutoHyperClockTable::PurgeImplLocked(OpData* op_data,
|
||||
|
||||
HandleImpl* const arr = array_.Get();
|
||||
|
||||
uint64_t next_with_shift = rewrite_lock.GetSavedHead();
|
||||
assert(!HandleImpl::IsEnd(next_with_shift));
|
||||
int home_shift = GetShiftFromNextWithShift(next_with_shift);
|
||||
NextWithShift next_with_shift = rewrite_lock.GetSavedHead();
|
||||
assert(!next_with_shift.IsEnd());
|
||||
int home_shift = next_with_shift.GetShift();
|
||||
(void)home;
|
||||
(void)home_shift;
|
||||
size_t next = GetNextFromNextWithShift(next_with_shift);
|
||||
size_t next = next_with_shift.GetNext();
|
||||
assert(next < array_.Count());
|
||||
HandleImpl* h = &arr[next];
|
||||
HandleImpl* prev_to_keep = nullptr;
|
||||
#ifndef NDEBUG
|
||||
uint64_t prev_to_keep_next_with_shift = 0;
|
||||
NextWithShift prev_to_keep_next_with_shift{};
|
||||
#endif
|
||||
// Whether there are entries between h and prev_to_keep that should be
|
||||
// purged from the chain.
|
||||
@@ -2743,13 +2722,13 @@ void AutoHyperClockTable::PurgeImplLocked(OpData* op_data,
|
||||
// update any new entries just inserted in parallel.
|
||||
// Can simply restart (GetSavedHead() already updated from CAS failure).
|
||||
next_with_shift = rewrite_lock.GetSavedHead();
|
||||
assert(!HandleImpl::IsEnd(next_with_shift));
|
||||
next = GetNextFromNextWithShift(next_with_shift);
|
||||
assert(!next_with_shift.IsEnd());
|
||||
next = next_with_shift.GetNext();
|
||||
assert(next < array_.Count());
|
||||
h = &arr[next];
|
||||
pending_purge = false;
|
||||
assert(prev_to_keep == nullptr);
|
||||
assert(GetShiftFromNextWithShift(next_with_shift) == home_shift);
|
||||
assert(next_with_shift.GetShift() == home_shift);
|
||||
continue;
|
||||
}
|
||||
pending_purge = false;
|
||||
@@ -2771,13 +2750,13 @@ void AutoHyperClockTable::PurgeImplLocked(OpData* op_data,
|
||||
}
|
||||
#endif
|
||||
|
||||
assert(GetShiftFromNextWithShift(next_with_shift) == home_shift);
|
||||
assert(next_with_shift.GetShift() == home_shift);
|
||||
|
||||
// Check for end marker
|
||||
if (HandleImpl::IsEnd(next_with_shift)) {
|
||||
if (next_with_shift.IsEnd()) {
|
||||
h = nullptr;
|
||||
} else {
|
||||
next = GetNextFromNextWithShift(next_with_shift);
|
||||
next = next_with_shift.GetNext();
|
||||
assert(next < array_.Count());
|
||||
h = &arr[next];
|
||||
assert(h != prev_to_keep);
|
||||
@@ -2849,7 +2828,7 @@ void AutoHyperClockTable::PurgeImpl(OpData* op_data, size_t home,
|
||||
// Ensure we are at the correct home for the shift in effect for the
|
||||
// chain head.
|
||||
for (;;) {
|
||||
int shift = GetShiftFromNextWithShift(rewrite_lock.GetSavedHead());
|
||||
int shift = rewrite_lock.GetSavedHead().GetShift();
|
||||
|
||||
if (shift > home_shift) {
|
||||
// Found a newer shift at candidate head, which must apply to us.
|
||||
@@ -3045,14 +3024,14 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::DoInsert(
|
||||
}
|
||||
|
||||
// Now insert into chain using head pointer
|
||||
uint64_t next_with_shift;
|
||||
NextWithShift next_with_shift;
|
||||
int home_shift = orig_home_shift;
|
||||
|
||||
// Might need to retry
|
||||
for (int i = 0;; ++i) {
|
||||
CHECK_TOO_MANY_ITERATIONS(i);
|
||||
next_with_shift = arr[home].head_next_with_shift.Load();
|
||||
int shift = GetShiftFromNextWithShift(next_with_shift);
|
||||
int shift = next_with_shift.GetShift();
|
||||
|
||||
if (UNLIKELY(shift != home_shift)) {
|
||||
// NOTE: shift increases with table growth
|
||||
@@ -3079,15 +3058,14 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::DoInsert(
|
||||
}
|
||||
|
||||
// Values to update to
|
||||
uint64_t head_next_with_shift = MakeNextWithShift(idx, home_shift);
|
||||
uint64_t chain_next_with_shift = next_with_shift;
|
||||
NextWithShift head_next_with_shift = NextWithShift::Make(idx, home_shift);
|
||||
NextWithShift chain_next_with_shift = next_with_shift;
|
||||
|
||||
// Preserve the locked state in head, without propagating to chain next
|
||||
// where it is meaningless (and not allowed)
|
||||
if (UNLIKELY((next_with_shift & HandleImpl::kNextEndFlags) ==
|
||||
HandleImpl::kHeadLocked)) {
|
||||
head_next_with_shift |= HandleImpl::kHeadLocked;
|
||||
chain_next_with_shift &= ~HandleImpl::kHeadLocked;
|
||||
if (UNLIKELY(next_with_shift.IsLockedNotEnd())) {
|
||||
head_next_with_shift.Set<NextWithShift::LockedFlag>(true);
|
||||
chain_next_with_shift.Set<NextWithShift::LockedFlag>(false);
|
||||
}
|
||||
|
||||
arr[idx].chain_next_with_shift.Store(chain_next_with_shift);
|
||||
@@ -3156,9 +3134,9 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
|
||||
// of a loop as possible.
|
||||
|
||||
HandleImpl* const arr = array_.Get();
|
||||
uint64_t next_with_shift = arr[home].head_next_with_shift.LoadRelaxed();
|
||||
for (size_t i = 0; !HandleImpl::IsEnd(next_with_shift) && i < 10; ++i) {
|
||||
HandleImpl* h = &arr[GetNextFromNextWithShift(next_with_shift)];
|
||||
NextWithShift next_with_shift = arr[home].head_next_with_shift.LoadRelaxed();
|
||||
for (size_t i = 0; !next_with_shift.IsEnd() && i < 10; ++i) {
|
||||
HandleImpl* h = &arr[next_with_shift.IsEnd()];
|
||||
// Attempt cheap key match without acquiring a read ref. This could give a
|
||||
// false positive, which is re-checked after acquiring read ref, or false
|
||||
// negative, which is re-checked in the full Lookup. Also, this is a
|
||||
@@ -3203,7 +3181,7 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
|
||||
// Read head or chain pointer
|
||||
next_with_shift = h ? h->chain_next_with_shift.Load()
|
||||
: arr[home].head_next_with_shift.Load();
|
||||
int shift = GetShiftFromNextWithShift(next_with_shift);
|
||||
int shift = next_with_shift.GetShift();
|
||||
|
||||
// Make sure it's usable
|
||||
size_t effective_home = home;
|
||||
@@ -3257,10 +3235,10 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
|
||||
}
|
||||
|
||||
// Check for end marker
|
||||
if (HandleImpl::IsEnd(next_with_shift)) {
|
||||
if (next_with_shift.IsEnd()) {
|
||||
// To ensure we didn't miss anything in the chain, the end marker must
|
||||
// point back to the correct home.
|
||||
if (LIKELY(GetNextFromNextWithShift(next_with_shift) == effective_home)) {
|
||||
if (LIKELY(next_with_shift.GetNext() == effective_home)) {
|
||||
// Complete, clean iteration of the chain, not found.
|
||||
// Clean up.
|
||||
if (read_ref_on_chain) {
|
||||
@@ -3276,7 +3254,7 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
|
||||
}
|
||||
|
||||
// Follow the next and check for full key match, home match, or neither
|
||||
h = &arr[GetNextFromNextWithShift(next_with_shift)];
|
||||
h = &arr[next_with_shift.GetNext()];
|
||||
bool full_match_or_unknown = false;
|
||||
if (MatchAndRef(&hashed_key, *h, shift, effective_home,
|
||||
&full_match_or_unknown)) {
|
||||
@@ -3600,8 +3578,7 @@ size_t AutoHyperClockTable::CalcMaxUsableLength(
|
||||
|
||||
namespace {
|
||||
bool IsHeadNonempty(const AutoHyperClockTable::HandleImpl& h) {
|
||||
return !AutoHyperClockTable::HandleImpl::IsEnd(
|
||||
h.head_next_with_shift.LoadRelaxed());
|
||||
return !h.head_next_with_shift.LoadRelaxed().IsEnd();
|
||||
}
|
||||
bool IsEntryAtHome(const AutoHyperClockTable::HandleImpl& h, int shift,
|
||||
size_t home) {
|
||||
|
||||
Vendored
+44
-27
@@ -775,6 +775,7 @@ class AutoHyperClockTable : public BaseClockTable {
|
||||
// chain--specifically the next entry in the chain.
|
||||
// * The end of a chain is given a special "end" marker and refers back
|
||||
// to the head of the chain.
|
||||
// These decorated pointers use the NextWithShift bit field struct below.
|
||||
//
|
||||
// Why do we need shift on each pointer? To make Lookup wait-free, we need
|
||||
// to be able to query a chain without missing anything, and preferably
|
||||
@@ -794,47 +795,63 @@ class AutoHyperClockTable : public BaseClockTable {
|
||||
// it is normal to see "under construction" entries on the chain, and it
|
||||
// is not safe to read their hashed key without either a read reference
|
||||
// on the entry or a rewrite lock on the chain.
|
||||
struct NextWithShift : public BitFields<uint64_t, NextWithShift> {
|
||||
// The "shift" associated with this decorated pointer (see description
|
||||
// above).
|
||||
using Shift = UnsignedBitField<NextWithShift, 6, NoPrevBitField>;
|
||||
// Marker for the end of a chain. Must also (a) point back to the head of
|
||||
// the chain (with end marker removed), and (b) set the LockedFlag
|
||||
// (below), so that attempting to lock an empty chain has no effect (not
|
||||
// needed, as the lock is only needed for removals).
|
||||
using EndFlag = BoolBitField<NextWithShift, Shift>;
|
||||
// Marker that some thread owning writes to the chain structure (except
|
||||
// for inserts), but only if not an "end" pointer. Also called the
|
||||
// "rewrite lock."
|
||||
using LockedFlag = BoolBitField<NextWithShift, EndFlag>;
|
||||
// The "next" associated with this decorated pointer, which is an index
|
||||
// into the table's array_ (see description above).
|
||||
using Next = UnsignedBitField<NextWithShift, 56, LockedFlag>;
|
||||
|
||||
// Marker in a "with_shift" head pointer for some thread owning writes
|
||||
// to the chain structure (except for inserts), but only if not an
|
||||
// "end" pointer. Also called the "rewrite lock."
|
||||
static constexpr uint64_t kHeadLocked = uint64_t{1} << 7;
|
||||
bool IsLocked() const { return Get<LockedFlag>(); }
|
||||
bool IsEnd() const {
|
||||
// End flag should imply locked flag
|
||||
assert(!Get<EndFlag>() || Get<LockedFlag>());
|
||||
return Get<EndFlag>();
|
||||
}
|
||||
bool IsLockedNotEnd() const {
|
||||
// NOTE: helping GCC to optimize this simpler code:
|
||||
// return IsLocked() && !IsEnd();
|
||||
constexpr U kEndFlag = U{1} << EndFlag::kBitOffset;
|
||||
constexpr U kLockedFlag = U{1} << LockedFlag::kBitOffset;
|
||||
return (underlying & (kEndFlag | kLockedFlag)) == kLockedFlag;
|
||||
}
|
||||
auto GetNext() const { return Get<Next>(); }
|
||||
auto GetShift() const { return Get<Shift>(); }
|
||||
|
||||
// Marker in a "with_shift" pointer for the end of a chain. Must also
|
||||
// point back to the head of the chain (with end marker removed).
|
||||
// Also includes the "locked" bit so that attempting to lock an empty
|
||||
// chain has no effect (not needed, as the lock is only needed for
|
||||
// removals).
|
||||
static constexpr uint64_t kNextEndFlags = (uint64_t{1} << 6) | kHeadLocked;
|
||||
static NextWithShift Make(size_t next, int shift) {
|
||||
return NextWithShift{}.With<Next>(next).With<Shift>(
|
||||
static_cast<uint8_t>(shift));
|
||||
}
|
||||
|
||||
static inline bool IsEnd(uint64_t next_with_shift) {
|
||||
// Assuming certain values never used, suffices to check this one bit
|
||||
constexpr auto kCheckBit = kNextEndFlags ^ kHeadLocked;
|
||||
return next_with_shift & kCheckBit;
|
||||
}
|
||||
|
||||
// Bottom bits to right shift away to get an array index from a
|
||||
// "with_shift" pointer.
|
||||
static constexpr int kNextShift = 8;
|
||||
|
||||
// A bit mask for the "shift" associated with each "with_shift" pointer.
|
||||
// Always bottommost bits.
|
||||
static constexpr int kShiftMask = 63;
|
||||
static NextWithShift MakeEnd(size_t next, int shift) {
|
||||
return Make(next, shift).With<EndFlag>(true).With<LockedFlag>(true);
|
||||
}
|
||||
};
|
||||
|
||||
// A marker for head_next_with_shift that indicates this HandleImpl is
|
||||
// heap allocated (standalone) rather than in the table.
|
||||
static constexpr uint64_t kStandaloneMarker = UINT64_MAX;
|
||||
static constexpr NextWithShift kStandaloneMarker{UINT64_MAX};
|
||||
|
||||
// A marker for head_next_with_shift indicating the head is not yet part
|
||||
// of the usable table, or for chain_next_with_shift indicating that the
|
||||
// entry is not present or is not yet part of a chain (must not be
|
||||
// "shareable" state).
|
||||
static constexpr uint64_t kUnusedMarker = 0;
|
||||
static constexpr NextWithShift kUnusedMarker{0};
|
||||
|
||||
// See above. The head pointer is logically independent of the rest of
|
||||
// the entry, including the chain next pointer.
|
||||
AcqRelAtomic<uint64_t> head_next_with_shift{kUnusedMarker};
|
||||
AcqRelAtomic<uint64_t> chain_next_with_shift{kUnusedMarker};
|
||||
AcqRelBitFieldsAtomic<NextWithShift> head_next_with_shift{kUnusedMarker};
|
||||
AcqRelBitFieldsAtomic<NextWithShift> chain_next_with_shift{kUnusedMarker};
|
||||
|
||||
// For supporting CreateStandalone and some fallback cases.
|
||||
inline bool IsStandalone() const {
|
||||
|
||||
+571
@@ -722,6 +722,88 @@ static void LoadAndCheckLatestOptions(const char* db_name, rocksdb_env_t* env,
|
||||
num_column_families);
|
||||
}
|
||||
|
||||
// Global state for tracking remote compaction calls
|
||||
typedef struct {
|
||||
int schedule_called;
|
||||
int wait_called;
|
||||
int cancel_called;
|
||||
char last_scheduled_job_id[256];
|
||||
char last_db_name[256];
|
||||
} RemoteCompactionState;
|
||||
|
||||
// Schedule callback - gets called when compaction is scheduled
|
||||
static rocksdb_compactionservice_scheduleresponse_t* RemoteCompactionSchedule(
|
||||
void* state, const rocksdb_compactionservice_jobinfo_t* info,
|
||||
const char* input, size_t input_len) {
|
||||
(void)input;
|
||||
(void)input_len;
|
||||
RemoteCompactionState* rcs = (RemoteCompactionState*)state;
|
||||
rcs->schedule_called++;
|
||||
|
||||
// Extract job info
|
||||
size_t db_name_len;
|
||||
const char* db_name =
|
||||
rocksdb_compactionservice_jobinfo_t_get_db_name(info, &db_name_len);
|
||||
memcpy(rcs->last_db_name, db_name, db_name_len);
|
||||
rcs->last_db_name[db_name_len] = '\0';
|
||||
|
||||
// Generate a job ID
|
||||
snprintf(rcs->last_scheduled_job_id, sizeof(rcs->last_scheduled_job_id),
|
||||
"job-%d", rcs->schedule_called);
|
||||
|
||||
// Create response with success status
|
||||
char* err = NULL;
|
||||
rocksdb_compactionservice_scheduleresponse_t* response =
|
||||
rocksdb_compactionservice_scheduleresponse_create(
|
||||
rcs->last_scheduled_job_id,
|
||||
rocksdb_compactionservice_jobstatus_success, &err);
|
||||
if (err) {
|
||||
free(err);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Wait callback - simulates waiting for remote compaction to complete
|
||||
static int RemoteCompactionWait(void* state, const char* scheduled_job_id,
|
||||
char** result, size_t* result_len) {
|
||||
RemoteCompactionState* rcs = (RemoteCompactionState*)state;
|
||||
rcs->wait_called++;
|
||||
|
||||
if (strcmp(scheduled_job_id, rcs->last_scheduled_job_id) != 0) {
|
||||
return rocksdb_compactionservice_jobstatus_failure;
|
||||
}
|
||||
|
||||
// For testing purposes, return kUseLocal to cause RocksDB to fall back to
|
||||
// local compaction. This tests the callback mechanism without needing a fully
|
||||
// serialized result. In a real scenario, this would communicate with a remote
|
||||
// worker that calls rocksdb_open_and_compact() and returns a properly
|
||||
// serialized CompactionServiceResult
|
||||
*result = NULL;
|
||||
*result_len = 0;
|
||||
|
||||
return rocksdb_compactionservice_jobstatus_use_local;
|
||||
}
|
||||
|
||||
// Cancel callback - cancels pending jobs
|
||||
static void RemoteCompactionCancel(void* state) {
|
||||
RemoteCompactionState* rcs = (RemoteCompactionState*)state;
|
||||
rcs->cancel_called++;
|
||||
}
|
||||
|
||||
// Destructor callback
|
||||
static void RemoteCompactionDestroy(void* state) { (void)state; }
|
||||
|
||||
// NULL schedule callback for testing failure handling
|
||||
static rocksdb_compactionservice_scheduleresponse_t* NullSchedule(
|
||||
void* state, const rocksdb_compactionservice_jobinfo_t* info,
|
||||
const char* input, size_t input_len) {
|
||||
(void)state;
|
||||
(void)info;
|
||||
(void)input;
|
||||
(void)input_len;
|
||||
return NULL; // Return NULL to simulate failure
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
@@ -1255,6 +1337,70 @@ int main(int argc, char** argv) {
|
||||
rocksdb_writebatch_destroy(wb);
|
||||
}
|
||||
|
||||
StartPhase("writebatch_vectors_cf");
|
||||
{
|
||||
const char* cf_name = "wb_vectors_cf";
|
||||
rocksdb_column_family_handle_t* wb_cf =
|
||||
rocksdb_create_column_family(db, options, cf_name, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
rocksdb_writebatch_t* wb = rocksdb_writebatch_create();
|
||||
|
||||
// Test putv_cf: concatenates multiple slices into a single key/value
|
||||
const char* put_keys[2] = {"k", "ey"};
|
||||
const size_t put_key_sizes[2] = {1, 2};
|
||||
const char* put_vals[3] = {"v", "a", "l"};
|
||||
const size_t put_val_sizes[3] = {1, 1, 1};
|
||||
rocksdb_writebatch_putv_cf(wb, wb_cf, 2, put_keys, put_key_sizes, 3,
|
||||
put_vals, put_val_sizes);
|
||||
rocksdb_write(db, woptions, wb, &err);
|
||||
CheckNoError(err);
|
||||
// putv_cf concatenates: key="k"+"ey"="key", value="v"+"a"+"l"="val"
|
||||
CheckGetCF(db, roptions, wb_cf, "key", "val");
|
||||
CheckGetCF(db, roptions, wb_cf, "k", NULL);
|
||||
CheckGetCF(db, roptions, wb_cf, "ey", NULL);
|
||||
|
||||
// Test deletev_cf: concatenates multiple slices for key
|
||||
rocksdb_writebatch_clear(wb);
|
||||
const char* del_keys[2] = {"k", "ey"};
|
||||
const size_t del_key_sizes[2] = {1, 2};
|
||||
rocksdb_writebatch_deletev_cf(wb, wb_cf, 2, del_keys, del_key_sizes);
|
||||
rocksdb_write(db, woptions, wb, &err);
|
||||
CheckNoError(err);
|
||||
CheckGetCF(db, roptions, wb_cf, "key", NULL);
|
||||
|
||||
// Test delete_rangev_cf: concatenates slices for range deletion
|
||||
rocksdb_writebatch_clear(wb);
|
||||
rocksdb_writebatch_put_cf(wb, wb_cf, "a", 1, "1", 1);
|
||||
rocksdb_writebatch_put_cf(wb, wb_cf, "b", 1, "2", 1);
|
||||
rocksdb_writebatch_put_cf(wb, wb_cf, "c", 1, "3", 1);
|
||||
rocksdb_write(db, woptions, wb, &err);
|
||||
CheckNoError(err);
|
||||
CheckGetCF(db, roptions, wb_cf, "a", "1");
|
||||
CheckGetCF(db, roptions, wb_cf, "b", "2");
|
||||
CheckGetCF(db, roptions, wb_cf, "c", "3");
|
||||
|
||||
rocksdb_writebatch_clear(wb);
|
||||
const char* range_start[2] = {"a", ""}; // "a" + "" = "a"
|
||||
const size_t range_start_sizes[2] = {1, 0};
|
||||
const char* range_end[2] = {"c", ""};
|
||||
const size_t range_end_sizes[2] = {1, 0};
|
||||
rocksdb_writebatch_delete_rangev_cf(wb, wb_cf, 2, range_start,
|
||||
range_start_sizes, range_end,
|
||||
range_end_sizes);
|
||||
rocksdb_write(db, woptions, wb, &err);
|
||||
CheckNoError(err);
|
||||
// Range [a, c) should delete "a" and "b", but not "c"
|
||||
CheckGetCF(db, roptions, wb_cf, "a", NULL);
|
||||
CheckGetCF(db, roptions, wb_cf, "b", NULL);
|
||||
CheckGetCF(db, roptions, wb_cf, "c", "3");
|
||||
|
||||
rocksdb_writebatch_destroy(wb);
|
||||
rocksdb_drop_column_family(db, wb_cf, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_column_family_handle_destroy(wb_cf);
|
||||
}
|
||||
|
||||
StartPhase("writebatch_vectors");
|
||||
{
|
||||
rocksdb_writebatch_t* wb = rocksdb_writebatch_create();
|
||||
@@ -1420,6 +1566,43 @@ int main(int argc, char** argv) {
|
||||
rocksdb_iter_destroy(iter);
|
||||
}
|
||||
|
||||
StartPhase("iter_slice");
|
||||
{
|
||||
// Test the new slice-based iterator API for better performance
|
||||
rocksdb_iterator_t* iter = rocksdb_create_iterator(db, roptions);
|
||||
CheckCondition(!rocksdb_iter_valid(iter));
|
||||
rocksdb_iter_seek_to_first(iter);
|
||||
CheckCondition(rocksdb_iter_valid(iter));
|
||||
|
||||
// Test rocksdb_iter_key_slice
|
||||
rocksdb_slice_t key_slice = rocksdb_iter_key_slice(iter);
|
||||
CheckEqual("box", key_slice.data, key_slice.size);
|
||||
|
||||
// Test rocksdb_iter_value_slice
|
||||
rocksdb_slice_t value_slice = rocksdb_iter_value_slice(iter);
|
||||
CheckEqual("c", value_slice.data, value_slice.size);
|
||||
|
||||
// Move to next entry and test again
|
||||
rocksdb_iter_next(iter);
|
||||
CheckCondition(rocksdb_iter_valid(iter));
|
||||
key_slice = rocksdb_iter_key_slice(iter);
|
||||
value_slice = rocksdb_iter_value_slice(iter);
|
||||
CheckEqual("foo", key_slice.data, key_slice.size);
|
||||
CheckEqual("hello", value_slice.data, value_slice.size);
|
||||
|
||||
// Test seeking with slice API
|
||||
rocksdb_iter_seek(iter, "b", 1);
|
||||
CheckCondition(rocksdb_iter_valid(iter));
|
||||
key_slice = rocksdb_iter_key_slice(iter);
|
||||
value_slice = rocksdb_iter_value_slice(iter);
|
||||
CheckEqual("box", key_slice.data, key_slice.size);
|
||||
CheckEqual("c", value_slice.data, value_slice.size);
|
||||
|
||||
rocksdb_iter_get_error(iter, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_iter_destroy(iter);
|
||||
}
|
||||
|
||||
StartPhase("wbwi_iter");
|
||||
{
|
||||
rocksdb_iterator_t* base_iter = rocksdb_create_iterator(db, roptions);
|
||||
@@ -1505,6 +1688,53 @@ int main(int argc, char** argv) {
|
||||
CheckMultiGetValues(3, vals, vals_sizes, errs, expected);
|
||||
}
|
||||
|
||||
StartPhase("zero_copy_get_pinned_v2");
|
||||
{
|
||||
// Test new zero-copy get functions
|
||||
|
||||
// Test rocksdb_get_pinned_v2
|
||||
rocksdb_pinnable_handle_t* handle =
|
||||
rocksdb_get_pinned_v2(db, roptions, "foo", 3, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(handle != NULL);
|
||||
size_t val_len;
|
||||
const char* val = rocksdb_pinnable_handle_get_value(handle, &val_len);
|
||||
CheckEqual("hello", val, val_len);
|
||||
rocksdb_pinnable_handle_destroy(handle);
|
||||
|
||||
// Test with non-existent key
|
||||
handle = rocksdb_get_pinned_v2(db, roptions, "notfound", 8, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(handle == NULL);
|
||||
|
||||
// Test rocksdb_get_into_buffer
|
||||
char buffer[100];
|
||||
unsigned char found;
|
||||
unsigned char success = rocksdb_get_into_buffer(
|
||||
db, roptions, "foo", 3, buffer, sizeof(buffer), &val_len, &found, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(success == 1);
|
||||
CheckCondition(found == 1);
|
||||
CheckCondition(val_len == 5);
|
||||
CheckCondition(memcmp(buffer, "hello", 5) == 0);
|
||||
|
||||
// Test with buffer too small
|
||||
success = rocksdb_get_into_buffer(db, roptions, "foo", 3, buffer,
|
||||
2, // Buffer too small
|
||||
&val_len, &found, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(success == 0); // Should fail due to small buffer
|
||||
CheckCondition(found == 1);
|
||||
CheckCondition(val_len == 5); // Should still report actual size
|
||||
|
||||
// Test with non-existent key
|
||||
success = rocksdb_get_into_buffer(db, roptions, "notfound", 8, buffer,
|
||||
sizeof(buffer), &val_len, &found, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(success == 0);
|
||||
CheckCondition(found == 0);
|
||||
}
|
||||
|
||||
StartPhase("pin_get");
|
||||
{
|
||||
CheckPinGet(db, roptions, "box", "c");
|
||||
@@ -1922,6 +2152,55 @@ int main(int argc, char** argv) {
|
||||
rocksdb_flush_wal(db, 1, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
// Test column family handle get name
|
||||
{
|
||||
size_t name_len;
|
||||
char* cf_name =
|
||||
rocksdb_column_family_handle_get_name(handles[1], &name_len);
|
||||
CheckCondition(name_len == 3);
|
||||
CheckCondition(memcmp(cf_name, "cf1", 3) == 0);
|
||||
rocksdb_free(cf_name);
|
||||
}
|
||||
|
||||
// Test zero-copy get with column families
|
||||
{
|
||||
rocksdb_pinnable_handle_t* handle =
|
||||
rocksdb_get_pinned_cf_v2(db, roptions, handles[1], "box", 3, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(handle != NULL);
|
||||
size_t val_len;
|
||||
const char* val = rocksdb_pinnable_handle_get_value(handle, &val_len);
|
||||
CheckEqual("c", val, val_len);
|
||||
rocksdb_pinnable_handle_destroy(handle);
|
||||
|
||||
// Test with non-existent key
|
||||
handle = rocksdb_get_pinned_cf_v2(db, roptions, handles[1], "notfound", 8,
|
||||
&err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(handle == NULL);
|
||||
|
||||
// Test rocksdb_get_into_buffer_cf
|
||||
char buffer[100];
|
||||
unsigned char found;
|
||||
unsigned char success = rocksdb_get_into_buffer_cf(
|
||||
db, roptions, handles[1], "buff", 4, buffer, sizeof(buffer), &val_len,
|
||||
&found, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(success == 1);
|
||||
CheckCondition(found == 1);
|
||||
CheckCondition(val_len == 7);
|
||||
CheckCondition(memcmp(buffer, "rocksdb", 7) == 0);
|
||||
|
||||
// Test with buffer too small
|
||||
success = rocksdb_get_into_buffer_cf(db, roptions, handles[1], "buff", 4,
|
||||
buffer, 3, // Buffer too small
|
||||
&val_len, &found, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(success == 0); // Should fail due to small buffer
|
||||
CheckCondition(found == 1);
|
||||
CheckCondition(val_len == 7); // Should still report actual size
|
||||
}
|
||||
|
||||
// Test WriteBatchWithIndex iteration with Column Family
|
||||
rocksdb_writebatch_wi_t* wbwi = rocksdb_writebatch_wi_create(0, true);
|
||||
rocksdb_writebatch_wi_put_cf(wbwi, handles[1], "boat", 4, "row",
|
||||
@@ -1998,6 +2277,74 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Test rocksdb_batched_multi_get_cf_slice for better performance
|
||||
// Build rocksdb_slice_t array directly to avoid conversion overhead
|
||||
rocksdb_slice_t batched_key_slices[4];
|
||||
batched_key_slices[0].data = "box";
|
||||
batched_key_slices[0].size = 3;
|
||||
batched_key_slices[1].data = "buff";
|
||||
batched_key_slices[1].size = 4;
|
||||
batched_key_slices[2].data = "barfooxx";
|
||||
batched_key_slices[2].size = 8;
|
||||
batched_key_slices[3].data = "box";
|
||||
batched_key_slices[3].size = 3;
|
||||
|
||||
const char* expected_value[4] = {"c", "rocksdb", NULL, "c"};
|
||||
char* batched_errs[4];
|
||||
rocksdb_pinnableslice_t* pvals[4];
|
||||
|
||||
rocksdb_batched_multi_get_cf_slice(db, roptions, handles[1], 4,
|
||||
batched_key_slices, pvals,
|
||||
batched_errs, false);
|
||||
|
||||
const char* val;
|
||||
size_t val_len;
|
||||
for (i = 0; i < 4; ++i) {
|
||||
CheckNoError(batched_errs[i]);
|
||||
if (pvals[i] != NULL) {
|
||||
val = rocksdb_pinnableslice_value(pvals[i], &val_len);
|
||||
CheckEqual(expected_value[i], val, val_len);
|
||||
rocksdb_pinnableslice_destroy(pvals[i]);
|
||||
} else {
|
||||
CheckEqual(expected_value[i], NULL, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Test rocksdb_batched_multi_get_cf_slice with sorted_input=true
|
||||
// Keys must be in sorted order for this optimization
|
||||
rocksdb_slice_t sorted_key_slices[3];
|
||||
sorted_key_slices[0].data = "box";
|
||||
sorted_key_slices[0].size = 3;
|
||||
sorted_key_slices[1].data = "buff";
|
||||
sorted_key_slices[1].size = 4;
|
||||
sorted_key_slices[2].data = "notfound";
|
||||
sorted_key_slices[2].size = 8;
|
||||
|
||||
const char* expected_value[3] = {"c", "rocksdb", NULL};
|
||||
char* batched_errs[3];
|
||||
rocksdb_pinnableslice_t* pvals[3];
|
||||
|
||||
rocksdb_batched_multi_get_cf_slice(db, roptions, handles[1], 3,
|
||||
sorted_key_slices, pvals, batched_errs,
|
||||
true);
|
||||
|
||||
const char* val;
|
||||
size_t val_len;
|
||||
for (i = 0; i < 3; ++i) {
|
||||
CheckNoError(batched_errs[i]);
|
||||
if (pvals[i] != NULL) {
|
||||
val = rocksdb_pinnableslice_value(pvals[i], &val_len);
|
||||
CheckEqual(expected_value[i], val, val_len);
|
||||
rocksdb_pinnableslice_destroy(pvals[i]);
|
||||
} else {
|
||||
CheckEqual(expected_value[i], NULL, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
unsigned char value_found = 0;
|
||||
|
||||
@@ -3469,6 +3816,17 @@ int main(int argc, char** argv) {
|
||||
rocksdb_transaction_put(txn, "foo", 3, "hello", 5, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
// test transaction get/set name (before commit)
|
||||
{
|
||||
rocksdb_transaction_set_name(txn, "test_txn", 8, &err);
|
||||
CheckNoError(err);
|
||||
size_t name_len;
|
||||
char* txn_name = rocksdb_transaction_get_name(txn, &name_len);
|
||||
CheckCondition(name_len == 8);
|
||||
CheckCondition(memcmp(txn_name, "test_txn", 8) == 0);
|
||||
rocksdb_free(txn_name);
|
||||
}
|
||||
|
||||
// read from outside transaction, before commit
|
||||
CheckTxnDBGet(txn_db, roptions, "foo", NULL);
|
||||
CheckTxnDBPinGet(txn_db, roptions, "foo", NULL);
|
||||
@@ -4207,6 +4565,219 @@ int main(int argc, char** argv) {
|
||||
rocksdb_cache_destroy(lru);
|
||||
}
|
||||
|
||||
StartPhase("remote_compaction_service");
|
||||
{
|
||||
RemoteCompactionState remote_state = {0, 0, 0, "", ""};
|
||||
|
||||
// Create compaction service
|
||||
rocksdb_compactionservice_t* service = rocksdb_compactionservice_create(
|
||||
&remote_state, // state
|
||||
RemoteCompactionDestroy, // destructor
|
||||
RemoteCompactionSchedule, // schedule callback
|
||||
"TestRemoteCompaction", // name
|
||||
RemoteCompactionWait, // wait callback
|
||||
RemoteCompactionCancel, // cancel_awaiting_jobs
|
||||
NULL); // on_installation
|
||||
|
||||
// Create options with remote compaction
|
||||
rocksdb_options_t* remote_options = rocksdb_options_create();
|
||||
rocksdb_options_set_create_if_missing(remote_options, 1);
|
||||
rocksdb_options_set_level0_file_num_compaction_trigger(remote_options, 2);
|
||||
rocksdb_options_set_write_buffer_size(remote_options,
|
||||
64 * 1024); // 64KB buffer
|
||||
rocksdb_options_set_max_bytes_for_level_base(remote_options,
|
||||
256 * 1024); // 256KB
|
||||
rocksdb_options_set_target_file_size_base(
|
||||
remote_options, 64 * 1024); // 64KB target file size
|
||||
// Disable automatic compactions to test manual compaction only
|
||||
rocksdb_options_set_disable_auto_compactions(remote_options, 1);
|
||||
rocksdb_options_set_compaction_service(remote_options, service);
|
||||
|
||||
// Destroy old DB and create new one
|
||||
rocksdb_close(db);
|
||||
rocksdb_destroy_db(remote_options, dbname, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
db = rocksdb_open(remote_options, dbname, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
// Create multiple SST files to trigger compaction
|
||||
rocksdb_flushoptions_t* flush_opts = rocksdb_flushoptions_create();
|
||||
rocksdb_flushoptions_set_wait(flush_opts, 1);
|
||||
|
||||
// Write and flush multiple times to create multiple L0 files
|
||||
// Write more data with larger values to ensure files are substantial
|
||||
for (int batch = 0; batch < 5; batch++) {
|
||||
for (int i = 0; i < 200; i++) {
|
||||
char key[20], val[1000];
|
||||
snprintf(key, sizeof(key), "key%d_%d", batch, i);
|
||||
// Fill value with repeated data to make it larger
|
||||
memset(val, 'a' + (batch % 26), sizeof(val) - 1);
|
||||
val[sizeof(val) - 1] = '\0';
|
||||
rocksdb_put(db, woptions, key, strlen(key), val, strlen(val), &err);
|
||||
CheckNoError(err);
|
||||
}
|
||||
rocksdb_flush(db, flush_opts, &err);
|
||||
CheckNoError(err);
|
||||
}
|
||||
rocksdb_flushoptions_destroy(flush_opts);
|
||||
|
||||
// Trigger manual compaction to invoke remote compaction service
|
||||
rocksdb_compact_range(db, NULL, 0, NULL, 0);
|
||||
|
||||
rocksdb_wait_for_compact_options_t* wco =
|
||||
rocksdb_wait_for_compact_options_create();
|
||||
rocksdb_wait_for_compact(db, wco, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_wait_for_compact_options_destroy(wco);
|
||||
|
||||
// Verify that callbacks were actually called
|
||||
CheckCondition(remote_state.schedule_called > 0);
|
||||
CheckCondition(remote_state.wait_called > 0);
|
||||
CheckCondition(strlen(remote_state.last_db_name) > 0);
|
||||
CheckCondition(strstr(remote_state.last_db_name, "rocksdb_c_test") != NULL);
|
||||
|
||||
// Verify data is still accessible after remote compaction
|
||||
// Just check a few keys to verify data integrity
|
||||
for (int batch = 0; batch < 5; batch++) {
|
||||
char key[20];
|
||||
snprintf(key, sizeof(key), "key%d_0", batch);
|
||||
size_t vallen;
|
||||
char* val = rocksdb_get(db, roptions, key, strlen(key), &vallen, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(val != NULL);
|
||||
CheckCondition(vallen == 999); // strlen of 1000-byte string
|
||||
free(val);
|
||||
}
|
||||
|
||||
// Test cancellation API directly
|
||||
RemoteCompactionCancel(&remote_state);
|
||||
CheckCondition(remote_state.cancel_called > 0);
|
||||
|
||||
// Cleanup
|
||||
rocksdb_close(db);
|
||||
rocksdb_destroy_db(remote_options, dbname, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_options_destroy(remote_options);
|
||||
|
||||
// Reopen DB with original options for subsequent tests
|
||||
db = rocksdb_open(options, dbname, &err);
|
||||
CheckNoError(err);
|
||||
}
|
||||
|
||||
StartPhase("remote_compaction_scheduleresponse");
|
||||
{
|
||||
// Test scheduleresponse creation and getters
|
||||
rocksdb_compactionservice_scheduleresponse_t* response;
|
||||
|
||||
// Test success response
|
||||
err = NULL;
|
||||
response = rocksdb_compactionservice_scheduleresponse_create(
|
||||
"test-job-123", rocksdb_compactionservice_jobstatus_success, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(response != NULL);
|
||||
CheckCondition(
|
||||
rocksdb_compactionservice_scheduleresponse_getstatus(response) ==
|
||||
rocksdb_compactionservice_jobstatus_success);
|
||||
|
||||
size_t job_id_len;
|
||||
const char* job_id =
|
||||
rocksdb_compactionservice_scheduleresponse_get_scheduled_job_id(
|
||||
response, &job_id_len);
|
||||
CheckCondition(job_id_len == strlen("test-job-123"));
|
||||
CheckCondition(memcmp(job_id, "test-job-123", job_id_len) == 0);
|
||||
rocksdb_compactionservice_scheduleresponse_t_destroy(response);
|
||||
|
||||
// Test failure response
|
||||
response = rocksdb_compactionservice_scheduleresponse_create_with_status(
|
||||
rocksdb_compactionservice_jobstatus_failure, &err);
|
||||
CheckCondition(response != NULL);
|
||||
CheckCondition(
|
||||
rocksdb_compactionservice_scheduleresponse_getstatus(response) ==
|
||||
rocksdb_compactionservice_jobstatus_failure);
|
||||
rocksdb_compactionservice_scheduleresponse_t_destroy(response);
|
||||
|
||||
response = rocksdb_compactionservice_scheduleresponse_create_with_status(
|
||||
999, &err);
|
||||
CheckCondition(response == NULL); // Invalid status
|
||||
if (err) {
|
||||
Free(&err);
|
||||
}
|
||||
}
|
||||
|
||||
StartPhase("remote_compaction_options_override");
|
||||
{
|
||||
// Test CompactionServiceOptionsOverride API
|
||||
rocksdb_compaction_service_options_override_t* override_opts =
|
||||
rocksdb_compaction_service_options_override_create();
|
||||
CheckCondition(override_opts != NULL);
|
||||
|
||||
// Set up override options
|
||||
rocksdb_compaction_service_options_override_set_env(override_opts, env);
|
||||
rocksdb_compaction_service_options_override_set_comparator(override_opts,
|
||||
cmp);
|
||||
|
||||
rocksdb_compaction_service_options_override_destroy(override_opts);
|
||||
}
|
||||
|
||||
StartPhase("remote_compaction_null_callback_handling");
|
||||
{
|
||||
// Test that NULL callback returns are handled gracefully
|
||||
// This simulates a failure in the remote compaction service
|
||||
rocksdb_compactionservice_t* null_service =
|
||||
rocksdb_compactionservice_create(NULL, NULL, NullSchedule,
|
||||
"NullTestService", NULL, NULL, NULL);
|
||||
|
||||
rocksdb_options_t* null_opts = rocksdb_options_create();
|
||||
rocksdb_options_set_create_if_missing(null_opts, 1);
|
||||
rocksdb_options_set_compaction_service(null_opts, null_service);
|
||||
|
||||
const char* null_db = "rocksdb_c_test_null_service";
|
||||
|
||||
rocksdb_t* null_db_handle = rocksdb_open(null_opts, null_db, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
// Write data and trigger compaction
|
||||
for (int i = 0; i < 100; i++) {
|
||||
char key[20], val[50];
|
||||
snprintf(key, sizeof(key), "key%d", i);
|
||||
snprintf(val, sizeof(val), "val%d", i);
|
||||
rocksdb_put(null_db_handle, woptions, key, strlen(key), val, strlen(val),
|
||||
&err);
|
||||
CheckNoError(err);
|
||||
}
|
||||
|
||||
// This should fall back to local compaction (not crash)
|
||||
rocksdb_compact_range(null_db_handle, NULL, 0, NULL, 0);
|
||||
|
||||
// Data should still be readable
|
||||
CheckGet(null_db_handle, roptions, "key50", "val50");
|
||||
|
||||
rocksdb_close(null_db_handle);
|
||||
rocksdb_destroy_db(null_opts, null_db, &err);
|
||||
rocksdb_options_destroy(null_opts);
|
||||
}
|
||||
|
||||
StartPhase("remote_compaction_canceled_flag");
|
||||
{
|
||||
// Test atomic cancellation flag API
|
||||
unsigned char* canceled = rocksdb_open_and_compact_canceled_create();
|
||||
CheckCondition(canceled != NULL);
|
||||
|
||||
// Set cancellation
|
||||
rocksdb_open_and_compact_canceled_set(canceled, 1);
|
||||
|
||||
// Use with OpenAndCompactOptions
|
||||
rocksdb_open_and_compact_options_t* oac_opts =
|
||||
rocksdb_open_and_compact_options_create();
|
||||
rocksdb_open_and_compact_options_set_canceled(oac_opts, canceled);
|
||||
rocksdb_open_and_compact_options_set_allow_resumption(oac_opts, 1);
|
||||
|
||||
// Cleanup
|
||||
rocksdb_open_and_compact_options_destroy(oac_opts);
|
||||
rocksdb_open_and_compact_canceled_destroy(canceled);
|
||||
}
|
||||
|
||||
StartPhase("sst_file_manager");
|
||||
{
|
||||
rocksdb_sst_file_manager_t* sst_file_manager;
|
||||
|
||||
@@ -180,6 +180,10 @@ class Compaction {
|
||||
const std::vector<CompactionInputFiles>* inputs() { return &inputs_; }
|
||||
|
||||
// Returns the LevelFilesBrief of the specified compaction input level.
|
||||
// Note that if the compaction includes standalone range deletion file,
|
||||
// this function returns the result after filtering out input files covered
|
||||
// by the range deletion file.
|
||||
// Use inputs() if you want to get the original input files.
|
||||
const LevelFilesBrief* input_levels(size_t compaction_input_level) const {
|
||||
return &input_levels_[compaction_input_level];
|
||||
}
|
||||
|
||||
@@ -1480,11 +1480,11 @@ CompactionJob::CreateFileHandlers(SubcompactionState* sub_compact,
|
||||
const CompactionFileCloseFunc close_file_func =
|
||||
[this, sub_compact, start_user_key, end_user_key](
|
||||
const Status& status,
|
||||
const ParsedInternalKey& prev_table_last_internal_key,
|
||||
const ParsedInternalKey& prev_iter_output_internal_key,
|
||||
const Slice& next_table_min_key, const CompactionIterator* c_iter,
|
||||
CompactionOutputs& outputs) {
|
||||
return this->FinishCompactionOutputFile(
|
||||
status, prev_table_last_internal_key, next_table_min_key,
|
||||
status, prev_iter_output_internal_key, next_table_min_key,
|
||||
start_user_key, end_user_key, c_iter, sub_compact, outputs);
|
||||
};
|
||||
|
||||
@@ -1499,8 +1499,8 @@ Status CompactionJob::ProcessKeyValue(
|
||||
const uint64_t kRecordStatsEvery = 1000;
|
||||
[[maybe_unused]] const std::optional<const Slice> end = sub_compact->end;
|
||||
|
||||
IterKey last_output_key;
|
||||
ParsedInternalKey last_output_ikey;
|
||||
IterKey prev_iter_output_key;
|
||||
ParsedInternalKey prev_iter_output_internal_key;
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"CompactionJob::ProcessKeyValueCompaction()::Processing",
|
||||
@@ -1551,9 +1551,9 @@ Status CompactionJob::ProcessKeyValue(
|
||||
// and `close_file_func`.
|
||||
// TODO: it would be better to have the compaction file open/close moved
|
||||
// into `CompactionOutputs` which has the output file information.
|
||||
status =
|
||||
sub_compact->AddToOutput(*c_iter, use_proximal_output, open_file_func,
|
||||
close_file_func, last_output_ikey);
|
||||
status = sub_compact->AddToOutput(*c_iter, use_proximal_output,
|
||||
open_file_func, close_file_func,
|
||||
prev_iter_output_internal_key);
|
||||
if (!status.ok()) {
|
||||
break;
|
||||
}
|
||||
@@ -1562,9 +1562,10 @@ Status CompactionJob::ProcessKeyValue(
|
||||
static_cast<void*>(const_cast<std::atomic<bool>*>(
|
||||
&manual_compaction_canceled_)));
|
||||
|
||||
last_output_key.SetInternalKey(c_iter->key(), &last_output_ikey);
|
||||
last_output_ikey.sequence = ikey.sequence;
|
||||
last_output_ikey.type = ikey.type;
|
||||
prev_iter_output_key.SetInternalKey(c_iter->key(),
|
||||
&prev_iter_output_internal_key);
|
||||
prev_iter_output_internal_key.sequence = ikey.sequence;
|
||||
prev_iter_output_internal_key.type = ikey.type;
|
||||
c_iter->Next();
|
||||
|
||||
#ifndef NDEBUG
|
||||
@@ -1871,7 +1872,7 @@ void CompactionJob::RecordDroppedKeys(
|
||||
|
||||
Status CompactionJob::FinishCompactionOutputFile(
|
||||
const Status& input_status,
|
||||
const ParsedInternalKey& prev_table_last_internal_key,
|
||||
const ParsedInternalKey& prev_iter_output_internal_key,
|
||||
const Slice& next_table_min_key, const Slice* comp_start_user_key,
|
||||
const Slice* comp_end_user_key, const CompactionIterator* c_iter,
|
||||
SubcompactionState* sub_compact, CompactionOutputs& outputs) {
|
||||
@@ -2049,7 +2050,7 @@ Status CompactionJob::FinishCompactionOutputFile(
|
||||
}
|
||||
|
||||
if (s.ok() && ShouldUpdateSubcompactionProgress(sub_compact, c_iter,
|
||||
prev_table_last_internal_key,
|
||||
prev_iter_output_internal_key,
|
||||
next_table_min_key, meta)) {
|
||||
UpdateSubcompactionProgress(c_iter, next_table_min_key, sub_compact);
|
||||
s = PersistSubcompactionProgress(sub_compact);
|
||||
@@ -2060,10 +2061,10 @@ Status CompactionJob::FinishCompactionOutputFile(
|
||||
|
||||
bool CompactionJob::ShouldUpdateSubcompactionProgress(
|
||||
const SubcompactionState* sub_compact, const CompactionIterator* c_iter,
|
||||
const ParsedInternalKey& prev_table_last_internal_key,
|
||||
const ParsedInternalKey& prev_iter_output_internal_key,
|
||||
const Slice& next_table_min_internal_key, const FileMetaData* meta) const {
|
||||
const auto* cfd = sub_compact->compaction->column_family_data();
|
||||
// No need to update when the output will not get persisted
|
||||
// No need to update when the progress will not get persisted
|
||||
if (compaction_progress_writer_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
@@ -2087,19 +2088,24 @@ bool CompactionJob::ShouldUpdateSubcompactionProgress(
|
||||
}
|
||||
|
||||
// LIMITATION: Compaction progress persistence disabled for file boundaries
|
||||
// contaning range deletions. Range deletions can span file boundaries, making
|
||||
// it difficult (but possible) to ensure adjacent output tables have different
|
||||
// user keys. See the last check for why different users keys of adjacent
|
||||
// output tables are needed
|
||||
// containing range deletions. Range deletions can span file boundaries,
|
||||
// making it difficult to ensure adjacent output tables have different user
|
||||
// keys. See the last check for why different users keys of adjacent output
|
||||
// tables are needed
|
||||
const ValueType next_table_min_internal_key_type =
|
||||
ExtractValueType(next_table_min_internal_key);
|
||||
const ValueType prev_table_last_internal_key_type =
|
||||
prev_table_last_internal_key.user_key.empty()
|
||||
const ValueType prev_iter_output_internal_key_type =
|
||||
prev_iter_output_internal_key.user_key.empty()
|
||||
? ValueType::kTypeValue
|
||||
: prev_table_last_internal_key.type;
|
||||
: prev_iter_output_internal_key.type;
|
||||
|
||||
if (next_table_min_internal_key_type == ValueType::kTypeRangeDeletion ||
|
||||
prev_table_last_internal_key_type == ValueType::kTypeRangeDeletion) {
|
||||
// Range deletes truncated to align with file boundaries may be output by the
|
||||
// compaction iterator with `ValueType::kTypeMaxValid` instead of the original
|
||||
// type.
|
||||
if ((next_table_min_internal_key_type == ValueType::kTypeRangeDeletion ||
|
||||
next_table_min_internal_key_type == ValueType::kTypeMaxValid) ||
|
||||
(prev_iter_output_internal_key_type == ValueType::kTypeRangeDeletion ||
|
||||
prev_iter_output_internal_key_type == ValueType::kTypeMaxValid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2109,9 +2115,9 @@ bool CompactionJob::ShouldUpdateSubcompactionProgress(
|
||||
const Slice next_table_min_user_key =
|
||||
ExtractUserKey(next_table_min_internal_key);
|
||||
const Slice prev_table_last_user_key =
|
||||
prev_table_last_internal_key.user_key.empty()
|
||||
prev_iter_output_internal_key.user_key.empty()
|
||||
? Slice()
|
||||
: prev_table_last_internal_key.user_key;
|
||||
: prev_iter_output_internal_key.user_key;
|
||||
|
||||
if (cfd->user_comparator()->EqualWithoutTimestamp(next_table_min_user_key,
|
||||
prev_table_last_user_key)) {
|
||||
|
||||
@@ -430,7 +430,7 @@ class CompactionJob {
|
||||
|
||||
Status FinishCompactionOutputFile(
|
||||
const Status& input_status,
|
||||
const ParsedInternalKey& prev_table_last_internal_key,
|
||||
const ParsedInternalKey& prev_iter_output_internal_key,
|
||||
const Slice& next_table_min_key, const Slice* comp_start_user_key,
|
||||
const Slice* comp_end_user_key, const CompactionIterator* c_iter,
|
||||
SubcompactionState* sub_compact, CompactionOutputs& outputs);
|
||||
@@ -545,7 +545,7 @@ class CompactionJob {
|
||||
|
||||
bool ShouldUpdateSubcompactionProgress(
|
||||
const SubcompactionState* sub_compact, const CompactionIterator* c_iter,
|
||||
const ParsedInternalKey& prev_table_last_internal_key,
|
||||
const ParsedInternalKey& prev_iter_output_internal_key,
|
||||
const Slice& next_table_min_internal_key, const FileMetaData* meta) const;
|
||||
|
||||
void UpdateSubcompactionProgress(const CompactionIterator* c_iter,
|
||||
|
||||
@@ -2424,6 +2424,7 @@ class ResumableCompactionJobTest : public CompactionJobTestBase {
|
||||
bool enable_cancel_ = false;
|
||||
std::atomic<int> stop_count_{0};
|
||||
std::atomic<bool> cancel_{false};
|
||||
SequenceNumber cancel_before_seqno = kMaxSequenceNumber;
|
||||
|
||||
void SetUp() override {
|
||||
CompactionJobTestBase::SetUp();
|
||||
@@ -2437,7 +2438,9 @@ class ResumableCompactionJobTest : public CompactionJobTestBase {
|
||||
if (enable_cancel_) {
|
||||
ParsedInternalKey parsed_key;
|
||||
if (ParseInternalKey(pair->second, &parsed_key, true).ok()) {
|
||||
if (parsed_key.user_key == kCancelBeforeThisKey) {
|
||||
if (parsed_key.user_key == kCancelBeforeThisKey &&
|
||||
(cancel_before_seqno == kMaxSequenceNumber ||
|
||||
parsed_key.sequence == cancel_before_seqno)) {
|
||||
cancel_.store(true);
|
||||
}
|
||||
}
|
||||
@@ -2665,7 +2668,7 @@ class ResumableCompactionJobTest : public CompactionJobTestBase {
|
||||
const std::initializer_list<mock::KVPair>& input_file_2,
|
||||
uint64_t last_sequence, const std::vector<uint64_t>& snapshots,
|
||||
const std::string& expected_next_key_to_compact,
|
||||
const std::vector<std::string>& expected_input_keys, bool exists_progress,
|
||||
const std::vector<std::string>& expected_input_keys,
|
||||
bool cancelled_past_mid_point = false) {
|
||||
std::shared_ptr<Statistics> stats = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
|
||||
@@ -2701,7 +2704,7 @@ class ResumableCompactionJobTest : public CompactionJobTestBase {
|
||||
|
||||
// Resume compaction
|
||||
CompactionProgress compaction_progress;
|
||||
if (exists_progress) {
|
||||
if (expected_next_key_to_compact != "") {
|
||||
compaction_progress.push_back(
|
||||
ReadAndParseProgress(compaction_progress_file));
|
||||
}
|
||||
@@ -2774,35 +2777,43 @@ TEST_F(ResumableCompactionJobTest, BasicProgressResume) {
|
||||
4U /* last_sequence */, {} /* snapshots */,
|
||||
kCancelBeforeThisKey /* expected_next_key_to_compact */,
|
||||
{"a", "b", "bb", kCancelBeforeThisKey} /* expected_input_keys */,
|
||||
true /* exists_progress */, true /* cancelled_past_mid_point*/);
|
||||
true /* cancelled_past_mid_point */);
|
||||
}
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnSameKey) {
|
||||
NewDB();
|
||||
|
||||
// `cancel_before_seqno` is set to 0U to force cancellation after
|
||||
// `kCancelBeforeThisKey@1` instead of `kCancelBeforeThisKey@2`.
|
||||
// The seqno is 0 because `kCancelBeforeThisKey@1` will have its sequence
|
||||
// number zeroed during compaction while `kCancelBeforeThisKey@2` won't be
|
||||
cancel_before_seqno = 0U;
|
||||
RunCancelAndResumeTest(
|
||||
{{KeyStr(kCancelBeforeThisKey, 1U, kTypeValue),
|
||||
"val1"}} /* input_file_1 */,
|
||||
{{KeyStr(kCancelBeforeThisKey, 2U, kTypeValue),
|
||||
"val2"}} /* input_file_2 */,
|
||||
2U /* last_sequence */, {1U} /* snapshots */,
|
||||
{{KeyStr(kCancelBeforeThisKey, 2U, kTypeValue), "val11"},
|
||||
{KeyStr("d", 3U, kTypeValue), "val2"}} /* input_file_2 */,
|
||||
3U /* last_sequence */, {1U} /* snapshots */,
|
||||
"" /* expected_next_key_to_compact */,
|
||||
{kCancelBeforeThisKey, kCancelBeforeThisKey} /* expected_input_keys */,
|
||||
false /* exists_progress */);
|
||||
{kCancelBeforeThisKey, kCancelBeforeThisKey,
|
||||
"d"} /* expected_input_keys */);
|
||||
}
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnDeleteRange) {
|
||||
NewDB();
|
||||
|
||||
RunCancelAndResumeTest(
|
||||
{{KeyStr(kCancelBeforeThisKey, 1U, kTypeValue),
|
||||
"val1"}} /* input_file_1 */,
|
||||
{{KeyStr(kCancelBeforeThisKey, 2U, kTypeRangeDeletion),
|
||||
"val2"}} /* input_file_2 */,
|
||||
2U /* last_sequence */, {1U} /* snapshots */,
|
||||
"" /* expected_next_key_to_compact */,
|
||||
{kCancelBeforeThisKey, kCancelBeforeThisKey} /* expected_input_keys */,
|
||||
false /* exists_progress */);
|
||||
{{KeyStr("a", 1U, kTypeValue), "val1"},
|
||||
{KeyStr("b", 2U, kTypeValue), "val2"},
|
||||
{KeyStr(kCancelBeforeThisKey, 3U, kTypeValue),
|
||||
"val3"}} /* input_file_1 */,
|
||||
{{KeyStr(kCancelBeforeThisKey, 4U, kTypeRangeDeletion),
|
||||
"range_deletion_end_key"},
|
||||
{KeyStr("d", 5U, kTypeValue), "val4"}} /* input_file_2 */,
|
||||
5U /* last_sequence */, {3U} /* snapshots */,
|
||||
"b" /* expected_next_key_to_compact */,
|
||||
{"a", "b", kCancelBeforeThisKey, kCancelBeforeThisKey,
|
||||
"d"} /* expected_input_keys */);
|
||||
}
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnMerge) {
|
||||
@@ -2817,8 +2828,7 @@ TEST_F(ResumableCompactionJobTest, NoProgressResumeOnMerge) {
|
||||
"val4"}} /* input_file_2 */,
|
||||
4U /* last_sequence */, {} /* snapshots */,
|
||||
"bb" /* expected_next_key_to_compact */,
|
||||
{"a", "b", "bb", kCancelBeforeThisKey} /* expected_input_keys */,
|
||||
true /* exists_progress */);
|
||||
{"a", "b", "bb", kCancelBeforeThisKey} /* expected_input_keys */);
|
||||
}
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnSingleDelete) {
|
||||
@@ -2834,8 +2844,7 @@ TEST_F(ResumableCompactionJobTest, NoProgressResumeOnSingleDelete) {
|
||||
5U /* last_sequence */, {3U} /* snapshots */,
|
||||
"b" /* expected_next_key_to_compact */,
|
||||
{"a", "b", kCancelBeforeThisKey, kCancelBeforeThisKey,
|
||||
"d"} /* expected_input_keys */,
|
||||
true /* exists_progress */);
|
||||
"d"} /* expected_input_keys */);
|
||||
}
|
||||
|
||||
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnDeletionAtBottom) {
|
||||
@@ -2851,8 +2860,7 @@ TEST_F(ResumableCompactionJobTest, NoProgressResumeOnDeletionAtBottom) {
|
||||
5U /* last_sequence */, {3U} /* snapshots */,
|
||||
"b" /* expected_next_key_to_compact */,
|
||||
{"a", "b", kCancelBeforeThisKey, kCancelBeforeThisKey,
|
||||
"d"} /* expected_input_keys */,
|
||||
true /* exists_progress */);
|
||||
"d"} /* expected_input_keys */);
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
|
||||
@@ -278,7 +278,11 @@ bool CompactionOutputs::ShouldStopBefore(const CompactionIterator& c_iter) {
|
||||
}
|
||||
|
||||
// reach the max file size
|
||||
if (current_output_file_size_ >= compaction_->max_output_file_size()) {
|
||||
uint64_t estimated_file_size = current_output_file_size_;
|
||||
if (compaction_->mutable_cf_options().target_file_size_is_upper_bound) {
|
||||
estimated_file_size += builder_->EstimatedTailSize();
|
||||
}
|
||||
if (estimated_file_size >= compaction_->max_output_file_size()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -360,7 +364,7 @@ Status CompactionOutputs::AddToOutput(
|
||||
const CompactionIterator& c_iter,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func,
|
||||
const ParsedInternalKey& prev_table_last_internal_key) {
|
||||
const ParsedInternalKey& prev_iter_output_internal_key) {
|
||||
Status s;
|
||||
bool is_range_del = c_iter.IsDeleteRangeSentinelKey();
|
||||
if (is_range_del && compaction_->bottommost_level()) {
|
||||
@@ -371,8 +375,8 @@ Status CompactionOutputs::AddToOutput(
|
||||
}
|
||||
const Slice& key = c_iter.key();
|
||||
if (ShouldStopBefore(c_iter) && HasBuilder()) {
|
||||
s = close_file_func(c_iter.InputStatus(), prev_table_last_internal_key, key,
|
||||
&c_iter, *this);
|
||||
s = close_file_func(c_iter.InputStatus(), prev_iter_output_internal_key,
|
||||
key, &c_iter, *this);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ class CompactionOutputs {
|
||||
Status AddToOutput(const CompactionIterator& c_iter,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func,
|
||||
const ParsedInternalKey& prev_table_last_internal_key);
|
||||
const ParsedInternalKey& prev_iter_output_internal_key);
|
||||
|
||||
// Close the current output. `open_file_func` is needed for creating new file
|
||||
// for range-dels only output file.
|
||||
|
||||
@@ -242,7 +242,7 @@ bool CompactionPicker::ExpandInputsToCleanCut(const std::string& /*cf_name*/,
|
||||
GetRange(*inputs, &smallest, &largest);
|
||||
inputs->clear();
|
||||
vstorage->GetOverlappingInputs(level, &smallest, &largest, &inputs->files,
|
||||
hint_index, &hint_index, true,
|
||||
hint_index, &hint_index, true, nullptr,
|
||||
next_smallest);
|
||||
} while (inputs->size() > old_size);
|
||||
|
||||
@@ -465,7 +465,8 @@ bool CompactionPicker::SetupOtherInputs(
|
||||
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
|
||||
VersionStorageInfo* vstorage, CompactionInputFiles* inputs,
|
||||
CompactionInputFiles* output_level_inputs, int* parent_index,
|
||||
int base_index, bool only_expand_towards_right) {
|
||||
int base_index, bool only_expand_towards_right,
|
||||
const FileMetaData* starting_l0_file) {
|
||||
assert(!inputs->empty());
|
||||
assert(output_level_inputs->empty());
|
||||
const int input_level = inputs->level;
|
||||
@@ -521,11 +522,11 @@ bool CompactionPicker::SetupOtherInputs(
|
||||
// Round-robin compaction only allows expansion towards the larger side.
|
||||
vstorage->GetOverlappingInputs(input_level, &smallest, &all_limit,
|
||||
&expanded_inputs.files, base_index,
|
||||
nullptr);
|
||||
nullptr, true, starting_l0_file);
|
||||
} else {
|
||||
vstorage->GetOverlappingInputs(input_level, &all_start, &all_limit,
|
||||
&expanded_inputs.files, base_index,
|
||||
nullptr);
|
||||
nullptr, true, starting_l0_file);
|
||||
}
|
||||
uint64_t expanded_inputs_size = TotalFileSize(expanded_inputs.files);
|
||||
if (!ExpandInputsToCleanCut(cf_name, vstorage, &expanded_inputs)) {
|
||||
@@ -1231,7 +1232,7 @@ void CompactionPicker::PickFilesMarkedForCompaction(
|
||||
|
||||
bool CompactionPicker::GetOverlappingL0Files(
|
||||
VersionStorageInfo* vstorage, CompactionInputFiles* start_level_inputs,
|
||||
int output_level, int* parent_index) {
|
||||
int output_level, int* parent_index, const FileMetaData* starting_l0_file) {
|
||||
// Two level 0 compaction won't run at the same time, so don't need to worry
|
||||
// about files on level 0 being compacted.
|
||||
assert(level0_compactions_in_progress()->empty());
|
||||
@@ -1242,7 +1243,11 @@ bool CompactionPicker::GetOverlappingL0Files(
|
||||
// which will include the picked file.
|
||||
start_level_inputs->files.clear();
|
||||
vstorage->GetOverlappingInputs(0, &smallest, &largest,
|
||||
&(start_level_inputs->files));
|
||||
&(start_level_inputs->files),
|
||||
/*hint_index=*/-1,
|
||||
/*file_index=*/nullptr,
|
||||
/*expand_range=*/true,
|
||||
/*starting_l0_file=*/starting_l0_file);
|
||||
|
||||
// If we include more L0 files in the same compaction run it can
|
||||
// cause the 'smallest' and 'largest' key to get extended to a
|
||||
|
||||
@@ -203,13 +203,16 @@ class CompactionPicker {
|
||||
const std::vector<CompactionInputFiles>& inputs, int level,
|
||||
int proximal_level) const;
|
||||
|
||||
// @param starting_l0_file If not null, restricts L0 file selection to only
|
||||
// include files at or older than starting_l0_file.
|
||||
bool SetupOtherInputs(const std::string& cf_name,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
VersionStorageInfo* vstorage,
|
||||
CompactionInputFiles* inputs,
|
||||
CompactionInputFiles* output_level_inputs,
|
||||
int* parent_index, int base_index,
|
||||
bool only_expand_towards_right = false);
|
||||
bool only_expand_towards_right = false,
|
||||
const FileMetaData* starting_l0_file = nullptr);
|
||||
|
||||
void GetGrandparents(VersionStorageInfo* vstorage,
|
||||
const CompactionInputFiles& inputs,
|
||||
@@ -222,9 +225,12 @@ class CompactionPicker {
|
||||
CompactionInputFiles* start_level_inputs,
|
||||
std::function<bool(const FileMetaData*)> skip_marked_file);
|
||||
|
||||
// @param starting_l0_file If not null, restricts L0 file selection to only
|
||||
// include files at or older than starting_l0_file.
|
||||
bool GetOverlappingL0Files(VersionStorageInfo* vstorage,
|
||||
CompactionInputFiles* start_level_inputs,
|
||||
int output_level, int* parent_index);
|
||||
int output_level, int* parent_index,
|
||||
const FileMetaData* starting_l0_file = nullptr);
|
||||
|
||||
// Register this compaction in the set of running compactions
|
||||
void RegisterCompaction(Compaction* c);
|
||||
|
||||
@@ -4716,6 +4716,53 @@ TEST_F(CompactionPickerTest, UniversalMaxReadAmpSmallDB) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, StandaloneRangeDeletionOnlyPicksOlderFiles) {
|
||||
NewVersionStorage(6, kCompactionStyleUniversal);
|
||||
|
||||
// Create L0 files with overlapping ranges
|
||||
// File 1: newest regular file (epoch 5), keys [100, 200]
|
||||
Add(0, 1U, "100", "200", 1U, 0, 100, 100, 0, false, Temperature::kUnknown,
|
||||
kUnknownOldestAncesterTime, kUnknownNewestKeyTime, Slice(), Slice(), 5);
|
||||
|
||||
// File 2: standalone range deletion (epoch 4), keys [150, 250]
|
||||
// This file should be marked as having only range deletions
|
||||
Add(0, 2U, "150", "250", 1U, 0, 200, 200, 0, true, Temperature::kUnknown,
|
||||
kUnknownOldestAncesterTime, kUnknownNewestKeyTime, Slice(), Slice(), 4);
|
||||
|
||||
// Manually set file 2 as standalone range deletion
|
||||
FileMetaData* range_del_file = file_map_[2U].first;
|
||||
range_del_file->num_entries = 1;
|
||||
range_del_file->num_range_deletions = 1;
|
||||
ASSERT_TRUE(range_del_file->FileIsStandAloneRangeTombstone());
|
||||
|
||||
Add(4, 10U, "000", "400", 1U);
|
||||
Add(5, 20U, "000", "400", 100);
|
||||
|
||||
UpdateVersionStorageInfo();
|
||||
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
|
||||
ASSERT_TRUE(universal_compaction_picker.NeedsCompaction(vstorage_.get()));
|
||||
|
||||
std::unique_ptr<Compaction> compaction(
|
||||
universal_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_,
|
||||
/*existing_snapshots=*/{}, /* snapshot_checker */ nullptr,
|
||||
vstorage_.get(), &log_buffer_));
|
||||
|
||||
ASSERT_NE(nullptr, compaction);
|
||||
ASSERT_EQ(2U, compaction->num_input_levels());
|
||||
// First input level should be L0 with only the standalone range del file
|
||||
// (file 2)
|
||||
ASSERT_EQ(0, compaction->level(0));
|
||||
ASSERT_EQ(1U, compaction->num_input_files(0));
|
||||
ASSERT_EQ(2U, compaction->input(0, 0)->fd.GetNumber());
|
||||
ASSERT_TRUE(compaction->input(0, 0)->FileIsStandAloneRangeTombstone());
|
||||
|
||||
// Second input level should be L4 with file 10
|
||||
ASSERT_EQ(4, compaction->level(1));
|
||||
ASSERT_EQ(1U, compaction->num_input_files(1));
|
||||
ASSERT_EQ(10U, compaction->input(1, 0)->fd.GetNumber());
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -1544,9 +1544,18 @@ Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
|
||||
}
|
||||
|
||||
if (output_level != 0) {
|
||||
// For standalone range deletion, we don't want to compact it with newer
|
||||
// L0 files that it doesn't cover.
|
||||
const FileMetaData* starting_l0_file =
|
||||
(start_level == 0 && start_level_inputs.size() == 1 &&
|
||||
start_level_inputs.files[0]->FileIsStandAloneRangeTombstone())
|
||||
? start_level_inputs.files[0]
|
||||
: nullptr;
|
||||
|
||||
if (start_level == 0) {
|
||||
if (!picker_->GetOverlappingL0Files(vstorage_, &start_level_inputs,
|
||||
output_level, nullptr)) {
|
||||
output_level, nullptr,
|
||||
starting_l0_file)) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
@@ -1557,7 +1566,8 @@ Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
|
||||
output_level_inputs.level = output_level;
|
||||
if (!picker_->SetupOtherInputs(cf_name_, mutable_cf_options_, vstorage_,
|
||||
&start_level_inputs, &output_level_inputs,
|
||||
&parent_index, -1)) {
|
||||
&parent_index, -1, false,
|
||||
starting_l0_file)) {
|
||||
return nullptr;
|
||||
}
|
||||
inputs.push_back(start_level_inputs);
|
||||
|
||||
@@ -109,12 +109,12 @@ Status SubcompactionState::AddToOutput(
|
||||
const CompactionIterator& iter, bool use_proximal_output,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func,
|
||||
const ParsedInternalKey& prev_table_last_internal_key) {
|
||||
const ParsedInternalKey& prev_iter_output_internal_key) {
|
||||
// update target output
|
||||
current_outputs_ =
|
||||
use_proximal_output ? &proximal_level_outputs_ : &compaction_outputs_;
|
||||
return current_outputs_->AddToOutput(iter, open_file_func, close_file_func,
|
||||
prev_table_last_internal_key);
|
||||
prev_iter_output_internal_key);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -221,7 +221,7 @@ class SubcompactionState {
|
||||
Status AddToOutput(const CompactionIterator& iter, bool use_proximal_output,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func,
|
||||
const ParsedInternalKey& prev_table_last_internal_key);
|
||||
const ParsedInternalKey& prev_iter_output_internal_key);
|
||||
|
||||
// Close all compaction output files, both output_to_proximal_level outputs
|
||||
// and normal outputs.
|
||||
|
||||
@@ -3868,6 +3868,75 @@ TEST_F(DBBasicTest, SkipWALIfMissingTableFiles) {
|
||||
ASSERT_OK(iter->status());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, BestEffortRecoveryFailureWithTableCacheUseAfterFree) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.env = env_;
|
||||
// Force multiple manifest files
|
||||
options.max_manifest_file_size = 1;
|
||||
options.max_manifest_space_amp_pct = 0;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Disable file deletions to preserve old manifest files for
|
||||
// best-efforts recovery to succeed
|
||||
ASSERT_OK(db_->DisableFileDeletions());
|
||||
|
||||
// Create multiple SST files to populate TableCache during
|
||||
// best-efforts recovery
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ASSERT_OK(Put("key" + std::to_string(i),
|
||||
std::string(1000, static_cast<char>('a' + i))));
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
// Verify we have multiple manifest files
|
||||
std::vector<std::string> files;
|
||||
ASSERT_OK(env_->GetChildren(dbname_, &files));
|
||||
int manifest_count = 0;
|
||||
for (const auto& file : files) {
|
||||
if (file.find("MANIFEST") != std::string::npos) {
|
||||
manifest_count++;
|
||||
}
|
||||
}
|
||||
ASSERT_GE(manifest_count, 2);
|
||||
|
||||
// Inject corruption after TableCache is populated (count > 3), but only once
|
||||
// (injected flag) to allow best-effort recovery to trigger retry and succeed.
|
||||
// This coerce the bug: first recovery caches SSTs with reference to column
|
||||
// family's options in table cache and retry deletes column family so the
|
||||
// reference becomes dangling.
|
||||
int count = 0;
|
||||
bool injected = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionBuilder::CheckConsistencyBeforeReturn", [&](void* arg) {
|
||||
count++;
|
||||
if (count > 3 && !injected) {
|
||||
ASSERT_NE(nullptr, arg);
|
||||
*(static_cast<Status*>(arg)) =
|
||||
Status::Corruption("Injected corruption");
|
||||
injected = true;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
options.best_efforts_recovery = true;
|
||||
|
||||
Status s = TryReopen(options);
|
||||
ASSERT_OK(s);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
std::string value;
|
||||
// Without the fix, ASAN detects use-after-free when accessing cached SST
|
||||
// files that hold dangling references to deleted ioptions.
|
||||
s = db_->Get(ReadOptions(), "key" + std::to_string(i), &value);
|
||||
ASSERT_TRUE(s.ok() || s.IsNotFound());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, DisableTrackWal) {
|
||||
// If WAL tracking was enabled, and then disabled during reopen,
|
||||
// the previously tracked WALs should be removed from MANIFEST.
|
||||
|
||||
@@ -710,12 +710,20 @@ class AlwaysTrueBitsBuilder : public FilterBitsBuilder {
|
||||
count_ = 0;
|
||||
// Interpreted as "always true" filter (0 probes over 1 byte of
|
||||
// payload, 5 bytes metadata)
|
||||
return Slice("\0\0\0\0\0\0", 6);
|
||||
return Slice("\0\0\0\0\0\0", kAlwaysTrueFilterBytes);
|
||||
}
|
||||
using FilterBitsBuilder::Finish;
|
||||
size_t ApproximateNumEntries(size_t) override { return SIZE_MAX; }
|
||||
size_t CalculateSpace(size_t /* num_entries */) override {
|
||||
return kAlwaysTrueFilterBytes;
|
||||
}
|
||||
double EstimatedFpRate(size_t /* num_entries */,
|
||||
size_t /* bytes */) override {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr size_t kAlwaysTrueFilterBytes = 6;
|
||||
size_t count_ = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -11541,6 +11541,71 @@ TEST_F(DBCompactionTest, RecordNewestKeyTimeForTtlCompaction) {
|
||||
ASSERT_EQ(NumTableFilesAtLevel(0), 0);
|
||||
}
|
||||
|
||||
// Test verifies compaction file cutting logic when using tail size estimation
|
||||
// maintains output files at or below the target file size.
|
||||
TEST_F(DBCompactionTest, CompactionRespectsTargetSizeWithTailEstimation) {
|
||||
const int kInitialKeyCount = 10000; // 10k keys
|
||||
const int kValueSize = 100; // 100 bytes per key
|
||||
const int kSeed = 301;
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.target_file_size_is_upper_bound = true;
|
||||
options.target_file_size_base = 256 * 1024;
|
||||
options.write_buffer_size = 2 * 1024 * 1024;
|
||||
options.level0_file_num_compaction_trigger = 100; // Never trigger L0->L1
|
||||
options.compression = kNoCompression;
|
||||
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.partition_filters = true;
|
||||
table_options.metadata_block_size = 4 * 1024;
|
||||
table_options.index_type = BlockBasedTableOptions::kBinarySearch;
|
||||
table_options.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Generate 2 L0 files
|
||||
// Generate first file with 10k keys (each ~100 bytes) approx 1.2MB total
|
||||
Random rnd(kSeed);
|
||||
for (int i = 0; i < kInitialKeyCount; i++) {
|
||||
ASSERT_OK(Put(Key(i), rnd.RandomString(kValueSize)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Generate second file with overlapping keys to force compaction (prevent
|
||||
// trivial move)
|
||||
for (int i = kInitialKeyCount / 2; i < kInitialKeyCount * 1.5; i++) {
|
||||
ASSERT_OK(Put(Key(i), rnd.RandomString(kValueSize)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Capture file metadata and assert two L0 files
|
||||
std::vector<LiveFileMetaData> file_metadata;
|
||||
db_->GetLiveFilesMetaData(&file_metadata);
|
||||
ASSERT_EQ(file_metadata.size(), 2);
|
||||
for (const auto& file : file_metadata) {
|
||||
ASSERT_EQ(file.level, 0);
|
||||
};
|
||||
|
||||
// Manually compact LO files to L1
|
||||
CompactRangeOptions cro;
|
||||
cro.change_level = true;
|
||||
cro.target_level = 1;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
// Verify that compacted output files are under target file size
|
||||
for (const auto& file : file_metadata) {
|
||||
if (file.level > 0) {
|
||||
EXPECT_LE(file.size, options.target_file_size_base)
|
||||
<< "Output file size exceeds target size: " << " File: " << file.name
|
||||
<< " level: " << file.level << " File size: " << file.size
|
||||
<< " Target size: " << options.target_file_size_base;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PeriodicCompactionListener : public EventListener {
|
||||
public:
|
||||
explicit PeriodicCompactionListener() {}
|
||||
|
||||
@@ -109,6 +109,7 @@ Status DBImpl::GetSortedWalFilesImpl(VectorWalPtr& files, bool need_seqnos) {
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
while (pending_purge_obsolete_files_ > 0 || bg_purge_scheduled_ > 0) {
|
||||
TEST_SYNC_POINT("DBImpl::GetSortedWalFilesImpl:WaitPurge");
|
||||
bg_cv_.Wait();
|
||||
}
|
||||
|
||||
|
||||
@@ -5519,7 +5519,7 @@ Status DBImpl::RenameTempFileToOptionsFile(const std::string& file_name,
|
||||
return s;
|
||||
}
|
||||
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
#ifndef NROCKSDB_THREAD_STATUS
|
||||
|
||||
void DBImpl::NewThreadStatusCfInfo(ColumnFamilyData* cfd) const {
|
||||
if (immutable_db_options_.enable_thread_tracking) {
|
||||
@@ -5546,7 +5546,7 @@ void DBImpl::NewThreadStatusCfInfo(ColumnFamilyData* /*cfd*/) const {}
|
||||
void DBImpl::EraseThreadStatusCfInfo(ColumnFamilyData* /*cfd*/) const {}
|
||||
|
||||
void DBImpl::EraseThreadStatusDbInfo() const {}
|
||||
#endif // ROCKSDB_USING_THREAD_STATUS
|
||||
#endif // !NROCKSDB_THREAD_STATUS
|
||||
|
||||
//
|
||||
// A global method that can dump out the build version
|
||||
|
||||
@@ -267,6 +267,9 @@ void DBImpl::FindObsoleteFiles(JobContext* job_context, bool force,
|
||||
if (!job_context->HaveSomethingToDelete()) {
|
||||
mutex_.AssertHeld();
|
||||
--pending_purge_obsolete_files_;
|
||||
if (pending_purge_obsolete_files_ == 0) {
|
||||
bg_cv_.SignalAll();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1102,11 +1102,6 @@ Status DBImplSecondary::InitializeCompactionWorkspace(
|
||||
return s;
|
||||
}
|
||||
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"Initialized compaction workspace with %zu subcompaction "
|
||||
"progress to resume",
|
||||
compaction_progress_.size());
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -1219,6 +1214,11 @@ Status DBImplSecondary::PrepareCompactionProgressState() {
|
||||
return HandleInvalidOrNoCompactionProgress(compaction_progress_file_path,
|
||||
scan_result);
|
||||
}
|
||||
|
||||
ROCKS_LOG_DEBUG(
|
||||
immutable_db_options_.info_log,
|
||||
"Loaded compaction progress with %zu subcompaction(s) from %s",
|
||||
compaction_progress_.size(), compaction_progress_file_path.c_str());
|
||||
return s;
|
||||
} else {
|
||||
return HandleInvalidOrNoCompactionProgress(
|
||||
@@ -1299,24 +1299,33 @@ Status DBImplSecondary::CompactWithoutInstallation(
|
||||
}
|
||||
Status s;
|
||||
|
||||
// TODO(hx235): Resuming compaction is currently incompatible with
|
||||
// paranoid_file_checks=true because OutputValidator hash verification would
|
||||
// fail during compaction resumption. Before interruption, resuming
|
||||
// compaction needs to persist the hash of each output file to enable
|
||||
// validation after resumption. Alternatively and preferably, we could move
|
||||
// the output verification to happen immediately after each output file is
|
||||
// created. This workaround currently disables resuming compaction when
|
||||
// paranoid_file_checks is enabled. Note that paranoid_file_checks is
|
||||
// disabled by default.
|
||||
bool allow_resumption =
|
||||
options.allow_resumption &&
|
||||
!cfd->GetLatestMutableCFOptions().paranoid_file_checks;
|
||||
const auto& mutable_cf_options = cfd->GetLatestMutableCFOptions();
|
||||
|
||||
if (options.allow_resumption &&
|
||||
cfd->GetLatestMutableCFOptions().paranoid_file_checks) {
|
||||
// TODO(hx235): Resuming compaction is currently incompatible with
|
||||
// output hash verification (enabled via paranoid_file_checks=true or
|
||||
// verify_output_flags containing kVerifyIteration) because resumed compaction
|
||||
// will lose the hash computed before interruption.
|
||||
// Potential solutions:
|
||||
// 1. Persist the hash state: Before interruption, save the current hash value
|
||||
// of each output file to disk, allowing validation to continue correctly
|
||||
// after resumption.
|
||||
// 2. Immediate verification: Move output verification to happen
|
||||
// immediately after each output file is created and closed, eliminating
|
||||
// the need to maintain hash state across resumption boundaries.
|
||||
bool output_hash_verification_enabled =
|
||||
mutable_cf_options.paranoid_file_checks ||
|
||||
!!(mutable_cf_options.verify_output_flags &
|
||||
VerifyOutputFlags::kVerifyIteration);
|
||||
|
||||
bool allow_resumption =
|
||||
options.allow_resumption && !output_hash_verification_enabled;
|
||||
|
||||
if (options.allow_resumption && output_hash_verification_enabled) {
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"Resume compaction configured but disabled due to "
|
||||
"incompatible with paranoid_file_checks=true");
|
||||
"incompatibility with output hash verification "
|
||||
"(paranoid_file_checks=true or verify_output_flags "
|
||||
"containing kVerifyIteration)");
|
||||
}
|
||||
|
||||
mutex_.Unlock();
|
||||
@@ -1345,8 +1354,8 @@ Status DBImplSecondary::CompactWithoutInstallation(
|
||||
CompactionOptions comp_options;
|
||||
comp_options.compression = kDisableCompressionOption;
|
||||
comp_options.output_file_size_limit = MaxFileSizeForLevel(
|
||||
cfd->GetLatestMutableCFOptions(), input.output_level,
|
||||
cfd->ioptions().compaction_style, vstorage->base_level(),
|
||||
mutable_cf_options, input.output_level, cfd->ioptions().compaction_style,
|
||||
vstorage->base_level(),
|
||||
cfd->ioptions().level_compaction_dynamic_level_bytes);
|
||||
|
||||
std::vector<CompactionInputFiles> input_files;
|
||||
@@ -1384,8 +1393,8 @@ Status DBImplSecondary::CompactWithoutInstallation(
|
||||
}
|
||||
c.reset(cfd->compaction_picker()->PickCompactionForCompactFiles(
|
||||
comp_options, input_files, input.output_level, vstorage,
|
||||
cfd->GetLatestMutableCFOptions(), mutable_db_options_, 0,
|
||||
earliest_snapshot, job_context.snapshot_checker));
|
||||
mutable_cf_options, mutable_db_options_, 0, earliest_snapshot,
|
||||
job_context.snapshot_checker));
|
||||
assert(c != nullptr);
|
||||
c->FinalizeInputInfo(version);
|
||||
|
||||
@@ -1731,6 +1740,11 @@ Status DBImplSecondary::FinalizeCompactionProgressWriter(
|
||||
return HandleCompactionProgressWriterCreationFailure(
|
||||
"" /* temp_file_path */, final_file_path, compaction_progress_writer);
|
||||
}
|
||||
|
||||
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
|
||||
"Finalized compaction progress writer onto %s",
|
||||
final_file_path.c_str());
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -4154,6 +4154,9 @@ INSTANTIATE_TEST_CASE_P(DBMultiScanIteratorTest, DBMultiScanIteratorTest,
|
||||
::testing::Bool());
|
||||
|
||||
TEST_P(DBMultiScanIteratorTest, BasicTest) {
|
||||
auto options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Create a file
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
std::stringstream ss;
|
||||
@@ -4196,6 +4199,8 @@ TEST_P(DBMultiScanIteratorTest, BasicTest) {
|
||||
}
|
||||
|
||||
TEST_P(DBMultiScanIteratorTest, MixedBoundsTest) {
|
||||
auto options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
// Create a file
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
std::stringstream ss;
|
||||
@@ -4602,6 +4607,11 @@ TEST_P(DBMultiScanIteratorTest, FragmentedRangeTombstones) {
|
||||
ASSERT_OK(s);
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
ColumnFamilyMetaData cf_meta;
|
||||
dbfull()->GetColumnFamilyMetaData(cfh, &cf_meta);
|
||||
// Only the L0 with range deletion is compacted.
|
||||
ASSERT_EQ(1, cf_meta.levels[0].files.size());
|
||||
ASSERT_EQ(0, cf_meta.levels[0].files[0].num_deletions);
|
||||
|
||||
// The first scan range overlaps the DB key range, while the second extends
|
||||
// beyond but overlaps the delete range
|
||||
@@ -4666,6 +4676,357 @@ TEST_P(DBMultiScanIteratorTest, FragmentedRangeTombstones) {
|
||||
iter.reset();
|
||||
}
|
||||
|
||||
TEST_P(DBMultiScanIteratorTest, ReseekAcrossBlocksSameUserKey) {
|
||||
// This test exposes a bug where multiscan reseeks backwards when
|
||||
// max_sequential_skip_in_iterations is triggered with the same user key
|
||||
// spanning multiple data blocks.
|
||||
|
||||
auto options = CurrentOptions();
|
||||
options.max_sequential_skip_in_iterations = 3;
|
||||
options.compression = kNoCompression;
|
||||
|
||||
// Force each internal key into its own block
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.flush_block_policy_factory =
|
||||
std::make_shared<FlushBlockEveryKeyPolicyFactory>();
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Taking a snapshot after each Put to preserve all versions during flush.
|
||||
std::vector<const Snapshot*> snapshots;
|
||||
for (int i = 0; i < 7; ++i) {
|
||||
ASSERT_OK(Put("key_a", "value_" + std::to_string(i)));
|
||||
snapshots.push_back(db_->GetSnapshot());
|
||||
}
|
||||
ASSERT_OK(Put("key_b", "value_b"));
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
|
||||
// Setup multiscan range covering both keys
|
||||
std::vector<std::string> key_ranges({"key_a", "key_c"});
|
||||
ReadOptions ro;
|
||||
Slice ub = key_ranges[1];
|
||||
ro.iterate_upper_bound = &ub;
|
||||
ro.fill_cache = GetParam();
|
||||
|
||||
MultiScanArgs scan_options(BytewiseComparator());
|
||||
scan_options.insert(key_ranges[0], key_ranges[1]);
|
||||
|
||||
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
|
||||
std::unique_ptr<Iterator> iter(dbfull()->NewIterator(ro, cfh));
|
||||
ASSERT_NE(iter, nullptr);
|
||||
iter->Prepare(scan_options);
|
||||
|
||||
std::vector<std::string> seen_keys;
|
||||
std::vector<std::string> seen_values;
|
||||
iter->Seek(key_ranges[0]);
|
||||
while (iter->status().ok() && iter->Valid()) {
|
||||
seen_keys.push_back(iter->key().ToString());
|
||||
seen_values.push_back(iter->value().ToString());
|
||||
iter->Next();
|
||||
}
|
||||
ASSERT_OK(iter->status()) << iter->status().ToString();
|
||||
|
||||
ASSERT_EQ(seen_keys.size(), 2) << "Should see key_a and key_b";
|
||||
ASSERT_EQ(seen_keys[0], "key_a");
|
||||
ASSERT_EQ(seen_keys[1], "key_b");
|
||||
ASSERT_EQ(seen_values[0], "value_6");
|
||||
ASSERT_EQ(seen_values[1], "value_b");
|
||||
|
||||
for (auto* snapshot : snapshots) {
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DBMultiScanIteratorTest, AsyncPrefetchAcrossMultipleFiles) {
|
||||
// Test async prefetch with multiple ranges within a single file
|
||||
auto options = CurrentOptions();
|
||||
options.target_file_size_base = 1 << 15; // 32KiB
|
||||
options.compaction_style = kCompactionStyleUniversal;
|
||||
options.num_levels = 50;
|
||||
options.compression = kNoCompression;
|
||||
options.statistics = CreateDBStatistics();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
Random rnd(303);
|
||||
|
||||
// Create a single large file with many keys
|
||||
// ~1MiB of data
|
||||
// Should be lots of files now
|
||||
for (int i = 0; i < 1000; ++i) {
|
||||
std::stringstream ss;
|
||||
ss << "k" << std::setw(5) << std::setfill('0') << i;
|
||||
// 1KiB values
|
||||
ASSERT_OK(Put(ss.str(), rnd.RandomString(1 << 10)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(db_->CompactRange({}, nullptr, nullptr));
|
||||
|
||||
ASSERT_GT(NumTableFilesAtLevel(49), 3);
|
||||
|
||||
// Set up multiple non-overlapping ranges in the same file
|
||||
// Every 32 values should be a file or so
|
||||
std::vector<std::string> key_ranges(
|
||||
{"k00000", "k00100", "k00500", "k00600", "k00800", "k00900"});
|
||||
ReadOptions ro;
|
||||
ro.fill_cache = GetParam();
|
||||
|
||||
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
|
||||
|
||||
MultiScanArgs scan_options(BytewiseComparator());
|
||||
scan_options.use_async_io = true;
|
||||
scan_options.insert(key_ranges[0], key_ranges[1]);
|
||||
scan_options.insert(key_ranges[2], key_ranges[3]);
|
||||
scan_options.insert(key_ranges[4], key_ranges[5]);
|
||||
|
||||
auto read_count_before =
|
||||
options.statistics->getTickerCount(NON_LAST_LEVEL_READ_COUNT);
|
||||
std::unique_ptr<MultiScan> iter =
|
||||
dbfull()->NewMultiScan(ro, cfh, scan_options);
|
||||
ASSERT_NE(iter, nullptr);
|
||||
auto read_count_after =
|
||||
options.statistics->getTickerCount(NON_LAST_LEVEL_READ_COUNT);
|
||||
ASSERT_EQ(read_count_after, read_count_before);
|
||||
|
||||
// Verify all three ranges can be scanned successfully
|
||||
try {
|
||||
for (auto range : *iter) {
|
||||
for (auto it : range) {
|
||||
it.first.ToString();
|
||||
}
|
||||
}
|
||||
} catch (MultiScanException& ex) {
|
||||
// Make sure exception contains the status
|
||||
ASSERT_NOK(ex.status());
|
||||
std::cerr << "Iterator returned status " << ex.what();
|
||||
abort();
|
||||
} catch (std::logic_error& ex) {
|
||||
std::cerr << "Iterator returned logic error " << ex.what();
|
||||
abort();
|
||||
}
|
||||
|
||||
iter.reset();
|
||||
}
|
||||
|
||||
TEST_P(DBMultiScanIteratorTest, AsyncPrefetchMultipleLevels) {
|
||||
// Test async prefetch with files in L0 and non-L0 levels
|
||||
// Similar setup to AsyncPrefetchAcrossMultipleFiles but with L0 files
|
||||
auto options = CurrentOptions();
|
||||
options.target_file_size_base = 1 << 15; // 32KiB
|
||||
options.compaction_style = kCompactionStyleUniversal;
|
||||
options.num_levels = 50;
|
||||
options.compression = kNoCompression;
|
||||
options.statistics = CreateDBStatistics();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
Random rnd(304);
|
||||
|
||||
// Create base files and compact to bottom level - ~500KiB of data
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
std::stringstream ss;
|
||||
ss << "k" << std::setw(5) << std::setfill('0') << i;
|
||||
ASSERT_OK(Put(ss.str(), rnd.RandomString(1 << 10)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange({}, nullptr, nullptr));
|
||||
|
||||
// Verify we have files at bottom level
|
||||
ASSERT_GT(NumTableFilesAtLevel(49), 0);
|
||||
|
||||
// Create additional L0 files with overlapping key ranges
|
||||
for (int i = 100; i < 150; ++i) {
|
||||
std::stringstream ss;
|
||||
ss << "k" << std::setw(5) << std::setfill('0') << i;
|
||||
ASSERT_OK(Put(ss.str(), rnd.RandomString(1 << 10)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Verify we now have files in both L0 and bottom level
|
||||
ASSERT_GT(NumTableFilesAtLevel(0), 0);
|
||||
ASSERT_GT(NumTableFilesAtLevel(49), 0);
|
||||
|
||||
// Set up multiple non-overlapping ranges
|
||||
std::vector<std::string> key_ranges(
|
||||
{"k00000", "k00100", "k00200", "k00300", "k00400", "k00500"});
|
||||
ReadOptions ro;
|
||||
ro.fill_cache = GetParam();
|
||||
|
||||
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
|
||||
|
||||
MultiScanArgs scan_options(BytewiseComparator());
|
||||
scan_options.use_async_io = true;
|
||||
scan_options.insert(key_ranges[0], key_ranges[1]);
|
||||
scan_options.insert(key_ranges[2], key_ranges[3]);
|
||||
scan_options.insert(key_ranges[4], key_ranges[5]);
|
||||
|
||||
std::unique_ptr<MultiScan> iter =
|
||||
dbfull()->NewMultiScan(ro, cfh, scan_options);
|
||||
ASSERT_NE(iter, nullptr);
|
||||
|
||||
// Verify all three ranges can be scanned successfully
|
||||
int total_keys = 0;
|
||||
try {
|
||||
for (auto range : *iter) {
|
||||
for (auto it : range) {
|
||||
it.first.ToString();
|
||||
total_keys++;
|
||||
}
|
||||
}
|
||||
} catch (MultiScanException& ex) {
|
||||
ASSERT_NOK(ex.status());
|
||||
std::cerr << "Iterator returned status " << ex.what();
|
||||
abort();
|
||||
} catch (std::logic_error& ex) {
|
||||
std::cerr << "Iterator returned logic error " << ex.what();
|
||||
abort();
|
||||
}
|
||||
|
||||
// Should have keys from all three ranges
|
||||
ASSERT_GT(total_keys, 0);
|
||||
iter.reset();
|
||||
}
|
||||
|
||||
TEST_P(DBMultiScanIteratorTest, AsyncPrefetchWithDeleteRange) {
|
||||
// Test async prefetch with delete ranges
|
||||
auto options = CurrentOptions();
|
||||
options.target_file_size_base = 1 << 15; // 32KiB
|
||||
options.compaction_style = kCompactionStyleUniversal;
|
||||
options.num_levels = 50;
|
||||
options.compression = kNoCompression;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
Random rnd(305);
|
||||
|
||||
// Create base data - ~500KiB
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
std::stringstream ss;
|
||||
ss << "k" << std::setw(5) << std::setfill('0') << i;
|
||||
ASSERT_OK(Put(ss.str(), rnd.RandomString(1 << 10)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Add delete ranges
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), dbfull()->DefaultColumnFamily(),
|
||||
"k00100", "k00200"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(db_->CompactRange({}, nullptr, nullptr));
|
||||
ASSERT_GT(NumTableFilesAtLevel(49), 0);
|
||||
|
||||
// Set up scan ranges that interact with delete ranges
|
||||
std::vector<std::string> key_ranges({"k00000", "k00500"});
|
||||
ReadOptions ro;
|
||||
ro.fill_cache = GetParam();
|
||||
|
||||
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
|
||||
|
||||
MultiScanArgs scan_options(BytewiseComparator());
|
||||
scan_options.use_async_io = true;
|
||||
scan_options.insert(key_ranges[0], key_ranges[1]);
|
||||
|
||||
std::unique_ptr<MultiScan> iter =
|
||||
dbfull()->NewMultiScan(ro, cfh, scan_options);
|
||||
ASSERT_NE(iter, nullptr);
|
||||
|
||||
// Verify ranges can be scanned successfully
|
||||
int total_keys = 0;
|
||||
try {
|
||||
for (auto range : *iter) {
|
||||
for (auto it : range) {
|
||||
std::string key = it.first.ToString();
|
||||
// Verify deleted keys are not returned
|
||||
ASSERT_TRUE((key < "k00100" || key >= "k00200"));
|
||||
total_keys++;
|
||||
}
|
||||
}
|
||||
} catch (MultiScanException& ex) {
|
||||
ASSERT_NOK(ex.status());
|
||||
std::cerr << "Iterator returned status " << ex.what();
|
||||
abort();
|
||||
} catch (std::logic_error& ex) {
|
||||
std::cerr << "Iterator returned logic error " << ex.what();
|
||||
abort();
|
||||
}
|
||||
|
||||
// Should have keys excluding deleted ranges
|
||||
ASSERT_EQ(total_keys, 400);
|
||||
iter.reset();
|
||||
}
|
||||
|
||||
TEST_P(DBMultiScanIteratorTest, AsyncPrefetchWithExternalFileIngestion) {
|
||||
// Test async prefetch with externally ingested files
|
||||
auto options = CurrentOptions();
|
||||
options.target_file_size_base = 1 << 15; // 32KiB
|
||||
options.compaction_style = kCompactionStyleUniversal;
|
||||
options.num_levels = 50;
|
||||
options.compression = kNoCompression;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
Random rnd(306);
|
||||
|
||||
// Create base data - ~200KiB
|
||||
for (int i = 0; i < 200; ++i) {
|
||||
std::stringstream ss;
|
||||
ss << "k" << std::setw(5) << std::setfill('0') << i;
|
||||
ASSERT_OK(Put(ss.str(), rnd.RandomString(1 << 10)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange({}, nullptr, nullptr));
|
||||
|
||||
// Create and ingest external SST file with new data
|
||||
std::string ingest_file = dbname_ + "/test_ingest.sst";
|
||||
{
|
||||
std::unique_ptr<SstFileWriter> writer;
|
||||
writer.reset(new SstFileWriter(EnvOptions(), options));
|
||||
ASSERT_OK(writer->Open(ingest_file));
|
||||
for (int i = 300; i < 500; ++i) {
|
||||
std::stringstream ss;
|
||||
ss << "k" << std::setw(5) << std::setfill('0') << i;
|
||||
ASSERT_OK(writer->Put(ss.str(), rnd.RandomString(1 << 10)));
|
||||
}
|
||||
ASSERT_OK(writer->Finish());
|
||||
}
|
||||
|
||||
IngestExternalFileOptions ifo;
|
||||
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
|
||||
ASSERT_OK(dbfull()->IngestExternalFile(cfh, {ingest_file}, ifo));
|
||||
|
||||
// Set up scan ranges that span both regular and ingested files
|
||||
std::vector<std::string> key_ranges({"k00000", "k00500"});
|
||||
ReadOptions ro;
|
||||
ro.fill_cache = GetParam();
|
||||
|
||||
MultiScanArgs scan_options(BytewiseComparator());
|
||||
scan_options.use_async_io = true;
|
||||
scan_options.insert(key_ranges[0], key_ranges[1]);
|
||||
|
||||
std::unique_ptr<MultiScan> iter =
|
||||
dbfull()->NewMultiScan(ro, cfh, scan_options);
|
||||
ASSERT_NE(iter, nullptr);
|
||||
|
||||
// Verify all ranges can be scanned successfully
|
||||
int total_keys = 0;
|
||||
try {
|
||||
for (auto range : *iter) {
|
||||
for (auto it : range) {
|
||||
it.first.ToString();
|
||||
total_keys++;
|
||||
}
|
||||
}
|
||||
} catch (MultiScanException& ex) {
|
||||
ASSERT_NOK(ex.status());
|
||||
std::cerr << "Iterator returned status " << ex.what();
|
||||
abort();
|
||||
} catch (std::logic_error& ex) {
|
||||
std::cerr << "Iterator returned logic error " << ex.what();
|
||||
abort();
|
||||
}
|
||||
|
||||
ASSERT_EQ(total_keys, 400);
|
||||
iter.reset();
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -3825,6 +3825,89 @@ TEST_F(DBRangeDelTest, RowCache) {
|
||||
// and should not turn db into read-only mdoe.
|
||||
ASSERT_OK(Put(Key(5), "foo"));
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, SeekForPrevTest) {
|
||||
// open db
|
||||
Options options = GetDefaultOptions();
|
||||
options.create_if_missing = true;
|
||||
options.compaction_style = kCompactionStyleUniversal;
|
||||
|
||||
// add SST partitioner, split sst file with prefix length 2
|
||||
options.sst_partitioner_factory = NewSstPartitionerFixedPrefixFactory(2);
|
||||
Reopen(options);
|
||||
|
||||
// File uses SST partitioner, so it will be split into 3 files
|
||||
// SST file 1: ka1, ka2
|
||||
// SST file 2: kb1
|
||||
// SST file 3: kc1, kc2
|
||||
// Delete range covers from ka2 to kc2, which means record ka2 and kb1, kc1
|
||||
// are covered by the delete range
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> kv = {{"ka1", "value_1"},
|
||||
{"ka2", "value_2"},
|
||||
{"kb1", "value_3"},
|
||||
{"kc1", "value_4"},
|
||||
{"kc2", "value_5"}};
|
||||
for (auto& p : kv) {
|
||||
ASSERT_OK(Put(p.first, p.second));
|
||||
}
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
// Compact to Lmax, it should have seq 0 now.
|
||||
ASSERT_OK(CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
// Open an iterator and create a snapshot, so that keys are not deleted
|
||||
// completely by delete range in SST
|
||||
ReadOptions read_opts;
|
||||
read_opts.snapshot = db_->GetSnapshot();
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_opts));
|
||||
iter->SeekToFirst();
|
||||
// iterate all the keys and validate the value
|
||||
for (int i = 0; iter->Valid(); iter->Next()) {
|
||||
ASSERT_EQ(kv[i].first, iter->key().ToString());
|
||||
ASSERT_EQ(kv[i].second, iter->value().ToString());
|
||||
i++;
|
||||
}
|
||||
|
||||
// use delete range to delete the record
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), "ka2",
|
||||
"kc2"));
|
||||
// Flush
|
||||
ASSERT_OK(Flush());
|
||||
// Compact to Lmax
|
||||
ASSERT_OK(CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
// Close the iterator and release the snapshot.
|
||||
ASSERT_OK(iter->status());
|
||||
iter.reset();
|
||||
db_->ReleaseSnapshot(read_opts.snapshot);
|
||||
|
||||
// create second iterator, seek each key and validate result
|
||||
std::unique_ptr<Iterator> iter2(db_->NewIterator(ReadOptions()));
|
||||
// Validate keys are deleted
|
||||
iter2->SeekToFirst();
|
||||
ASSERT_TRUE(iter2->Valid());
|
||||
ASSERT_EQ("ka1", iter2->key().ToString());
|
||||
iter2->Next();
|
||||
ASSERT_TRUE(iter2->Valid());
|
||||
ASSERT_EQ("kc2", iter2->key().ToString());
|
||||
iter2->Next();
|
||||
ASSERT_FALSE(iter2->Valid());
|
||||
|
||||
// Validate seek for prev result
|
||||
for (auto& p : kv) {
|
||||
iter2->SeekForPrev(p.first);
|
||||
ASSERT_TRUE(iter2->Valid());
|
||||
if (p.first == "kc2") {
|
||||
ASSERT_EQ("kc2", iter2->key().ToString());
|
||||
} else {
|
||||
ASSERT_EQ("ka1", iter2->key().ToString());
|
||||
}
|
||||
}
|
||||
ASSERT_OK(iter2->status());
|
||||
iter2.reset();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+4
-4
@@ -5179,7 +5179,7 @@ TEST_F(DBTest, DynamicMemtableOptions) {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
#ifndef NROCKSDB_THREAD_STATUS
|
||||
namespace {
|
||||
bool VerifyOperationCount(Env* env, ThreadStatus::OperationType op_type,
|
||||
int expected_count) {
|
||||
@@ -5637,7 +5637,7 @@ TEST_P(DBTestWithParam, PreShutdownCompactionMiddle) {
|
||||
ASSERT_EQ(operation_count[ThreadStatus::OP_COMPACTION], 0);
|
||||
}
|
||||
|
||||
#endif // ROCKSDB_USING_THREAD_STATUS
|
||||
#endif // !NROCKSDB_THREAD_STATUS
|
||||
|
||||
TEST_F(DBTest, FlushOnDestroy) {
|
||||
WriteOptions wo;
|
||||
@@ -6372,9 +6372,9 @@ TEST_F(DBTest, MergeTestTime) {
|
||||
|
||||
ASSERT_EQ(1, count);
|
||||
ASSERT_EQ(4000000, TestGetTickerCount(options, MERGE_OPERATION_TOTAL_TIME));
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
#ifndef NROCKSDB_THREAD_STATUS
|
||||
ASSERT_GT(TestGetTickerCount(options, FLUSH_WRITE_BYTES), 0);
|
||||
#endif // ROCKSDB_USING_THREAD_STATUS
|
||||
#endif // !NROCKSDB_THREAD_STATUS
|
||||
}
|
||||
|
||||
TEST_P(DBTestWithParam, MergeCompactionTimeTest) {
|
||||
|
||||
+7
-17
@@ -163,9 +163,7 @@ TEST_F(EventListenerTest, OnSingleDBCompactionTest) {
|
||||
options.max_bytes_for_level_base = options.target_file_size_base * 2;
|
||||
options.max_bytes_for_level_multiplier = 2;
|
||||
options.compression = kNoCompression;
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
options.enable_thread_tracking = true;
|
||||
#endif // ROCKSDB_USING_THREAD_STATUS
|
||||
options.enable_thread_tracking = ThreadStatus::kEnabled;
|
||||
options.level0_file_num_compaction_trigger = kNumL0Files;
|
||||
options.table_properties_collector_factories.push_back(
|
||||
std::make_shared<TestPropertiesCollectorFactory>());
|
||||
@@ -229,7 +227,7 @@ class TestFlushListener : public EventListener {
|
||||
ASSERT_EQ(info.file_checksum, kUnknownFileChecksum);
|
||||
ASSERT_EQ(info.file_checksum_func_name, kUnknownFileChecksumFuncName);
|
||||
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
#ifndef NROCKSDB_THREAD_STATUS
|
||||
// Verify the id of the current thread that created this table
|
||||
// file matches the id of any active flush or compaction thread.
|
||||
uint64_t thread_id = env_->GetThreadID();
|
||||
@@ -246,7 +244,7 @@ class TestFlushListener : public EventListener {
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(found_match);
|
||||
#endif // ROCKSDB_USING_THREAD_STATUS
|
||||
#endif // !NROCKSDB_THREAD_STATUS
|
||||
}
|
||||
|
||||
void OnFlushCompleted(DB* db, const FlushJobInfo& info) override {
|
||||
@@ -310,9 +308,7 @@ TEST_F(EventListenerTest, OnSingleDBFlushTest) {
|
||||
Options options;
|
||||
options.env = CurrentOptions().env;
|
||||
options.write_buffer_size = k110KB;
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
options.enable_thread_tracking = true;
|
||||
#endif // ROCKSDB_USING_THREAD_STATUS
|
||||
options.enable_thread_tracking = ThreadStatus::kEnabled;
|
||||
TestFlushListener* listener = new TestFlushListener(options.env, this);
|
||||
options.listeners.emplace_back(listener);
|
||||
std::vector<std::string> cf_names = {"pikachu", "ilya", "muromec",
|
||||
@@ -357,9 +353,7 @@ TEST_F(EventListenerTest, MultiCF) {
|
||||
Options options;
|
||||
options.env = CurrentOptions().env;
|
||||
options.write_buffer_size = k110KB;
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
options.enable_thread_tracking = true;
|
||||
#endif // ROCKSDB_USING_THREAD_STATUS
|
||||
options.enable_thread_tracking = ThreadStatus::kEnabled;
|
||||
options.atomic_flush = atomic_flush;
|
||||
options.create_if_missing = true;
|
||||
DestroyAndReopen(options);
|
||||
@@ -407,9 +401,7 @@ TEST_F(EventListenerTest, MultiCF) {
|
||||
TEST_F(EventListenerTest, MultiDBMultiListeners) {
|
||||
Options options;
|
||||
options.env = CurrentOptions().env;
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
options.enable_thread_tracking = true;
|
||||
#endif // ROCKSDB_USING_THREAD_STATUS
|
||||
options.enable_thread_tracking = ThreadStatus::kEnabled;
|
||||
options.table_properties_collector_factories.push_back(
|
||||
std::make_shared<TestPropertiesCollectorFactory>());
|
||||
std::vector<TestFlushListener*> listeners;
|
||||
@@ -497,9 +489,7 @@ TEST_F(EventListenerTest, MultiDBMultiListeners) {
|
||||
TEST_F(EventListenerTest, DisableBGCompaction) {
|
||||
Options options;
|
||||
options.env = CurrentOptions().env;
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
options.enable_thread_tracking = true;
|
||||
#endif // ROCKSDB_USING_THREAD_STATUS
|
||||
options.enable_thread_tracking = ThreadStatus::kEnabled;
|
||||
TestFlushListener* listener = new TestFlushListener(options.env, this);
|
||||
const int kCompactionTrigger = 1;
|
||||
const int kSlowdownTrigger = 5;
|
||||
|
||||
@@ -303,6 +303,47 @@ TEST_F(ObsoleteFilesTest, BlobFiles) {
|
||||
ASSERT_EQ(deleted_files, expected_deleted_files);
|
||||
}
|
||||
|
||||
TEST_F(ObsoleteFilesTest, GetSortedWalFilesHangsAfterNoopPurge) {
|
||||
// This test used to trigger a hang in `DB::GetSortedWalFiles()`, where it
|
||||
// would wait for a no-op purge that did not signal the CV upon completion.
|
||||
|
||||
// Grab an iterator and flush to switch the super version. That way, when the
|
||||
// iterator is destroyed, it will go through the purge path.
|
||||
DB* db = db_; // Only using `db` makes it clear we only use DB-level APIs.
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key", "value"));
|
||||
std::unique_ptr<Iterator> iter(db->NewIterator(ReadOptions()));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
|
||||
// Sync points ensure `GetSortedWalFiles()` waits for a purge after
|
||||
// `FindObsoleteFiles()` releases the mutex but before its corresponding purge
|
||||
// completes.
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"FindObsoleteFiles::PostMutexUnlock", [&](void* /* arg */) {
|
||||
TEST_SYNC_POINT(
|
||||
"ObsoleteFilesTest::GetSortedWalFilesHangsAfterNoopPurge:"
|
||||
"InCallback:1");
|
||||
TEST_SYNC_POINT(
|
||||
"ObsoleteFilesTest::GetSortedWalFilesHangsAfterNoopPurge:"
|
||||
"InCallback:2");
|
||||
});
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"ObsoleteFilesTest::GetSortedWalFilesHangsAfterNoopPurge:InCallback:1",
|
||||
"ObsoleteFilesTest::GetSortedWalFilesHangsAfterNoopPurge:Thread:Begin"},
|
||||
{"DBImpl::GetSortedWalFilesImpl:WaitPurge",
|
||||
"ObsoleteFilesTest::GetSortedWalFilesHangsAfterNoopPurge:InCallback:2"},
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
port::Thread get_sorted_wal_files_thread([db]() {
|
||||
TEST_SYNC_POINT(
|
||||
"ObsoleteFilesTest::GetSortedWalFilesHangsAfterNoopPurge:Thread:Begin");
|
||||
VectorWalPtr files;
|
||||
ASSERT_OK(db->GetSortedWalFiles(files));
|
||||
});
|
||||
iter.reset();
|
||||
get_sorted_wal_files_thread.join();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+7
-4
@@ -557,16 +557,19 @@ struct SubcompactionProgress {
|
||||
Slice key_slice(next_internal_key_to_compact);
|
||||
if (ParseInternalKey(key_slice, &parsed_key, false /* log_err_key */)
|
||||
.ok()) {
|
||||
oss << "user_key=\"" << parsed_key.user_key.ToString(false /* hex */)
|
||||
<< "\" (hex:" << parsed_key.user_key.ToString(true /* hex */)
|
||||
<< ")";
|
||||
oss << "user_key(hex)=" << parsed_key.user_key.ToString(true /* hex */);
|
||||
oss << ", seq=";
|
||||
if (parsed_key.sequence == kMaxSequenceNumber) {
|
||||
oss << "kMaxSequenceNumber";
|
||||
} else {
|
||||
oss << parsed_key.sequence;
|
||||
}
|
||||
oss << ", type=" << static_cast<int>(parsed_key.type);
|
||||
oss << ", type=";
|
||||
if (parsed_key.type == kValueTypeForSeek) {
|
||||
oss << "kValueTypeForSeek";
|
||||
} else {
|
||||
oss << static_cast<int>(parsed_key.type);
|
||||
}
|
||||
} else {
|
||||
oss << "raw=" << key_slice.ToString(true /* hex */);
|
||||
}
|
||||
|
||||
+175
-79
@@ -16,6 +16,7 @@
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -980,7 +981,8 @@ class LevelIterator final : public InternalIterator {
|
||||
nullptr,
|
||||
bool allow_unprepared_value = false,
|
||||
std::unique_ptr<TruncatedRangeDelIterator>*** range_tombstone_iter_ptr_ =
|
||||
nullptr)
|
||||
nullptr,
|
||||
Statistics* db_statistics = nullptr, SystemClock* clock = nullptr)
|
||||
: table_cache_(table_cache),
|
||||
read_options_(read_options),
|
||||
file_options_(file_options),
|
||||
@@ -1005,7 +1007,9 @@ class LevelIterator final : public InternalIterator {
|
||||
allow_unprepared_value_(allow_unprepared_value),
|
||||
is_next_read_sequential_(false),
|
||||
to_return_sentinel_(false),
|
||||
scan_opts_(nullptr) {
|
||||
scan_opts_(nullptr),
|
||||
db_statistics_(db_statistics),
|
||||
clock_(clock) {
|
||||
// Empty level is not supported.
|
||||
assert(flevel_ != nullptr && flevel_->num_files > 0);
|
||||
if (range_tombstone_iter_ptr_) {
|
||||
@@ -1013,7 +1017,15 @@ class LevelIterator final : public InternalIterator {
|
||||
}
|
||||
}
|
||||
|
||||
~LevelIterator() override { delete file_iter_.Set(nullptr); }
|
||||
~LevelIterator() override {
|
||||
delete file_iter_.Set(nullptr);
|
||||
// Clean up any prepared iterators that weren't used
|
||||
for (auto& entry : prepared_iters_) {
|
||||
delete entry.second;
|
||||
}
|
||||
prepared_iters_.clear();
|
||||
assert(prepared_iters_.size() == 0);
|
||||
}
|
||||
|
||||
// Seek to the first file with a key >= target.
|
||||
// If range_tombstone_iter_ is not nullptr, then we pretend that file
|
||||
@@ -1124,10 +1136,12 @@ class LevelIterator final : public InternalIterator {
|
||||
|
||||
void Prepare(const MultiScanArgs* so) override {
|
||||
// We assume here that scan_opts is sorted such that
|
||||
// scan_opts[0].range.start < scan_opts[1].range.start, and non overlapping
|
||||
// scan_opts[0].range.start < scan_opts[1].range.start, and non
|
||||
// overlapping
|
||||
if (so == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
scan_opts_ = so;
|
||||
|
||||
// Verify comparator is consistent
|
||||
@@ -1188,8 +1202,8 @@ class LevelIterator final : public InternalIterator {
|
||||
continue;
|
||||
}
|
||||
auto const metadata = flevel_->files[i].file_metadata;
|
||||
if (metadata->num_entries == metadata->num_range_deletions) {
|
||||
// Skip range deletion only files.
|
||||
if (metadata->FileIsStandAloneRangeTombstone()) {
|
||||
// Skip stand alone range deletion files.
|
||||
continue;
|
||||
}
|
||||
auto& args = GetMultiScanArgForFile(i);
|
||||
@@ -1197,9 +1211,35 @@ class LevelIterator final : public InternalIterator {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StopWatch timer(clock_, db_statistics_, MULTISCAN_PREPARE_ITERATORS);
|
||||
|
||||
// Propagate multiscan configs
|
||||
for (auto& file_to_arg : *file_to_scan_opts_) {
|
||||
file_to_arg.second.CopyConfigFrom(*so);
|
||||
assert(OverlapRange(*file_to_arg.second.GetScanRanges().begin(),
|
||||
file_to_arg.first) &&
|
||||
OverlapRange(*file_to_arg.second.GetScanRanges().rbegin(),
|
||||
file_to_arg.first));
|
||||
}
|
||||
|
||||
if (so->use_async_io) {
|
||||
auto before = file_index_;
|
||||
// Pre-create and prepare only relevant file iterators
|
||||
for (auto& file_to_arg : *file_to_scan_opts_) {
|
||||
size_t file_index = file_to_arg.first;
|
||||
|
||||
file_index_ = file_index;
|
||||
// Create iterator for this file
|
||||
auto iter = NewFileIterator();
|
||||
if (iter != nullptr) {
|
||||
// If we have async enabled, lets prepare all our iterators.
|
||||
iter->Prepare(&file_to_arg.second);
|
||||
// Store the prepared iterator
|
||||
prepared_iters_[file_index] = iter;
|
||||
}
|
||||
}
|
||||
file_index_ = before;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1276,7 +1316,7 @@ class LevelIterator final : public InternalIterator {
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
bool OverlapRange(const ScanOptions& opts);
|
||||
bool OverlapRange(const ScanOptions& opts, size_t file_index);
|
||||
#endif
|
||||
|
||||
TableCache* table_cache_;
|
||||
@@ -1334,9 +1374,15 @@ class LevelIterator final : public InternalIterator {
|
||||
bool to_return_sentinel_ = false;
|
||||
const MultiScanArgs* scan_opts_ = nullptr;
|
||||
|
||||
Statistics* db_statistics_ = nullptr;
|
||||
SystemClock* clock_ = nullptr;
|
||||
|
||||
// Our stored scan_opts for each prefix
|
||||
std::unique_ptr<ScanOptionsMap> file_to_scan_opts_ = nullptr;
|
||||
|
||||
// Map to store pre-created iterators by file index
|
||||
std::unordered_map<size_t, InternalIterator*> prepared_iters_;
|
||||
|
||||
// Sets flags for if we should return the sentinel key next.
|
||||
// The condition for returning sentinel is reaching the end of current
|
||||
// file_iter_: !Valid() && status.().ok().
|
||||
@@ -1613,8 +1659,19 @@ bool LevelIterator::SkipEmptyFileForward() {
|
||||
const ScanOptions& opts =
|
||||
GetMultiScanArgForFile(file_index_).GetScanRanges().front();
|
||||
if (opts.range.start.has_value()) {
|
||||
InternalKey target(*opts.range.start.AsPtr(), kMaxSequenceNumber,
|
||||
kValueTypeForSeek);
|
||||
InternalKey target;
|
||||
const size_t ts_size =
|
||||
user_comparator_.user_comparator()->timestamp_size();
|
||||
if (ts_size == 0) {
|
||||
target = InternalKey(opts.range.start.value(), kMaxSequenceNumber,
|
||||
kValueTypeForSeek);
|
||||
} else {
|
||||
std::string seek_key;
|
||||
AppendKeyWithMaxTimestamp(&seek_key, opts.range.start.value(),
|
||||
ts_size);
|
||||
target =
|
||||
InternalKey(seek_key, kMaxSequenceNumber, kValueTypeForSeek);
|
||||
}
|
||||
file_iter_.Seek(target.Encode());
|
||||
}
|
||||
} else {
|
||||
@@ -1662,14 +1719,14 @@ void LevelIterator::SkipEmptyFileBackward() {
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
bool LevelIterator::OverlapRange(const ScanOptions& opts) {
|
||||
bool LevelIterator::OverlapRange(const ScanOptions& opts, size_t file_index) {
|
||||
return (user_comparator_.CompareWithoutTimestamp(
|
||||
opts.range.start.value(), /*a_has_ts=*/false,
|
||||
ExtractUserKey(flevel_->files[file_index_].largest_key),
|
||||
ExtractUserKey(flevel_->files[file_index].largest_key),
|
||||
/*b_has_ts=*/true) <= 0 &&
|
||||
user_comparator_.CompareWithoutTimestamp(
|
||||
opts.range.limit.value(), /*a_has_ts=*/false,
|
||||
ExtractUserKey(flevel_->files[file_index_].smallest_key),
|
||||
ExtractUserKey(flevel_->files[file_index].smallest_key),
|
||||
/*b_has_ts=*/true) > 0);
|
||||
}
|
||||
#endif
|
||||
@@ -1680,15 +1737,6 @@ void LevelIterator::SetFileIterator(InternalIterator* iter) {
|
||||
}
|
||||
|
||||
InternalIterator* old_iter = file_iter_.Set(iter);
|
||||
if (iter && scan_opts_) {
|
||||
if (FileHasMultiScanArg(file_index_)) {
|
||||
const MultiScanArgs& new_opts = GetMultiScanArgForFile(file_index_);
|
||||
assert(OverlapRange(*new_opts.GetScanRanges().begin()) &&
|
||||
OverlapRange(*new_opts.GetScanRanges().rbegin()));
|
||||
file_iter_.Prepare(&new_opts);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the read pattern for PrefetchBuffer.
|
||||
if (is_next_read_sequential_) {
|
||||
file_iter_.UpdateReadaheadState(old_iter);
|
||||
@@ -1718,7 +1766,24 @@ void LevelIterator::InitFileIterator(size_t new_file_index) {
|
||||
// no need to change anything
|
||||
} else {
|
||||
file_index_ = new_file_index;
|
||||
if (!prepared_iters_.empty()) {
|
||||
auto prepared_it = prepared_iters_.find(file_index_);
|
||||
if (prepared_it != prepared_iters_.end()) {
|
||||
InternalIterator* iter = prepared_it->second;
|
||||
prepared_iters_.erase(prepared_it);
|
||||
SetFileIterator(iter);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
InternalIterator* iter = NewFileIterator();
|
||||
if (FileHasMultiScanArg(file_index_)) {
|
||||
auto& args = GetMultiScanArgForFile(file_index_);
|
||||
assert(OverlapRange(*args.GetScanRanges().begin(), file_index_) &&
|
||||
OverlapRange(*args.GetScanRanges().rbegin(), file_index_));
|
||||
iter->Prepare(&args);
|
||||
}
|
||||
|
||||
SetFileIterator(iter);
|
||||
}
|
||||
}
|
||||
@@ -2181,7 +2246,7 @@ InternalIterator* Version::TEST_GetLevelIterator(
|
||||
cfd_->internal_stats()->GetFileReadHist(level),
|
||||
TableReaderCaller::kUserIterator, IsFilterSkipped(level), level,
|
||||
nullptr /* range_del_agg */, nullptr /* compaction_boundaries */,
|
||||
allow_unprepared_value, &tombstone_iter_ptr);
|
||||
allow_unprepared_value, &tombstone_iter_ptr, db_statistics_, clock_);
|
||||
if (read_options.ignore_range_deletions) {
|
||||
merge_iter_builder->AddIterator(level_iter);
|
||||
} else {
|
||||
@@ -2321,7 +2386,7 @@ void Version::AddIteratorsForLevel(const ReadOptions& read_options,
|
||||
TableReaderCaller::kUserIterator, IsFilterSkipped(level), level,
|
||||
/*range_del_agg=*/nullptr,
|
||||
/*compaction_boundaries=*/nullptr, allow_unprepared_value,
|
||||
&tombstone_iter_ptr);
|
||||
&tombstone_iter_ptr, db_statistics_, clock_);
|
||||
if (read_options.ignore_range_deletions) {
|
||||
merge_iter_builder->AddIterator(level_iter);
|
||||
} else {
|
||||
@@ -2378,7 +2443,7 @@ Status Version::OverlapWithLevelIterator(const ReadOptions& read_options,
|
||||
mutable_cf_options_, should_sample_file_read(),
|
||||
cfd_->internal_stats()->GetFileReadHist(level),
|
||||
TableReaderCaller::kUserIterator, IsFilterSkipped(level), level,
|
||||
&range_del_agg, nullptr, false));
|
||||
&range_del_agg, nullptr, false, nullptr, db_statistics_, clock_));
|
||||
status = OverlapWithIterator(ucmp, smallest_user_key, largest_user_key,
|
||||
iter.get(), overlap);
|
||||
}
|
||||
@@ -4560,7 +4625,8 @@ bool VersionStorageInfo::OverlapInLevel(int level,
|
||||
void VersionStorageInfo::GetOverlappingInputs(
|
||||
int level, const InternalKey* begin, const InternalKey* end,
|
||||
std::vector<FileMetaData*>* inputs, int hint_index, int* file_index,
|
||||
bool expand_range, InternalKey** next_smallest) const {
|
||||
bool expand_range, const FileMetaData* starting_l0_file,
|
||||
InternalKey** next_smallest) const {
|
||||
if (level >= num_non_empty_levels_) {
|
||||
// this level is empty, no overlapping inputs
|
||||
return;
|
||||
@@ -4593,7 +4659,19 @@ void VersionStorageInfo::GetOverlappingInputs(
|
||||
|
||||
// index stores the file index need to check.
|
||||
std::list<size_t> index;
|
||||
for (size_t i = 0; i < level_files_brief_[level].num_files; i++) {
|
||||
size_t start_index = 0;
|
||||
if (starting_l0_file != nullptr) {
|
||||
uint64_t starting_file_number = starting_l0_file->fd.GetNumber();
|
||||
for (size_t i = 0; i < level_files_brief_[level].num_files; i++) {
|
||||
if (level_files_brief_[level].files[i].fd.GetNumber() ==
|
||||
starting_file_number) {
|
||||
start_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert(start_index < level_files_brief_[level].num_files);
|
||||
}
|
||||
for (size_t i = start_index; i < level_files_brief_[level].num_files; i++) {
|
||||
index.emplace_back(i);
|
||||
}
|
||||
|
||||
@@ -5493,6 +5571,15 @@ void VersionSet::Reset() {
|
||||
if (column_family_set_) {
|
||||
WriteBufferManager* wbm = column_family_set_->write_buffer_manager();
|
||||
WriteController* wc = column_family_set_->write_controller();
|
||||
|
||||
// Clear TableCache to prevent use-after-free: Reset() deletes old
|
||||
// ColumnFamilySet but reuses table_cache_, which may contain
|
||||
// BlockBasedTable entries with dangling references to deleted CFD's
|
||||
// ioptions.
|
||||
if (table_cache_) {
|
||||
table_cache_->EraseUnRefEntries();
|
||||
}
|
||||
|
||||
// db_id becomes the source of truth after DBImpl::Recover():
|
||||
// https://github.com/facebook/rocksdb/blob/v7.3.1/db/db_impl/db_impl_open.cc#L527
|
||||
// Note: we may not be able to recover db_id from MANIFEST if
|
||||
@@ -5525,8 +5612,11 @@ void VersionSet::Reset() {
|
||||
void VersionSet::UpdatedMutableDbOptions(
|
||||
const MutableDBOptions& updated_options, InstrumentedMutex* mu) {
|
||||
// Must be holding mutex if not called during initialization
|
||||
if (manifest_file_size_ > 0) {
|
||||
if (mu) {
|
||||
mu->AssertHeld();
|
||||
} else {
|
||||
// manifest_file_size_ must be 0 if called from the constructor
|
||||
assert(manifest_file_size_ == 0);
|
||||
}
|
||||
file_options_.writable_file_max_buffer_size =
|
||||
updated_options.writable_file_max_buffer_size;
|
||||
@@ -5626,8 +5716,8 @@ Status VersionSet::ProcessManifestWrites(
|
||||
// the preceding version edits in the same atomic group, and update
|
||||
// their `remaining_entries_` member variable because we are NOT going
|
||||
// to write the version edits' of dropped CF to the MANIFEST. If we
|
||||
// don't update, then Recover can report corrupted atomic group because
|
||||
// the `remaining_entries_` do not match.
|
||||
// don't update, then Recover can report corrupted atomic group
|
||||
// because the `remaining_entries_` do not match.
|
||||
if (!batch_edits.empty()) {
|
||||
if (batch_edits.back()->IsInAtomicGroup() &&
|
||||
batch_edits.back()->GetRemainingEntries() > 0) {
|
||||
@@ -5932,10 +6022,12 @@ Status VersionSet::ProcessManifestWrites(
|
||||
#ifndef NDEBUG
|
||||
if (batch_edits.size() > 1 && batch_edits.size() - 1 == idx) {
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"VersionSet::ProcessManifestWrites:BeforeWriteLastVersionEdit:0",
|
||||
"VersionSet::ProcessManifestWrites:BeforeWriteLastVersionEdit:"
|
||||
"0",
|
||||
nullptr);
|
||||
TEST_SYNC_POINT(
|
||||
"VersionSet::ProcessManifestWrites:BeforeWriteLastVersionEdit:1");
|
||||
"VersionSet::ProcessManifestWrites:BeforeWriteLastVersionEdit:"
|
||||
"1");
|
||||
}
|
||||
++idx;
|
||||
#endif /* !NDEBUG */
|
||||
@@ -5972,8 +6064,8 @@ Status VersionSet::ProcessManifestWrites(
|
||||
file_options_.temperature, dir_contains_current_file);
|
||||
if (!io_s.ok()) {
|
||||
s = io_s;
|
||||
// Quarantine old manifest file in case new manifest file's CURRENT file
|
||||
// wasn't created successfully and the old manifest is needed.
|
||||
// Quarantine old manifest file in case new manifest file's CURRENT
|
||||
// file wasn't created successfully and the old manifest is needed.
|
||||
limbo_descriptor_log_file_number.push_back(manifest_file_number_);
|
||||
files_to_quarantine_if_commit_fail.push_back(
|
||||
&limbo_descriptor_log_file_number);
|
||||
@@ -6122,21 +6214,21 @@ Status VersionSet::ProcessManifestWrites(
|
||||
// that renaming tmp file to CURRENT failed.
|
||||
//
|
||||
// On local POSIX-compliant FS, the CURRENT must point to the original
|
||||
// MANIFEST. We can delete the new MANIFEST for simplicity, but we can also
|
||||
// keep it. Future recovery will ignore this MANIFEST. It's also ok for the
|
||||
// process not to crash and continue using the db. Any future LogAndApply()
|
||||
// call will switch to a new MANIFEST and update CURRENT, still ignoring
|
||||
// this one.
|
||||
// MANIFEST. We can delete the new MANIFEST for simplicity, but we can
|
||||
// also keep it. Future recovery will ignore this MANIFEST. It's also ok
|
||||
// for the process not to crash and continue using the db. Any future
|
||||
// LogAndApply() call will switch to a new MANIFEST and update CURRENT,
|
||||
// still ignoring this one.
|
||||
//
|
||||
// On non-local FS, it is
|
||||
// possible that the rename operation succeeded on the server (remote)
|
||||
// side, but the client somehow returns a non-ok status to RocksDB. Note
|
||||
// that this does not violate atomicity. Should we delete the new MANIFEST
|
||||
// successfully, a subsequent recovery attempt will likely see the CURRENT
|
||||
// pointing to the new MANIFEST, thus fail. We will not be able to open the
|
||||
// DB again. Therefore, if manifest operations succeed, we should keep the
|
||||
// the new MANIFEST. If the process proceeds, any future LogAndApply() call
|
||||
// will switch to a new MANIFEST and update CURRENT. If user tries to
|
||||
// pointing to the new MANIFEST, thus fail. We will not be able to open
|
||||
// the DB again. Therefore, if manifest operations succeed, we should keep
|
||||
// the the new MANIFEST. If the process proceeds, any future LogAndApply()
|
||||
// call will switch to a new MANIFEST and update CURRENT. If user tries to
|
||||
// re-open the DB,
|
||||
// a) CURRENT points to the new MANIFEST, and the new MANIFEST is present.
|
||||
// b) CURRENT points to the original MANIFEST, and the original MANIFEST
|
||||
@@ -6265,9 +6357,9 @@ Status VersionSet::LogAndApply(
|
||||
first_writer.cv.Wait();
|
||||
}
|
||||
if (first_writer.done) {
|
||||
// All non-CF-manipulation operations can be grouped together and committed
|
||||
// to MANIFEST. They should all have finished. The status code is stored in
|
||||
// the first manifest writer.
|
||||
// All non-CF-manipulation operations can be grouped together and
|
||||
// committed to MANIFEST. They should all have finished. The status code
|
||||
// is stored in the first manifest writer.
|
||||
#ifndef NDEBUG
|
||||
for (const auto& writer : writers) {
|
||||
assert(writer.done);
|
||||
@@ -6321,8 +6413,8 @@ void VersionSet::LogAndApplyCFHelper(VersionEdit* edit,
|
||||
assert(!edit->HasLastSequence());
|
||||
edit->SetLastSequence(*max_last_sequence);
|
||||
if (edit->IsColumnFamilyDrop()) {
|
||||
// if we drop column family, we have to make sure to save max column family,
|
||||
// so that we don't reuse existing ID
|
||||
// if we drop column family, we have to make sure to save max column
|
||||
// family, so that we don't reuse existing ID
|
||||
edit->SetMaxColumnFamily(column_family_set_->GetMaxColumnFamily());
|
||||
}
|
||||
}
|
||||
@@ -6611,7 +6703,8 @@ void VersionSet::RecoverEpochNumbers() {
|
||||
Status VersionSet::ListColumnFamilies(std::vector<std::string>* column_families,
|
||||
const std::string& dbname,
|
||||
FileSystem* fs) {
|
||||
// Read "CURRENT" file, which contains a pointer to the current manifest file
|
||||
// Read "CURRENT" file, which contains a pointer to the current manifest
|
||||
// file
|
||||
std::string manifest_path;
|
||||
uint64_t manifest_file_number;
|
||||
Status s = GetCurrentManifestPath(dbname, fs, /*is_retry=*/false,
|
||||
@@ -6768,9 +6861,9 @@ Status VersionSet::ReduceNumberOfLevels(const std::string& dbname,
|
||||
|
||||
// Get the checksum information including the checksum and checksum function
|
||||
// name of all SST and blob files in VersionSet. Store the information in
|
||||
// FileChecksumList which contains a map from file number to its checksum info.
|
||||
// If DB is not running, make sure call VersionSet::Recover() to load the file
|
||||
// metadata from Manifest to VersionSet before calling this function.
|
||||
// FileChecksumList which contains a map from file number to its checksum
|
||||
// info. If DB is not running, make sure call VersionSet::Recover() to load
|
||||
// the file metadata from Manifest to VersionSet before calling this function.
|
||||
Status VersionSet::GetLiveFilesChecksumInfo(FileChecksumList* checksum_list) {
|
||||
// Clean the previously stored checksum information if any.
|
||||
Status s;
|
||||
@@ -6912,8 +7005,8 @@ Status VersionSet::WriteCurrentStateToManifest(
|
||||
// WARNING: This method doesn't hold a mutex!!
|
||||
|
||||
// This is done without DB mutex lock held, but only within single-threaded
|
||||
// LogAndApply. Column family manipulations can only happen within LogAndApply
|
||||
// (the same single thread), so we're safe to iterate.
|
||||
// LogAndApply. Column family manipulations can only happen within
|
||||
// LogAndApply (the same single thread), so we're safe to iterate.
|
||||
|
||||
assert(io_s.ok());
|
||||
if (db_options_->write_dbid_to_manifest) {
|
||||
@@ -6947,9 +7040,9 @@ Status VersionSet::WriteCurrentStateToManifest(
|
||||
}
|
||||
|
||||
// New manifest should rollover the WAL deletion record from previous
|
||||
// manifest. Otherwise, when an addition record of a deleted WAL gets added to
|
||||
// this new manifest later (which can happens in e.g, SyncWAL()), this new
|
||||
// manifest creates an illusion that such WAL hasn't been deleted.
|
||||
// manifest. Otherwise, when an addition record of a deleted WAL gets added
|
||||
// to this new manifest later (which can happens in e.g, SyncWAL()), this
|
||||
// new manifest creates an illusion that such WAL hasn't been deleted.
|
||||
VersionEdit wal_deletions;
|
||||
wal_deletions.DeleteWalsBefore(min_log_number_to_keep());
|
||||
std::string wal_deletions_record;
|
||||
@@ -7081,9 +7174,9 @@ Status VersionSet::WriteCurrentStateToManifest(
|
||||
// TODO(aekmekji): in CompactionJob::GenSubcompactionBoundaries(), this
|
||||
// function is called repeatedly with consecutive pairs of slices. For example
|
||||
// if the slice list is [a, b, c, d] this function is called with arguments
|
||||
// (a,b) then (b,c) then (c,d). Knowing this, an optimization is possible where
|
||||
// we avoid doing binary search for the keys b and c twice and instead somehow
|
||||
// maintain state of where they first appear in the files.
|
||||
// (a,b) then (b,c) then (c,d). Knowing this, an optimization is possible
|
||||
// where we avoid doing binary search for the keys b and c twice and instead
|
||||
// somehow maintain state of where they first appear in the files.
|
||||
uint64_t VersionSet::ApproximateSize(const SizeApproximationOptions& options,
|
||||
const ReadOptions& read_options,
|
||||
Version* v, const Slice& start,
|
||||
@@ -7104,19 +7197,20 @@ uint64_t VersionSet::ApproximateSize(const SizeApproximationOptions& options,
|
||||
}
|
||||
|
||||
// Outline of the optimization that uses options.files_size_error_margin.
|
||||
// When approximating the files total size that is used to store a keys range,
|
||||
// we first sum up the sizes of the files that fully fall into the range.
|
||||
// Then we sum up the sizes of all the files that may intersect with the range
|
||||
// (this includes all files in L0 as well). Then, if total_intersecting_size
|
||||
// is smaller than total_full_size * options.files_size_error_margin - we can
|
||||
// infer that the intersecting files have a sufficiently negligible
|
||||
// contribution to the total size, and we can approximate the storage required
|
||||
// for the keys in range as just half of the intersecting_files_size.
|
||||
// E.g., if the value of files_size_error_margin is 0.1, then the error of the
|
||||
// approximation is limited to only ~10% of the total size of files that fully
|
||||
// fall into the keys range. In such case, this helps to avoid a costly
|
||||
// process of binary searching the intersecting files that is required only
|
||||
// for a more precise calculation of the total size.
|
||||
// When approximating the files total size that is used to store a keys
|
||||
// range, we first sum up the sizes of the files that fully fall into the
|
||||
// range. Then we sum up the sizes of all the files that may intersect with
|
||||
// the range (this includes all files in L0 as well). Then, if
|
||||
// total_intersecting_size is smaller than total_full_size *
|
||||
// options.files_size_error_margin - we can infer that the intersecting
|
||||
// files have a sufficiently negligible contribution to the total size, and
|
||||
// we can approximate the storage required for the keys in range as just
|
||||
// half of the intersecting_files_size. E.g., if the value of
|
||||
// files_size_error_margin is 0.1, then the error of the approximation is
|
||||
// limited to only ~10% of the total size of files that fully fall into the
|
||||
// keys range. In such case, this helps to avoid a costly process of binary
|
||||
// searching the intersecting files that is required only for a more precise
|
||||
// calculation of the total size.
|
||||
|
||||
autovector<FdWithKeyRange*, 32> first_files;
|
||||
autovector<FdWithKeyRange*, 16> last_files;
|
||||
@@ -7188,10 +7282,11 @@ uint64_t VersionSet::ApproximateSize(const SizeApproximationOptions& options,
|
||||
total_intersecting_size += file_ptr->fd.GetFileSize();
|
||||
}
|
||||
|
||||
// Now scan all the first & last files at each level, and estimate their size.
|
||||
// If the total_intersecting_size is less than X% of the total_full_size - we
|
||||
// want to approximate the result in order to avoid the costly binary search
|
||||
// inside ApproximateSize. We use half of file size as an approximation below.
|
||||
// Now scan all the first & last files at each level, and estimate their
|
||||
// size. If the total_intersecting_size is less than X% of the
|
||||
// total_full_size - we want to approximate the result in order to avoid the
|
||||
// costly binary search inside ApproximateSize. We use half of file size as
|
||||
// an approximation below.
|
||||
|
||||
const double margin = options.files_size_error_margin;
|
||||
if (margin > 0 && total_intersecting_size <
|
||||
@@ -7459,7 +7554,8 @@ InternalIterator* VersionSet::MakeInputIterator(
|
||||
/*no per level latency histogram=*/nullptr,
|
||||
TableReaderCaller::kCompaction, /*skip_filters=*/false,
|
||||
/*level=*/static_cast<int>(c->level(which)), range_del_agg,
|
||||
c->boundaries(which), false, &tombstone_iter_ptr);
|
||||
c->boundaries(which), false, &tombstone_iter_ptr,
|
||||
db_options_->statistics.get(), clock_);
|
||||
range_tombstones.emplace_back(nullptr, tombstone_iter_ptr);
|
||||
}
|
||||
}
|
||||
@@ -7864,8 +7960,8 @@ Status ReactiveVersionSet::MaybeSwitchManifest(
|
||||
}
|
||||
} else if (s.IsPathNotFound()) {
|
||||
// This can happen if the primary switches to a new MANIFEST after the
|
||||
// secondary reads the CURRENT file but before the secondary actually tries
|
||||
// to open the MANIFEST.
|
||||
// secondary reads the CURRENT file but before the secondary actually
|
||||
// tries to open the MANIFEST.
|
||||
s = Status::TryAgain(
|
||||
"The primary may have switched to a new MANIFEST and deleted the old "
|
||||
"one.");
|
||||
|
||||
+11
-4
@@ -268,8 +268,13 @@ class VersionStorageInfo {
|
||||
bool expand_range = true, // if set, returns files which overlap the
|
||||
// range and overlap each other. If false,
|
||||
// then just files intersecting the range
|
||||
InternalKey** next_smallest = nullptr) // if non-null, returns the
|
||||
const; // smallest key of next file not included
|
||||
const FileMetaData* starting_l0_file =
|
||||
nullptr, // If not null, restricts L0 file selection to only include
|
||||
// files at or older than starting_l0_file.
|
||||
InternalKey** next_smallest =
|
||||
nullptr // if non-null, returns the
|
||||
// smallest key of next file not included
|
||||
) const;
|
||||
void GetCleanInputsWithinInterval(
|
||||
int level, const InternalKey* begin, // nullptr means before all keys
|
||||
const InternalKey* end, // nullptr means after all keys
|
||||
@@ -286,8 +291,10 @@ class VersionStorageInfo {
|
||||
int hint_index, // index of overlap file
|
||||
int* file_index, // return index of overlap file
|
||||
bool within_interval = false, // if set, force the inputs within interval
|
||||
InternalKey** next_smallest = nullptr) // if non-null, returns the
|
||||
const; // smallest key of next file not included
|
||||
InternalKey** next_smallest =
|
||||
nullptr // if non-null, returns the
|
||||
// smallest key of next file not included
|
||||
) const;
|
||||
|
||||
// Returns true iff some file in the specified level overlaps
|
||||
// some part of [*smallest_user_key,*largest_user_key].
|
||||
|
||||
+7
-1
@@ -192,7 +192,13 @@ void WalManager::PurgeObsoleteWALFiles() {
|
||||
s.ToString().c_str());
|
||||
continue;
|
||||
}
|
||||
if (now_seconds - file_m_time > db_options_.WAL_ttl_seconds) {
|
||||
|
||||
// Avoid expression `now_seconds - file_m_time` when
|
||||
// `file_m_time > now_seconds` to prevent unsigned underflow in case
|
||||
// system clock goes backwards. Both timestamps are based on wall clock
|
||||
// time, which is not guaranteed to be monotonic.
|
||||
if (file_m_time <= now_seconds &&
|
||||
now_seconds - file_m_time > db_options_.WAL_ttl_seconds) {
|
||||
s = DeleteDBFile(&db_options_, file_path, archival_dir, false,
|
||||
/*force_fg=*/!wal_in_db_path_);
|
||||
if (!s.ok()) {
|
||||
|
||||
+46
-8
@@ -19,6 +19,7 @@
|
||||
#include "rocksdb/write_batch.h"
|
||||
#include "rocksdb/write_buffer_manager.h"
|
||||
#include "table/mock_table.h"
|
||||
#include "test_util/mock_time_env.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "test_util/testutil.h"
|
||||
#include "util/string_util.h"
|
||||
@@ -39,7 +40,7 @@ class WalManagerTest : public testing::Test {
|
||||
EXPECT_OK(DestroyDB(dbname_, Options()));
|
||||
}
|
||||
|
||||
void Init() {
|
||||
void Init(SystemClock* clock_override) {
|
||||
ASSERT_OK(env_->CreateDirIfMissing(dbname_));
|
||||
ASSERT_OK(env_->CreateDirIfMissing(ArchivalDirectory(dbname_)));
|
||||
db_options_.db_paths.emplace_back(dbname_,
|
||||
@@ -47,7 +48,11 @@ class WalManagerTest : public testing::Test {
|
||||
db_options_.wal_dir = dbname_;
|
||||
db_options_.env = env_.get();
|
||||
db_options_.fs = env_->GetFileSystem();
|
||||
db_options_.clock = env_->GetSystemClock().get();
|
||||
if (clock_override == nullptr) {
|
||||
db_options_.clock = env_->GetSystemClock().get();
|
||||
} else {
|
||||
db_options_.clock = clock_override;
|
||||
}
|
||||
|
||||
versions_.reset(new VersionSet(
|
||||
dbname_, &db_options_, MutableDBOptions{}, env_options_,
|
||||
@@ -124,7 +129,7 @@ class WalManagerTest : public testing::Test {
|
||||
};
|
||||
|
||||
TEST_F(WalManagerTest, ReadFirstRecordCache) {
|
||||
Init();
|
||||
Init(nullptr /* clock_override */);
|
||||
std::string path = dbname_ + "/000001.log";
|
||||
std::unique_ptr<FSWritableFile> file;
|
||||
ASSERT_OK(env_->GetFileSystem()->NewWritableFile(path, FileOptions(), &file,
|
||||
@@ -221,7 +226,7 @@ int CountRecords(TransactionLogIterator* iter) {
|
||||
TEST_F(WalManagerTest, WALArchivalSizeLimit) {
|
||||
db_options_.WAL_ttl_seconds = 0;
|
||||
db_options_.WAL_size_limit_MB = 1000;
|
||||
Init();
|
||||
Init(nullptr /* clock_override */);
|
||||
|
||||
// TEST : Create WalManager with huge size limit and no ttl.
|
||||
// Create some archived files and call PurgeObsoleteWALFiles().
|
||||
@@ -258,7 +263,7 @@ TEST_F(WalManagerTest, WALArchivalSizeLimit) {
|
||||
|
||||
TEST_F(WalManagerTest, WALArchivalTtl) {
|
||||
db_options_.WAL_ttl_seconds = 1000;
|
||||
Init();
|
||||
Init(nullptr /* clock_override */);
|
||||
|
||||
// TEST : Create WalManager with a ttl and no size limit.
|
||||
// Create some archived log files and call PurgeObsoleteWALFiles().
|
||||
@@ -282,8 +287,41 @@ TEST_F(WalManagerTest, WALArchivalTtl) {
|
||||
ASSERT_TRUE(log_files.empty());
|
||||
}
|
||||
|
||||
TEST_F(WalManagerTest, WALArchivalTtlClockGoesBackwards) {
|
||||
// This test used to trigger an unsigned underflow bug, where WAL files were
|
||||
// incorrectly deleted when the system time moved backwards between writing
|
||||
// to a WAL and running `WalManager::PurgeObsoleteWALFiles()`.
|
||||
constexpr int kNumLogs = 5;
|
||||
constexpr int kEntriesPerLog = 100;
|
||||
|
||||
db_options_.WAL_ttl_seconds = 86400; // One day
|
||||
|
||||
// Configure mock clock to lag one second behind system time. That way, the
|
||||
// WAL file's mtime will appear to be in the future when
|
||||
// `WalManager::PurgeObsoleteWALFiles()` runs.
|
||||
int64_t now_seconds;
|
||||
ASSERT_OK(env_->GetSystemClock()->GetCurrentTime(&now_seconds));
|
||||
auto mock_clock = std::make_shared<MockSystemClock>(env_->GetSystemClock());
|
||||
mock_clock->SetCurrentTime(static_cast<uint64_t>(now_seconds - 1));
|
||||
db_options_.clock = mock_clock.get();
|
||||
|
||||
Init(mock_clock.get() /* clock */);
|
||||
|
||||
CreateArchiveLogs(kNumLogs, kEntriesPerLog);
|
||||
|
||||
const std::string archive_dir = ArchivalDirectory(dbname_);
|
||||
ASSERT_EQ(kNumLogs,
|
||||
ListSpecificFiles(env_.get(), archive_dir, kWalFile).size());
|
||||
|
||||
wal_manager_->PurgeObsoleteWALFiles();
|
||||
|
||||
// All files must still be present because TTL has not elapsed.
|
||||
ASSERT_EQ(kNumLogs,
|
||||
ListSpecificFiles(env_.get(), archive_dir, kWalFile).size());
|
||||
}
|
||||
|
||||
TEST_F(WalManagerTest, TransactionLogIteratorMoveOverZeroFiles) {
|
||||
Init();
|
||||
Init(nullptr /* clock_override */);
|
||||
RollTheLog(false);
|
||||
Put("key1", std::string(1024, 'a'));
|
||||
// Create a zero record WAL file.
|
||||
@@ -297,7 +335,7 @@ TEST_F(WalManagerTest, TransactionLogIteratorMoveOverZeroFiles) {
|
||||
}
|
||||
|
||||
TEST_F(WalManagerTest, TransactionLogIteratorJustEmptyFile) {
|
||||
Init();
|
||||
Init(nullptr /* clock_override */);
|
||||
RollTheLog(false);
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
// Check that an empty iterator is returned
|
||||
@@ -305,7 +343,7 @@ TEST_F(WalManagerTest, TransactionLogIteratorJustEmptyFile) {
|
||||
}
|
||||
|
||||
TEST_F(WalManagerTest, TransactionLogIteratorNewFileWhileScanning) {
|
||||
Init();
|
||||
Init(nullptr /* clock_override */);
|
||||
CreateArchiveLogs(2, 100);
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
CreateArchiveLogs(1, 100);
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "db_stress_tool/db_stress_filters.h"
|
||||
#include "db_stress_tool/db_stress_table_properties_collector.h"
|
||||
#include "db_stress_tool/db_stress_wide_merge_operator.h"
|
||||
#include "file/file_util.h"
|
||||
#include "options/options_parser.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
#include "rocksdb/filter_policy.h"
|
||||
@@ -194,10 +195,10 @@ std::shared_ptr<Cache> StressTest::NewCache(size_t capacity,
|
||||
exit(1);
|
||||
} else if (EndsWith(cache_type, "hyper_clock_cache")) {
|
||||
size_t estimated_entry_charge;
|
||||
if (cache_type == "fixed_hyper_clock_cache" ||
|
||||
cache_type == "hyper_clock_cache") {
|
||||
if (cache_type == "fixed_hyper_clock_cache") {
|
||||
estimated_entry_charge = FLAGS_block_size;
|
||||
} else if (cache_type == "auto_hyper_clock_cache") {
|
||||
} else if (cache_type == "auto_hyper_clock_cache" ||
|
||||
cache_type == "hyper_clock_cache") {
|
||||
estimated_entry_charge = 0;
|
||||
} else {
|
||||
fprintf(stderr, "Cache type not supported.");
|
||||
@@ -349,7 +350,6 @@ bool StressTest::BuildOptionsTable() {
|
||||
"1",
|
||||
"2",
|
||||
}},
|
||||
{"max_sequential_skip_in_iterations", {"4", "8", "12"}},
|
||||
{"block_based_table_factory",
|
||||
{
|
||||
keepRibbonFilterPolicyOnly ? "{filter_policy=ribbonfilter:2.35}"
|
||||
@@ -362,6 +362,13 @@ bool StressTest::BuildOptionsTable() {
|
||||
std::to_string(FLAGS_block_size + (FLAGS_seed & 0xFFFU)) + "}",
|
||||
}},
|
||||
};
|
||||
if (FLAGS_use_multiscan == 0) {
|
||||
// TODO: this can fail MultiScan when consecutive data blocks share the
|
||||
// same user at boundary. MultiScan uses user key to locate the block to
|
||||
// reach which can move the scan earlier than its current block.
|
||||
options_tbl.emplace("max_sequential_skip_in_iterations",
|
||||
std::vector<std::string>{"4", "8", "12"});
|
||||
}
|
||||
if (FLAGS_compaction_style == kCompactionStyleUniversal &&
|
||||
FLAGS_universal_max_read_amp > 0) {
|
||||
// level0_file_num_compaction_trigger needs to be at most max_read_amp
|
||||
@@ -1695,7 +1702,10 @@ Status StressTest::TestMultiScan(ThreadState* thread,
|
||||
std::vector<std::string> end_key_strs;
|
||||
// TODO support reverse BytewiseComparator in the stress test
|
||||
MultiScanArgs scan_opts(options_.comparator);
|
||||
scan_opts.use_async_io = FLAGS_multiscan_use_async_io;
|
||||
scan_opts.use_async_io =
|
||||
FLAGS_multiscan_use_async_io &&
|
||||
CheckFSFeatureSupport(options_.env->GetFileSystem().get(),
|
||||
FSSupportedOps::kAsyncIO);
|
||||
start_key_strs.reserve(num_scans);
|
||||
end_key_strs.reserve(num_scans);
|
||||
|
||||
|
||||
Vendored
+126
-41
@@ -1655,42 +1655,6 @@ void GenerateFilesAndRequest(Env* env, const std::string& fname,
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(EnvPosixTest, MultiReadIOUringError) {
|
||||
// In this test we don't do aligned read, so we can't do direct I/O.
|
||||
EnvOptions soptions;
|
||||
soptions.use_direct_reads = soptions.use_direct_writes = false;
|
||||
std::string fname = test::PerThreadDBPath(env_, "testfile");
|
||||
|
||||
std::vector<std::string> scratches;
|
||||
std::vector<ReadRequest> reqs;
|
||||
GenerateFilesAndRequest(env_, fname, &reqs, &scratches);
|
||||
// Query the data
|
||||
std::unique_ptr<RandomAccessFile> file;
|
||||
ASSERT_OK(env_->NewRandomAccessFile(fname, &file, soptions));
|
||||
|
||||
bool io_uring_wait_cqe_called = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_wait_cqe:return",
|
||||
[&](void* arg) {
|
||||
if (!io_uring_wait_cqe_called) {
|
||||
io_uring_wait_cqe_called = true;
|
||||
ssize_t& ret = *(static_cast<ssize_t*>(arg));
|
||||
ret = 1;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status s = file->MultiRead(reqs.data(), reqs.size());
|
||||
if (io_uring_wait_cqe_called) {
|
||||
ASSERT_NOK(s);
|
||||
} else {
|
||||
s.PermitUncheckedError();
|
||||
}
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(EnvPosixTest, MultiReadIOUringError2) {
|
||||
// In this test we don't do aligned read, so we can't do direct I/O.
|
||||
EnvOptions soptions;
|
||||
@@ -1706,19 +1670,20 @@ TEST_F(EnvPosixTest, MultiReadIOUringError2) {
|
||||
|
||||
bool io_uring_submit_and_wait_called = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_submit_and_wait:return1",
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_sq_ready:return1",
|
||||
[&](void* arg) {
|
||||
io_uring_submit_and_wait_called = true;
|
||||
ssize_t* ret = static_cast<ssize_t*>(arg);
|
||||
(*ret)--;
|
||||
unsigned* ret = static_cast<unsigned*>(arg);
|
||||
*ret = 1;
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_submit_and_wait:return2",
|
||||
[&](void* arg) {
|
||||
struct io_uring* iu = static_cast<struct io_uring*>(arg);
|
||||
struct io_uring_cqe* cqe;
|
||||
assert(io_uring_wait_cqe(iu, &cqe) == 0);
|
||||
io_uring_cqe_seen(iu, cqe);
|
||||
// CQ should be empty after drain - peek should fail
|
||||
int ret = io_uring_peek_cqe(iu, &cqe);
|
||||
assert(-EAGAIN == ret); // No CQEs available
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
@@ -3640,6 +3605,126 @@ TEST_F(TestAsyncRead, ReadAsync) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test ReadAsync -> MultiRead -> Poll with real io_uring (not mock).
|
||||
// This verifies that MultiRead doesn't interfere with async read buffers.
|
||||
TEST_F(TestAsyncRead, InterleavingIOUringOperations) {
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
// Use the real filesystem directly (not the mock ReadAsyncFS).
|
||||
std::shared_ptr<FileSystem> fs = env_->GetFileSystem();
|
||||
std::string fname = test::PerThreadDBPath(env_, "testfile_iouring");
|
||||
|
||||
constexpr size_t kSectorSize = 4096;
|
||||
constexpr size_t kNumSectors = 8;
|
||||
|
||||
// 1. Create & write to a file.
|
||||
{
|
||||
std::unique_ptr<FSWritableFile> wfile;
|
||||
ASSERT_OK(
|
||||
fs->NewWritableFile(fname, FileOptions(), &wfile, nullptr /*dbg*/));
|
||||
|
||||
for (size_t i = 0; i < kNumSectors; ++i) {
|
||||
auto data = NewAligned(kSectorSize * 8, static_cast<char>(i + 1));
|
||||
Slice slice(data.get(), kSectorSize);
|
||||
ASSERT_OK(wfile->Append(slice, IOOptions(), nullptr));
|
||||
}
|
||||
ASSERT_OK(wfile->Close(IOOptions(), nullptr));
|
||||
}
|
||||
|
||||
// 2. Test interleaved ReadAsync and MultiRead operations.
|
||||
{
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
ASSERT_OK(fs->NewRandomAccessFile(fname, FileOptions(), &file, nullptr));
|
||||
|
||||
IOOptions opts;
|
||||
std::vector<void*> io_handles(kNumSectors);
|
||||
std::vector<FSReadRequest> async_reqs(kNumSectors);
|
||||
std::vector<std::unique_ptr<char, Deleter>> async_data;
|
||||
std::vector<size_t> vals;
|
||||
IOHandleDeleter del_fn;
|
||||
|
||||
// Initialize async read requests.
|
||||
for (size_t i = 0; i < kNumSectors; i++) {
|
||||
async_reqs[i].offset = i * kSectorSize;
|
||||
async_reqs[i].len = kSectorSize;
|
||||
async_data.emplace_back(NewAligned(kSectorSize, 0));
|
||||
async_reqs[i].scratch = async_data.back().get();
|
||||
vals.push_back(i);
|
||||
}
|
||||
|
||||
// Callback function for async reads.
|
||||
std::function<void(FSReadRequest&, void*)> callback =
|
||||
[&](FSReadRequest& req, void* cb_arg) {
|
||||
assert(cb_arg != nullptr);
|
||||
size_t i = *(reinterpret_cast<size_t*>(cb_arg));
|
||||
async_reqs[i].offset = req.offset;
|
||||
async_reqs[i].result = req.result;
|
||||
async_reqs[i].status = req.status;
|
||||
};
|
||||
|
||||
// Submit asynchronous read requests.
|
||||
for (size_t i = 0; i < kNumSectors; i++) {
|
||||
void* cb_arg = static_cast<void*>(&(vals[i]));
|
||||
IOStatus s = file->ReadAsync(async_reqs[i], opts, callback, cb_arg,
|
||||
&(io_handles[i]), &del_fn, nullptr);
|
||||
if (s.IsNotSupported()) {
|
||||
// io_uring not supported on this system, skip the test.
|
||||
fprintf(stderr, "Skipping test - io_uring not supported: %s\n",
|
||||
s.ToString().c_str());
|
||||
for (size_t j = 0; j < i; j++) {
|
||||
if (io_handles[j] != nullptr) {
|
||||
del_fn(io_handles[j]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// For any other error, fail the test.
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
// Do a MultiRead on same sectors while async reads are submitted.
|
||||
std::vector<FSReadRequest> multi_reqs(kNumSectors);
|
||||
std::vector<std::unique_ptr<char, Deleter>> multi_data;
|
||||
for (size_t i = 0; i < kNumSectors; i++) {
|
||||
multi_reqs[i].offset = i * kSectorSize;
|
||||
multi_reqs[i].len = kSectorSize;
|
||||
multi_data.emplace_back(NewAligned(kSectorSize, 0));
|
||||
multi_reqs[i].scratch = multi_data.back().get();
|
||||
}
|
||||
ASSERT_OK(file->MultiRead(multi_reqs.data(), kNumSectors, opts, nullptr));
|
||||
|
||||
// Check the status of MultiRead requests (should all succeed).
|
||||
for (size_t i = 0; i < kNumSectors; i++) {
|
||||
auto buf = NewAligned(kSectorSize * 8, static_cast<char>(i + 1));
|
||||
Slice expected_data(buf.get(), kSectorSize);
|
||||
|
||||
ASSERT_EQ(multi_reqs[i].offset, i * kSectorSize);
|
||||
ASSERT_OK(multi_reqs[i].status);
|
||||
ASSERT_EQ(expected_data.ToString(), multi_reqs[i].result.ToString());
|
||||
}
|
||||
|
||||
// Poll for the submitted async requests.
|
||||
ASSERT_OK(fs->Poll(io_handles, kNumSectors));
|
||||
|
||||
// Check the status of async read requests (should all succeed).
|
||||
for (size_t i = 0; i < kNumSectors; i++) {
|
||||
auto buf = NewAligned(kSectorSize * 8, static_cast<char>(i + 1));
|
||||
Slice expected_data(buf.get(), kSectorSize);
|
||||
|
||||
ASSERT_EQ(async_reqs[i].offset, i * kSectorSize);
|
||||
ASSERT_OK(async_reqs[i].status);
|
||||
ASSERT_EQ(expected_data.ToString(), async_reqs[i].result.ToString());
|
||||
}
|
||||
|
||||
// Delete io_handles.
|
||||
for (size_t i = 0; i < io_handles.size(); i++) {
|
||||
del_fn(io_handles[i]);
|
||||
}
|
||||
}
|
||||
#else
|
||||
fprintf(stderr, "Skipping test - ROCKSDB_IOURING_PRESENT not defined\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
struct StaticDestructionTester {
|
||||
bool activated = false;
|
||||
~StaticDestructionTester() {
|
||||
|
||||
Vendored
+14
-7
@@ -270,7 +270,10 @@ class PosixFileSystem : public FileSystem {
|
||||
options
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
,
|
||||
!IsIOUringEnabled() ? nullptr : thread_local_io_urings_.get()
|
||||
!IsIOUringEnabled() ? nullptr
|
||||
: thread_local_async_read_io_urings_.get(),
|
||||
!IsIOUringEnabled() ? nullptr
|
||||
: thread_local_multi_read_io_urings_.get()
|
||||
#endif
|
||||
));
|
||||
}
|
||||
@@ -1087,8 +1090,9 @@ class PosixFileSystem : public FileSystem {
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
// io_uring_queue_init.
|
||||
struct io_uring* iu = nullptr;
|
||||
if (thread_local_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(thread_local_io_urings_->Get());
|
||||
if (thread_local_async_read_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(
|
||||
thread_local_async_read_io_urings_->Get());
|
||||
}
|
||||
|
||||
// Init failed, platform doesn't support io_uring.
|
||||
@@ -1161,8 +1165,9 @@ class PosixFileSystem : public FileSystem {
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
// io_uring_queue_init.
|
||||
struct io_uring* iu = nullptr;
|
||||
if (thread_local_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(thread_local_io_urings_->Get());
|
||||
if (thread_local_async_read_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(
|
||||
thread_local_async_read_io_urings_->Get());
|
||||
}
|
||||
|
||||
// Init failed, platform doesn't support io_uring.
|
||||
@@ -1277,7 +1282,8 @@ class PosixFileSystem : public FileSystem {
|
||||
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
// io_uring instance
|
||||
std::unique_ptr<ThreadLocalPtr> thread_local_io_urings_;
|
||||
std::unique_ptr<ThreadLocalPtr> thread_local_async_read_io_urings_;
|
||||
std::unique_ptr<ThreadLocalPtr> thread_local_multi_read_io_urings_;
|
||||
#endif
|
||||
|
||||
size_t page_size_;
|
||||
@@ -1337,7 +1343,8 @@ PosixFileSystem::PosixFileSystem()
|
||||
// io_uring can be created.
|
||||
struct io_uring* new_io_uring = CreateIOUring();
|
||||
if (new_io_uring != nullptr) {
|
||||
thread_local_io_urings_.reset(new ThreadLocalPtr(DeleteIOUring));
|
||||
thread_local_async_read_io_urings_.reset(new ThreadLocalPtr(DeleteIOUring));
|
||||
thread_local_multi_read_io_urings_.reset(new ThreadLocalPtr(DeleteIOUring));
|
||||
delete new_io_uring;
|
||||
}
|
||||
#endif
|
||||
|
||||
Vendored
+303
-112
@@ -589,7 +589,8 @@ PosixRandomAccessFile::PosixRandomAccessFile(
|
||||
const EnvOptions& options
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
,
|
||||
ThreadLocalPtr* thread_local_io_urings
|
||||
ThreadLocalPtr* thread_local_async_read_io_urings,
|
||||
ThreadLocalPtr* thread_local_multi_read_io_urings
|
||||
#endif
|
||||
)
|
||||
: filename_(fname),
|
||||
@@ -598,7 +599,8 @@ PosixRandomAccessFile::PosixRandomAccessFile(
|
||||
logical_sector_size_(logical_block_size)
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
,
|
||||
thread_local_io_urings_(thread_local_io_urings)
|
||||
thread_local_async_read_io_urings_(thread_local_async_read_io_urings),
|
||||
thread_local_multi_read_io_urings_(thread_local_multi_read_io_urings)
|
||||
#endif
|
||||
{
|
||||
assert(!options.use_direct_reads || !options.use_mmap_reads);
|
||||
@@ -659,6 +661,83 @@ IOStatus PosixRandomAccessFile::Read(uint64_t offset, size_t n,
|
||||
return s;
|
||||
}
|
||||
|
||||
// MultiRead: Perform multiple concurrent read requests using io_uring.
|
||||
//
|
||||
// OVERVIEW:
|
||||
// This function batches multiple read requests and submits them concurrently
|
||||
// to io_uring for improved I/O performance. It operates synchronously from the
|
||||
// caller's perspective (blocks until all reads complete) but uses io_uring's
|
||||
// async capabilities internally for parallel I/O execution.
|
||||
//
|
||||
// IO_URING LIFECYCLE:
|
||||
// 1. Preparation Phase:
|
||||
// - Allocate SQEs (Submission Queue Entries) for read requests
|
||||
// - Limited by: min(pending_work, io_uring_sq_space_left(), kIoUringDepth -
|
||||
// inflight)
|
||||
// - Uses io_uring_sq_space_left() to query available SQ slots
|
||||
// - Each SQE is tracked in wrap_cache for completion matching
|
||||
//
|
||||
// 2. Submission Phase:
|
||||
// - Loop: while io_uring_sq_ready() > 0 (SQEs pending submission)
|
||||
// - Call io_uring_submit_and_wait() to submit SQEs and wait for CQEs
|
||||
// - Handles retryable errors (EINTR, EAGAIN) by continuing
|
||||
// - Breaks on terminal errors (logs error, sets err variable)
|
||||
//
|
||||
// 3. Completion Phase:
|
||||
// - Non-blocking CQE reaping via io_uring_for_each_cqe()
|
||||
// - Matches CQEs to requests using user_data pointer
|
||||
// - Processes results: updates bytes read, handles partial reads
|
||||
// - Removes completed requests from wrap_cache
|
||||
//
|
||||
// 4. Loop Iteration:
|
||||
// - Repeats until: all requests submitted AND all completions reaped
|
||||
// - Termination condition: (num_reqs == reqs_off) &&
|
||||
// resubmit_rq_list.empty() && wrap_cache.empty()
|
||||
//
|
||||
// ERROR HANDLING STRATEGY:
|
||||
// - Retryable submission errors (-EINTR, -EAGAIN): Retry submission
|
||||
// - Memory pressure (-ENOMEM): Mark memory_pressure_on_submission, attempt
|
||||
// recovery
|
||||
// - Terminal submission errors: Break, enter teardown path
|
||||
// - Retryable CQE errors (-EINTR, -EAGAIN): Add to resubmit_rq_list for retry
|
||||
// - Terminal CQE errors: Set ios to IOError, continue processing other CQEs
|
||||
// - Teardown path: If SQEs remain unsubmitted after error, reap submitted CQEs,
|
||||
// destroy io_uring instance, return error
|
||||
//
|
||||
// PARTIAL READ HANDLING:
|
||||
// - Short reads (bytes_read < requested): Request added to resubmit_rq_list
|
||||
// - finished_len tracks cumulative bytes read across resubmissions
|
||||
// - iov.iov_base/iov_len adjusted on each resubmission attempt
|
||||
// - UpdateResult() determines if read should be retried based on:
|
||||
// * Direct I/O alignment requirements
|
||||
// * EOF detection
|
||||
// * Error conditions
|
||||
//
|
||||
// RESUBMISSION LOGIC:
|
||||
// - resubmit_rq_list: Requests needing retry (short reads, EINTR/EAGAIN errors)
|
||||
// - Prioritized in SQE allocation loop: resubmits before new requests
|
||||
// - List cleared after SQE preparation
|
||||
// - Requests remain in wrap_cache across resubmissions until fully complete
|
||||
//
|
||||
// CONCURRENCY CONTROL:
|
||||
// - wrap_cache.size(): Tracks total inflight requests (SQ + CQ)
|
||||
// - io_uring_sq_ready(): Queries SQEs prepared but not yet submitted
|
||||
// - io_uring_sq_space_left(): Queries available SQ slots
|
||||
// - Max concurrency: kIoUringDepth (256)
|
||||
//
|
||||
// ACCOUNTING CORRECTNESS:
|
||||
// - Uses io_uring native APIs (io_uring_sq_ready, io_uring_sq_space_left)
|
||||
// instead of manual counters for robustness
|
||||
// - wrap_cache is the authoritative source for inflight request tracking
|
||||
// - Re-query io_uring_sq_ready() after submission loop to detect
|
||||
// unsubmitted SQEs (indicates submission errors)
|
||||
//
|
||||
// THREAD SAFETY:
|
||||
// - Uses thread-local io_uring instance (thread_local_multi_read_io_urings_)
|
||||
// - IORING_SETUP_SINGLE_ISSUER: Only one thread submits to this ring
|
||||
// - IORING_SETUP_DEFER_TASKRUN: Task work runs in submitting thread
|
||||
// - No cross-thread coordination required
|
||||
//
|
||||
IOStatus PosixRandomAccessFile::MultiRead(FSReadRequest* reqs, size_t num_reqs,
|
||||
const IOOptions& options,
|
||||
IODebugContext* dbg) {
|
||||
@@ -672,12 +751,16 @@ IOStatus PosixRandomAccessFile::MultiRead(FSReadRequest* reqs, size_t num_reqs,
|
||||
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
struct io_uring* iu = nullptr;
|
||||
if (thread_local_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(thread_local_io_urings_->Get());
|
||||
if (thread_local_multi_read_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(
|
||||
thread_local_multi_read_io_urings_->Get());
|
||||
if (iu == nullptr) {
|
||||
iu = CreateIOUring();
|
||||
unsigned int flags = 0;
|
||||
flags |= IORING_SETUP_SINGLE_ISSUER;
|
||||
flags |= IORING_SETUP_DEFER_TASKRUN;
|
||||
iu = CreateIOUring(flags);
|
||||
if (iu != nullptr) {
|
||||
thread_local_io_urings_->Reset(iu);
|
||||
thread_local_multi_read_io_urings_->Reset(iu);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -688,8 +771,6 @@ IOStatus PosixRandomAccessFile::MultiRead(FSReadRequest* reqs, size_t num_reqs,
|
||||
return FSRandomAccessFile::MultiRead(reqs, num_reqs, options, dbg);
|
||||
}
|
||||
|
||||
IOStatus ios = IOStatus::OK();
|
||||
|
||||
struct WrappedReadRequest {
|
||||
FSReadRequest* req;
|
||||
struct iovec iov;
|
||||
@@ -698,118 +779,199 @@ IOStatus PosixRandomAccessFile::MultiRead(FSReadRequest* reqs, size_t num_reqs,
|
||||
};
|
||||
|
||||
autovector<WrappedReadRequest, 32> req_wraps;
|
||||
autovector<WrappedReadRequest*, 4> incomplete_rq_list;
|
||||
autovector<WrappedReadRequest*, 4> resubmit_rq_list;
|
||||
std::unordered_set<WrappedReadRequest*> wrap_cache;
|
||||
|
||||
for (size_t i = 0; i < num_reqs; i++) {
|
||||
req_wraps.emplace_back(&reqs[i]);
|
||||
}
|
||||
|
||||
IOStatus ios = IOStatus::OK();
|
||||
size_t reqs_off = 0;
|
||||
while (num_reqs > reqs_off || !incomplete_rq_list.empty()) {
|
||||
size_t this_reqs = (num_reqs - reqs_off) + incomplete_rq_list.size();
|
||||
|
||||
// If requests exceed depth, split it into batches
|
||||
if (this_reqs > kIoUringDepth) {
|
||||
this_reqs = kIoUringDepth;
|
||||
}
|
||||
|
||||
assert(incomplete_rq_list.size() <= this_reqs);
|
||||
for (size_t i = 0; i < this_reqs; i++) {
|
||||
WrappedReadRequest* rep_to_submit;
|
||||
if (i < incomplete_rq_list.size()) {
|
||||
rep_to_submit = incomplete_rq_list[i];
|
||||
while ((num_reqs > reqs_off) || !resubmit_rq_list.empty() ||
|
||||
!wrap_cache.empty()) {
|
||||
assert(resubmit_rq_list.size() + wrap_cache.size() <= kIoUringDepth);
|
||||
// Total number of requests that still need to be submitted, includes:
|
||||
//
|
||||
// 1) requests NOT yet submitted (num_reqs - reqs_off)
|
||||
// 2) requests on resubmission list (resubmit_rq_list)
|
||||
//
|
||||
// capped by min of the # of remaining entries in IO ring submission queue
|
||||
// and the max IO ring depth less the inflight requests.
|
||||
size_t new_sqe_reqs_count = std::min({
|
||||
num_reqs - reqs_off + resubmit_rq_list.size(),
|
||||
static_cast<size_t>(io_uring_sq_space_left(iu)),
|
||||
kIoUringDepth - wrap_cache.size() // queue depth less inflight requests
|
||||
});
|
||||
for (size_t i = 0; i < new_sqe_reqs_count; i++) {
|
||||
WrappedReadRequest* req;
|
||||
if (i < resubmit_rq_list.size()) {
|
||||
req = resubmit_rq_list[i];
|
||||
} else {
|
||||
rep_to_submit = &req_wraps[reqs_off++];
|
||||
req = &req_wraps[reqs_off++];
|
||||
}
|
||||
assert(rep_to_submit->req->len > rep_to_submit->finished_len);
|
||||
rep_to_submit->iov.iov_base =
|
||||
rep_to_submit->req->scratch + rep_to_submit->finished_len;
|
||||
rep_to_submit->iov.iov_len =
|
||||
rep_to_submit->req->len - rep_to_submit->finished_len;
|
||||
assert(req->req->len > req->finished_len);
|
||||
req->iov.iov_base = req->req->scratch + req->finished_len;
|
||||
req->iov.iov_len = req->req->len - req->finished_len;
|
||||
|
||||
struct io_uring_sqe* sqe;
|
||||
sqe = io_uring_get_sqe(iu);
|
||||
io_uring_prep_readv(
|
||||
sqe, fd_, &rep_to_submit->iov, 1,
|
||||
rep_to_submit->req->offset + rep_to_submit->finished_len);
|
||||
io_uring_sqe_set_data(sqe, rep_to_submit);
|
||||
wrap_cache.emplace(rep_to_submit);
|
||||
// NULL is unexpected as we do maintain proper ring accounting.
|
||||
assert(sqe);
|
||||
io_uring_prep_readv(sqe, fd_, &req->iov, 1,
|
||||
req->req->offset + req->finished_len);
|
||||
io_uring_sqe_set_data(sqe, req);
|
||||
wrap_cache.emplace(req);
|
||||
}
|
||||
incomplete_rq_list.clear();
|
||||
resubmit_rq_list.clear();
|
||||
|
||||
ssize_t ret =
|
||||
io_uring_submit_and_wait(iu, static_cast<unsigned int>(this_reqs));
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_submit_and_wait:return1",
|
||||
&ret);
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_submit_and_wait:return2",
|
||||
iu);
|
||||
|
||||
if (static_cast<size_t>(ret) != this_reqs) {
|
||||
fprintf(stderr, "ret = %ld this_reqs: %ld\n", (long)ret, (long)this_reqs);
|
||||
// If error happens and we submitted fewer than expected, it is an
|
||||
// exception case and we don't retry here. We should still consume
|
||||
// what is is submitted in the ring.
|
||||
for (ssize_t i = 0; i < ret; i++) {
|
||||
struct io_uring_cqe* cqe = nullptr;
|
||||
io_uring_wait_cqe(iu, &cqe);
|
||||
if (cqe != nullptr) {
|
||||
io_uring_cqe_seen(iu, cqe);
|
||||
struct io_uring_cqe* cqe = nullptr;
|
||||
unsigned head;
|
||||
ssize_t err = 0;
|
||||
bool memory_pressure_on_submission = false;
|
||||
unsigned reqs_pending_submission;
|
||||
unsigned reqs_submitted = 0;
|
||||
while ((reqs_pending_submission = io_uring_sq_ready(iu))) {
|
||||
// MultiRead is synchronous in nature. io_uring_submit_and_wait provides
|
||||
// batching semantics (submit + best effort wait in one syscall), while
|
||||
// io_uring_submit enables async producer/consumer semantics (submit
|
||||
// only, requires separate reaping). We chose batching approach to
|
||||
// reduce the volume of syscalls and context switches.
|
||||
ssize_t ret = io_uring_submit_and_wait(iu, reqs_pending_submission);
|
||||
if (ret < 0) {
|
||||
if (-EINTR == ret || -EAGAIN == ret) {
|
||||
// Submission failed due to rare, retryable syscall error. Try again.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return IOStatus::IOError("io_uring_submit_and_wait() requested " +
|
||||
std::to_string(this_reqs) + " but returned " +
|
||||
std::to_string(ret));
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < this_reqs; i++) {
|
||||
struct io_uring_cqe* cqe = nullptr;
|
||||
WrappedReadRequest* req_wrap;
|
||||
|
||||
// We could use the peek variant here, but this seems safer in terms
|
||||
// of our initial wait not reaping all completions
|
||||
ret = io_uring_wait_cqe(iu, &cqe);
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_wait_cqe:return", &ret);
|
||||
if (ret) {
|
||||
ios = IOStatus::IOError("io_uring_wait_cqe() returns " +
|
||||
std::to_string(ret));
|
||||
|
||||
if (cqe != nullptr) {
|
||||
io_uring_cqe_seen(iu, cqe);
|
||||
if (-ENOMEM == ret) {
|
||||
fprintf(stderr,
|
||||
"PosixRandomAccessFile::MultiRead: io_uring_submit_and_wait "
|
||||
"experienced terse memory condition.\n");
|
||||
// Best effort to reclaim resources in terse condition.
|
||||
memory_pressure_on_submission = true;
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
"PosixRandomAccessFile::MultiRead: "
|
||||
"io_uring_submit_and_wait returned terminal error: %zd.\n",
|
||||
ret);
|
||||
err = ret;
|
||||
}
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
|
||||
req_wrap = static_cast<WrappedReadRequest*>(io_uring_cqe_get_data(cqe));
|
||||
// Reset cqe data to catch any stray reuse of it
|
||||
static_cast<struct io_uring_cqe*>(cqe)->user_data = 0xd5d5d5d5d5d5d5d5;
|
||||
// Check that we got a valid unique cqe data
|
||||
auto wrap_check = wrap_cache.find(req_wrap);
|
||||
if (wrap_check == wrap_cache.end()) {
|
||||
if (0 == ret) {
|
||||
// This scenario is unexpected for any modern kernel!
|
||||
// We deliberately error out to avoid bugs around infinite loops.
|
||||
fprintf(stderr,
|
||||
"PosixRandomAccessFile::MultiRead: "
|
||||
"Bad cqe data from IO uring - %p\n",
|
||||
req_wrap);
|
||||
port::PrintStack();
|
||||
ios = IOStatus::IOError("io_uring_cqe_get_data() returned " +
|
||||
std::to_string((uint64_t)req_wrap));
|
||||
continue;
|
||||
"io_uring_submit_and_wait returned 0 submissions!\n");
|
||||
break;
|
||||
}
|
||||
wrap_cache.erase(wrap_check);
|
||||
reqs_submitted += static_cast<unsigned int>(ret);
|
||||
};
|
||||
reqs_pending_submission = io_uring_sq_ready(iu);
|
||||
|
||||
FSReadRequest* req = req_wrap->req;
|
||||
size_t bytes_read = 0;
|
||||
bool read_again = false;
|
||||
UpdateResult(cqe, filename_, req->len, req_wrap->iov.iov_len,
|
||||
false /*async_read*/, use_direct_io(),
|
||||
GetRequiredBufferAlignment(), req_wrap->finished_len, req,
|
||||
bytes_read, read_again);
|
||||
int32_t res = cqe->res;
|
||||
if (res >= 0) {
|
||||
if (bytes_read == 0) {
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_sq_ready:return1",
|
||||
&reqs_pending_submission);
|
||||
|
||||
// Error occurred or IO uring stopped submitting outstanding requests.
|
||||
if (reqs_pending_submission && !memory_pressure_on_submission) {
|
||||
// IO ring is initialized once in thread-local variable and then reused
|
||||
// to handle the consecutive MultiRead API calls. Therefore, it's crucial
|
||||
// to reap all the submitted requests.
|
||||
//
|
||||
// NOTE: Loop will run indefinitely until we reap all the completions!!!
|
||||
size_t nr = 0;
|
||||
assert(reqs_pending_submission <= wrap_cache.size());
|
||||
size_t nr_await_cqe = wrap_cache.size() - reqs_pending_submission;
|
||||
while (nr < nr_await_cqe) {
|
||||
// blocking
|
||||
io_uring_wait_cqes(iu, &cqe,
|
||||
static_cast<unsigned int>(nr_await_cqe - nr),
|
||||
nullptr, nullptr);
|
||||
size_t reaped_cqe_count = 0;
|
||||
io_uring_for_each_cqe(iu, head, cqe) { reaped_cqe_count++; }
|
||||
if (reaped_cqe_count > 0) {
|
||||
io_uring_cq_advance(iu, static_cast<unsigned int>(reaped_cqe_count));
|
||||
nr += reaped_cqe_count;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_submit_and_wait:return2",
|
||||
iu);
|
||||
|
||||
// While all the submitted completions have been reaped successfully,
|
||||
// IO ring submission queue still contains at least one non-submitted
|
||||
// request. Destroy io_uring (discards unsubmitted SQEs).
|
||||
//
|
||||
// NOTE: This is a rare scenario and should not happen in normal cases.
|
||||
// Hence, this should NOT materially impact the performance metrics.
|
||||
io_uring_queue_exit(iu);
|
||||
delete iu;
|
||||
thread_local_multi_read_io_urings_->Reset(nullptr);
|
||||
|
||||
if (err < 0) {
|
||||
return IOStatus::IOError(
|
||||
"io_uring_submit_and_wait() failed with an error " +
|
||||
std::to_string(err));
|
||||
}
|
||||
return IOStatus::IOError(
|
||||
"io_uring_submit_and_wait() requested " +
|
||||
std::to_string(reqs_submitted + reqs_pending_submission) +
|
||||
" but returned " + std::to_string(reqs_submitted));
|
||||
}
|
||||
|
||||
if ((0 == reqs_submitted) && wrap_cache.size() > reqs_pending_submission) {
|
||||
// If no requests have been submitted and there is at least one request
|
||||
// pending completion, wait for at least one completion to arrive.
|
||||
// This is a guardrail to prevent the busy CPU loops.
|
||||
//
|
||||
// NOTE: it's not really a tight CPU-burning loop in the traditional sense
|
||||
// as it's naturally throttled by the io_uring_submit_and_wait() syscall.
|
||||
io_uring_wait_cqe(iu, &cqe);
|
||||
}
|
||||
|
||||
unsigned int nr = 0;
|
||||
io_uring_for_each_cqe(iu, head, cqe) { // non-blocking
|
||||
if (cqe->user_data) { // non-discarded, valid user data only!
|
||||
nr++;
|
||||
WrappedReadRequest* req_wrap =
|
||||
static_cast<WrappedReadRequest*>(io_uring_cqe_get_data(cqe));
|
||||
// Reset cqe data to catch any stray reuse of it
|
||||
static_cast<struct io_uring_cqe*>(cqe)->user_data = 0xd5d5d5d5d5d5d5d5;
|
||||
// Check that we got a valid unique cqe data
|
||||
auto wrap_check = wrap_cache.find(req_wrap);
|
||||
if (wrap_check == wrap_cache.end()) {
|
||||
fprintf(stderr,
|
||||
"PosixRandomAccessFile::MultiRead: "
|
||||
"Bad cqe data from IO uring - %p\n",
|
||||
req_wrap);
|
||||
port::PrintStack();
|
||||
ios = IOStatus::IOError("io_uring_cqe_get_data() returned " +
|
||||
std::to_string((uint64_t)req_wrap));
|
||||
continue;
|
||||
}
|
||||
wrap_cache.erase(wrap_check);
|
||||
if (cqe->res < 0) {
|
||||
if (-EINTR == cqe->res || -EAGAIN == cqe->res) {
|
||||
resubmit_rq_list.push_back(req_wrap);
|
||||
} else {
|
||||
ios = IOStatus::IOError("io_uring_for_each_cqe() returns " +
|
||||
std::to_string(cqe->res));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// cqe->res >= 0
|
||||
FSReadRequest* req = req_wrap->req;
|
||||
size_t bytes_read = 0;
|
||||
bool read_again = false;
|
||||
UpdateResult(cqe, filename_, req->len, req_wrap->iov.iov_len,
|
||||
false /*async_read*/, use_direct_io(),
|
||||
GetRequiredBufferAlignment(), req_wrap->finished_len, req,
|
||||
bytes_read, read_again);
|
||||
|
||||
if (0 == bytes_read) {
|
||||
if (read_again) {
|
||||
Slice tmp_slice;
|
||||
req->status =
|
||||
@@ -819,14 +981,15 @@ IOStatus PosixRandomAccessFile::MultiRead(FSReadRequest* reqs, size_t num_reqs,
|
||||
req->result =
|
||||
Slice(req->scratch, req_wrap->finished_len + tmp_slice.size());
|
||||
}
|
||||
// else It means EOF so no need to do anything.
|
||||
// else it means EOF so no need to do anything.
|
||||
} else if (bytes_read < req_wrap->iov.iov_len) {
|
||||
incomplete_rq_list.push_back(req_wrap);
|
||||
resubmit_rq_list.push_back(req_wrap);
|
||||
}
|
||||
}
|
||||
io_uring_cqe_seen(iu, cqe);
|
||||
}
|
||||
wrap_cache.clear();
|
||||
if (nr > 0) {
|
||||
io_uring_cq_advance(iu, nr);
|
||||
}
|
||||
}
|
||||
return ios;
|
||||
#else
|
||||
@@ -923,12 +1086,16 @@ IOStatus PosixRandomAccessFile::ReadAsync(
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
// io_uring_queue_init.
|
||||
struct io_uring* iu = nullptr;
|
||||
if (thread_local_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(thread_local_io_urings_->Get());
|
||||
if (thread_local_async_read_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(
|
||||
thread_local_async_read_io_urings_->Get());
|
||||
if (iu == nullptr) {
|
||||
iu = CreateIOUring();
|
||||
unsigned int flags = 0;
|
||||
flags |= IORING_SETUP_SINGLE_ISSUER;
|
||||
flags |= IORING_SETUP_DEFER_TASKRUN;
|
||||
iu = CreateIOUring(flags);
|
||||
if (iu != nullptr) {
|
||||
thread_local_io_urings_->Reset(iu);
|
||||
thread_local_async_read_io_urings_->Reset(iu);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -966,11 +1133,35 @@ IOStatus PosixRandomAccessFile::ReadAsync(
|
||||
io_uring_sqe_set_data(sqe, posix_handle);
|
||||
|
||||
// Step 4: io_uring_submit
|
||||
ssize_t ret = io_uring_submit(iu);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "io_uring_submit error: %ld\n", long(ret));
|
||||
return IOStatus::IOError("io_uring_submit() requested but returned " +
|
||||
std::to_string(ret));
|
||||
ssize_t ret;
|
||||
do {
|
||||
ret = io_uring_submit(iu);
|
||||
if (ret < 0) {
|
||||
if (-EINTR == ret || -EAGAIN == ret) {
|
||||
// Submission failed due to transient error. Try again.
|
||||
continue;
|
||||
}
|
||||
fprintf(stderr,
|
||||
"PosixRandomAccessFile::ReadAsync: "
|
||||
"io_uring_submit returned terminal error = %zd\n",
|
||||
ret);
|
||||
break;
|
||||
}
|
||||
if (0 == ret) {
|
||||
// Unexpected. Will be reported as error.
|
||||
break;
|
||||
}
|
||||
} while (ret < 1);
|
||||
if (ret <= 0) {
|
||||
return IOStatus::IOError(
|
||||
"PosixRandomAccessFile::ReadAsync: io_uring_submit() returned " +
|
||||
std::to_string(ret));
|
||||
}
|
||||
if (ret > 1) {
|
||||
fprintf(stderr,
|
||||
"PosixRandomAccessFile::ReadAsync: "
|
||||
"io_uring_submit() returned = %zd\n",
|
||||
ret);
|
||||
}
|
||||
return IOStatus::OK();
|
||||
#else
|
||||
|
||||
Vendored
+16
-4
@@ -11,6 +11,16 @@
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
#include <liburing.h>
|
||||
#include <sys/uio.h>
|
||||
|
||||
// Compatibility defines for io_uring flags that may not be present in older
|
||||
// kernel headers. These values are fixed and won't change, so it's safe to
|
||||
// define them even if the running kernel doesn't support them.
|
||||
#ifndef IORING_SETUP_SINGLE_ISSUER
|
||||
#define IORING_SETUP_SINGLE_ISSUER (1U << 12)
|
||||
#endif
|
||||
#ifndef IORING_SETUP_DEFER_TASKRUN
|
||||
#define IORING_SETUP_DEFER_TASKRUN (1U << 13)
|
||||
#endif
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -297,9 +307,9 @@ inline void DeleteIOUring(void* p) {
|
||||
delete iu;
|
||||
}
|
||||
|
||||
inline struct io_uring* CreateIOUring() {
|
||||
inline struct io_uring* CreateIOUring(unsigned int flags = 0) {
|
||||
struct io_uring* new_io_uring = new struct io_uring;
|
||||
int ret = io_uring_queue_init(kIoUringDepth, new_io_uring, 0);
|
||||
int ret = io_uring_queue_init(kIoUringDepth, new_io_uring, flags);
|
||||
if (ret) {
|
||||
delete new_io_uring;
|
||||
new_io_uring = nullptr;
|
||||
@@ -315,7 +325,8 @@ class PosixRandomAccessFile : public FSRandomAccessFile {
|
||||
bool use_direct_io_;
|
||||
size_t logical_sector_size_;
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
ThreadLocalPtr* thread_local_io_urings_;
|
||||
ThreadLocalPtr* thread_local_async_read_io_urings_;
|
||||
ThreadLocalPtr* thread_local_multi_read_io_urings_;
|
||||
#endif
|
||||
|
||||
public:
|
||||
@@ -323,7 +334,8 @@ class PosixRandomAccessFile : public FSRandomAccessFile {
|
||||
size_t logical_block_size, const EnvOptions& options
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
,
|
||||
ThreadLocalPtr* thread_local_io_urings
|
||||
ThreadLocalPtr* thread_local_async_read_io_urings,
|
||||
ThreadLocalPtr* thread_local_multi_read_io_urings
|
||||
#endif
|
||||
);
|
||||
virtual ~PosixRandomAccessFile();
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# This file contains the vast majority of folly-related build configuration
|
||||
# for the checkout_folly and build_folly targets, so that this file can be
|
||||
# hashed for purposes of caching folly builds and not hitting that cache when
|
||||
# something here changes.
|
||||
|
||||
# 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 ($(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 += $(FOLLY_PATH)/lib/libfolly.a $(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 $(LIBEVENT_PATH)/lib/libevent-2.1.so $(LIBSODIUM_PATH)/lib/libsodium.a -ldl
|
||||
ifneq ($(DEBUG_LEVEL),0)
|
||||
PLATFORM_LDFLAGS += $(FMT_LIB_PATH)/libfmtd.a $(GLOG_LIB_PATH)/libglogd.so $(GFLAGS_PATH)/lib/libgflags_debug.so.2.2
|
||||
else
|
||||
PLATFORM_LDFLAGS += $(FMT_LIB_PATH)/libfmt.a $(GLOG_LIB_PATH)/libglog.so $(GFLAGS_PATH)/lib/libgflags.so.2.2
|
||||
endif
|
||||
PLATFORM_LDFLAGS += -Wl,-rpath=$(LIBEVENT_PATH)/lib -Wl,-rpath=$(GLOG_LIB_PATH) -Wl,-rpath=$(GFLAGS_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
|
||||
ifneq ($(strip $(BOOST_SOURCE_PATH)),)
|
||||
BOOST_INCLUDE = $(shell (ls -d $(BOOST_SOURCE_PATH)/boost*/))
|
||||
# AIX: pre-defined system headers are surrounded by an extern "C" block
|
||||
ifeq ($(PLATFORM), OS_AIX)
|
||||
PLATFORM_CCFLAGS += -I$(BOOST_INCLUDE)
|
||||
PLATFORM_CXXFLAGS += -I$(BOOST_INCLUDE)
|
||||
else
|
||||
PLATFORM_CCFLAGS += -isystem $(BOOST_INCLUDE)
|
||||
PLATFORM_CXXFLAGS += -isystem $(BOOST_INCLUDE)
|
||||
endif
|
||||
endif # BOOST_SOURCE_PATH
|
||||
ifneq ($(strip $(FMT_SOURCE_PATH)),)
|
||||
FMT_INCLUDE = $(shell (ls -d $(FMT_SOURCE_PATH)/fmt*/include/))
|
||||
# AIX: pre-defined system headers are surrounded by an extern "C" block
|
||||
ifeq ($(PLATFORM), OS_AIX)
|
||||
PLATFORM_CCFLAGS += -I$(FMT_INCLUDE)
|
||||
PLATFORM_CXXFLAGS += -I$(FMT_INCLUDE)
|
||||
else
|
||||
PLATFORM_CCFLAGS += -isystem $(FMT_INCLUDE)
|
||||
PLATFORM_CXXFLAGS += -isystem $(FMT_INCLUDE)
|
||||
endif
|
||||
endif # FMT_SOURCE_PATH
|
||||
# AIX: pre-defined system headers are surrounded by an extern "C" block
|
||||
ifeq ($(PLATFORM), OS_AIX)
|
||||
PLATFORM_CCFLAGS += -I$(FOLLY_DIR)
|
||||
PLATFORM_CXXFLAGS += -I$(FOLLY_DIR)
|
||||
else
|
||||
PLATFORM_CCFLAGS += -isystem $(FOLLY_DIR)
|
||||
PLATFORM_CXXFLAGS += -isystem $(FOLLY_DIR)
|
||||
endif
|
||||
PLATFORM_CCFLAGS += -DUSE_FOLLY -DFOLLY_NO_CONFIG
|
||||
PLATFORM_CXXFLAGS += -DUSE_FOLLY -DFOLLY_NO_CONFIG
|
||||
# TODO: fix linking with fbcode compiler config
|
||||
PLATFORM_LDFLAGS += -lglog
|
||||
endif
|
||||
|
||||
FOLLY_COMMIT_HASH = abe68f7e917e8b7a0ee2fe066c972dc98fd35aa1
|
||||
|
||||
# For public CI runs, checkout folly in a way that can build with RocksDB.
|
||||
# This is mostly intended as a test-only simulation of Meta-internal folly
|
||||
# integration.
|
||||
checkout_folly:
|
||||
if [ -e third-party/folly ]; then \
|
||||
cd third-party/folly && ${GIT_COMMAND} fetch origin; \
|
||||
else \
|
||||
cd third-party && ${GIT_COMMAND} clone https://github.com/facebook/folly.git; \
|
||||
fi
|
||||
@# Pin to a particular version for public CI, so that PR authors don't
|
||||
@# need to worry about folly breaking our integration. Update periodically
|
||||
cd third-party/folly && git reset --hard $(FOLLY_COMMIT_HASH)
|
||||
@# Apparently missing include
|
||||
perl -pi -e 's/(#include <atomic>)/$$1\n#include <cstring>/' third-party/folly/folly/lang/Exception.h
|
||||
@# const mismatch
|
||||
perl -pi -e 's/: environ/: (const char**)(environ)/' third-party/folly/folly/Subprocess.cpp
|
||||
@# Use gnu.org mirrors to improve download speed (ftp.gnu.org is often super slow)
|
||||
cd third-party/folly && perl -pi -e 's/ftp.gnu.org/ftpmirror.gnu.org/' `git grep -l ftp.gnu.org` README.md
|
||||
@# NOTE: boost and fmt source will be needed for any build including `USE_FOLLY_LITE` builds as those depend on those headers
|
||||
cd third-party/folly && GETDEPS_USE_WGET=1 $(PYTHON) build/fbcode_builder/getdeps.py fetch boost && GETDEPS_USE_WGET=1 $(PYTHON) build/fbcode_builder/getdeps.py fetch fmt
|
||||
|
||||
CXX_M_FLAGS = $(filter -m%, $(CXXFLAGS))
|
||||
|
||||
FOLLY_BUILD_FLAGS = --no-tests
|
||||
# NOTE: To avoid ODR violations, we must build folly in debug mode iff
|
||||
# building RocksDB in debug mode.
|
||||
ifneq ($(DEBUG_LEVEL),0)
|
||||
FOLLY_BUILD_FLAGS += --build-type Debug
|
||||
endif
|
||||
|
||||
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
|
||||
cd third-party/folly && \
|
||||
CXXFLAGS=" $(CXX_M_FLAGS) -DHAVE_CXX11_ATOMIC " GETDEPS_USE_WGET=1 $(PYTHON) build/fbcode_builder/getdeps.py build $(FOLLY_BUILD_FLAGS)
|
||||
@# In the folly build, glog and gflags are only built as dynamic libraries,
|
||||
@# not static. This patchelf command is needed to reliably have the glog
|
||||
@# library find its dependency gflags, because apparently the rpath of the
|
||||
@# final binary is not used in resolving that transitive dependency.
|
||||
FOLLY_INST_PATH=`cd third-party/folly && $(PYTHON) build/fbcode_builder/getdeps.py show-inst-dir`; \
|
||||
cd "$$FOLLY_INST_PATH" && patchelf --add-rpath $$PWD/../gflags-*/lib ../glog-*/lib*/libglog*.so.*.*.*
|
||||
@@ -90,6 +90,10 @@ class Compressor {
|
||||
return CompressionType::kDisableCompressionOption;
|
||||
}
|
||||
|
||||
// Return a distinct but functionally equivalent Compressor. This is often
|
||||
// needed to implement MaybeCloneSpecialized() in wrapper compressors.
|
||||
virtual std::unique_ptr<Compressor> Clone() const = 0;
|
||||
|
||||
// Utility struct for providing sample data for the compression dictionary.
|
||||
// Potentially extensible by callers of Compressor (but not recommended)
|
||||
struct DictSampleArgs {
|
||||
@@ -121,7 +125,7 @@ class Compressor {
|
||||
// dictionary associated with a returned compressor must be read from
|
||||
// GetSerializedDict().
|
||||
virtual std::unique_ptr<Compressor> MaybeCloneSpecialized(
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) {
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const {
|
||||
// Default implementation: no specialization
|
||||
(void)block_type;
|
||||
(void)dict_samples;
|
||||
@@ -131,6 +135,18 @@ class Compressor {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// A convenience function when a clone is needed and may or may not be
|
||||
// specialized.
|
||||
std::unique_ptr<Compressor> CloneMaybeSpecialized(
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const {
|
||||
auto clone = MaybeCloneSpecialized(block_type, std::move(dict_samples));
|
||||
if (clone == nullptr) {
|
||||
clone = Clone();
|
||||
assert(clone != nullptr);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
// A WorkingArea is an optional structure (both for callers and
|
||||
// implementations) that can enable optimizing repeated compressions by
|
||||
// reusing working space or thread-local tracking of statistics or trends.
|
||||
@@ -473,9 +489,19 @@ class CompressorWrapper : public Compressor {
|
||||
return wrapped_->GetPreferredCompressionType();
|
||||
}
|
||||
|
||||
// NOTE: Clone() not implemented here because it needs to be in the derived
|
||||
// class
|
||||
|
||||
// NOTE: MaybeCloneSpecialized() is only implemented here for convenience
|
||||
// when the wrapped Compressor uses the default implementation of
|
||||
// MaybeCloneSpecialized(). This needs to be overridden if not.
|
||||
std::unique_ptr<Compressor> MaybeCloneSpecialized(
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) override {
|
||||
return wrapped_->MaybeCloneSpecialized(block_type, std::move(dict_samples));
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const override {
|
||||
auto clone =
|
||||
wrapped_->MaybeCloneSpecialized(block_type, std::move(dict_samples));
|
||||
// Assert default no-op MaybeCloneSpecialized()
|
||||
assert(clone == nullptr);
|
||||
return clone;
|
||||
}
|
||||
|
||||
ManagedWorkingArea ObtainWorkingArea() override {
|
||||
|
||||
@@ -528,6 +528,17 @@ struct AdvancedColumnFamilyOptions {
|
||||
// Dynamically changeable through SetOptions() API
|
||||
int target_file_size_multiplier = 1;
|
||||
|
||||
// If true, RocksDB will consider the estimated tail size (filter + index +
|
||||
// meta blocks) when deciding whether to cut a compaction output file. This
|
||||
// helps prevent output files from exceeding the target_file_size_base due to
|
||||
// large tail blocks. When disabled, only the data block size is considered,
|
||||
// which may result in SST files exceeding the target_file_size_base.
|
||||
//
|
||||
// Default: false
|
||||
//
|
||||
// Dynamically changeable through SetOptions() API
|
||||
bool target_file_size_is_upper_bound = false;
|
||||
|
||||
// If true, RocksDB will pick target size of each level dynamically.
|
||||
// We will pick a base level b >= 1. L0 will be directly merged into level b,
|
||||
// instead of always into level 1. Level 1 to b-1 need to be empty.
|
||||
|
||||
@@ -148,6 +148,14 @@ typedef struct rocksdb_statistics_histogram_data_t
|
||||
rocksdb_statistics_histogram_data_t;
|
||||
typedef struct rocksdb_wait_for_compact_options_t
|
||||
rocksdb_wait_for_compact_options_t;
|
||||
|
||||
/* rocksdb_slice_t: Optimized slice type for high-performance C API operations
|
||||
* This struct is ABI-compatible with rocksdb::Slice for zero-copy interop.
|
||||
* Used by slice iterator functions and batched operations. */
|
||||
typedef struct rocksdb_slice_t {
|
||||
const char* data;
|
||||
size_t size;
|
||||
} rocksdb_slice_t;
|
||||
typedef struct rocksdb_flushjobinfo_t rocksdb_flushjobinfo_t;
|
||||
typedef struct rocksdb_compactionjobinfo_t rocksdb_compactionjobinfo_t;
|
||||
typedef struct rocksdb_subcompactionjobinfo_t rocksdb_subcompactionjobinfo_t;
|
||||
@@ -158,6 +166,31 @@ typedef struct rocksdb_writestallinfo_t rocksdb_writestallinfo_t;
|
||||
typedef struct rocksdb_writestallcondition_t rocksdb_writestallcondition_t;
|
||||
typedef struct rocksdb_memtableinfo_t rocksdb_memtableinfo_t;
|
||||
|
||||
// Remote Compaction typedef
|
||||
typedef struct rocksdb_compactionservice_scheduleresponse_t
|
||||
rocksdb_compactionservice_scheduleresponse_t;
|
||||
typedef struct rocksdb_compactionservice_jobinfo_t
|
||||
rocksdb_compactionservice_jobinfo_t;
|
||||
typedef struct rocksdb_compactionservice_t rocksdb_compactionservice_t;
|
||||
typedef struct rocksdb_compaction_service_options_override_t
|
||||
rocksdb_compaction_service_options_override_t;
|
||||
typedef struct rocksdb_open_and_compact_options_t
|
||||
rocksdb_open_and_compact_options_t;
|
||||
typedef rocksdb_compactionservice_scheduleresponse_t* (
|
||||
*rocksdb_compaction_service_schedule_cb)(
|
||||
void* state, const rocksdb_compactionservice_jobinfo_t* info,
|
||||
const char* compaction_service_input, size_t input_len);
|
||||
|
||||
typedef int (*rocksdb_compaction_service_wait_cb)(void* state,
|
||||
const char* scheduled_job_id,
|
||||
char** result,
|
||||
size_t* result_len);
|
||||
|
||||
typedef void (*rocksdb_compaction_service_cancel_awaiting_jobs_cb)(void* state);
|
||||
|
||||
typedef void (*rocksdb_compaction_service_on_installation_cb)(
|
||||
void* state, const char* scheduled_job_id, int status);
|
||||
|
||||
/* DB operations */
|
||||
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_t* rocksdb_open(
|
||||
@@ -610,6 +643,16 @@ extern ROCKSDB_LIBRARY_API void rocksdb_batched_multi_get_cf(
|
||||
const char* const* keys_list, const size_t* keys_list_sizes,
|
||||
rocksdb_pinnableslice_t** values, char** errs, const bool sorted_input);
|
||||
|
||||
/* Batched MultiGet with slice array: Takes rocksdb_slice_t array directly,
|
||||
* avoiding key conversion. faster than rocksdb_batched_multi_get_cf for
|
||||
* operations with many keys. Eliminates overhead of converting keys from
|
||||
* separate pointer+size arrays to Slice objects. */
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_batched_multi_get_cf_slice(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
rocksdb_column_family_handle_t* column_family, size_t num_keys,
|
||||
const rocksdb_slice_t* keys_list, rocksdb_pinnableslice_t** values,
|
||||
char** errs, const bool sorted_input);
|
||||
|
||||
// The value is only allocated (using malloc) and returned if it is found and
|
||||
// value_found isn't NULL. In that case the user is responsible for freeing it.
|
||||
extern ROCKSDB_LIBRARY_API unsigned char rocksdb_key_may_exist(
|
||||
@@ -776,6 +819,18 @@ extern ROCKSDB_LIBRARY_API const char* rocksdb_iter_timestamp(
|
||||
const rocksdb_iterator_t*, size_t* tslen);
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_iter_get_error(
|
||||
const rocksdb_iterator_t*, char** errptr);
|
||||
|
||||
/* Slice iterator functions: Return rocksdb_slice_t directly for better
|
||||
* performance. These functions avoid the overhead of passing output parameters
|
||||
* and provide zero-copy access to key/value/timestamp data. faster than
|
||||
* traditional rocksdb_iter_key/value/timestamp functions. */
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_slice_t
|
||||
rocksdb_iter_key_slice(const rocksdb_iterator_t* iter);
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_slice_t
|
||||
rocksdb_iter_value_slice(const rocksdb_iterator_t* iter);
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_slice_t
|
||||
rocksdb_iter_timestamp_slice(const rocksdb_iterator_t* iter);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_iter_refresh(
|
||||
const rocksdb_iterator_t* iter, char** errptr);
|
||||
|
||||
@@ -3440,6 +3495,197 @@ extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_wait_for_compact_options_get_timeout(
|
||||
rocksdb_wait_for_compact_options_t* opt);
|
||||
|
||||
/* High-performance zero-copy Get variants
|
||||
These functions avoid unnecessary memory allocations and copies.
|
||||
The returned buffer is valid until the handle is destroyed.
|
||||
Bindings should migrate to these for better performance. */
|
||||
|
||||
/* Zero-copy get that returns a handle to pinned data.
|
||||
The data remains valid until rocksdb_pinnable_handle_destroy is called.
|
||||
Returns NULL on error or not found. Check errptr to distinguish. */
|
||||
typedef struct rocksdb_pinnable_handle_t rocksdb_pinnable_handle_t;
|
||||
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_pinnable_handle_t* rocksdb_get_pinned_v2(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options, const char* key,
|
||||
size_t keylen, char** errptr);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_pinnable_handle_t* rocksdb_get_pinned_cf_v2(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
rocksdb_column_family_handle_t* column_family, const char* key,
|
||||
size_t keylen, char** errptr);
|
||||
|
||||
/* Get the data pointer and size from a pinnable handle.
|
||||
The data pointer is valid until the handle is destroyed. */
|
||||
extern ROCKSDB_LIBRARY_API const char* rocksdb_pinnable_handle_get_value(
|
||||
const rocksdb_pinnable_handle_t* handle, size_t* vallen);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_pinnable_handle_destroy(
|
||||
rocksdb_pinnable_handle_t* handle);
|
||||
|
||||
/* Direct get into caller-provided buffer.
|
||||
Returns 1 if value fits in buffer, 0 if buffer too small.
|
||||
Sets *vallen to actual value size.
|
||||
If buffer is too small, no data is copied but *vallen is set. */
|
||||
extern ROCKSDB_LIBRARY_API unsigned char rocksdb_get_into_buffer(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options, const char* key,
|
||||
size_t keylen, char* buffer, size_t buffer_size, size_t* vallen,
|
||||
unsigned char* found, char** errptr);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char rocksdb_get_into_buffer_cf(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
rocksdb_column_family_handle_t* column_family, const char* key,
|
||||
size_t keylen, char* buffer, size_t buffer_size, size_t* vallen,
|
||||
unsigned char* found, char** errptr);
|
||||
|
||||
// Remote compaction
|
||||
enum {
|
||||
rocksdb_compactionservice_jobstatus_success = 0,
|
||||
rocksdb_compactionservice_jobstatus_failure = 1,
|
||||
rocksdb_compactionservice_jobstatus_aborted = 2,
|
||||
rocksdb_compactionservice_jobstatus_use_local = 3,
|
||||
};
|
||||
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_compactionservice_scheduleresponse_t*
|
||||
rocksdb_compactionservice_scheduleresponse_create(const char* scheduled_job_id,
|
||||
int status, char** errptr);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_compactionservice_scheduleresponse_t*
|
||||
rocksdb_compactionservice_scheduleresponse_create_with_status(int status,
|
||||
char** errptr);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_compactionservice_scheduleresponse_getstatus(
|
||||
const rocksdb_compactionservice_scheduleresponse_t* response);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_compactionservice_scheduleresponse_get_scheduled_job_id(
|
||||
const rocksdb_compactionservice_scheduleresponse_t* response, size_t* len);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compactionservice_scheduleresponse_t_destroy(
|
||||
rocksdb_compactionservice_scheduleresponse_t* response);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_compactionservice_jobinfo_t_get_db_name(
|
||||
const rocksdb_compactionservice_jobinfo_t* info, size_t* len);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_compactionservice_jobinfo_t_get_db_id(
|
||||
const rocksdb_compactionservice_jobinfo_t* info, size_t* len);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_compactionservice_jobinfo_t_get_db_session_id(
|
||||
const rocksdb_compactionservice_jobinfo_t* info, size_t* len);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_compactionservice_jobinfo_t_get_cf_name(
|
||||
const rocksdb_compactionservice_jobinfo_t* info, size_t* len);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint32_t
|
||||
rocksdb_compactionservice_jobinfo_t_get_cf_id(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_compactionservice_jobinfo_t_get_job_id(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int rocksdb_compactionservice_jobinfo_t_get_priority(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_compactionservice_jobinfo_t_get_compaction_reason(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_compactionservice_jobinfo_t_get_base_input_level(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_compactionservice_jobinfo_t_get_output_level(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_compactionservice_jobinfo_t_is_full_compaction(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_compactionservice_jobinfo_t_is_manual_compaction(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_compactionservice_jobinfo_t_is_bottommost_level(
|
||||
const rocksdb_compactionservice_jobinfo_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_compactionservice_t*
|
||||
rocksdb_compactionservice_create(
|
||||
void* state, void (*destructor)(void*),
|
||||
rocksdb_compaction_service_schedule_cb schedule, const char* name,
|
||||
rocksdb_compaction_service_wait_cb wait,
|
||||
rocksdb_compaction_service_cancel_awaiting_jobs_cb cancel_awaiting_jobs,
|
||||
rocksdb_compaction_service_on_installation_cb on_installation);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_compaction_service(
|
||||
rocksdb_options_t* options, rocksdb_compactionservice_t* service);
|
||||
|
||||
// CompactionServiceOptionsOverride
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_compaction_service_options_override_t*
|
||||
rocksdb_compaction_service_options_override_create(void);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_destroy(
|
||||
rocksdb_compaction_service_options_override_t* override_options);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_set_env(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_env_t* env);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_set_comparator(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_comparator_t* comparator);
|
||||
|
||||
// Atomic bool management for cancellation
|
||||
// Creates an atomic bool that can be used for cancellation.
|
||||
// User must call rocksdb_open_and_compact_canceled_destroy() to free it.
|
||||
extern ROCKSDB_LIBRARY_API unsigned char*
|
||||
rocksdb_open_and_compact_canceled_create(void);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_open_and_compact_canceled_destroy(
|
||||
unsigned char* canceled);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_open_and_compact_canceled_set(
|
||||
unsigned char* canceled, unsigned char value);
|
||||
|
||||
// OpenAndCompactOptions
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_open_and_compact_options_t*
|
||||
rocksdb_open_and_compact_options_create(void);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_open_and_compact_options_destroy(
|
||||
rocksdb_open_and_compact_options_t* options);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_open_and_compact_options_set_canceled(
|
||||
rocksdb_open_and_compact_options_t* options, unsigned char* canceled);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_open_and_compact_options_set_allow_resumption(
|
||||
rocksdb_open_and_compact_options_t* options,
|
||||
unsigned char allow_resumption);
|
||||
|
||||
// OpenAndCompact - main functions
|
||||
extern ROCKSDB_LIBRARY_API char* rocksdb_open_and_compact(
|
||||
const char* db_path, const char* output_directory, const char* input,
|
||||
size_t input_len, size_t* output_len,
|
||||
const rocksdb_compaction_service_options_override_t* override_options,
|
||||
char** errptr);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API char* rocksdb_open_and_compact_with_options(
|
||||
const rocksdb_open_and_compact_options_t* options, const char* db_path,
|
||||
const char* output_directory, const char* input, size_t input_len,
|
||||
size_t* output_len,
|
||||
const rocksdb_compaction_service_options_override_t* override_options,
|
||||
char** errptr);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* end extern "C" */
|
||||
#endif
|
||||
@@ -284,9 +284,7 @@ class CompactionFilter : public Customizable {
|
||||
std::string* new_value,
|
||||
std::vector<std::pair<std::string, std::string>>* /* new_columns */,
|
||||
std::string* skip_until) const {
|
||||
#ifdef NDEBUG
|
||||
(void)existing_columns;
|
||||
#endif
|
||||
|
||||
assert(!existing_value || !existing_columns);
|
||||
assert(value_type == ValueType::kWideColumnEntity || existing_value);
|
||||
|
||||
@@ -247,15 +247,7 @@ class ManagedPtr {
|
||||
public:
|
||||
ManagedPtr() = default;
|
||||
ManagedPtr(T* ptr, Owner* owner) : ptr_(ptr), owner_(owner) {}
|
||||
~ManagedPtr() {
|
||||
if (ptr_ && owner_) {
|
||||
if constexpr (std::is_member_function_pointer_v<decltype(Fn)>) {
|
||||
(owner_->*Fn)(ptr_);
|
||||
} else {
|
||||
Fn(owner_, ptr_);
|
||||
}
|
||||
}
|
||||
}
|
||||
~ManagedPtr() { Free(); }
|
||||
// No copies
|
||||
ManagedPtr(const ManagedPtr&) = delete;
|
||||
ManagedPtr& operator=(const ManagedPtr&) = delete;
|
||||
@@ -267,6 +259,10 @@ class ManagedPtr {
|
||||
other.owner_ = nullptr;
|
||||
}
|
||||
ManagedPtr& operator=(ManagedPtr&& other) noexcept {
|
||||
if (this == &other) {
|
||||
return *this;
|
||||
}
|
||||
Free();
|
||||
ptr_ = other.ptr_;
|
||||
owner_ = other.owner_;
|
||||
other.ptr_ = nullptr;
|
||||
@@ -284,6 +280,16 @@ class ManagedPtr {
|
||||
private:
|
||||
T* ptr_ = nullptr;
|
||||
Owner* owner_ = nullptr;
|
||||
|
||||
void Free() {
|
||||
if (ptr_ && owner_) {
|
||||
if constexpr (std::is_member_function_pointer_v<decltype(Fn)>) {
|
||||
(owner_->*Fn)(ptr_);
|
||||
} else {
|
||||
Fn(owner_, ptr_);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T, typename comp>
|
||||
|
||||
@@ -35,12 +35,6 @@
|
||||
#include "rocksdb/version.h"
|
||||
#include "rocksdb/wide_columns.h"
|
||||
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#define ROCKSDB_DEPRECATED_FUNC __attribute__((__deprecated__))
|
||||
#elif _WIN32
|
||||
#define ROCKSDB_DEPRECATED_FUNC __declspec(deprecated)
|
||||
#endif
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
struct ColumnFamilyOptions;
|
||||
@@ -1111,9 +1105,12 @@ class DB {
|
||||
// details. For optimal performance, ensure that either all entries in
|
||||
// scan_opts specify the range limit, or none of them do.
|
||||
//
|
||||
// NOTE: iterate_upper_bound in ReadOptions will be ignored. Instead, the
|
||||
// range.limit in ScanOptions is consulted to determine the upper bound key,
|
||||
// if specified.
|
||||
// NOTE: NOT YET SUPPORTED in DBs using user timestamp (see
|
||||
// Comparator::timestamp_size())
|
||||
//
|
||||
// NOTE: iterate_upper_bound in ReadOptions will
|
||||
// be ignored. Instead, the range.limit in ScanOptions is consulted to
|
||||
// determine the upper bound key, if specified.
|
||||
//
|
||||
// Example usage -
|
||||
// std::vector<ScanOptions> scans{{.start = Slice("bar")},
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
#undef LoadLibrary
|
||||
#endif
|
||||
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#if defined(__GNUC__) || defined(__clang__) // ODR-SAFE (essentially)
|
||||
#define ROCKSDB_PRINTF_FORMAT_ATTR(format_param, dots_param) \
|
||||
__attribute__((__format__(__printf__, format_param, dots_param)))
|
||||
#else
|
||||
|
||||
@@ -558,7 +558,7 @@ class FileSystem : public Customizable {
|
||||
}
|
||||
|
||||
// This seems to clash with a macro on Windows, so #undef it here
|
||||
#ifdef DeleteFile
|
||||
#ifdef DeleteFile // ODR-SAFE
|
||||
#undef DeleteFile
|
||||
#endif
|
||||
// Delete the named file.
|
||||
@@ -719,7 +719,7 @@ class FileSystem : public Customizable {
|
||||
const ImmutableDBOptions& db_options) const;
|
||||
|
||||
// This seems to clash with a macro on Windows, so #undef it here
|
||||
#ifdef GetFreeSpace
|
||||
#ifdef GetFreeSpace // ODR-SAFE
|
||||
#undef GetFreeSpace
|
||||
#endif
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
#include "rocksdb/version.h"
|
||||
#include "rocksdb/write_buffer_manager.h"
|
||||
|
||||
#ifdef max
|
||||
#ifdef max // ODR-SAFE
|
||||
#undef max
|
||||
#endif
|
||||
|
||||
|
||||
@@ -15,12 +15,6 @@
|
||||
#include "rocksdb/types.h"
|
||||
#include "rocksdb/wide_columns.h"
|
||||
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#define ROCKSDB_DEPRECATED_FUNC __attribute__((__deprecated__))
|
||||
#elif _WIN32
|
||||
#define ROCKSDB_DEPRECATED_FUNC __declspec(deprecated)
|
||||
#endif
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class Comparator;
|
||||
@@ -117,7 +111,7 @@ class SstFileWriter {
|
||||
// REQUIRES: user_key is after any previously added point (Put/Merge/Delete)
|
||||
// key according to the comparator.
|
||||
// REQUIRES: comparator is *not* timestamp-aware.
|
||||
ROCKSDB_DEPRECATED_FUNC Status Add(const Slice& user_key, const Slice& value);
|
||||
[[deprecated]] Status Add(const Slice& user_key, const Slice& value);
|
||||
|
||||
// Add a Put key with value to currently opened file
|
||||
// REQUIRES: user_key is after any previously added point (Put/Merge/Delete)
|
||||
|
||||
@@ -692,6 +692,9 @@ enum Histograms : uint32_t {
|
||||
// Number of operations per transaction.
|
||||
NUM_OP_PER_TRANSACTION,
|
||||
|
||||
// MultiScan Prefill iterator Prepare cost
|
||||
MULTISCAN_PREPARE_ITERATORS,
|
||||
|
||||
HISTOGRAM_ENUM_MAX
|
||||
};
|
||||
|
||||
|
||||
@@ -22,24 +22,16 @@
|
||||
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
|
||||
#if !defined(NROCKSDB_THREAD_STATUS)
|
||||
#define ROCKSDB_USING_THREAD_STATUS
|
||||
#endif
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// TODO(yhchiang): remove this function once c++14 is available
|
||||
// as std::max will be able to cover this.
|
||||
// Current MS compiler does not support constexpr
|
||||
template <int A, int B>
|
||||
struct constexpr_max {
|
||||
static const int result = (A > B) ? A : B;
|
||||
};
|
||||
|
||||
// A structure that describes the current status of a thread.
|
||||
// The status of active threads can be fetched using
|
||||
// ROCKSDB_NAMESPACE::GetThreadList().
|
||||
struct ThreadStatus {
|
||||
// Whether RocksDB was built with !NROCKSDB_THREAD_STATUS for
|
||||
// ROCKSDB_NAMESPACE::GetThreadList() to be supported.
|
||||
static const bool kEnabled;
|
||||
|
||||
// The type of a thread.
|
||||
enum ThreadType : int {
|
||||
HIGH_PRIORITY = 0, // RocksDB BG thread in high-pri thread pool
|
||||
@@ -102,8 +94,8 @@ struct ThreadStatus {
|
||||
|
||||
// The maximum number of properties of an operation.
|
||||
// This number should be set to the biggest NUM_XXX_PROPERTIES.
|
||||
static const int kNumOperationProperties =
|
||||
constexpr_max<NUM_COMPACTION_PROPERTIES, NUM_FLUSH_PROPERTIES>::result;
|
||||
static constexpr int kNumOperationProperties =
|
||||
std::max(int{NUM_COMPACTION_PROPERTIES}, int{NUM_FLUSH_PROPERTIES});
|
||||
|
||||
// The type used to refer to a thread state.
|
||||
// A state describes lower-level action of a thread
|
||||
|
||||
@@ -621,7 +621,14 @@ class BackupEngineAppendOnlyBase {
|
||||
// The backup will stop ASAP and the call to CreateNewBackup will
|
||||
// return Status::Incomplete(). It will not clean up after itself, but
|
||||
// the state will remain consistent. The state will be cleaned up the
|
||||
// next time you call CreateNewBackup or GarbageCollect.
|
||||
// next time you call CreateNewBackup or GarbageCollect for the same backup
|
||||
// directory on a new BackupEngine object.
|
||||
//
|
||||
// NOTE: This is a one-way operation. Once StopBackup() is called on a
|
||||
// BackupEngine instance, all subsequent backup requests (CreateNewBackup,
|
||||
// CreateNewBackupWithMetadata) will fail with Status::Incomplete().
|
||||
// To create new backups after calling StopBackup(), you must open a new
|
||||
// BackupEngine instance.
|
||||
virtual void StopBackup() = 0;
|
||||
|
||||
// Will delete any files left over from incomplete creation or deletion of
|
||||
|
||||
@@ -68,7 +68,7 @@ class EnvMirror : public EnvWrapper {
|
||||
assert(as == bs);
|
||||
return as;
|
||||
}
|
||||
#if defined(_MSC_VER)
|
||||
#if defined(_MSC_VER) // ODR-SAFE
|
||||
#pragma warning(push)
|
||||
// logical operation on address of string constant
|
||||
#pragma warning(disable : 4130)
|
||||
@@ -87,7 +87,7 @@ class EnvMirror : public EnvWrapper {
|
||||
*r = ar;
|
||||
return as;
|
||||
}
|
||||
#if defined(_MSC_VER)
|
||||
#if defined(_MSC_VER) // ODR-SAFE
|
||||
#pragma warning(pop)
|
||||
#endif
|
||||
Status DeleteFile(const std::string& f) override {
|
||||
|
||||
@@ -9,10 +9,6 @@
|
||||
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
|
||||
#ifdef FAILED
|
||||
#undef FAILED
|
||||
#endif
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class LDBCommandExecuteResult {
|
||||
|
||||
@@ -6,19 +6,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
// Try to migrate DB created with old_opts to be use new_opts.
|
||||
// Multiple column families is not supported.
|
||||
// It is best-effort. No guarantee to succeed.
|
||||
// A full compaction may be executed.
|
||||
// Prepares a database to be compatible with new_opts after using old_opts.
|
||||
// Restructures the LSM tree but does NOT apply new_opts - you must call
|
||||
// DB::Open(new_opts, dbname) afterward to actually use the new configuration.
|
||||
// It is best-effort with no guarantee to succeed. A full compaction may be
|
||||
// executed.
|
||||
//
|
||||
// Limitations: single column family only
|
||||
//
|
||||
// WARNING: using this to migrate from non-FIFO to FIFO compaction
|
||||
// with `Options::compaction_options_fifo.max_table_files_size` > 0 can cause
|
||||
// the whole DB to be dropped right after migration if the migrated data is
|
||||
// larger than `max_table_files_size`
|
||||
Status OptionChangeMigration(std::string dbname, const Options& old_opts,
|
||||
Status OptionChangeMigration(const std::string& dbname, const Options& old_opts,
|
||||
const Options& new_opts);
|
||||
|
||||
// Multi-CF version: Prepares a database with multiple column families to be
|
||||
// compatible with new options after using old options.
|
||||
//
|
||||
// REQUIREMENTS:
|
||||
// - old_cf_descs and new_cf_descs MUST have the same number of CFs
|
||||
// - old_cf_descs and new_cf_descs MUST have the same CF names IN THE SAME ORDER
|
||||
// - Adding or dropping CFs is NOT supported - use CreateColumnFamily() or
|
||||
// DropColumnFamily() separately before/after migration
|
||||
//
|
||||
// The function will return InvalidArgument status if these requirements are
|
||||
// violated.
|
||||
//
|
||||
// WARNING: using this to migrate from non-FIFO to FIFO compaction
|
||||
// with `max_table_files_size` > 0 can cause the whole DB to be dropped right
|
||||
// after migration if the migrated data is larger than `max_table_files_size`
|
||||
Status OptionChangeMigration(
|
||||
const std::string& dbname, const DBOptions& old_db_opts,
|
||||
const std::vector<ColumnFamilyDescriptor>& old_cf_descs,
|
||||
const DBOptions& new_db_opts,
|
||||
const std::vector<ColumnFamilyDescriptor>& new_cf_descs);
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -12,8 +12,8 @@
|
||||
// NOTE: in 'main' development branch, this should be the *next*
|
||||
// minor or major version number planned for release.
|
||||
#define ROCKSDB_MAJOR 10
|
||||
#define ROCKSDB_MINOR 8
|
||||
#define ROCKSDB_PATCH 3
|
||||
#define ROCKSDB_MINOR 10
|
||||
#define ROCKSDB_PATCH 1
|
||||
|
||||
// Make it easy to do conditional compilation based on version checks, i.e.
|
||||
// #if ROCKSDB_VERSION_GE(4, 5, 6)
|
||||
|
||||
+57
-30
@@ -1210,6 +1210,9 @@ jint Java_org_rocksdb_RocksDB_getDirect(JNIEnv* env, jclass /*jdb*/,
|
||||
db->DefaultColumnFamily(), key.slice(), &value.pinnable_slice());
|
||||
}
|
||||
|
||||
if (s.IsNotFound()) {
|
||||
return ROCKSDB_NAMESPACE::KVException::kNotFound;
|
||||
}
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(env, s);
|
||||
return value.Fetch();
|
||||
} catch (ROCKSDB_NAMESPACE::KVException& e) {
|
||||
@@ -1453,10 +1456,13 @@ jbyteArray Java_org_rocksdb_RocksDB_get__J_3BII(JNIEnv* env, jclass,
|
||||
try {
|
||||
ROCKSDB_NAMESPACE::JByteArraySlice key(env, jkey, jkey_off, jkey_len);
|
||||
ROCKSDB_NAMESPACE::JByteArrayPinnableSlice value(env);
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(
|
||||
env,
|
||||
ROCKSDB_NAMESPACE::Status s =
|
||||
db->Get(ROCKSDB_NAMESPACE::ReadOptions(), db->DefaultColumnFamily(),
|
||||
key.slice(), &value.pinnable_slice()));
|
||||
key.slice(), &value.pinnable_slice());
|
||||
if (s.IsNotFound()) {
|
||||
return nullptr;
|
||||
}
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(env, s);
|
||||
return value.NewByteArray();
|
||||
|
||||
} catch (ROCKSDB_NAMESPACE::KVException&) {
|
||||
@@ -1484,9 +1490,13 @@ jbyteArray Java_org_rocksdb_RocksDB_get__J_3BIIJ(JNIEnv* env, jclass,
|
||||
try {
|
||||
ROCKSDB_NAMESPACE::JByteArraySlice key(env, jkey, jkey_off, jkey_len);
|
||||
ROCKSDB_NAMESPACE::JByteArrayPinnableSlice value(env);
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(
|
||||
env, db->Get(ROCKSDB_NAMESPACE::ReadOptions(), cf_handle, key.slice(),
|
||||
&value.pinnable_slice()));
|
||||
ROCKSDB_NAMESPACE::Status s =
|
||||
db->Get(ROCKSDB_NAMESPACE::ReadOptions(), cf_handle, key.slice(),
|
||||
&value.pinnable_slice());
|
||||
if (s.IsNotFound()) {
|
||||
return nullptr;
|
||||
}
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(env, s);
|
||||
return value.NewByteArray();
|
||||
|
||||
} catch (ROCKSDB_NAMESPACE::KVException&) {
|
||||
@@ -1509,11 +1519,13 @@ jbyteArray Java_org_rocksdb_RocksDB_get__JJ_3BII(JNIEnv* env, jclass,
|
||||
try {
|
||||
ROCKSDB_NAMESPACE::JByteArraySlice key(env, jkey, jkey_off, jkey_len);
|
||||
ROCKSDB_NAMESPACE::JByteArrayPinnableSlice value(env);
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(
|
||||
env,
|
||||
db->Get(
|
||||
*reinterpret_cast<ROCKSDB_NAMESPACE::ReadOptions*>(jropt_handle),
|
||||
db->DefaultColumnFamily(), key.slice(), &value.pinnable_slice()));
|
||||
ROCKSDB_NAMESPACE::Status s = db->Get(
|
||||
*reinterpret_cast<ROCKSDB_NAMESPACE::ReadOptions*>(jropt_handle),
|
||||
db->DefaultColumnFamily(), key.slice(), &value.pinnable_slice());
|
||||
if (s.IsNotFound()) {
|
||||
return nullptr;
|
||||
}
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(env, s);
|
||||
return value.NewByteArray();
|
||||
} catch (ROCKSDB_NAMESPACE::KVException&) {
|
||||
return nullptr;
|
||||
@@ -1538,10 +1550,13 @@ jbyteArray Java_org_rocksdb_RocksDB_get__JJ_3BIIJ(
|
||||
try {
|
||||
ROCKSDB_NAMESPACE::JByteArraySlice key(env, jkey, jkey_off, jkey_len);
|
||||
ROCKSDB_NAMESPACE::JByteArrayPinnableSlice value(env);
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(
|
||||
env, db->Get(*reinterpret_cast<ROCKSDB_NAMESPACE::ReadOptions*>(
|
||||
jropt_handle),
|
||||
cf_handle, key.slice(), &value.pinnable_slice()));
|
||||
ROCKSDB_NAMESPACE::Status s = db->Get(
|
||||
*reinterpret_cast<ROCKSDB_NAMESPACE::ReadOptions*>(jropt_handle),
|
||||
cf_handle, key.slice(), &value.pinnable_slice());
|
||||
if (s.IsNotFound()) {
|
||||
return nullptr;
|
||||
}
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(env, s);
|
||||
return value.NewByteArray();
|
||||
} catch (ROCKSDB_NAMESPACE::KVException&) {
|
||||
return nullptr;
|
||||
@@ -1563,10 +1578,13 @@ jint Java_org_rocksdb_RocksDB_get__J_3BII_3BII(JNIEnv* env, jclass,
|
||||
ROCKSDB_NAMESPACE::JByteArraySlice key(env, jkey, jkey_off, jkey_len);
|
||||
ROCKSDB_NAMESPACE::JByteArrayPinnableSlice value(env, jval, jval_off,
|
||||
jval_len);
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(
|
||||
env,
|
||||
ROCKSDB_NAMESPACE::Status s =
|
||||
db->Get(ROCKSDB_NAMESPACE::ReadOptions(), db->DefaultColumnFamily(),
|
||||
key.slice(), &value.pinnable_slice()));
|
||||
key.slice(), &value.pinnable_slice());
|
||||
if (s.IsNotFound()) {
|
||||
return ROCKSDB_NAMESPACE::KVException::kNotFound;
|
||||
}
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(env, s);
|
||||
return value.Fetch();
|
||||
|
||||
} catch (ROCKSDB_NAMESPACE::KVException& e) {
|
||||
@@ -1595,9 +1613,13 @@ jint Java_org_rocksdb_RocksDB_get__J_3BII_3BIIJ(JNIEnv* env, jclass,
|
||||
ROCKSDB_NAMESPACE::JByteArraySlice key(env, jkey, jkey_off, jkey_len);
|
||||
ROCKSDB_NAMESPACE::JByteArrayPinnableSlice value(env, jval, jval_off,
|
||||
jval_len);
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(
|
||||
env, db->Get(ROCKSDB_NAMESPACE::ReadOptions(), cf_handle, key.slice(),
|
||||
&value.pinnable_slice()));
|
||||
ROCKSDB_NAMESPACE::Status s =
|
||||
db->Get(ROCKSDB_NAMESPACE::ReadOptions(), cf_handle, key.slice(),
|
||||
&value.pinnable_slice());
|
||||
if (s.IsNotFound()) {
|
||||
return ROCKSDB_NAMESPACE::KVException::kNotFound;
|
||||
}
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(env, s);
|
||||
return value.Fetch();
|
||||
|
||||
} catch (ROCKSDB_NAMESPACE::KVException& e) {
|
||||
@@ -1621,11 +1643,13 @@ jint Java_org_rocksdb_RocksDB_get__JJ_3BII_3BII(JNIEnv* env, jclass,
|
||||
ROCKSDB_NAMESPACE::JByteArraySlice key(env, jkey, jkey_off, jkey_len);
|
||||
ROCKSDB_NAMESPACE::JByteArrayPinnableSlice value(env, jval, jval_off,
|
||||
jval_len);
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(
|
||||
env,
|
||||
db->Get(
|
||||
*reinterpret_cast<ROCKSDB_NAMESPACE::ReadOptions*>(jropt_handle),
|
||||
db->DefaultColumnFamily(), key.slice(), &value.pinnable_slice()));
|
||||
ROCKSDB_NAMESPACE::Status s = db->Get(
|
||||
*reinterpret_cast<ROCKSDB_NAMESPACE::ReadOptions*>(jropt_handle),
|
||||
db->DefaultColumnFamily(), key.slice(), &value.pinnable_slice());
|
||||
if (s.IsNotFound()) {
|
||||
return ROCKSDB_NAMESPACE::KVException::kNotFound;
|
||||
}
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(env, s);
|
||||
return value.Fetch();
|
||||
|
||||
} catch (ROCKSDB_NAMESPACE::KVException& e) {
|
||||
@@ -1652,10 +1676,13 @@ jint Java_org_rocksdb_RocksDB_get__JJ_3BII_3BIIJ(
|
||||
ROCKSDB_NAMESPACE::JByteArraySlice key(env, jkey, jkey_off, jkey_len);
|
||||
ROCKSDB_NAMESPACE::JByteArrayPinnableSlice value(env, jval, jval_off,
|
||||
jval_len);
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(
|
||||
env, db->Get(*reinterpret_cast<ROCKSDB_NAMESPACE::ReadOptions*>(
|
||||
jropt_handle),
|
||||
cf_handle, key.slice(), &value.pinnable_slice()));
|
||||
ROCKSDB_NAMESPACE::Status s = db->Get(
|
||||
*reinterpret_cast<ROCKSDB_NAMESPACE::ReadOptions*>(jropt_handle),
|
||||
cf_handle, key.slice(), &value.pinnable_slice());
|
||||
if (s.IsNotFound()) {
|
||||
return ROCKSDB_NAMESPACE::KVException::kNotFound;
|
||||
}
|
||||
ROCKSDB_NAMESPACE::KVException::ThrowOnError(env, s);
|
||||
return value.Fetch();
|
||||
|
||||
} catch (ROCKSDB_NAMESPACE::KVException& e) {
|
||||
|
||||
@@ -352,6 +352,8 @@ const std::vector<std::pair<Histograms, std::string>> HistogramsNameMap = {
|
||||
{TABLE_OPEN_PREFETCH_TAIL_READ_BYTES,
|
||||
"rocksdb.table.open.prefetch.tail.read.bytes"},
|
||||
{NUM_OP_PER_TRANSACTION, "rocksdb.num.op.per.transaction"},
|
||||
{MULTISCAN_PREPARE_ITERATORS,
|
||||
"rocksdb.multiscan.op.prepare.iterators.micros"},
|
||||
};
|
||||
|
||||
std::shared_ptr<Statistics> CreateDBStatistics() {
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
#ifndef NROCKSDB_THREAD_STATUS
|
||||
const bool ThreadStatus::kEnabled = true;
|
||||
|
||||
std::string ThreadStatus::GetThreadTypeName(
|
||||
ThreadStatus::ThreadType thread_type) {
|
||||
switch (thread_type) {
|
||||
@@ -117,6 +119,7 @@ std::map<std::string, uint64_t> ThreadStatus::InterpretOperationProperties(
|
||||
}
|
||||
|
||||
#else
|
||||
const bool ThreadStatus::kEnabled = false;
|
||||
|
||||
std::string ThreadStatus::GetThreadTypeName(
|
||||
ThreadStatus::ThreadType /*thread_type*/) {
|
||||
@@ -159,5 +162,5 @@ std::map<std::string, uint64_t> ThreadStatus::InterpretOperationProperties(
|
||||
return std::map<std::string, uint64_t>();
|
||||
}
|
||||
|
||||
#endif // ROCKSDB_USING_THREAD_STATUS
|
||||
#endif // !NROCKSDB_THREAD_STATUS
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
#ifndef NROCKSDB_THREAD_STATUS
|
||||
|
||||
thread_local ThreadStatusData* ThreadStatusUpdater::thread_status_data_ =
|
||||
nullptr;
|
||||
@@ -324,5 +324,5 @@ void ThreadStatusUpdater::SetThreadOperationProperty(int /*i*/,
|
||||
void ThreadStatusUpdater::IncreaseThreadOperationProperty(int /*i*/,
|
||||
uint64_t /*delta*/) {}
|
||||
|
||||
#endif // ROCKSDB_USING_THREAD_STATUS
|
||||
#endif // !NROCKSDB_THREAD_STATUS
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -47,7 +47,7 @@ class ColumnFamilyHandle;
|
||||
|
||||
// The structure that keeps constant information about a column family.
|
||||
struct ConstantColumnFamilyInfo {
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
#ifndef NROCKSDB_THREAD_STATUS
|
||||
public:
|
||||
ConstantColumnFamilyInfo(const void* _db_key, const std::string& _db_name,
|
||||
const std::string& _cf_name)
|
||||
@@ -55,13 +55,13 @@ struct ConstantColumnFamilyInfo {
|
||||
const void* db_key;
|
||||
const std::string db_name;
|
||||
const std::string cf_name;
|
||||
#endif // ROCKSDB_USING_THREAD_STATUS
|
||||
#endif // !NROCKSDB_THREAD_STATUS
|
||||
};
|
||||
|
||||
// the internal data-structure that is used to reflect the current
|
||||
// status of a thread using a set of atomic pointers.
|
||||
struct ThreadStatusData {
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
#ifndef NROCKSDB_THREAD_STATUS
|
||||
explicit ThreadStatusData() {
|
||||
enable_tracking.store(false);
|
||||
thread_id.store(0);
|
||||
@@ -86,7 +86,7 @@ struct ThreadStatusData {
|
||||
std::atomic<ThreadStatus::OperationStage> operation_stage;
|
||||
std::atomic<uint64_t> op_properties[ThreadStatus::kNumOperationProperties];
|
||||
std::atomic<ThreadStatus::StateType> state_type;
|
||||
#endif // ROCKSDB_USING_THREAD_STATUS
|
||||
#endif // !NROCKSDB_THREAD_STATUS
|
||||
};
|
||||
|
||||
// The class that stores and updates the status of the current thread
|
||||
@@ -190,7 +190,7 @@ class ThreadStatusUpdater {
|
||||
const std::vector<ColumnFamilyHandle*>& handles, bool check_exist);
|
||||
|
||||
protected:
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
#ifndef NROCKSDB_THREAD_STATUS
|
||||
// The thread-local variable for storing thread status.
|
||||
static thread_local ThreadStatusData* thread_status_data_;
|
||||
|
||||
@@ -220,7 +220,7 @@ class ThreadStatusUpdater {
|
||||
|
||||
#else
|
||||
static ThreadStatusData* thread_status_data_;
|
||||
#endif // ROCKSDB_USING_THREAD_STATUS
|
||||
#endif // !NROCKSDB_THREAD_STATUS
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -12,7 +12,7 @@
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
#ifndef NDEBUG
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
#ifndef NROCKSDB_THREAD_STATUS
|
||||
void ThreadStatusUpdater::TEST_VerifyColumnFamilyInfoMap(
|
||||
const std::vector<ColumnFamilyHandle*>& handles, bool check_exist) {
|
||||
std::unique_lock<std::mutex> lock(thread_list_mutex_);
|
||||
@@ -37,7 +37,7 @@ void ThreadStatusUpdater::TEST_VerifyColumnFamilyInfoMap(
|
||||
const std::vector<ColumnFamilyHandle*>& /*handles*/, bool /*check_exist*/) {
|
||||
}
|
||||
|
||||
#endif // ROCKSDB_USING_THREAD_STATUS
|
||||
#endif // !NROCKSDB_THREAD_STATUS
|
||||
#endif // !NDEBUG
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
#ifndef NROCKSDB_THREAD_STATUS
|
||||
thread_local ThreadStatusUpdater*
|
||||
ThreadStatusUtil::thread_updater_local_cache_ = nullptr;
|
||||
thread_local bool ThreadStatusUtil::thread_updater_initialized_ = false;
|
||||
@@ -171,9 +171,10 @@ AutoThreadOperationStageUpdater::~AutoThreadOperationStageUpdater() {
|
||||
ThreadStatusUpdater* ThreadStatusUtil::thread_updater_local_cache_ = nullptr;
|
||||
bool ThreadStatusUtil::thread_updater_initialized_ = false;
|
||||
|
||||
bool ThreadStatusUtil::MaybeInitThreadLocalUpdater(const Env* /*env*/) {
|
||||
return false;
|
||||
}
|
||||
void ThreadStatusUtil::RegisterThread(
|
||||
const Env* /*env*/, ThreadStatus::ThreadType /*thread_type*/) {}
|
||||
|
||||
void ThreadStatusUtil::UnregisterThread() {}
|
||||
|
||||
void ThreadStatusUtil::SetEnableTracking(bool /*enable_tracking*/) {}
|
||||
|
||||
@@ -204,11 +205,15 @@ void ThreadStatusUtil::EraseDatabaseInfo(const DB* /*db*/) {}
|
||||
|
||||
void ThreadStatusUtil::ResetThreadStatus() {}
|
||||
|
||||
bool ThreadStatusUtil::MaybeInitThreadLocalUpdater(const Env* /*env*/) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AutoThreadOperationStageUpdater::AutoThreadOperationStageUpdater(
|
||||
ThreadStatus::OperationStage /*stage*/) {}
|
||||
|
||||
AutoThreadOperationStageUpdater::~AutoThreadOperationStageUpdater() {}
|
||||
|
||||
#endif // ROCKSDB_USING_THREAD_STATUS
|
||||
#endif // !NROCKSDB_THREAD_STATUS
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -90,7 +90,7 @@ class ThreadStatusUtil {
|
||||
// a non-null pointer.
|
||||
static bool MaybeInitThreadLocalUpdater(const Env* env);
|
||||
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
#ifndef NROCKSDB_THREAD_STATUS
|
||||
// A boolean flag indicating whether thread_updater_local_cache_
|
||||
// is initialized. It is set to true when an Env uses any
|
||||
// ThreadStatusUtil functions using the current thread other
|
||||
@@ -130,7 +130,7 @@ class AutoThreadOperationStageUpdater {
|
||||
explicit AutoThreadOperationStageUpdater(ThreadStatus::OperationStage stage);
|
||||
~AutoThreadOperationStageUpdater();
|
||||
|
||||
#ifdef ROCKSDB_USING_THREAD_STATUS
|
||||
#ifndef NROCKSDB_THREAD_STATUS
|
||||
private:
|
||||
ThreadStatus::OperationStage prev_stage_;
|
||||
#endif
|
||||
|
||||
+11
-3
@@ -454,6 +454,10 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
||||
{offsetof(struct MutableCFOptions, target_file_size_multiplier),
|
||||
OptionType::kInt, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kMutable}},
|
||||
{"target_file_size_is_upper_bound",
|
||||
{offsetof(struct MutableCFOptions, target_file_size_is_upper_bound),
|
||||
OptionType::kBoolean, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kMutable}},
|
||||
{"arena_block_size",
|
||||
{offsetof(struct MutableCFOptions, arena_block_size),
|
||||
OptionType::kSizeT, OptionVerificationType::kNormal,
|
||||
@@ -1074,10 +1078,12 @@ uint64_t MultiplyCheckOverflow(uint64_t op1, double op2) {
|
||||
if (op1 == 0 || op2 <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (std::numeric_limits<uint64_t>::max() / op1 < op2) {
|
||||
return op1;
|
||||
|
||||
if (op1 * op2 < static_cast<double>(std::numeric_limits<uint64_t>::max())) {
|
||||
return static_cast<uint64_t>(op1 * op2);
|
||||
}
|
||||
return static_cast<uint64_t>(op1 * op2);
|
||||
|
||||
return op1;
|
||||
}
|
||||
|
||||
// when level_compaction_dynamic_level_bytes is true and leveled compaction
|
||||
@@ -1172,6 +1178,8 @@ void MutableCFOptions::Dump(Logger* log) const {
|
||||
target_file_size_base);
|
||||
ROCKS_LOG_INFO(log, " target_file_size_multiplier: %d",
|
||||
target_file_size_multiplier);
|
||||
ROCKS_LOG_INFO(log, " target_file_size_is_upper_bound: %d",
|
||||
target_file_size_is_upper_bound);
|
||||
ROCKS_LOG_INFO(log, " max_bytes_for_level_base: %" PRIu64,
|
||||
max_bytes_for_level_base);
|
||||
ROCKS_LOG_INFO(log, " max_bytes_for_level_multiplier: %f",
|
||||
|
||||
@@ -132,6 +132,8 @@ struct MutableCFOptions {
|
||||
max_compaction_bytes(options.max_compaction_bytes),
|
||||
target_file_size_base(options.target_file_size_base),
|
||||
target_file_size_multiplier(options.target_file_size_multiplier),
|
||||
target_file_size_is_upper_bound(
|
||||
options.target_file_size_is_upper_bound),
|
||||
max_bytes_for_level_base(options.max_bytes_for_level_base),
|
||||
max_bytes_for_level_multiplier(options.max_bytes_for_level_multiplier),
|
||||
ttl(options.ttl),
|
||||
@@ -207,6 +209,7 @@ struct MutableCFOptions {
|
||||
max_compaction_bytes(0),
|
||||
target_file_size_base(0),
|
||||
target_file_size_multiplier(0),
|
||||
target_file_size_is_upper_bound(false),
|
||||
max_bytes_for_level_base(0),
|
||||
max_bytes_for_level_multiplier(0),
|
||||
ttl(0),
|
||||
@@ -306,6 +309,7 @@ struct MutableCFOptions {
|
||||
uint64_t max_compaction_bytes;
|
||||
uint64_t target_file_size_base;
|
||||
int target_file_size_multiplier;
|
||||
bool target_file_size_is_upper_bound;
|
||||
uint64_t max_bytes_for_level_base;
|
||||
double max_bytes_for_level_multiplier;
|
||||
uint64_t ttl;
|
||||
|
||||
@@ -63,6 +63,7 @@ AdvancedColumnFamilyOptions::AdvancedColumnFamilyOptions(const Options& options)
|
||||
level0_stop_writes_trigger(options.level0_stop_writes_trigger),
|
||||
target_file_size_base(options.target_file_size_base),
|
||||
target_file_size_multiplier(options.target_file_size_multiplier),
|
||||
target_file_size_is_upper_bound(options.target_file_size_is_upper_bound),
|
||||
level_compaction_dynamic_level_bytes(
|
||||
options.level_compaction_dynamic_level_bytes),
|
||||
max_bytes_for_level_multiplier(options.max_bytes_for_level_multiplier),
|
||||
@@ -269,6 +270,9 @@ void ColumnFamilyOptions::Dump(Logger* log) const {
|
||||
target_file_size_base);
|
||||
ROCKS_LOG_HEADER(log, " Options.target_file_size_multiplier: %d",
|
||||
target_file_size_multiplier);
|
||||
ROCKS_LOG_HEADER(log,
|
||||
" Options.target_file_size_is_upper_bound: %d",
|
||||
target_file_size_is_upper_bound);
|
||||
ROCKS_LOG_HEADER(log,
|
||||
" Options.max_bytes_for_level_base: %" PRIu64,
|
||||
max_bytes_for_level_base);
|
||||
|
||||
@@ -254,6 +254,8 @@ void UpdateColumnFamilyOptions(const MutableCFOptions& moptions,
|
||||
cf_opts->max_compaction_bytes = moptions.max_compaction_bytes;
|
||||
cf_opts->target_file_size_base = moptions.target_file_size_base;
|
||||
cf_opts->target_file_size_multiplier = moptions.target_file_size_multiplier;
|
||||
cf_opts->target_file_size_is_upper_bound =
|
||||
moptions.target_file_size_is_upper_bound;
|
||||
cf_opts->max_bytes_for_level_base = moptions.max_bytes_for_level_base;
|
||||
cf_opts->max_bytes_for_level_multiplier =
|
||||
moptions.max_bytes_for_level_multiplier;
|
||||
|
||||
@@ -611,6 +611,7 @@ TEST_F(OptionsSettableTest, ColumnFamilyOptionsAllFieldsSettable) {
|
||||
"max_sequential_skip_in_iterations=4294971408;"
|
||||
"arena_block_size=1893;"
|
||||
"target_file_size_multiplier=35;"
|
||||
"target_file_size_is_upper_bound=false;"
|
||||
"min_write_buffer_number_to_merge=9;"
|
||||
"max_write_buffer_number=84;"
|
||||
"write_buffer_size=1653;"
|
||||
|
||||
@@ -590,7 +590,6 @@ TEST_MAIN_SOURCES = \
|
||||
table/block_based/block_based_table_reader_test.cc \
|
||||
table/block_based/block_test.cc \
|
||||
table/block_based/data_block_hash_index_test.cc \
|
||||
table/block_based/index_builder_test.cc \
|
||||
table/block_based/full_filter_block_test.cc \
|
||||
table/block_based/partitioned_filter_block_test.cc \
|
||||
table/cleanable_test.cc \
|
||||
|
||||
@@ -110,6 +110,19 @@ FilterBlockBuilder* CreateFilterBlockBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
// A convenience function for populating the Compressor* fields; see ~Rep()
|
||||
Compressor* MaybeCloneSpecialized(
|
||||
Compressor* compressor, CacheEntryRole block_type,
|
||||
Compressor::DictSampleArgs&& dict_samples = {}) {
|
||||
auto specialized =
|
||||
compressor->MaybeCloneSpecialized(block_type, std::move(dict_samples));
|
||||
if (specialized) {
|
||||
// Caller is responsible for freeing when distinct
|
||||
return specialized.release();
|
||||
} else {
|
||||
return compressor;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// kBlockBasedTableMagicNumber was picked by running
|
||||
@@ -824,15 +837,17 @@ struct BlockBasedTableBuilder::Rep {
|
||||
|
||||
// *** Compressors & decompressors - Yes, it seems like a lot here but ***
|
||||
// *** these are distinct fields to minimize extra conditionals and ***
|
||||
// *** field reads on hot code paths. ***
|
||||
// *** field reads on hot code paths. And to avoid interlocked ***
|
||||
// *** instructions associated with shared_ptr. ***
|
||||
|
||||
// A compressor for blocks in general, without dictionary compression
|
||||
std::unique_ptr<Compressor> basic_compressor;
|
||||
// A compressor using dictionary compression (when applicable)
|
||||
std::unique_ptr<Compressor> compressor_with_dict;
|
||||
// Once configured/determined, points to one of the above Compressors to
|
||||
// use on data blocks.
|
||||
Compressor* data_block_compressor = nullptr;
|
||||
// A compressor for data blocks, which might be tuned differently and might
|
||||
// use dictionary compression (when applicable). See ~Rep() for some details.
|
||||
UnownedPtr<Compressor> data_block_compressor = nullptr;
|
||||
// A compressor for index blocks, which might be tuned differently from
|
||||
// basic_compressor. See ~Rep() for some details.
|
||||
UnownedPtr<Compressor> index_block_compressor = nullptr;
|
||||
// A decompressor corresponding to basic_compressor (when non-nullptr).
|
||||
// Used for verification and cache warming.
|
||||
std::shared_ptr<Decompressor> basic_decompressor;
|
||||
@@ -853,7 +868,7 @@ struct BlockBasedTableBuilder::Rep {
|
||||
compression_types_used;
|
||||
|
||||
// Working area for basic_compressor when compression_parallel_threads==1
|
||||
WorkingAreaPair basic_working_area;
|
||||
WorkingAreaPair index_block_working_area;
|
||||
// Working area for data_block_compressor, for emit/compaction thread
|
||||
WorkingAreaPair data_block_working_area;
|
||||
|
||||
@@ -894,6 +909,7 @@ struct BlockBasedTableBuilder::Rep {
|
||||
std::unique_ptr<FilterBlockBuilder> filter_builder;
|
||||
OffsetableCacheKey base_cache_key;
|
||||
const TableFileCreationReason reason;
|
||||
const bool target_file_size_is_upper_bound;
|
||||
|
||||
BlockHandle pending_handle; // Handle to add to index block
|
||||
|
||||
@@ -1041,6 +1057,8 @@ struct BlockBasedTableBuilder::Rep {
|
||||
use_delta_encoding_for_index_values(table_opt.format_version >= 4 &&
|
||||
!table_opt.block_align),
|
||||
reason(tbo.reason),
|
||||
target_file_size_is_upper_bound(
|
||||
tbo.moptions.target_file_size_is_upper_bound),
|
||||
flush_block_policy(
|
||||
table_options.flush_block_policy_factory->NewFlushBlockPolicy(
|
||||
table_options, data_block)),
|
||||
@@ -1096,7 +1114,10 @@ struct BlockBasedTableBuilder::Rep {
|
||||
filter_context, tbo.compression_opts, tbo.compression_type);
|
||||
if (basic_compressor) {
|
||||
if (table_options.enable_index_compression) {
|
||||
basic_working_area.compress = basic_compressor->ObtainWorkingArea();
|
||||
index_block_compressor = MaybeCloneSpecialized(
|
||||
basic_compressor.get(), CacheEntryRole::kIndexBlock);
|
||||
index_block_working_area.compress =
|
||||
index_block_compressor->ObtainWorkingArea();
|
||||
}
|
||||
max_dict_sample_bytes = basic_compressor->GetMaxSampleSizeIfWantDict(
|
||||
CacheEntryRole::kDataBlock);
|
||||
@@ -1111,8 +1132,10 @@ struct BlockBasedTableBuilder::Rep {
|
||||
tbo.compression_opts.max_dict_buffer_bytes);
|
||||
}
|
||||
} else {
|
||||
// No distinct data block compressor using dictionary
|
||||
data_block_compressor = basic_compressor.get();
|
||||
// No distinct data block compressor using dictionary, but
|
||||
// implementation might still want to specialize for data blocks
|
||||
data_block_compressor = MaybeCloneSpecialized(
|
||||
basic_compressor.get(), CacheEntryRole::kDataBlock);
|
||||
data_block_working_area.compress =
|
||||
data_block_compressor->ObtainWorkingArea();
|
||||
}
|
||||
@@ -1126,8 +1149,9 @@ struct BlockBasedTableBuilder::Rep {
|
||||
if (table_options.verify_compression) {
|
||||
verify_decompressor = basic_decompressor.get();
|
||||
if (table_options.enable_index_compression) {
|
||||
basic_working_area.verify = verify_decompressor->ObtainWorkingArea(
|
||||
basic_compressor->GetPreferredCompressionType());
|
||||
index_block_working_area.verify =
|
||||
verify_decompressor->ObtainWorkingArea(
|
||||
index_block_compressor->GetPreferredCompressionType());
|
||||
}
|
||||
if (state == State::kUnbuffered) {
|
||||
assert(data_block_compressor);
|
||||
@@ -1292,8 +1316,19 @@ struct BlockBasedTableBuilder::Rep {
|
||||
}
|
||||
|
||||
~Rep() {
|
||||
// Delete working areas before their compressors.
|
||||
index_block_working_area = {};
|
||||
data_block_working_area = {};
|
||||
// Must have been cleaned up by StopParallelCompression
|
||||
assert(pc_rep == nullptr);
|
||||
// Delete specialized compressors if they were distinct (avoiding extra
|
||||
// fields and interlocked instructions with shared_ptr)
|
||||
if (data_block_compressor.get() != basic_compressor.get()) {
|
||||
delete data_block_compressor.get();
|
||||
}
|
||||
if (index_block_compressor.get() != basic_compressor.get()) {
|
||||
delete index_block_compressor.get();
|
||||
}
|
||||
}
|
||||
|
||||
Rep(const Rep&) = delete;
|
||||
@@ -1611,6 +1646,17 @@ void BlockBasedTableBuilder::Flush(const Slice* first_key_in_next_block) {
|
||||
rep_->data_begin_offset += uncompressed_block_data.size();
|
||||
MaybeEnterUnbuffered(first_key_in_next_block);
|
||||
} else {
|
||||
// Increment num_data_blocks when a data block is finalized in the
|
||||
// emit thread to avoid data races with write worker threads
|
||||
++r->props.num_data_blocks;
|
||||
|
||||
// Notify filter builder that a data block has been finalized
|
||||
// This must happen on the emit thread before the block is added to the
|
||||
// ring buffer to avoid race conditions with worker threads
|
||||
if (r->filter_builder) {
|
||||
r->filter_builder->OnDataBlockFinalized(r->props.num_data_blocks);
|
||||
}
|
||||
|
||||
if (r->IsParallelCompressionActive()) {
|
||||
EmitBlockForParallel(r->data_block.MutableBuffer(), r->last_ikey,
|
||||
first_key_in_next_block);
|
||||
@@ -1715,9 +1761,11 @@ void BlockBasedTableBuilder::WriteBlock(const Slice& uncompressed_block_data,
|
||||
assert(!r->IsParallelCompressionActive());
|
||||
CompressionType type;
|
||||
bool is_data_block = block_type == BlockType::kData;
|
||||
// NOTE: only index and data blocks are currently compressed
|
||||
assert(is_data_block || block_type == BlockType::kIndex);
|
||||
Status compress_status = CompressAndVerifyBlock(
|
||||
uncompressed_block_data, is_data_block,
|
||||
is_data_block ? r->data_block_working_area : r->basic_working_area,
|
||||
is_data_block ? r->data_block_working_area : r->index_block_working_area,
|
||||
&r->single_threaded_compressed_output, &type);
|
||||
r->SetStatus(compress_status);
|
||||
if (UNLIKELY(!ok())) {
|
||||
@@ -1735,7 +1783,6 @@ void BlockBasedTableBuilder::WriteBlock(const Slice& uncompressed_block_data,
|
||||
if (is_data_block) {
|
||||
r->props.data_size = r->get_offset();
|
||||
r->props.uncompressed_data_size += uncompressed_block_data.size();
|
||||
++r->props.num_data_blocks;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1784,7 +1831,6 @@ void BlockBasedTableBuilder::BGWorker(WorkingAreaPair& working_area) {
|
||||
if (LIKELY(ios.ok())) {
|
||||
rep_->props.data_size = rep_->get_offset();
|
||||
rep_->props.uncompressed_data_size += block_rep->uncompressed.size();
|
||||
++rep_->props.num_data_blocks;
|
||||
|
||||
rep_->index_builder->FinishIndexEntry(
|
||||
rep_->pending_handle, block_rep->prepared_index_entry.get(),
|
||||
@@ -1833,13 +1879,13 @@ Status BlockBasedTableBuilder::CompressAndVerifyBlock(
|
||||
Rep* r = rep_.get();
|
||||
Status status;
|
||||
|
||||
Compressor* compressor = nullptr;
|
||||
UnownedPtr<Compressor> compressor = nullptr;
|
||||
Decompressor* verify_decomp = nullptr;
|
||||
if (is_data_block) {
|
||||
compressor = r->data_block_compressor;
|
||||
verify_decomp = r->data_block_verify_decompressor.get();
|
||||
} else {
|
||||
compressor = r->basic_compressor.get();
|
||||
compressor = r->index_block_compressor;
|
||||
verify_decomp = r->verify_decompressor.get();
|
||||
}
|
||||
|
||||
@@ -1940,6 +1986,9 @@ void BlockBasedTableBuilder::WriteMaybeCompressedBlock(
|
||||
const Slice& block_contents, CompressionType comp_type, BlockHandle* handle,
|
||||
BlockType block_type, const Slice* uncompressed_block_data,
|
||||
bool* skip_delta_encoding) {
|
||||
// Must have pre-checked status in single-threaded context
|
||||
assert(status().ok());
|
||||
assert(io_status().ok());
|
||||
rep_->SetIOStatus(WriteMaybeCompressedBlockImpl(
|
||||
block_contents, comp_type, handle, block_type, uncompressed_block_data,
|
||||
skip_delta_encoding));
|
||||
@@ -2014,8 +2063,6 @@ IOStatus BlockBasedTableBuilder::WriteMaybeCompressedBlockImpl(
|
||||
|
||||
handle->set_offset(offset);
|
||||
handle->set_size(block_contents.size());
|
||||
assert(status().ok());
|
||||
assert(io_status().ok());
|
||||
if (uncompressed_block_data == nullptr) {
|
||||
uncompressed_block_data = &block_contents;
|
||||
assert(comp_type == kNoCompression);
|
||||
@@ -2103,7 +2150,7 @@ void BlockBasedTableBuilder::MaybeStartParallelCompression() {
|
||||
// that latency. So even with some optimizations, turning on the parallel
|
||||
// framework when compression is disabled just eats more CPU with little-to-no
|
||||
// improvement in throughput.
|
||||
if (rep_->data_block_compressor == nullptr) {
|
||||
if (!rep_->data_block_compressor) {
|
||||
// Force the generally best configuration for no compression: no parallelism
|
||||
return;
|
||||
}
|
||||
@@ -2140,8 +2187,9 @@ void BlockBasedTableBuilder::StopParallelCompression(bool abort) {
|
||||
pc_rep.SetAbort(pc_rep.emit_thread_state);
|
||||
} else if (pc_rep.emit_thread_state !=
|
||||
ParallelCompressionRep::ThreadState::kEnd) {
|
||||
// In case we didn't do a final flush with no next key
|
||||
assert(rep_->props.num_data_blocks == 0);
|
||||
// In case we didn't do a final flush with no next key, which might have
|
||||
// been skipped if !ok() was set after the start of Finish()
|
||||
assert(rep_->props.num_data_blocks == 0 || !ok());
|
||||
pc_rep.SetNoMoreToEmit(pc_rep.emit_thread_state, pc_rep.emit_slot);
|
||||
}
|
||||
#ifdef BBTB_PC_WATCHDOG
|
||||
@@ -2449,8 +2497,8 @@ void BlockBasedTableBuilder::WritePropertiesBlock(
|
||||
void BlockBasedTableBuilder::WriteCompressionDictBlock(
|
||||
MetaIndexBuilder* meta_index_builder) {
|
||||
Slice compression_dict;
|
||||
if (rep_->compressor_with_dict) {
|
||||
compression_dict = rep_->compressor_with_dict->GetSerializedDict();
|
||||
if (rep_->data_block_compressor) {
|
||||
compression_dict = rep_->data_block_compressor->GetSerializedDict();
|
||||
}
|
||||
if (!compression_dict.empty()) {
|
||||
BlockHandle compression_dict_block_handle;
|
||||
@@ -2545,6 +2593,7 @@ void BlockBasedTableBuilder::MaybeEnterUnbuffered(
|
||||
// The below code is neither safe nor necessary for handling zero data
|
||||
// blocks.
|
||||
// For PostPopulateCompressionProperties()
|
||||
assert(!r->data_block_compressor);
|
||||
r->data_block_compressor = r->basic_compressor.get();
|
||||
return;
|
||||
}
|
||||
@@ -2586,15 +2635,12 @@ void BlockBasedTableBuilder::MaybeEnterUnbuffered(
|
||||
|
||||
assert(samples.sample_data.size() > 0);
|
||||
|
||||
// final sample data block flushed, now we can generate dictionary
|
||||
r->compressor_with_dict = r->basic_compressor->MaybeCloneSpecialized(
|
||||
CacheEntryRole::kDataBlock, std::move(samples));
|
||||
// final sample data block flushed, now we can generate dictionary (or it
|
||||
// might opt not to use a dictionary and that's ok)
|
||||
r->data_block_compressor =
|
||||
MaybeCloneSpecialized(r->basic_compressor.get(),
|
||||
CacheEntryRole::kDataBlock, std::move(samples));
|
||||
|
||||
// The compressor might opt not to use a dictionary, in which case we
|
||||
// can use the same compressor as for e.g. index blocks.
|
||||
r->data_block_compressor = r->compressor_with_dict
|
||||
? r->compressor_with_dict.get()
|
||||
: r->basic_compressor.get();
|
||||
Slice serialized_dict = r->data_block_compressor->GetSerializedDict();
|
||||
if (r->verify_decompressor) {
|
||||
if (serialized_dict.empty()) {
|
||||
@@ -2688,6 +2734,20 @@ void BlockBasedTableBuilder::MaybeEnterUnbuffered(
|
||||
Status BlockBasedTableBuilder::Finish() {
|
||||
Rep* r = rep_.get();
|
||||
assert(r->state != Rep::State::kClosed);
|
||||
|
||||
#ifndef NDEBUG
|
||||
{
|
||||
// This sync point callback is a simple approximation of a failure detected
|
||||
// in parallel compression after the start of calling Finish() but before
|
||||
// Finish() calls Flush()
|
||||
IOStatus s = rep_->GetIOStatus();
|
||||
TEST_SYNC_POINT_CALLBACK("BlockBasedTableBuilder::Finish:ParallelIOStatus",
|
||||
&s);
|
||||
if (!s.ok()) {
|
||||
rep_->SetIOStatus(s);
|
||||
}
|
||||
}
|
||||
#endif // !NDEBUG
|
||||
// To make sure properties block is able to keep the accurate size of index
|
||||
// block, we will finish writing all index entries first, in Flush().
|
||||
Flush(/*first_key_in_next_block=*/nullptr);
|
||||
@@ -2701,6 +2761,8 @@ Status BlockBasedTableBuilder::Finish() {
|
||||
|
||||
r->props.tail_start_offset = r->offset.LoadRelaxed();
|
||||
|
||||
uint64_t last_estimated_tail_size = EstimatedTailSize();
|
||||
|
||||
// Write meta blocks, metaindex block and footer in the following order.
|
||||
// 1. [meta block: filter]
|
||||
// 2. [meta block: index]
|
||||
@@ -2727,6 +2789,24 @@ Status BlockBasedTableBuilder::Finish() {
|
||||
r->state = Rep::State::kClosed;
|
||||
r->tail_size = r->offset.LoadRelaxed() - r->props.tail_start_offset;
|
||||
|
||||
// Assert tail size estimation is an overestimate only when tail size
|
||||
// estimation option is enabled for compaction files with supported
|
||||
// index/filter types:
|
||||
// - Shortened indexes (kBinarySearch, kBinarySearchWithFirstKey)
|
||||
// - Partitioned indexes (kTwoLevelIndexSearch)
|
||||
// - Full filters
|
||||
// - Partitioned filters
|
||||
if (r->target_file_size_is_upper_bound &&
|
||||
r->reason == TableFileCreationReason::kCompaction &&
|
||||
r->table_options.index_type != BlockBasedTableOptions::kHashSearch) {
|
||||
ROCKS_LOG_WARN(r->ioptions.info_log,
|
||||
"File number: %" PRIu64 ", Estimated tail size = %" PRIu64
|
||||
" bytes, Actual tail size = %" PRIu64 " bytes",
|
||||
r->props.orig_file_number, last_estimated_tail_size,
|
||||
r->tail_size);
|
||||
assert(r->tail_size <= last_estimated_tail_size);
|
||||
}
|
||||
|
||||
return r->GetStatus();
|
||||
}
|
||||
|
||||
@@ -2764,6 +2844,49 @@ uint64_t BlockBasedTableBuilder::EstimatedFileSize() const {
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t BlockBasedTableBuilder::EstimatedTailSize() const {
|
||||
uint64_t estimated_tail_size = 0;
|
||||
|
||||
// 1. Estimate index size
|
||||
if (rep_->table_options.index_type ==
|
||||
BlockBasedTableOptions::kTwoLevelIndexSearch) {
|
||||
assert(rep_->p_index_builder_);
|
||||
estimated_tail_size += rep_->p_index_builder_->CurrentIndexSizeEstimate();
|
||||
} else {
|
||||
assert(rep_->index_builder);
|
||||
estimated_tail_size += rep_->index_builder->CurrentIndexSizeEstimate();
|
||||
}
|
||||
|
||||
// 2. Estimate filter size
|
||||
if (rep_->filter_builder) {
|
||||
estimated_tail_size += rep_->filter_builder->CurrentFilterSizeEstimate();
|
||||
}
|
||||
|
||||
// 3. Estimate compression dictionary size
|
||||
if (rep_->data_block_compressor) {
|
||||
Slice dict = rep_->data_block_compressor->GetSerializedDict();
|
||||
if (!dict.empty()) {
|
||||
estimated_tail_size += dict.size();
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Estimate range deletion block size
|
||||
if (!rep_->range_del_block.empty()) {
|
||||
estimated_tail_size += rep_->range_del_block.CurrentSizeEstimate();
|
||||
}
|
||||
|
||||
// 5. Estimate properties block size conservatively (~1-2KB)
|
||||
estimated_tail_size += 2048;
|
||||
|
||||
// 6. Estimate meta-index block size conservatively (~1KB)
|
||||
estimated_tail_size += 1024;
|
||||
|
||||
// 7. Add footer size
|
||||
estimated_tail_size += Footer::kMaxEncodedLength;
|
||||
|
||||
return estimated_tail_size;
|
||||
}
|
||||
|
||||
uint64_t BlockBasedTableBuilder::GetTailSize() const { return rep_->tail_size; }
|
||||
|
||||
bool BlockBasedTableBuilder::NeedCompact() const {
|
||||
|
||||
@@ -89,11 +89,15 @@ class BlockBasedTableBuilder : public TableBuilder {
|
||||
// Finish() call, returns the size of the final generated file.
|
||||
uint64_t FileSize() const override;
|
||||
|
||||
// Estimated size of the file generated so far. This is used when
|
||||
// FileSize() cannot estimate final SST size, e.g. parallel compression
|
||||
// is enabled.
|
||||
// Estimated size of the file generated so far (based on data blocks, this
|
||||
// estimate does not include meta blocks). This is used when FileSize() cannot
|
||||
// estimate final SST size, e.g. parallel compression is enabled.
|
||||
uint64_t EstimatedFileSize() const override;
|
||||
|
||||
// Estimated tail size of the SST file generated so far. The "tail" refers to
|
||||
// all blocks written after data blocks (index + filter).
|
||||
uint64_t EstimatedTailSize() const override;
|
||||
|
||||
// Get the size of the "tail" part of a SST file. "Tail" refers to
|
||||
// all blocks after data blocks till the end of the SST file.
|
||||
uint64_t GetTailSize() const override;
|
||||
|
||||
@@ -1045,13 +1045,6 @@ void BlockBasedTableIterator::Prepare(const MultiScanArgs* multiscan_opts) {
|
||||
}
|
||||
|
||||
void BlockBasedTableIterator::SeekMultiScan(const Slice* seek_target) {
|
||||
if (SeekMultiScanImpl(seek_target)) {
|
||||
is_out_of_bound_ = true;
|
||||
assert(!Valid());
|
||||
}
|
||||
}
|
||||
|
||||
bool BlockBasedTableIterator::SeekMultiScanImpl(const Slice* seek_target) {
|
||||
assert(multi_scan_ && multi_scan_status_.ok());
|
||||
// This is a MultiScan and Preapre() has been called.
|
||||
|
||||
@@ -1063,49 +1056,59 @@ bool BlockBasedTableIterator::SeekMultiScanImpl(const Slice* seek_target) {
|
||||
if (!seek_target) {
|
||||
// start key must be set for multi-scan
|
||||
multi_scan_status_ = Status::InvalidArgument("No seek key for MultiScan");
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr auto out_of_bound = true;
|
||||
|
||||
// Check the case where there is no range prepared on this table
|
||||
if (multi_scan_->scan_opts->size() == 0) {
|
||||
// out of bound
|
||||
return out_of_bound;
|
||||
MarkPreparedRangeExhausted();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check whether seek key is moving forward.
|
||||
if (!multi_scan_->prev_seek_key_.empty()) {
|
||||
if (user_comparator_.CompareWithoutTimestamp(ExtractUserKey(*seek_target),
|
||||
/*a_has_ts=*/true,
|
||||
multi_scan_->prev_seek_key_,
|
||||
/*b_has_ts=*/false) < 0) {
|
||||
// The seek target moved backward
|
||||
multi_scan_status_ =
|
||||
Status::InvalidArgument("Unexpected seek key moving backward");
|
||||
return false;
|
||||
}
|
||||
if (multi_scan_->prev_seek_key_.empty() ||
|
||||
icomp_.Compare(*seek_target, multi_scan_->prev_seek_key_) > 0) {
|
||||
// If seek key is empty or is larger than previous seek key, update the
|
||||
// previous seek key. Otherwise use the previous seek key as the adjusted
|
||||
// seek target moving forward. This prevents seek target going backward,
|
||||
// which would visit pages that have been unpinned.
|
||||
// This issue is caused by sub-optimal range delete handling inside merge
|
||||
// iterator.
|
||||
// TODO xingbo issues:14068 : Optimize the handling of range delete iterator
|
||||
// inside merge iterator, so that it doesn't move seek key backward. After
|
||||
// that we could return error if the key moves backward here.
|
||||
multi_scan_->prev_seek_key_ = seek_target->ToString();
|
||||
} else {
|
||||
// Seek key is adjusted to previous one, we can return here directly.
|
||||
return;
|
||||
}
|
||||
multi_scan_->prev_seek_key_ = ExtractUserKey(*seek_target).ToString();
|
||||
|
||||
// There are still a few cases we need to handle
|
||||
// table: _____[prepared range 1]_____[prepared range 2]_____
|
||||
// seek : 1 2 3 4 5
|
||||
// Case 1: seek before the first prepared ranges, return out of bound
|
||||
// Case 2: seek at the beginning of a prepared range (expected case)
|
||||
// Case 3: seek within a prepared range (unexpected, but supported)
|
||||
// Case 4: seek between 2 of the prepared ranges, return out of bound
|
||||
// Case 5: seek after all of the prepared ranges, should move on to next file
|
||||
// The reason this could happen is due to seek key adjustment due to delete
|
||||
// range file.
|
||||
// E.g. LSM has 3 levels, each level has only 1 file:
|
||||
// L1 : key : 0---10
|
||||
// L2 : Delete range key : 0-5
|
||||
// L3 : key : 0---10
|
||||
// When a range 2-8 was prepared, the prepared key would be 2 on L3 file, but
|
||||
// the seek key would be 5, as the seek key was updated by the largest key of
|
||||
// delete range. This causes all of the cases above to be possible, when the
|
||||
// ranges are adjusted in the above examples.
|
||||
// There are 3 different Cases we need to handle:
|
||||
// The following diagram explain different seek targets seeking at various
|
||||
// position on the table, while the next_scan_idx points to the PreparedRange
|
||||
// 2.
|
||||
//
|
||||
// next_scan_idx: -------------------┐
|
||||
// ▼
|
||||
// table: : __[PreparedRange 1]__[PreparedRange 2]__[PreparedRange 3]__
|
||||
// Seek target: <----- Case 1 ------>▲<------------- Case 2 -------------->
|
||||
// │
|
||||
// Case 3
|
||||
//
|
||||
// Case 1: seek before the start of next prepared ranges. This could happen
|
||||
// due to too many delete tomestone triggered reseek or delete range.
|
||||
// Case 2: seek after the start of next prepared range.
|
||||
// This could happen due to seek key adjustment from delete range file.
|
||||
// E.g. LSM has 3 levels, each level has only 1 file:
|
||||
// L1 : key : 0---10
|
||||
// L2 : Delete range key : 0-5
|
||||
// L3 : key : 0---10
|
||||
// When a range 2-8 was prepared, the prepared key would be 2 on L3 file,
|
||||
// but the seek key would be 5, as the seek key was updated by the largest
|
||||
// key of delete range. This causes all of the cases above to be possible,
|
||||
// when the ranges are adjusted in the above examples.
|
||||
// Case 3: seek at the beginning of a prepared range (expected case)
|
||||
|
||||
// Allow reseek on the start of the last prepared range due to too many
|
||||
// tombstone
|
||||
@@ -1113,83 +1116,151 @@ bool BlockBasedTableIterator::SeekMultiScanImpl(const Slice* seek_target) {
|
||||
std::min(multi_scan_->next_scan_idx,
|
||||
multi_scan_->block_index_ranges_per_scan.size() - 1);
|
||||
|
||||
auto user_seek_target = ExtractUserKey(*seek_target);
|
||||
|
||||
auto compare_next_scan_start_result =
|
||||
user_comparator_.CompareWithoutTimestamp(
|
||||
ExtractUserKey(*seek_target), /*a_has_ts=*/true,
|
||||
user_seek_target, /*a_has_ts=*/true,
|
||||
multi_scan_->scan_opts->GetScanRanges()[multi_scan_->next_scan_idx]
|
||||
.range.start.value(),
|
||||
/*b_has_ts=*/false);
|
||||
|
||||
if (compare_next_scan_start_result != 0) {
|
||||
// The seek key is not exactly same as what was prepared.
|
||||
// The seek target is not exactly same as what was prepared.
|
||||
if (compare_next_scan_start_result < 0) {
|
||||
// Needs to handle Cases: 1, 3, 4
|
||||
//
|
||||
// next_scan_idx : |
|
||||
// V
|
||||
// table: _____[prepared range 1]_____[prepared range 2]_____
|
||||
// seek : 1 3 4
|
||||
|
||||
// Case 1: Seek key is before the start key of the first range
|
||||
// Case 1:
|
||||
if (multi_scan_->next_scan_idx == 0) {
|
||||
return out_of_bound;
|
||||
// This should not happen, even when seek target is adjusted by delete
|
||||
// range. The reason is that if the seek target is before the start key
|
||||
// of the first prepared range, its end key needs to be >= the smallest
|
||||
// key of this file, otherwise it is skipped in level iterator. If its
|
||||
// end key is >= the smallest key of this file, then this range will be
|
||||
// prepared for this file. As delete range could only adjust seek
|
||||
// target forward, so it would never be before the start key of the
|
||||
// first prepared range.
|
||||
assert(false && "Seek target before the first prepared range");
|
||||
MarkPreparedRangeExhausted();
|
||||
return;
|
||||
}
|
||||
// Case: 3, 4
|
||||
MultiScanUnexpectedSeekTarget(
|
||||
seek_target, std::get<0>(multi_scan_->block_index_ranges_per_scan
|
||||
[multi_scan_->next_scan_idx - 1]));
|
||||
|
||||
auto seek_target_before_previous_prepared_range =
|
||||
user_comparator_.CompareWithoutTimestamp(
|
||||
user_seek_target, /*a_has_ts=*/true,
|
||||
multi_scan_->scan_opts
|
||||
->GetScanRanges()[multi_scan_->next_scan_idx - 1]
|
||||
.range.start.value(),
|
||||
/*b_has_ts=*/false) < 0;
|
||||
// Not expected to happen
|
||||
// This should never happen, the reason is that the
|
||||
// multi_scan_->next_scan_idx is set to a non zero value is due to a seek
|
||||
// target larger or equal to the start key of multi_scan_->next_scan_idx-1
|
||||
// happended earlier. If a seek happens before the start key of
|
||||
// multi_scan_->next_scan_idx-1, it would seek a key that is less than
|
||||
// what was seeked before.
|
||||
assert(!seek_target_before_previous_prepared_range);
|
||||
if (seek_target_before_previous_prepared_range) {
|
||||
multi_scan_status_ = Status::InvalidArgument(
|
||||
"Seek target is before the previous prepared range at index " +
|
||||
std::to_string(multi_scan_->next_scan_idx));
|
||||
return;
|
||||
}
|
||||
// It should only be possible to seek a key between the start of current
|
||||
// prepared scan and start of next prepared range.
|
||||
MultiScanUnexpectedSeekTarget(seek_target, &user_seek_target);
|
||||
} else {
|
||||
// Needs to handle Cases: 3, 4, 5
|
||||
// next_scan_idx :|
|
||||
// V
|
||||
// table: ____[prepared range 1]_____[prepared range 2]_____
|
||||
// seek : 3 4 5
|
||||
MultiScanUnexpectedSeekTarget(
|
||||
seek_target,
|
||||
std::get<0>(
|
||||
multi_scan_
|
||||
->block_index_ranges_per_scan[multi_scan_->next_scan_idx]));
|
||||
// Case 2:
|
||||
MultiScanUnexpectedSeekTarget(seek_target, &user_seek_target);
|
||||
}
|
||||
} else {
|
||||
if (multi_scan_->next_scan_idx >=
|
||||
multi_scan_->block_index_ranges_per_scan.size()) {
|
||||
// Seeking a range that is out side of prepared ranges.
|
||||
return out_of_bound;
|
||||
}
|
||||
// Case 2:
|
||||
assert(multi_scan_->next_scan_idx <
|
||||
multi_scan_->block_index_ranges_per_scan.size());
|
||||
|
||||
auto [cur_scan_start_idx, cur_scan_end_idx] =
|
||||
multi_scan_->block_index_ranges_per_scan[multi_scan_->next_scan_idx];
|
||||
// We should have the data block already loaded
|
||||
++multi_scan_->next_scan_idx;
|
||||
if (cur_scan_start_idx >= cur_scan_end_idx) {
|
||||
if (multi_scan_->next_scan_idx <
|
||||
multi_scan_->block_index_ranges_per_scan.size()) {
|
||||
return out_of_bound;
|
||||
} else {
|
||||
ResetDataIter();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
is_out_of_bound_ = false;
|
||||
// No blocks are prepared for this range at current file.
|
||||
MarkPreparedRangeExhausted();
|
||||
return;
|
||||
}
|
||||
|
||||
MultiScanSeekTargetFromBlock(seek_target, cur_scan_start_idx);
|
||||
// max_sequential_skip_in_iterations can trigger a reseek on the start
|
||||
// key of a scan range, even though the multiscan is already past
|
||||
// `cur_scan_start_idx` (e.g., a user key spans multiple data blocks).
|
||||
size_t block_idx =
|
||||
std::max(cur_scan_start_idx, multi_scan_->cur_data_block_idx);
|
||||
MultiScanSeekTargetFromBlock(seek_target, block_idx);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void BlockBasedTableIterator::MultiScanUnexpectedSeekTarget(
|
||||
const Slice* seek_target, size_t block_idx) {
|
||||
const Slice* seek_target, const Slice* user_seek_target) {
|
||||
// linear search the block that contains the seek target, and unpin blocks
|
||||
// that are before it.
|
||||
|
||||
// The logic here could be confusing when there is a delete range involved.
|
||||
// E.g. we have an LSM with 3 levels, each level has only 1 file:
|
||||
// L1: data file : 0---10
|
||||
// L2: Delete range : 0-5
|
||||
// L3: data file : 0---10
|
||||
//
|
||||
// MultiScan on ranges 1-2, 3-4, and 5-6.
|
||||
// When user first do Seek(1), on level 2, due to delete range 0-5, the seek
|
||||
// key is adjusted to 5 at level 3. Therefore, we will internally do Seek(5)
|
||||
// and unpins all blocks until 5 at level 3. Then the next scan's blocks from
|
||||
// 3-4 are unpinned at level 3. It is confusing that maybe block 3-4 should
|
||||
// not be unpinned, as next scan would need it. But Seek(5) implies that these
|
||||
// keys are all covered by some range deletion, so the next Seek(3) will also
|
||||
// do Seek(5) internally, so the blocks from 3-4 could be safely unpinned.
|
||||
|
||||
// advance to the right prepared range
|
||||
while (
|
||||
multi_scan_->next_scan_idx <
|
||||
multi_scan_->block_index_ranges_per_scan.size() &&
|
||||
(user_comparator_.CompareWithoutTimestamp(
|
||||
*user_seek_target, /*a_has_ts=*/true,
|
||||
multi_scan_->scan_opts->GetScanRanges()[multi_scan_->next_scan_idx]
|
||||
.range.start.value(),
|
||||
/*b_has_ts=*/false) >= 0)) {
|
||||
multi_scan_->next_scan_idx++;
|
||||
}
|
||||
|
||||
// next_scan_idx is guaranteed to be higher than 0. If the seek key is before
|
||||
// the start key of first prepared range, it is already handled by caller
|
||||
// SeekMultiScan. It is equal, it would not call this funciton. If it is
|
||||
// after, next_scan_idx would be advanced by the loop above.
|
||||
assert(multi_scan_->next_scan_idx > 0);
|
||||
// Get the current range
|
||||
auto cur_scan_idx = multi_scan_->next_scan_idx - 1;
|
||||
auto [cur_scan_start_idx, cur_scan_end_idx] =
|
||||
multi_scan_->block_index_ranges_per_scan[cur_scan_idx];
|
||||
|
||||
if (cur_scan_start_idx >= cur_scan_end_idx) {
|
||||
// No blocks are prepared for this range at current file.
|
||||
MarkPreparedRangeExhausted();
|
||||
return;
|
||||
}
|
||||
|
||||
// Unpin all the blocks from multi_scan_->cur_data_block_idx to
|
||||
// cur_scan_start_idx
|
||||
for (auto unpin_block_idx = multi_scan_->cur_data_block_idx;
|
||||
unpin_block_idx < cur_scan_start_idx; unpin_block_idx++) {
|
||||
if (!multi_scan_->pinned_data_blocks[unpin_block_idx].IsEmpty()) {
|
||||
multi_scan_->pinned_data_blocks[unpin_block_idx].Reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Take the max here to ensure we don't move backwards.
|
||||
size_t block_idx =
|
||||
std::max(cur_scan_start_idx, multi_scan_->cur_data_block_idx);
|
||||
auto const& data_block_separators = multi_scan_->data_block_separators;
|
||||
while (block_idx < data_block_separators.size() &&
|
||||
(user_comparator_.CompareWithoutTimestamp(
|
||||
ExtractUserKey(*seek_target), /*a_has_ts=*/true,
|
||||
*user_seek_target, /*a_has_ts=*/true,
|
||||
data_block_separators[block_idx],
|
||||
/*b_has_ts=*/false) > 0)) {
|
||||
// Unpin the blocks that are passed
|
||||
if (!multi_scan_->pinned_data_blocks[block_idx].IsEmpty()) {
|
||||
multi_scan_->pinned_data_blocks[block_idx].Reset();
|
||||
}
|
||||
@@ -1197,30 +1268,11 @@ void BlockBasedTableIterator::MultiScanUnexpectedSeekTarget(
|
||||
}
|
||||
|
||||
if (block_idx >= data_block_separators.size()) {
|
||||
// Handle case 5, when seek key is larger than the last block in the last
|
||||
// prepared range.
|
||||
ResetDataIter();
|
||||
assert(!Valid());
|
||||
// All of the prepared blocks for this file is exhausted.
|
||||
MarkPreparedRangeExhausted();
|
||||
return;
|
||||
}
|
||||
|
||||
// // The iterator from previous seek may have moved forward a few blocks,
|
||||
// // In that case, have block_idx catch up the cur_data_block_idx
|
||||
// // Note no need to handle block unpin, as it has been handled during
|
||||
// iterating block_idx = std::max(block_idx, multi_scan_->cur_data_block_idx);
|
||||
|
||||
// advance to the right prepared range
|
||||
while (
|
||||
multi_scan_->next_scan_idx <
|
||||
multi_scan_->block_index_ranges_per_scan.size() &&
|
||||
(user_comparator_.CompareWithoutTimestamp(
|
||||
ExtractUserKey(*seek_target), /*a_has_ts=*/true,
|
||||
multi_scan_->scan_opts->GetScanRanges()[multi_scan_->next_scan_idx]
|
||||
.range.start.value(),
|
||||
/*b_has_ts=*/false) >= 0)) {
|
||||
multi_scan_->next_scan_idx++;
|
||||
}
|
||||
|
||||
// The current block may contain the data for the target key
|
||||
MultiScanSeekTargetFromBlock(seek_target, block_idx);
|
||||
}
|
||||
@@ -1257,6 +1309,7 @@ void BlockBasedTableIterator::MultiScanSeekTargetFromBlock(
|
||||
block_iter_points_to_real_block_ = true;
|
||||
block_iter_.Seek(*seek_target);
|
||||
FindKeyForward();
|
||||
CheckOutOfBound();
|
||||
}
|
||||
|
||||
void BlockBasedTableIterator::FindBlockForwardInMultiScan() {
|
||||
@@ -1275,20 +1328,7 @@ void BlockBasedTableIterator::FindBlockForwardInMultiScan() {
|
||||
// for this file, it may need to continue to scan into the next file, so
|
||||
// we do not set is_out_of_bound_ in this case.
|
||||
if (multi_scan_->cur_data_block_idx + 1 >= cur_scan_end_idx) {
|
||||
if (multi_scan_->next_scan_idx >=
|
||||
multi_scan_->block_index_ranges_per_scan.size()) {
|
||||
// We are done with this file, should let LevelIter advance to the
|
||||
// next file instead of ending the scan
|
||||
ResetDataIter();
|
||||
assert(!is_out_of_bound_);
|
||||
assert(!Valid());
|
||||
return;
|
||||
}
|
||||
// We don't ResetDataIter() here since next scan might be reading from
|
||||
// the same block. ResetDataIter() will free the underlying block cache
|
||||
// handle and we don't want the block to be unpinned.
|
||||
is_out_of_bound_ = true;
|
||||
assert(!Valid());
|
||||
MarkPreparedRangeExhausted();
|
||||
return;
|
||||
}
|
||||
// Move to the next pinned data block
|
||||
@@ -1419,7 +1459,7 @@ Status BlockBasedTableIterator::CollectBlockHandles(
|
||||
std::vector<std::tuple<size_t, size_t>>* block_index_ranges_per_scan,
|
||||
std::vector<std::string>* data_block_separators) {
|
||||
// print file name and level
|
||||
if (kVerbose) {
|
||||
if (UNLIKELY(kVerbose)) {
|
||||
auto file_name = table_->get_rep()->file->file_name();
|
||||
auto level = table_->get_rep()->level;
|
||||
printf("file name : %s, level %d\n", file_name.c_str(), level);
|
||||
@@ -1480,11 +1520,16 @@ Status BlockBasedTableIterator::CollectBlockHandles(
|
||||
}
|
||||
block_index_ranges_per_scan->emplace_back(
|
||||
scan_block_handles->size() - num_blocks, scan_block_handles->size());
|
||||
if (kVerbose) {
|
||||
if (UNLIKELY(kVerbose)) {
|
||||
printf("separators :");
|
||||
for (const auto& separator : *data_block_separators) {
|
||||
printf("%s, ", separator.c_str());
|
||||
}
|
||||
printf("\nblock_index_ranges_per_scan :");
|
||||
for (auto const& block_index_range : *block_index_ranges_per_scan) {
|
||||
printf("[%zu, %zu], ", std::get<0>(block_index_range),
|
||||
std::get<1>(block_index_range));
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
@@ -1664,7 +1709,6 @@ Status BlockBasedTableIterator::ExecuteIO(
|
||||
assert(false);
|
||||
return s;
|
||||
}
|
||||
assert(async_read.io_handle);
|
||||
for (auto& req : *read_reqs) {
|
||||
if (!req.status.ok()) {
|
||||
assert(false);
|
||||
|
||||
@@ -381,6 +381,27 @@ class BlockBasedTableIterator : public InternalIteratorBase<Slice> {
|
||||
bool block_iter_points_to_real_block_;
|
||||
// See InternalIteratorBase::IsOutOfBound().
|
||||
bool is_out_of_bound_ = false;
|
||||
|
||||
// Mark prepared ranges as exhausted for multiscan.
|
||||
void MarkPreparedRangeExhausted() {
|
||||
assert(multi_scan_ != nullptr);
|
||||
if (multi_scan_->next_scan_idx <
|
||||
multi_scan_->block_index_ranges_per_scan.size()) {
|
||||
// If there are more prepared ranges, we don't ResetDataIter() here,
|
||||
// because next scan might be reading from the same block. ResetDataIter()
|
||||
// will free the underlying block cache handle and we don't want the
|
||||
// block to be unpinned.
|
||||
// Set out of bound to mark the current prepared range as exhausted.
|
||||
is_out_of_bound_ = true;
|
||||
} else {
|
||||
// This is the last prepared range of this file, there might be more
|
||||
// data on next file. Reset data iterator to indicate the iterator is
|
||||
// no longer valid on this file. Let LevelIter advance to the next file
|
||||
// instead of ending the scan.
|
||||
ResetDataIter();
|
||||
}
|
||||
}
|
||||
|
||||
// During cache lookup to find readahead size, index_iter_ is iterated and it
|
||||
// can point to a different block.
|
||||
// If Prepare() is called, index_iter_ is used to prefetch data blocks for the
|
||||
@@ -612,12 +633,8 @@ class BlockBasedTableIterator : public InternalIteratorBase<Slice> {
|
||||
|
||||
// *** BEGIN APIs relevant to multiscan ***
|
||||
|
||||
// Wrapper for SeekMultiScanImpl for handling out of bound
|
||||
void SeekMultiScan(const Slice* target);
|
||||
|
||||
// Return true if the result is out of bound
|
||||
bool SeekMultiScanImpl(const Slice* seek_target);
|
||||
|
||||
void FindBlockForwardInMultiScan();
|
||||
|
||||
void PrepareReadAsyncCallBack(FSReadRequest& req, void* cb_arg) {
|
||||
@@ -635,14 +652,12 @@ class BlockBasedTableIterator : public InternalIteratorBase<Slice> {
|
||||
std::to_string(async_state->offset) + " and async callback " +
|
||||
std::to_string(req.offset));
|
||||
}
|
||||
} else {
|
||||
assert(async_state->status.IsAborted());
|
||||
}
|
||||
}
|
||||
|
||||
void MultiScanSeekTargetFromBlock(const Slice* seek_target, size_t block_idx);
|
||||
void MultiScanUnexpectedSeekTarget(const Slice* seek_target,
|
||||
size_t block_idx);
|
||||
const Slice* user_seek_target);
|
||||
|
||||
// Return true, if there is an error, or end of file
|
||||
bool MultiScanLoadDataBlock(size_t idx) {
|
||||
|
||||
@@ -3232,7 +3232,8 @@ Status BlockBasedTable::GetKVPairsFromDataBlocks(
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlockBasedTable::DumpTable(WritableFile* out_file) {
|
||||
Status BlockBasedTable::DumpTable(WritableFile* out_file,
|
||||
bool show_sequence_number_type) {
|
||||
WritableFileStringStreamAdapter out_file_wrapper(out_file);
|
||||
std::ostream out_stream(&out_file_wrapper);
|
||||
// Output Footer
|
||||
@@ -3325,15 +3326,15 @@ Status BlockBasedTable::DumpTable(WritableFile* out_file) {
|
||||
out_stream << "Range deletions:\n"
|
||||
"--------------------------------------\n";
|
||||
for (; range_del_iter->Valid(); range_del_iter->Next()) {
|
||||
DumpKeyValue(range_del_iter->key(), range_del_iter->value(),
|
||||
out_stream);
|
||||
DumpKeyValue(range_del_iter->key(), range_del_iter->value(), out_stream,
|
||||
show_sequence_number_type);
|
||||
}
|
||||
out_stream << "\n";
|
||||
}
|
||||
delete range_del_iter;
|
||||
}
|
||||
// Output Data blocks
|
||||
s = DumpDataBlocks(out_stream);
|
||||
s = DumpDataBlocks(out_stream, show_sequence_number_type);
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
@@ -3398,7 +3399,8 @@ Status BlockBasedTable::DumpIndexBlock(std::ostream& out_stream) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlockBasedTable::DumpDataBlocks(std::ostream& out_stream) {
|
||||
Status BlockBasedTable::DumpDataBlocks(std::ostream& out_stream,
|
||||
bool show_sequence_number_type) {
|
||||
// TODO: plumb Env::IOActivity, Env::IOPriority
|
||||
const ReadOptions read_options;
|
||||
std::unique_ptr<InternalIteratorBase<IndexValue>> blockhandles_iter(
|
||||
@@ -3455,7 +3457,8 @@ Status BlockBasedTable::DumpDataBlocks(std::ostream& out_stream) {
|
||||
out_stream << "Error reading the block - Skipped \n";
|
||||
break;
|
||||
}
|
||||
DumpKeyValue(datablock_iter->key(), datablock_iter->value(), out_stream);
|
||||
DumpKeyValue(datablock_iter->key(), datablock_iter->value(), out_stream,
|
||||
show_sequence_number_type);
|
||||
}
|
||||
out_stream << "\n";
|
||||
}
|
||||
@@ -3477,14 +3480,26 @@ Status BlockBasedTable::DumpDataBlocks(std::ostream& out_stream) {
|
||||
}
|
||||
|
||||
void BlockBasedTable::DumpKeyValue(const Slice& key, const Slice& value,
|
||||
std::ostream& out_stream) {
|
||||
InternalKey ikey;
|
||||
ikey.DecodeFrom(key);
|
||||
std::ostream& out_stream,
|
||||
bool show_sequence_number_type) {
|
||||
ParsedInternalKey result;
|
||||
auto s = ParseInternalKey(key, &result, true);
|
||||
if (!s.ok()) {
|
||||
out_stream << "Error parsing internal key - Skipped \n";
|
||||
return;
|
||||
}
|
||||
|
||||
out_stream << " HEX " << ikey.user_key().ToString(true) << ": "
|
||||
<< value.ToString(true) << "\n";
|
||||
if (show_sequence_number_type) {
|
||||
out_stream << " HEX " << result.user_key.ToString(true)
|
||||
<< " seq: " << result.sequence
|
||||
<< " type: " << std::to_string(result.type) << " : "
|
||||
<< value.ToString(true) << "\n";
|
||||
} else {
|
||||
out_stream << " HEX " << result.user_key.ToString(true) << ": "
|
||||
<< value.ToString(true) << "\n";
|
||||
}
|
||||
|
||||
std::string str_key = ikey.user_key().ToString();
|
||||
std::string str_key = result.user_key.ToString();
|
||||
std::string str_value = value.ToString();
|
||||
std::string res_key, res_value;
|
||||
char cspace = ' ';
|
||||
|
||||
@@ -208,7 +208,8 @@ class BlockBasedTable : public TableReader {
|
||||
size_t ApproximateMemoryUsage() const override;
|
||||
|
||||
// convert SST file to a human readable form
|
||||
Status DumpTable(WritableFile* out_file) override;
|
||||
Status DumpTable(WritableFile* out_file,
|
||||
bool show_sequence_number_type = false) override;
|
||||
|
||||
Status VerifyChecksum(const ReadOptions& readOptions,
|
||||
TableReaderCaller caller,
|
||||
@@ -549,9 +550,11 @@ class BlockBasedTable : public TableReader {
|
||||
|
||||
// Helper functions for DumpTable()
|
||||
Status DumpIndexBlock(std::ostream& out_stream);
|
||||
Status DumpDataBlocks(std::ostream& out_stream);
|
||||
Status DumpDataBlocks(std::ostream& out_stream,
|
||||
bool show_sequence_number_type = false);
|
||||
void DumpKeyValue(const Slice& key, const Slice& value,
|
||||
std::ostream& out_stream);
|
||||
std::ostream& out_stream,
|
||||
bool show_sequence_number_type = false);
|
||||
|
||||
// Returns false if prefix_extractor exists and is compatible with that used
|
||||
// in building the table file, otherwise true.
|
||||
|
||||
@@ -1274,13 +1274,13 @@ TEST_P(BlockBasedTableReaderMultiScanAsyncIOTest, MultiScanPrepare) {
|
||||
ExtractUserKey(kv[33 * kEntriesPerBlock].first));
|
||||
iter->Prepare(&scan_options);
|
||||
iter->Seek(kv[32 * kEntriesPerBlock].first);
|
||||
auto key = iter->key();
|
||||
ASSERT_OK(iter->status());
|
||||
iter->Seek(kv[34 * kEntriesPerBlock].first);
|
||||
ASSERT_OK(iter->status());
|
||||
// Seek key could not going backward
|
||||
iter->Seek(kv[30 * kEntriesPerBlock].first);
|
||||
ASSERT_EQ(iter->status(),
|
||||
Status::InvalidArgument("Unexpected seek key moving backward"));
|
||||
// When seek key goes backward, it is adjusted to the last seeked position.
|
||||
// Assert the key read is same as before.
|
||||
ASSERT_EQ(key, iter->key());
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
// Test prefetch limit reached.
|
||||
iter.reset(table->NewIterator(
|
||||
@@ -1333,9 +1333,10 @@ TEST_P(BlockBasedTableReaderMultiScanAsyncIOTest, MultiScanPrepare) {
|
||||
std::cout << random_seed << std::endl;
|
||||
SCOPED_TRACE("Random seed " + std::to_string(random_seed));
|
||||
|
||||
int last_read_key_index = rnd.Uniform(100);
|
||||
while (last_read_key_index < 100) {
|
||||
iter->Seek(kv[last_read_key_index * kEntriesPerBlock].first);
|
||||
// Search key always start from the start key of first prepared range.
|
||||
int last_read_key_index = rnd.Uniform(100) + 5 * kEntriesPerBlock;
|
||||
while (last_read_key_index < 100 * kEntriesPerBlock) {
|
||||
iter->Seek(kv[last_read_key_index].first);
|
||||
EXPECT_OK(iter->status());
|
||||
// iterate for a few keys
|
||||
while (iter->Valid()) {
|
||||
|
||||
@@ -68,6 +68,18 @@ class FilterBlockBuilder {
|
||||
// For reporting stats on how many entries the builder considered unique
|
||||
virtual size_t EstimateEntriesAdded() = 0;
|
||||
|
||||
// Returns an estimate of the current filter size based on the builder's
|
||||
// state. Implementations should cache the estimate and update it via
|
||||
// UpdateFilterSizeEstimate() to avoid recalculating on every key add.
|
||||
//
|
||||
// Can be called at any time during table construction, even before calling
|
||||
// Finish(). Used during table construction to determine when to cut files.
|
||||
virtual size_t CurrentFilterSizeEstimate() = 0;
|
||||
|
||||
// Provides a hook for filter builder when a data block is finalized, such as
|
||||
// to update cached filter size estimates.
|
||||
virtual void OnDataBlockFinalized(uint64_t /* num_data_blocks */) {}
|
||||
|
||||
// When using AddWithPrevKey, this must be called before Finish(). (May also
|
||||
// be called without AddWithPrevKey, but prev_key_without_ts must be
|
||||
// accurate regardless.)
|
||||
@@ -110,6 +122,11 @@ class FilterBlockBuilder {
|
||||
return filter;
|
||||
}
|
||||
#endif // NDEBUG
|
||||
|
||||
protected:
|
||||
// Update cached filter size estimate. Subclasses should override to update
|
||||
// estimates based on their internal state.
|
||||
virtual void UpdateFilterSizeEstimate(uint64_t /* num_data_blocks */) {}
|
||||
};
|
||||
|
||||
// A FilterBlockReader is used to parse filter from SST table.
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
|
||||
#include "cache/cache_entry_roles.h"
|
||||
#include "cache/cache_reservation_manager.h"
|
||||
#include "logging/logging.h"
|
||||
#include "port/lang.h"
|
||||
@@ -29,8 +28,8 @@
|
||||
#include "table/block_based/block_based_table_reader.h"
|
||||
#include "table/block_based/filter_policy_internal.h"
|
||||
#include "table/block_based/full_filter_block.h"
|
||||
#include "util/atomic.h"
|
||||
#include "util/bloom_impl.h"
|
||||
#include "util/coding.h"
|
||||
#include "util/hash.h"
|
||||
#include "util/math.h"
|
||||
#include "util/ribbon_config.h"
|
||||
@@ -61,7 +60,7 @@ Slice FinishAlwaysTrue(std::unique_ptr<const char[]>* /*buf*/) {
|
||||
|
||||
// Base class for filter builders using the XXH3 preview hash,
|
||||
// also known as Hash64 or GetSliceHash64.
|
||||
class XXPH3FilterBitsBuilder : public BuiltinFilterBitsBuilder {
|
||||
class XXPH3FilterBitsBuilder : public FilterBitsBuilder {
|
||||
public:
|
||||
explicit XXPH3FilterBitsBuilder(
|
||||
std::atomic<int64_t>* aggregate_rounding_balance,
|
||||
@@ -126,8 +125,11 @@ class XXPH3FilterBitsBuilder : public BuiltinFilterBitsBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an estimate of the number of entries added to the
|
||||
// filter. This method is thread-safe and can be safely called
|
||||
// from background threads during parallel compression.
|
||||
size_t EstimateEntriesAdded() override {
|
||||
return hash_entries_info_.entries.size();
|
||||
return hash_entries_info_.entries_count.LoadRelaxed();
|
||||
}
|
||||
|
||||
Status MaybePostVerify(const Slice& filter_content) override;
|
||||
@@ -147,6 +149,7 @@ class XXPH3FilterBitsBuilder : public BuiltinFilterBitsBuilder {
|
||||
hash_entries_info_.xor_checksum ^= hash;
|
||||
}
|
||||
hash_entries_info_.entries.push_back(hash);
|
||||
hash_entries_info_.entries_count.FetchAddRelaxed(1);
|
||||
if (cache_res_mgr_ &&
|
||||
// Traditional rounding to whole bucket size
|
||||
((hash_entries_info_.entries.size() %
|
||||
@@ -314,6 +317,10 @@ class XXPH3FilterBitsBuilder : public BuiltinFilterBitsBuilder {
|
||||
// and has near-minimal peak memory use.
|
||||
std::deque<uint64_t> entries;
|
||||
|
||||
// Tracks the number of entries added for thread-safe
|
||||
// size estimation.
|
||||
RelaxedAtomic<size_t> entries_count{0};
|
||||
|
||||
// If cache_res_mgr_ != nullptr,
|
||||
// it manages cache charge for buckets of hash entries in (new) Bloom
|
||||
// or Ribbon Filter construction.
|
||||
@@ -332,6 +339,8 @@ class XXPH3FilterBitsBuilder : public BuiltinFilterBitsBuilder {
|
||||
void Swap(HashEntriesInfo* other) {
|
||||
assert(other != nullptr);
|
||||
std::swap(entries, other->entries);
|
||||
entries_count.StoreRelaxed(
|
||||
other->entries_count.ExchangeRelaxed(entries_count.LoadRelaxed()));
|
||||
std::swap(cache_res_bucket_handles, other->cache_res_bucket_handles);
|
||||
std::swap(xor_checksum, other->xor_checksum);
|
||||
std::swap(prev_alt_hash, other->prev_alt_hash);
|
||||
@@ -339,6 +348,7 @@ class XXPH3FilterBitsBuilder : public BuiltinFilterBitsBuilder {
|
||||
|
||||
void Reset() {
|
||||
entries.clear();
|
||||
entries_count.StoreRelaxed(0);
|
||||
cache_res_bucket_handles.clear();
|
||||
xor_checksum = 0;
|
||||
prev_alt_hash = {};
|
||||
@@ -1066,7 +1076,7 @@ class Standard128RibbonBitsReader : public BuiltinFilterBitsReader {
|
||||
|
||||
using LegacyBloomImpl = LegacyLocalityBloomImpl</*ExtraRotates*/ false>;
|
||||
|
||||
class LegacyBloomBitsBuilder : public BuiltinFilterBitsBuilder {
|
||||
class LegacyBloomBitsBuilder : public FilterBitsBuilder {
|
||||
public:
|
||||
explicit LegacyBloomBitsBuilder(const int bits_per_key, Logger* info_log);
|
||||
|
||||
|
||||
@@ -90,6 +90,19 @@ class FilterBitsBuilder {
|
||||
// <= the specified number of bytes. Callers (including RocksDB) should
|
||||
// only use this result for optimizing performance and not as a guarantee.
|
||||
virtual size_t ApproximateNumEntries(size_t bytes) = 0;
|
||||
|
||||
// Calculate number of bytes needed for a new filter, including
|
||||
// metadata. Passing the result to ApproximateNumEntries should
|
||||
// (ideally, usually) return >= the num_entry passed in.
|
||||
// When optimize_filters_for_memory is enabled, this function
|
||||
// is not authoritative but represents a target size that should
|
||||
// be close to the average size.
|
||||
virtual size_t CalculateSpace(size_t num_entries) = 0;
|
||||
|
||||
// Returns an estimate of the FP rate of the returned filter if
|
||||
// `num_entries` keys are added and the filter returned by Finish
|
||||
// is `bytes` bytes.
|
||||
virtual double EstimatedFpRate(size_t num_entries, size_t bytes) = 0;
|
||||
};
|
||||
|
||||
// A class that checks if a key can be in filter
|
||||
@@ -109,24 +122,6 @@ class FilterBitsReader {
|
||||
}
|
||||
};
|
||||
|
||||
// Exposes any extra information needed for testing built-in
|
||||
// FilterBitsBuilders
|
||||
class BuiltinFilterBitsBuilder : public FilterBitsBuilder {
|
||||
public:
|
||||
// Calculate number of bytes needed for a new filter, including
|
||||
// metadata. Passing the result to ApproximateNumEntries should
|
||||
// (ideally, usually) return >= the num_entry passed in.
|
||||
// When optimize_filters_for_memory is enabled, this function
|
||||
// is not authoritative but represents a target size that should
|
||||
// be close to the average size.
|
||||
virtual size_t CalculateSpace(size_t num_entries) = 0;
|
||||
|
||||
// Returns an estimate of the FP rate of the returned filter if
|
||||
// `num_entries` keys are added and the filter returned by Finish
|
||||
// is `bytes` bytes.
|
||||
virtual double EstimatedFpRate(size_t num_entries, size_t bytes) = 0;
|
||||
};
|
||||
|
||||
// Base class for RocksDB built-in filter reader with
|
||||
// extra useful functionalities for inernal.
|
||||
class BuiltinFilterBitsReader : public FilterBitsReader {
|
||||
|
||||
@@ -30,6 +30,35 @@ size_t FullFilterBlockBuilder::EstimateEntriesAdded() {
|
||||
return filter_bits_builder_->EstimateEntriesAdded();
|
||||
}
|
||||
|
||||
void FullFilterBlockBuilder::OnDataBlockFinalized(uint64_t num_data_blocks) {
|
||||
UpdateFilterSizeEstimate(num_data_blocks);
|
||||
}
|
||||
|
||||
size_t FullFilterBlockBuilder::CurrentFilterSizeEstimate() {
|
||||
return estimated_filter_size_;
|
||||
}
|
||||
|
||||
void FullFilterBlockBuilder::UpdateFilterSizeEstimate(
|
||||
uint64_t num_data_blocks) {
|
||||
size_t entries_added = filter_bits_builder_->EstimateEntriesAdded();
|
||||
|
||||
if (entries_added == 0) {
|
||||
estimated_filter_size_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
size_t filter_size = filter_bits_builder_->CalculateSpace(entries_added);
|
||||
|
||||
// Reserve filter space for next data block ~2x the average.
|
||||
size_t buffer_size = 0;
|
||||
if (num_data_blocks > 0) {
|
||||
buffer_size = (filter_size / num_data_blocks) * 2;
|
||||
estimated_filter_size_ = filter_size + buffer_size;
|
||||
} else {
|
||||
estimated_filter_size_ = filter_size;
|
||||
}
|
||||
}
|
||||
|
||||
void FullFilterBlockBuilder::AddWithPrevKey(
|
||||
const Slice& key_without_ts, const Slice& /*prev_key_without_ts*/) {
|
||||
FullFilterBlockBuilder::Add(key_without_ts);
|
||||
|
||||
@@ -57,6 +57,8 @@ class FullFilterBlockBuilder : public FilterBlockBuilder {
|
||||
return filter_bits_builder_->EstimateEntriesAdded() == 0;
|
||||
}
|
||||
size_t EstimateEntriesAdded() override;
|
||||
size_t CurrentFilterSizeEstimate() override;
|
||||
void OnDataBlockFinalized(uint64_t num_data_blocks) override;
|
||||
Status Finish(const BlockHandle& last_partition_block_handle, Slice* filter,
|
||||
std::unique_ptr<const char[]>* filter_owner = nullptr) override;
|
||||
using FilterBlockBuilder::Finish;
|
||||
@@ -73,6 +75,8 @@ class FullFilterBlockBuilder : public FilterBlockBuilder {
|
||||
|
||||
std::unique_ptr<FilterBitsBuilder> filter_bits_builder_;
|
||||
|
||||
void UpdateFilterSizeEstimate(uint64_t num_data_blocks_written) override;
|
||||
|
||||
private:
|
||||
// important: all of these might point to invalid addresses
|
||||
// at the time of destruction of this filter block. destructor
|
||||
@@ -80,6 +84,8 @@ class FullFilterBlockBuilder : public FilterBlockBuilder {
|
||||
const SliceTransform* const prefix_extractor_;
|
||||
const bool whole_key_filtering_;
|
||||
std::unique_ptr<const char[]> filter_data_;
|
||||
|
||||
size_t estimated_filter_size_ = 0;
|
||||
};
|
||||
|
||||
// A FilterBlockReader is used to parse filter from SST table.
|
||||
|
||||
@@ -52,6 +52,13 @@ class TestFilterBitsBuilder : public FilterBitsBuilder {
|
||||
|
||||
size_t ApproximateNumEntries(size_t bytes) override { return bytes / 4; }
|
||||
|
||||
size_t CalculateSpace(size_t num_entries) override { return num_entries * 4; }
|
||||
|
||||
double EstimatedFpRate(size_t /* num_entries */,
|
||||
size_t /* bytes */) override {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<uint32_t> hash_entries_;
|
||||
};
|
||||
@@ -229,6 +236,14 @@ class CountUniqueFilterBitsBuilderWrapper : public FilterBitsBuilder {
|
||||
return b_->ApproximateNumEntries(bytes);
|
||||
}
|
||||
|
||||
size_t CalculateSpace(size_t num_entries) override {
|
||||
return b_->CalculateSpace(num_entries);
|
||||
}
|
||||
|
||||
double EstimatedFpRate(size_t num_entries, size_t bytes) override {
|
||||
return b_->EstimatedFpRate(num_entries, bytes);
|
||||
}
|
||||
|
||||
size_t CountUnique() { return uniq_.size(); }
|
||||
};
|
||||
|
||||
|
||||
@@ -117,20 +117,18 @@ Slice ShortenedIndexBuilder::FindShortInternalKeySuccessor(
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t ShortenedIndexBuilder::EstimateCurrentIndexSize() const {
|
||||
void ShortenedIndexBuilder::UpdateIndexSizeEstimate() {
|
||||
uint64_t current_size =
|
||||
must_use_separator_with_seq_
|
||||
must_use_separator_with_seq_.LoadRelaxed()
|
||||
? index_block_builder_.CurrentSizeEstimate()
|
||||
: index_block_builder_without_seq_.CurrentSizeEstimate();
|
||||
|
||||
if (num_index_entries_ == 0) {
|
||||
return current_size;
|
||||
uint64_t final_estimate = current_size;
|
||||
if (num_index_entries_ > 0) {
|
||||
// Add buffer to generously account (in most cases) for the next index entry
|
||||
final_estimate += (2 * (current_size / num_index_entries_));
|
||||
}
|
||||
|
||||
uint64_t avg_entry_size = current_size / num_index_entries_;
|
||||
|
||||
// Add buffer to generously account (in most cases) for the next index entry
|
||||
return current_size + (2 * avg_entry_size);
|
||||
estimated_index_size_.StoreRelaxed(final_estimate);
|
||||
}
|
||||
|
||||
PartitionedIndexBuilder* PartitionedIndexBuilder::CreateIndexBuilder(
|
||||
@@ -188,8 +186,8 @@ void PartitionedIndexBuilder::MakeNewSubIndexBuilder() {
|
||||
// must_use_separator_with_seq_ is true (internal-key mode) (set to false by
|
||||
// default on Creation) so that flush policy can point to
|
||||
// sub_index_builder_->index_block_builder_
|
||||
if (must_use_separator_with_seq_) {
|
||||
sub_index_builder_->must_use_separator_with_seq_ = true;
|
||||
if (must_use_separator_with_seq_.LoadRelaxed()) {
|
||||
sub_index_builder_->must_use_separator_with_seq_.StoreRelaxed(true);
|
||||
builder_to_monitor = &sub_index_builder_->index_block_builder_;
|
||||
} else {
|
||||
builder_to_monitor = &sub_index_builder_->index_block_builder_without_seq_;
|
||||
@@ -237,6 +235,11 @@ void PartitionedIndexBuilder::MaybeFlush(const Slice& index_key,
|
||||
index_key, EncodedBlockHandle(index_value).AsSlice()));
|
||||
if (do_flush) {
|
||||
assert(entries_.back().value.get() == sub_index_builder_);
|
||||
|
||||
// Update estimate of completed partitions when a partition is flushed
|
||||
estimated_completed_partitions_size_.FetchAddRelaxed(
|
||||
sub_index_builder_->CurrentIndexSizeEstimate());
|
||||
|
||||
cut_filter_block = true;
|
||||
MakeNewSubIndexBuilder();
|
||||
}
|
||||
@@ -254,9 +257,15 @@ void PartitionedIndexBuilder::FinishIndexEntry(const BlockHandle& block_handle,
|
||||
skip_delta_encoding);
|
||||
std::swap(entries_.back().key, entry->separator_with_seq);
|
||||
|
||||
if (!must_use_separator_with_seq_ && entry->must_use_separator_with_seq) {
|
||||
// Update cached size estimate when data blocks are finalized for more
|
||||
// accurate tail size estimation. This is needed for parallel compression
|
||||
// which uses FinishIndexEntry() instead of AddIndexEntry().
|
||||
UpdateIndexSizeEstimate();
|
||||
|
||||
if (!must_use_separator_with_seq_.LoadRelaxed() &&
|
||||
entry->must_use_separator_with_seq) {
|
||||
// We need to apply !must_use_separator_with_seq to all sub-index builders
|
||||
must_use_separator_with_seq_ = true;
|
||||
must_use_separator_with_seq_.StoreRelaxed(true);
|
||||
flush_policy_->Retarget(sub_index_builder_->index_block_builder_);
|
||||
}
|
||||
// NOTE: not compatible with coupled partitioned filters so don't need to
|
||||
@@ -278,10 +287,15 @@ Slice PartitionedIndexBuilder::AddIndexEntry(
|
||||
separator_scratch, skip_delta_encoding);
|
||||
entries_.back().key.assign(sep.data(), sep.size());
|
||||
|
||||
if (!must_use_separator_with_seq_ &&
|
||||
sub_index_builder_->must_use_separator_with_seq_) {
|
||||
// Update cached size estimate when data blocks are finalized for more
|
||||
// accurate tail size estimation. This ensures the estimate reflects current
|
||||
// state after each data block is added.
|
||||
UpdateIndexSizeEstimate();
|
||||
|
||||
if (!must_use_separator_with_seq_.LoadRelaxed() &&
|
||||
sub_index_builder_->must_use_separator_with_seq_.LoadRelaxed()) {
|
||||
// We need to apply !must_use_separator_with_seq to all sub-index builders
|
||||
must_use_separator_with_seq_ = true;
|
||||
must_use_separator_with_seq_.StoreRelaxed(true);
|
||||
flush_policy_->Retarget(sub_index_builder_->index_block_builder_);
|
||||
}
|
||||
if (UNLIKELY(first_key_in_next_block == nullptr)) {
|
||||
@@ -315,7 +329,7 @@ Status PartitionedIndexBuilder::Finish(
|
||||
const Slice handle_delta_encoding_slice(handle_delta_encoding);
|
||||
index_block_builder_.Add(last_entry.key, handle_encoding.AsSlice(),
|
||||
&handle_delta_encoding_slice);
|
||||
if (!must_use_separator_with_seq_) {
|
||||
if (!must_use_separator_with_seq_.LoadRelaxed()) {
|
||||
index_block_builder_without_seq_.Add(ExtractUserKey(last_entry.key),
|
||||
handle_encoding.AsSlice(),
|
||||
&handle_delta_encoding_slice);
|
||||
@@ -324,7 +338,7 @@ Status PartitionedIndexBuilder::Finish(
|
||||
}
|
||||
// If there is no sub_index left, then return the 2nd level index.
|
||||
if (UNLIKELY(entries_.empty())) {
|
||||
if (must_use_separator_with_seq_) {
|
||||
if (must_use_separator_with_seq_.LoadRelaxed()) {
|
||||
index_blocks->index_block_contents = index_block_builder_.Finish();
|
||||
} else {
|
||||
index_blocks->index_block_contents =
|
||||
@@ -338,7 +352,8 @@ Status PartitionedIndexBuilder::Finish(
|
||||
// expect more calls to Finish
|
||||
Entry& entry = entries_.front();
|
||||
// Apply the policy to all sub-indexes
|
||||
entry.value->must_use_separator_with_seq_ = must_use_separator_with_seq_;
|
||||
entry.value->must_use_separator_with_seq_.StoreRelaxed(
|
||||
must_use_separator_with_seq_.LoadRelaxed());
|
||||
auto s = entry.value->Finish(index_blocks);
|
||||
index_size_ += index_blocks->index_block_contents.size();
|
||||
finishing_indexes_ = true;
|
||||
@@ -347,4 +362,49 @@ Status PartitionedIndexBuilder::Finish(
|
||||
}
|
||||
|
||||
size_t PartitionedIndexBuilder::NumPartitions() const { return partition_cnt_; }
|
||||
|
||||
void PartitionedIndexBuilder::UpdateIndexSizeEstimate() {
|
||||
uint64_t total_size = 0;
|
||||
|
||||
// Ignore last entry which is a placeholder for the partition being built
|
||||
size_t completed_partitions = entries_.size() > 0 ? entries_.size() - 1 : 0;
|
||||
|
||||
// Use running estimate of completed partitions instead of IndexSize() which
|
||||
// is only available after calling Finish().
|
||||
uint64_t completed_partitions_size =
|
||||
estimated_completed_partitions_size_.LoadRelaxed();
|
||||
total_size += completed_partitions_size;
|
||||
|
||||
// Add current active partition size if it exists
|
||||
uint64_t current_sub_index_size = 0;
|
||||
if (sub_index_builder_ != nullptr) {
|
||||
current_sub_index_size = sub_index_builder_->CurrentIndexSizeEstimate();
|
||||
total_size += current_sub_index_size;
|
||||
}
|
||||
|
||||
// Add buffer for top-level index and next partition
|
||||
uint64_t buffer_size = 0;
|
||||
if (completed_partitions > 0) {
|
||||
// Calculate top-level index size. Each top-level entry consists of:
|
||||
// separator key (~20-50 bytes) + BlockHandle (~20 bytes) + overhead
|
||||
// Estimate ~70 bytes per top-level entry as a reasonable average
|
||||
auto estimated_top_level_size = completed_partitions * 70;
|
||||
total_size += completed_partitions * 70;
|
||||
|
||||
// Buffer for next partition + next top-level entry
|
||||
uint64_t avg_partition_size =
|
||||
completed_partitions_size / completed_partitions;
|
||||
uint64_t avg_top_level_entry_size =
|
||||
estimated_top_level_size / completed_partitions;
|
||||
|
||||
buffer_size = 2 * (avg_partition_size + avg_top_level_entry_size);
|
||||
total_size += buffer_size;
|
||||
} else if (sub_index_builder_ != nullptr) {
|
||||
// For the first partition, estimate using the current partition's state
|
||||
buffer_size = 2 * current_sub_index_size;
|
||||
total_size += buffer_size;
|
||||
}
|
||||
estimated_index_size_.StoreRelaxed(total_size);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
Loaded 100 of 135 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user