mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-07 11:32:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fbc1cd5bc | ||
|
|
60f24d394d | ||
|
|
268a719a30 | ||
|
|
9791103976 | ||
|
|
4982688c21 | ||
|
|
ef5a37aca8 |
No files matched your search
@@ -1,21 +0,0 @@
|
||||
name: cache-getdeps-downloads
|
||||
description: Cache getdeps downloads to avoid unreliable mirrors and speed up builds
|
||||
outputs:
|
||||
cache-hit:
|
||||
description: Whether the cache was hit
|
||||
value: ${{ steps.cache-downloads.outputs.cache-hit }}
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Cache getdeps downloads
|
||||
id: cache-downloads
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
# Use a fixed path that we control - folly.mk will sync with getdeps downloads dir
|
||||
path: /tmp/rocksdb-getdeps-cache
|
||||
# Use a rolling cache key - the cache accumulates downloads over time
|
||||
# The key includes a weekly timestamp to ensure periodic refresh
|
||||
key: getdeps-downloads-${{ runner.os }}-${{ runner.arch }}-week-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
getdeps-downloads-${{ runner.os }}-${{ runner.arch }}-week-
|
||||
getdeps-downloads-${{ runner.os }}-${{ runner.arch }}-
|
||||
@@ -54,7 +54,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- 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
|
||||
@@ -69,7 +68,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/cache-folly"
|
||||
id: cache-folly
|
||||
@@ -88,7 +86,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- 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"
|
||||
@@ -113,20 +110,6 @@ 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:
|
||||
@@ -165,7 +148,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- 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 VERBOSE=1 -j20 && ctest -j20)"
|
||||
- uses: "./.github/actions/post-steps"
|
||||
@@ -102,7 +102,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/cache-folly"
|
||||
id: cache-folly
|
||||
@@ -121,7 +120,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- run: USE_FOLLY_LITE=1 EXTRA_CXXFLAGS=-DGLOG_USE_GLOG_EXPORT V=1 make -j32 all
|
||||
- uses: "./.github/actions/post-steps"
|
||||
@@ -135,7 +133,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/cache-folly"
|
||||
id: cache-folly
|
||||
|
||||
@@ -268,7 +268,6 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"util/dynamic_bloom.cc",
|
||||
"util/file_checksum_helper.cc",
|
||||
"util/hash.cc",
|
||||
"util/io_dispatcher_imp.cc",
|
||||
"util/murmurhash.cc",
|
||||
"util/random.cc",
|
||||
"util/rate_limiter.cc",
|
||||
@@ -5215,12 +5214,6 @@ cpp_unittest_wrapper(name="interval_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="io_dispatcher_test",
|
||||
srcs=["util/io_dispatcher_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="io_posix_test",
|
||||
srcs=["env/io_posix_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
|
||||
@@ -219,10 +219,6 @@ 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")
|
||||
|
||||
+1
-24
@@ -1,32 +1,9 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 10.11.2 (02/25/2026)
|
||||
## 10.9.1 (12/11/2025)
|
||||
### Bug Fixes
|
||||
* Fix longstanding failures that can arise from reading and/or compacting old DB dirs with range deletions (likely from version < 5.19.0) in many newer versions.
|
||||
|
||||
## 10.11.1 (02/06/2026)
|
||||
### Bug Fixes
|
||||
* Fixed a bug in PosixFS AbortIO that could cause it to ignore an IO completion and a subsequent Poll to hang
|
||||
|
||||
## 10.11.0 (01/23/2026)
|
||||
### Public API Changes
|
||||
* New SetOptions API that allows setting options for multiple CFs, avoiding the need to reserialize OPTIONS file for each CF
|
||||
* Remove remaining pieces of Lua integration
|
||||
|
||||
### Behavior Changes
|
||||
* The new default for `BlockBasedTableOptions::format_version` is 7, which has been supported since RocksDB 10.4.0 and is required in order to use CompressionManagers supporting custom compression types.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a small performance bug with `format_version=7` when decompressing formats other than Snappy and ZSTD.
|
||||
* Fixed an infinite compaction loop bug with User-Defined Timestamps (UDT) where bottommost files were repeatedly marked for compaction even though their timestamp could not be collapsed.
|
||||
* Bugfix for persisted UDT record sequence number zeroing logic.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -296,28 +296,6 @@ $(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; \
|
||||
@@ -390,6 +368,8 @@ ifdef COMPILE_WITH_TSAN
|
||||
# Turn off -pg when enabling TSAN testing, because that induces
|
||||
# a link failure. TODO: find the root cause
|
||||
PROFILING_FLAGS =
|
||||
# LUA is not supported under TSAN
|
||||
LUA_PATH =
|
||||
# Limit keys for crash test under TSAN to avoid error:
|
||||
# "ThreadSanitizer: DenseSlabAllocator overflow. Dying."
|
||||
CRASH_TEST_EXT_ARGS += --max_key=1000000
|
||||
@@ -506,6 +486,32 @@ ifndef DISABLE_WARNING_AS_ERROR
|
||||
endif
|
||||
|
||||
|
||||
ifdef LUA_PATH
|
||||
|
||||
ifndef LUA_INCLUDE
|
||||
LUA_INCLUDE=$(LUA_PATH)/include
|
||||
endif
|
||||
|
||||
LUA_INCLUDE_FILE=$(LUA_INCLUDE)/lualib.h
|
||||
|
||||
ifeq ("$(wildcard $(LUA_INCLUDE_FILE))", "")
|
||||
# LUA_INCLUDE_FILE does not exist
|
||||
$(error Cannot find lualib.h under $(LUA_INCLUDE). Try to specify both LUA_PATH and LUA_INCLUDE manually)
|
||||
endif
|
||||
LUA_FLAGS = -I$(LUA_INCLUDE) -DLUA -DLUA_COMPAT_ALL
|
||||
CFLAGS += $(LUA_FLAGS)
|
||||
CXXFLAGS += $(LUA_FLAGS)
|
||||
|
||||
ifndef LUA_LIB
|
||||
LUA_LIB = $(LUA_PATH)/lib/liblua.a
|
||||
endif
|
||||
ifeq ("$(wildcard $(LUA_LIB))", "") # LUA_LIB does not exist
|
||||
$(error $(LUA_LIB) does not exist. Try to specify both LUA_PATH and LUA_LIB manually)
|
||||
endif
|
||||
EXEC_LDFLAGS += $(LUA_LIB)
|
||||
|
||||
endif
|
||||
|
||||
ifeq ($(NO_THREEWAY_CRC32C), 1)
|
||||
CXXFLAGS += -DNO_THREEWAY_CRC32C
|
||||
endif
|
||||
@@ -576,8 +582,8 @@ ifneq ($(filter check-headers, $(MAKECMDGOALS)),)
|
||||
# TODO: add/support JNI headers
|
||||
DEV_HEADER_DIRS := $(sort include/ $(dir $(ALL_SOURCES)))
|
||||
# Some headers like in port/ are platform-specific
|
||||
DEV_HEADERS_TO_CHECK := $(shell $(FIND) $(DEV_HEADER_DIRS) -type f -name '*.h' | grep -E -v 'port/|plugin/|range_tree/|secondary_index/')
|
||||
PUBLIC_HEADERS_TO_CHECK := $(shell $(FIND) include/ -type f -name '*.h')
|
||||
DEV_HEADERS_TO_CHECK := $(shell $(FIND) $(DEV_HEADER_DIRS) -type f -name '*.h' | grep -E -v 'port/|plugin/|lua/|range_tree/|secondary_index/')
|
||||
PUBLIC_HEADERS_TO_CHECK := $(shell $(FIND) include/ -type f -name '*.h' | grep -E -v 'lua/')
|
||||
else
|
||||
DEV_HEADERS_TO_CHECK :=
|
||||
PUBLIC_HEADERS_TO_CHECK :=
|
||||
@@ -1922,9 +1928,6 @@ blob_source_test: $(OBJ_DIR)/db/blob/blob_source_test.o $(TEST_LIBRARY) $(LIBRAR
|
||||
blob_garbage_meter_test: $(OBJ_DIR)/db/blob/blob_garbage_meter_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
io_dispatcher_test: $(OBJ_DIR)/util/io_dispatcher_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
timer_test: $(OBJ_DIR)/util/timer_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
|
||||
@@ -831,6 +831,7 @@ fi
|
||||
if test -n "$WITH_JEMALLOC_FLAG"; then
|
||||
echo "WITH_JEMALLOC_FLAG=$WITH_JEMALLOC_FLAG" >> "$OUTPUT"
|
||||
fi
|
||||
echo "LUA_PATH=$LUA_PATH" >> "$OUTPUT"
|
||||
if test -n "$USE_FOLLY"; then
|
||||
echo "USE_FOLLY=$USE_FOLLY" >> "$OUTPUT"
|
||||
fi
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 ROCKSDB_NAMESPACE, and ROCKSDB_ASSERT_STATUS_CHECKED are
|
||||
# * 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.
|
||||
@@ -19,7 +19,7 @@ BAD=""
|
||||
# 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|_WIN32|OS_WIN|ODR-SAFE|__cplusplus|ROCKSDB_DLL|ROCKSDB_LIBRARY_EXPORTS'
|
||||
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."
|
||||
|
||||
@@ -19,3 +19,4 @@ BENCHMARK_BASE=/mnt/gvfs/third-party2/benchmark/780c7a0f9cf0967961e69ad08e61cddd
|
||||
KERNEL_HEADERS_BASE=/mnt/gvfs/third-party2/kernel-headers/624a2f8f6c93c3c1df8aa4a6255d8202631a6c80/fb/platform010/da39a3e
|
||||
BINUTILS_BASE=/mnt/gvfs/third-party2/binutils/39579e8603b48b3540f8b0633f43adf29acccb8b/2.37/centos8-native/da39a3e
|
||||
VALGRIND_BASE=/mnt/gvfs/third-party2/valgrind/cd9cc656d49ecb53797ce4d055e49fde29fd57ff/3.19.0/platform010/76ebdda
|
||||
LUA_BASE=/mnt/gvfs/third-party2/lua/363787fa5cac2a8aa20638909210443278fa138e/5.3.4/platform010/9079c97
|
||||
@@ -164,4 +164,12 @@ EXEC_LDFLAGS_SHARED="$SNAPPY_LIBS $ZLIB_LIBS $BZIP_LIBS $LZ4_LIBS $ZSTD_LIBS $GF
|
||||
|
||||
VALGRIND_VER="$VALGRIND_BASE/bin/"
|
||||
|
||||
export CC CXX AR CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD
|
||||
LUA_PATH="$LUA_BASE"
|
||||
|
||||
if test -z $PIC_BUILD; then
|
||||
LUA_LIB=" $LUA_PATH/lib/liblua.a"
|
||||
else
|
||||
LUA_LIB=" $LUA_PATH/lib/liblua_pic.a"
|
||||
fi
|
||||
|
||||
export CC CXX AR CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD LUA_PATH LUA_LIB
|
||||
@@ -172,4 +172,4 @@ EXEC_LDFLAGS_SHARED="$SNAPPY_LIBS $ZLIB_LIBS $BZIP_LIBS $LZ4_LIBS $ZSTD_LIBS $GF
|
||||
|
||||
VALGRIND_VER="$VALGRIND_BASE/bin/"
|
||||
|
||||
export CC CXX AR AS CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD
|
||||
export CC CXX AR AS CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD LUA_PATH LUA_LIB
|
||||
@@ -1,123 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pre-download packages with unreliable mirrors using fallback mirrors.
|
||||
Reads package info from folly's getdeps manifest files.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
import subprocess
|
||||
import configparser
|
||||
|
||||
def sha256_file(path):
|
||||
"""Calculate SHA256 hash of a file."""
|
||||
h = hashlib.sha256()
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
for chunk in iter(lambda: f.read(65536), b''):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def parse_manifest(manifest_path):
|
||||
"""Parse a getdeps manifest file to extract download info."""
|
||||
config = configparser.ConfigParser()
|
||||
try:
|
||||
config.read(manifest_path)
|
||||
if 'download' in config:
|
||||
return {
|
||||
'url': config['download'].get('url', ''),
|
||||
'sha256': config['download'].get('sha256', ''),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_fallback_mirrors(url):
|
||||
"""Get fallback mirror URLs for a given URL."""
|
||||
# Fallback mirror patterns for known unreliable hosts
|
||||
mirror_fallbacks = {
|
||||
"ftp.gnu.org/gnu/": [
|
||||
"https://mirrors.kernel.org/gnu/",
|
||||
"https://ftpmirror.gnu.org/gnu/",
|
||||
"https://ftp.gnu.org/gnu/",
|
||||
],
|
||||
"ftpmirror.gnu.org/gnu/": [
|
||||
"https://mirrors.kernel.org/gnu/",
|
||||
"https://ftpmirror.gnu.org/gnu/",
|
||||
"https://ftp.gnu.org/gnu/",
|
||||
],
|
||||
}
|
||||
|
||||
for pattern, mirrors in mirror_fallbacks.items():
|
||||
if pattern in url:
|
||||
# Extract the path after the pattern
|
||||
path_start = url.find(pattern) + len(pattern)
|
||||
path = url[path_start:]
|
||||
return [mirror + path for mirror in mirrors]
|
||||
return [url] # No fallback, use original
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 4:
|
||||
print(f"Usage: {sys.argv[0]} <download_dir> <cache_dir> <manifests_dir>")
|
||||
sys.exit(1)
|
||||
|
||||
download_dir, cache_dir, manifests_dir = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
|
||||
# Packages known to have unreliable mirrors
|
||||
packages_to_check = ["autoconf", "automake", "libtool"]
|
||||
|
||||
for package in packages_to_check:
|
||||
manifest_path = os.path.join(manifests_dir, package)
|
||||
if not os.path.exists(manifest_path):
|
||||
continue
|
||||
|
||||
info = parse_manifest(manifest_path)
|
||||
if not info or not info['url'] or not info['sha256']:
|
||||
continue
|
||||
|
||||
# Determine filename from URL
|
||||
url = info['url']
|
||||
expected_sha256 = info['sha256']
|
||||
url_filename = os.path.basename(url)
|
||||
|
||||
# getdeps uses format: {package}-{filename}
|
||||
filename = f"{package}-{url_filename}"
|
||||
filepath = os.path.join(download_dir, filename)
|
||||
cache_path = os.path.join(cache_dir, filename)
|
||||
|
||||
# Check if already valid
|
||||
if os.path.exists(filepath) and sha256_file(filepath) == expected_sha256:
|
||||
print(f" {filename}: OK (already downloaded)")
|
||||
continue
|
||||
|
||||
# Check cache
|
||||
if os.path.exists(cache_path) and sha256_file(cache_path) == expected_sha256:
|
||||
print(f" {filename}: OK (from cache)")
|
||||
subprocess.run(['cp', cache_path, filepath], check=True)
|
||||
continue
|
||||
|
||||
# Try fallback mirrors
|
||||
mirrors = get_fallback_mirrors(url)
|
||||
downloaded = False
|
||||
for mirror_url in mirrors:
|
||||
print(f" {filename}: trying {mirror_url}...")
|
||||
try:
|
||||
subprocess.run(['wget', '-q', '-O', filepath, mirror_url], check=True, timeout=120)
|
||||
if sha256_file(filepath) == expected_sha256:
|
||||
print(f" {filename}: OK (downloaded)")
|
||||
subprocess.run(['cp', filepath, cache_path], check=False)
|
||||
downloaded = True
|
||||
break
|
||||
else:
|
||||
os.remove(filepath)
|
||||
except Exception:
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
|
||||
if not downloaded:
|
||||
print(f" {filename}: WARNING - all mirrors failed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -101,5 +101,6 @@ get_lib_base benchmark LATEST platform010
|
||||
get_lib_base kernel-headers fb platform010
|
||||
get_lib_base binutils LATEST centos8-native
|
||||
get_lib_base valgrind LATEST platform010
|
||||
get_lib_base lua 5.3.4 platform010
|
||||
|
||||
git diff $OUTPUT
|
||||
Vendored
+1
-1
@@ -644,7 +644,7 @@ using TypedHandle = SharedCache::TypedHandle;
|
||||
|
||||
TEST_P(CacheTest, SetCapacity) {
|
||||
if (IsHyperClock()) {
|
||||
// TODO: update test & code for limited support
|
||||
// TODO: update test & code for limited supoort
|
||||
ROCKSDB_GTEST_BYPASS(
|
||||
"HyperClockCache doesn't support arbitrary capacity "
|
||||
"adjustments.");
|
||||
|
||||
Vendored
+414
-355
File diff suppressed because it is too large.
Load diff
Vendored
+69
-135
@@ -317,89 +317,40 @@ struct ClockHandle : public ClockHandleBasicData {
|
||||
// | acquire counter | release counter | hit bit | state marker |
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
struct SlotMeta : public BitFields<uint64_t, SlotMeta> {
|
||||
// For reading or updating counters in meta word.
|
||||
static constexpr uint8_t kCounterNumBits = 30;
|
||||
// Number of times the a reference has been acquired (or attempted)
|
||||
// since last reset by eviction processing
|
||||
using AcquireCounter =
|
||||
UnsignedBitField<SlotMeta, kCounterNumBits, NoPrevBitField>;
|
||||
// Number of times the a reference has been released (or attempted)
|
||||
// since last reset by eviction processing
|
||||
using ReleaseCounter =
|
||||
UnsignedBitField<SlotMeta, kCounterNumBits, AcquireCounter>;
|
||||
// Metadata bit in support of secondary cache
|
||||
using HitFlag = BoolBitField<SlotMeta, ReleaseCounter>;
|
||||
// Occupied means any state other than empty
|
||||
using OccupiedFlag = BoolBitField<SlotMeta, HitFlag>;
|
||||
// Shareable means the entry is reference counted (visible or invisible)
|
||||
// (only set if also occupied)
|
||||
using ShareableFlag = BoolBitField<SlotMeta, OccupiedFlag>;
|
||||
// Visible is only set if also shareable (invisible can't be found by
|
||||
// Lookup)
|
||||
using VisibleFlag = BoolBitField<SlotMeta, ShareableFlag>;
|
||||
// For reading or updating counters in meta word.
|
||||
static constexpr uint8_t kCounterNumBits = 30;
|
||||
static constexpr uint64_t kCounterMask = (uint64_t{1} << kCounterNumBits) - 1;
|
||||
|
||||
// Convenience functions
|
||||
uint32_t GetAcquireCounter() const { return Get<AcquireCounter>(); }
|
||||
void SetAcquireCounter(uint32_t val) { Set<AcquireCounter>(val); }
|
||||
uint32_t GetReleaseCounter() const { return Get<ReleaseCounter>(); }
|
||||
void SetReleaseCounter(uint32_t val) { Set<ReleaseCounter>(val); }
|
||||
uint32_t GetRefcount() const {
|
||||
return Get<AcquireCounter>() - Get<ReleaseCounter>();
|
||||
}
|
||||
bool GetHit() const { return Get<HitFlag>(); }
|
||||
void SetHit(bool val) { Set<HitFlag>(val); }
|
||||
static constexpr uint8_t kAcquireCounterShift = 0;
|
||||
static constexpr uint64_t kAcquireIncrement = uint64_t{1}
|
||||
<< kAcquireCounterShift;
|
||||
static constexpr uint8_t kReleaseCounterShift = kCounterNumBits;
|
||||
static constexpr uint64_t kReleaseIncrement = uint64_t{1}
|
||||
<< kReleaseCounterShift;
|
||||
|
||||
// Some distinct states for the various state flags
|
||||
bool IsEmpty() const {
|
||||
bool rv = !Get<OccupiedFlag>();
|
||||
if (rv) {
|
||||
assert(!Get<ShareableFlag>());
|
||||
assert(!Get<VisibleFlag>());
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
// For setting the hit bit
|
||||
static constexpr uint8_t kHitBitShift = 2U * kCounterNumBits;
|
||||
static constexpr uint64_t kHitBitMask = uint64_t{1} << kHitBitShift;
|
||||
|
||||
bool IsUnderConstruction() const {
|
||||
bool rv = Get<OccupiedFlag>() && !Get<ShareableFlag>();
|
||||
if (rv) {
|
||||
assert(!Get<VisibleFlag>());
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
void SetUnderConstruction() {
|
||||
Set<OccupiedFlag>(true);
|
||||
Set<ShareableFlag>(false);
|
||||
Set<VisibleFlag>(false);
|
||||
}
|
||||
// For reading or updating the state marker in meta word
|
||||
static constexpr uint8_t kStateShift = kHitBitShift + 1;
|
||||
|
||||
bool IsShareable() const { return Get<ShareableFlag>(); }
|
||||
bool IsInvisible() const {
|
||||
bool rv = Get<ShareableFlag>() && !Get<VisibleFlag>();
|
||||
if (rv) {
|
||||
assert(Get<OccupiedFlag>());
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
void SetInvisible() {
|
||||
Set<OccupiedFlag>(true);
|
||||
Set<ShareableFlag>(true);
|
||||
Set<VisibleFlag>(false);
|
||||
}
|
||||
// Bits contribution to state marker.
|
||||
// Occupied means any state other than empty
|
||||
static constexpr uint8_t kStateOccupiedBit = 0b100;
|
||||
// Shareable means the entry is reference counted (visible or invisible)
|
||||
// (only set if also occupied)
|
||||
static constexpr uint8_t kStateShareableBit = 0b010;
|
||||
// Visible is only set if also shareable
|
||||
static constexpr uint8_t kStateVisibleBit = 0b001;
|
||||
|
||||
bool IsVisible() const {
|
||||
bool rv = Get<ShareableFlag>() && Get<VisibleFlag>();
|
||||
if (rv) {
|
||||
assert(Get<OccupiedFlag>());
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
void SetVisible() {
|
||||
Set<OccupiedFlag>(true);
|
||||
Set<ShareableFlag>(true);
|
||||
Set<VisibleFlag>(true);
|
||||
}
|
||||
};
|
||||
// Complete state markers (not shifted into full word)
|
||||
static constexpr uint8_t kStateEmpty = 0b000;
|
||||
static constexpr uint8_t kStateConstruction = kStateOccupiedBit;
|
||||
static constexpr uint8_t kStateInvisible =
|
||||
kStateOccupiedBit | kStateShareableBit;
|
||||
static constexpr uint8_t kStateVisible =
|
||||
kStateOccupiedBit | kStateShareableBit | kStateVisibleBit;
|
||||
|
||||
// Constants for initializing the countdown clock. (Countdown clock is only
|
||||
// in effect with zero refs, acquire counter == release counter, and in that
|
||||
@@ -413,7 +364,7 @@ struct ClockHandle : public ClockHandleBasicData {
|
||||
// TODO: make these coundown values tuning parameters for eviction?
|
||||
|
||||
// See above. Mutable for read reference counting.
|
||||
mutable BitFieldsAtomic<SlotMeta> meta{};
|
||||
mutable AcqRelAtomic<uint64_t> meta{};
|
||||
}; // struct ClockHandle
|
||||
|
||||
class BaseClockTable {
|
||||
@@ -480,9 +431,9 @@ class BaseClockTable {
|
||||
bool IsEvictionEffortExceeded(const BaseClockTable::EvictionData& data) const;
|
||||
#ifndef NDEBUG
|
||||
// Acquire N references
|
||||
void TEST_RefN(ClockHandle& handle, uint32_t n);
|
||||
void TEST_RefN(ClockHandle& handle, size_t n);
|
||||
// Helper for TEST_ReleaseN
|
||||
void TEST_ReleaseNMinus1(ClockHandle* handle, uint32_t n);
|
||||
void TEST_ReleaseNMinus1(ClockHandle* handle, size_t n);
|
||||
#endif
|
||||
|
||||
private: // fns
|
||||
@@ -538,13 +489,13 @@ class BaseClockTable {
|
||||
// TODO: is this separation needed if we don't do background evictions?
|
||||
ALIGN_AS(CACHE_LINE_SIZE)
|
||||
// Number of elements in the table.
|
||||
Atomic<size_t> occupancy_{};
|
||||
AcqRelAtomic<size_t> occupancy_{};
|
||||
|
||||
// Memory usage by entries tracked by the cache (including standalone)
|
||||
Atomic<size_t> usage_{};
|
||||
AcqRelAtomic<size_t> usage_{};
|
||||
|
||||
// Part of usage by standalone entries (not in table)
|
||||
Atomic<size_t> standalone_usage_{};
|
||||
AcqRelAtomic<size_t> standalone_usage_{};
|
||||
|
||||
// Maximum total charge of all elements stored in the table.
|
||||
// (Relaxed: eventual consistency/update is OK)
|
||||
@@ -635,7 +586,7 @@ class FixedHyperClockTable : public BaseClockTable {
|
||||
bool GrowIfNeeded(size_t new_occupancy, InsertState& state);
|
||||
|
||||
HandleImpl* DoInsert(const ClockHandleBasicData& proto,
|
||||
uint32_t initial_countdown, bool take_ref,
|
||||
uint64_t initial_countdown, bool take_ref,
|
||||
InsertState& state);
|
||||
|
||||
// Runs the clock eviction algorithm trying to reclaim at least
|
||||
@@ -663,7 +614,7 @@ class FixedHyperClockTable : public BaseClockTable {
|
||||
}
|
||||
|
||||
// Release N references
|
||||
void TEST_ReleaseN(HandleImpl* handle, uint32_t n);
|
||||
void TEST_ReleaseN(HandleImpl* handle, size_t n);
|
||||
#endif
|
||||
|
||||
// The load factor p is a real number in (0, 1) such that at all
|
||||
@@ -824,7 +775,6 @@ 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
|
||||
@@ -844,63 +794,47 @@ 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>;
|
||||
|
||||
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" 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;
|
||||
|
||||
static NextWithShift Make(size_t next, int shift) {
|
||||
return NextWithShift{}.With<Next>(next).With<Shift>(
|
||||
static_cast<uint8_t>(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 MakeEnd(size_t next, int shift) {
|
||||
return Make(next, shift).With<EndFlag>(true).With<LockedFlag>(true);
|
||||
}
|
||||
};
|
||||
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;
|
||||
|
||||
// A marker for head_next_with_shift that indicates this HandleImpl is
|
||||
// heap allocated (standalone) rather than in the table.
|
||||
static constexpr NextWithShift kStandaloneMarker{UINT64_MAX};
|
||||
static constexpr uint64_t 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 NextWithShift kUnusedMarker{0};
|
||||
static constexpr uint64_t kUnusedMarker = 0;
|
||||
|
||||
// See above. The head pointer is logically independent of the rest of
|
||||
// the entry, including the chain next pointer.
|
||||
BitFieldsAtomic<NextWithShift> head_next_with_shift{kUnusedMarker};
|
||||
BitFieldsAtomic<NextWithShift> chain_next_with_shift{kUnusedMarker};
|
||||
AcqRelAtomic<uint64_t> head_next_with_shift{kUnusedMarker};
|
||||
AcqRelAtomic<uint64_t> chain_next_with_shift{kUnusedMarker};
|
||||
|
||||
// For supporting CreateStandalone and some fallback cases.
|
||||
inline bool IsStandalone() const {
|
||||
@@ -946,7 +880,7 @@ class AutoHyperClockTable : public BaseClockTable {
|
||||
bool GrowIfNeeded(size_t new_occupancy, InsertState& state);
|
||||
|
||||
HandleImpl* DoInsert(const ClockHandleBasicData& proto,
|
||||
uint32_t initial_countdown, bool take_ref,
|
||||
uint64_t initial_countdown, bool take_ref,
|
||||
InsertState& state);
|
||||
|
||||
// Runs the clock eviction algorithm trying to reclaim at least
|
||||
@@ -974,7 +908,7 @@ class AutoHyperClockTable : public BaseClockTable {
|
||||
}
|
||||
|
||||
// Release N references
|
||||
void TEST_ReleaseN(HandleImpl* handle, uint32_t n);
|
||||
void TEST_ReleaseN(HandleImpl* handle, size_t n);
|
||||
#endif
|
||||
|
||||
// Maximum ratio of number of occupied slots to number of usable slots. The
|
||||
@@ -1056,7 +990,7 @@ class AutoHyperClockTable : public BaseClockTable {
|
||||
// To maximize parallelization of Grow() operations, this field is only
|
||||
// updated opportunistically after Grow() operations and in DoInsert() where
|
||||
// it is found to be out-of-date. See CatchUpLengthInfoNoWait().
|
||||
Atomic<uint64_t> length_info_;
|
||||
AcqRelAtomic<uint64_t> length_info_;
|
||||
|
||||
// An already-computed version of the usable length times the max load
|
||||
// factor. Could be slightly out of date but GrowIfNeeded()/Grow() handle
|
||||
@@ -1179,8 +1113,8 @@ class ALIGN_AS(CACHE_LINE_SIZE) ClockCacheShard final : public CacheShardBase {
|
||||
return table_.TEST_MutableOccupancyLimit();
|
||||
}
|
||||
// Acquire/release N references
|
||||
void TEST_RefN(HandleImpl* handle, uint32_t n);
|
||||
void TEST_ReleaseN(HandleImpl* handle, uint32_t n);
|
||||
void TEST_RefN(HandleImpl* handle, size_t n);
|
||||
void TEST_ReleaseN(HandleImpl* handle, size_t n);
|
||||
#endif
|
||||
|
||||
private: // data
|
||||
|
||||
Vendored
+7
-7
@@ -1503,7 +1503,7 @@ TEST_P(BasicSecondaryCacheTest, FullCapacityTest) {
|
||||
/*context*/ this, Cache::Priority::LOW);
|
||||
ASSERT_EQ(handle1, nullptr);
|
||||
|
||||
// k1 promotion can fail with strict_capacity_limit=true, but Lookup still
|
||||
// k1 promotion can fail with strict_capacit_limit=true, but Lookup still
|
||||
// succeeds using a standalone handle
|
||||
handle1 = cache->Lookup(k1.AsSlice(), GetHelper(),
|
||||
/*context*/ this, Cache::Priority::LOW);
|
||||
@@ -1680,7 +1680,7 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheCorrectness2) {
|
||||
// After Flush is successful, RocksDB will do the paranoid check for the new
|
||||
// SST file. Meta blocks are always cached in the block cache and they
|
||||
// will not be evicted. When block_2 is cache miss and read out, it is
|
||||
// inserted to the block cache. Therefore, block_1 is evicted from block
|
||||
// inserted to the block cache. Thefore, block_1 is evicted from block
|
||||
// cache and successfully inserted to the secondary cache. Here are 2
|
||||
// lookups in the secondary cache for block_1 and block_2.
|
||||
ASSERT_EQ(secondary_cache->num_inserts(), 1u);
|
||||
@@ -1721,7 +1721,7 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheCorrectness2) {
|
||||
v = Get(Key(0));
|
||||
ASSERT_EQ(1007, v.size());
|
||||
// This Get needs to access block_1, since block_1 is not in block cache
|
||||
// there is one secondary cache lookup. Then, block_1 is cached in the
|
||||
// there is one econdary cache lookup. Then, block_1 is cached in the
|
||||
// block cache.
|
||||
ASSERT_EQ(secondary_cache->num_inserts(), 2u);
|
||||
ASSERT_EQ(secondary_cache->num_lookups(), 5u);
|
||||
@@ -1785,7 +1785,7 @@ TEST_P(DBSecondaryCacheTest, NoSecondaryCacheInsertion) {
|
||||
std::string v = Get(Key(0));
|
||||
ASSERT_EQ(1000, v.size());
|
||||
// Since the block cache is large enough, all the blocks are cached. we
|
||||
// do not need to lookup the secondary cache.
|
||||
// do not need to lookup the seondary cache.
|
||||
ASSERT_EQ(secondary_cache->num_inserts(), 0u);
|
||||
ASSERT_EQ(secondary_cache->num_lookups(), 2u);
|
||||
|
||||
@@ -2150,7 +2150,7 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadBasic) {
|
||||
ASSERT_OK(Flush());
|
||||
Compact("a", "z");
|
||||
|
||||
// do the read for all the key value pairs, so all the blocks should be in
|
||||
// do th eread for all the key value pairs, so all the blocks should be in
|
||||
// cache
|
||||
uint32_t start_insert = cache->GetInsertCount();
|
||||
uint32_t start_lookup = cache->GetLookupcount();
|
||||
@@ -2464,7 +2464,7 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionBasic) {
|
||||
std::string v = Get(Key(0));
|
||||
ASSERT_EQ(1007, v.size());
|
||||
|
||||
// Check the data in first block. Cache miss, directly read from SST file.
|
||||
// Check the data in first block. Cache miss, direclty read from SST file.
|
||||
ASSERT_EQ(secondary_cache->num_inserts(), 0u);
|
||||
ASSERT_EQ(secondary_cache->num_lookups(), 0u);
|
||||
|
||||
@@ -2598,7 +2598,7 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionChange) {
|
||||
}
|
||||
|
||||
// Two DB test. We create 2 DBs sharing the same block cache and secondary
|
||||
// cache. We disable the secondary cache option for DB2.
|
||||
// cache. We diable the secondary cache option for DB2.
|
||||
TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionTwoDB) {
|
||||
if (IsHyperClock()) {
|
||||
ROCKSDB_GTEST_BYPASS("Test depends on LRUCache-specific behaviors");
|
||||
|
||||
+1
-5
@@ -8,7 +8,7 @@ DB_STRESS_CMD?=./db_stress
|
||||
include common.mk
|
||||
|
||||
CRASHTEST_MAKE=$(MAKE) -f crash_test.mk
|
||||
CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --cleanup_cmd='$(DB_CLEANUP_CMD)' --destroy_db_initially=1
|
||||
CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --cleanup_cmd='$(DB_CLEANUP_CMD)'
|
||||
|
||||
.PHONY: crash_test crash_test_with_atomic_flush crash_test_with_txn \
|
||||
crash_test_with_wc_txn crash_test_with_wp_txn crash_test_with_wup_txn \
|
||||
@@ -34,7 +34,6 @@ CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --
|
||||
whitebox_crash_test_with_txn whitebox_crash_test_with_ts \
|
||||
whitebox_crash_test_with_optimistic_txn \
|
||||
whitebox_crash_test_with_tiered_storage \
|
||||
crash_test_db_cleanup \
|
||||
|
||||
crash_test: $(DB_STRESS_CMD)
|
||||
# Do not parallelize
|
||||
@@ -162,9 +161,6 @@ whitebox_crash_test_with_optimistic_txn: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_PY) --optimistic_txn whitebox --random_kill_odd \
|
||||
$(CRASH_TEST_KILL_ODD) $(CRASH_TEST_EXT_ARGS)
|
||||
|
||||
crash_test_db_cleanup: $(DB_STRESS_CMD)
|
||||
$(DB_STRESS_CMD) --delete_dir_and_exit=$(TEST_TMPDIR)
|
||||
|
||||
# Old names DEPRECATED
|
||||
crash_test_with_txn: crash_test_with_wc_txn
|
||||
whitebox_crash_test_with_txn: whitebox_crash_test_with_wc_txn
|
||||
|
||||
@@ -67,16 +67,6 @@ BlobFileBuilder::BlobFileBuilder(
|
||||
min_blob_size_(mutable_cf_options->min_blob_size),
|
||||
blob_file_size_(mutable_cf_options->blob_file_size),
|
||||
blob_compression_type_(mutable_cf_options->blob_compression_type),
|
||||
// TODO: support most CompressionOptions with a new CF option
|
||||
// blob_compression_opts
|
||||
// TODO with schema change: support custom compression manager and options
|
||||
// such as max_compressed_bytes_per_kb
|
||||
// NOTE: returns nullptr for kNoCompression
|
||||
blob_compressor_(GetBuiltinV2CompressionManager()->GetCompressor(
|
||||
CompressionOptions{}, blob_compression_type_)),
|
||||
blob_compressor_wa_(blob_compressor_
|
||||
? blob_compressor_->ObtainWorkingArea()
|
||||
: Compressor::ManagedWorkingArea{}),
|
||||
prepopulate_blob_cache_(mutable_cf_options->prepopulate_blob_cache),
|
||||
file_options_(file_options),
|
||||
write_options_(write_options),
|
||||
@@ -123,7 +113,7 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
|
||||
}
|
||||
|
||||
Slice blob = value;
|
||||
GrowableBuffer compressed_blob;
|
||||
std::string compressed_blob;
|
||||
|
||||
{
|
||||
const Status s = CompressBlobIfNeeded(&blob, &compressed_blob);
|
||||
@@ -264,27 +254,36 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
|
||||
}
|
||||
|
||||
Status BlobFileBuilder::CompressBlobIfNeeded(
|
||||
Slice* blob, GrowableBuffer* compressed_blob) const {
|
||||
Slice* blob, std::string* compressed_blob) const {
|
||||
assert(blob);
|
||||
assert(compressed_blob);
|
||||
assert(compressed_blob->empty());
|
||||
assert(immutable_options_);
|
||||
|
||||
if (!blob_compressor_) {
|
||||
assert(blob_compression_type_ == kNoCompression);
|
||||
if (blob_compression_type_ == kNoCompression) {
|
||||
return Status::OK();
|
||||
}
|
||||
assert(blob_compression_type_ != kNoCompression);
|
||||
|
||||
// WART: always stored as compressed even when that increases the size.
|
||||
// TODO: allow user CompressionOptions, including max_compressed_bytes_per_kb
|
||||
CompressionOptions opts;
|
||||
CompressionContext context(blob_compression_type_, opts);
|
||||
|
||||
Status s;
|
||||
StopWatch stop_watch(immutable_options_->clock, immutable_options_->stats,
|
||||
BLOB_DB_COMPRESSION_MICROS);
|
||||
s = LegacyForceBuiltinCompression(*blob_compressor_, &blob_compressor_wa_,
|
||||
*blob, compressed_blob);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
|
||||
blob_compression_type_);
|
||||
|
||||
constexpr uint32_t compression_format_version = 2;
|
||||
|
||||
bool success = false;
|
||||
|
||||
{
|
||||
StopWatch stop_watch(immutable_options_->clock, immutable_options_->stats,
|
||||
BLOB_DB_COMPRESSION_MICROS);
|
||||
success = OLD_CompressData(*blob, info, compression_format_version,
|
||||
compressed_blob);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
return Status::Corruption("Error compressing blob");
|
||||
}
|
||||
|
||||
*blob = Slice(*compressed_blob);
|
||||
|
||||
@@ -10,14 +10,12 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "rocksdb/advanced_compression.h"
|
||||
#include "rocksdb/advanced_options.h"
|
||||
#include "rocksdb/compression_type.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "rocksdb/types.h"
|
||||
#include "util/aligned_buffer.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -78,8 +76,7 @@ class BlobFileBuilder {
|
||||
private:
|
||||
bool IsBlobFileOpen() const;
|
||||
Status OpenBlobFileIfNeeded();
|
||||
Status CompressBlobIfNeeded(Slice* blob,
|
||||
GrowableBuffer* compressed_blob) const;
|
||||
Status CompressBlobIfNeeded(Slice* blob, std::string* compressed_blob) const;
|
||||
Status WriteBlobToFile(const Slice& key, const Slice& blob,
|
||||
uint64_t* blob_file_number, uint64_t* blob_offset);
|
||||
Status CloseBlobFile();
|
||||
@@ -94,8 +91,6 @@ class BlobFileBuilder {
|
||||
uint64_t min_blob_size_;
|
||||
uint64_t blob_file_size_;
|
||||
CompressionType blob_compression_type_;
|
||||
std::unique_ptr<Compressor> blob_compressor_;
|
||||
mutable Compressor::ManagedWorkingArea blob_compressor_wa_;
|
||||
PrepopulateBlobCache prepopulate_blob_cache_;
|
||||
const FileOptions* file_options_;
|
||||
const WriteOptions* write_options_;
|
||||
|
||||
@@ -457,12 +457,11 @@ TEST_F(BlobFileBuilderTest, CompressionError) {
|
||||
nullptr /*IOTracer*/, nullptr /*BlobFileCompletionCallback*/,
|
||||
BlobFileCreationReason::kFlush, &blob_file_paths, &blob_file_additions);
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"LegacyForceBuiltinCompression:TamperWithStatus", [](void* arg) {
|
||||
Status* ret = static_cast<Status*>(arg);
|
||||
ASSERT_OK(*ret);
|
||||
*ret = Status::Corruption("Tampered result");
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack("CompressData:TamperWithReturnValue",
|
||||
[](void* arg) {
|
||||
bool* ret = static_cast<bool*>(arg);
|
||||
*ret = false;
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
constexpr char key[] = "1";
|
||||
@@ -470,7 +469,7 @@ TEST_F(BlobFileBuilderTest, CompressionError) {
|
||||
|
||||
std::string blob_index;
|
||||
|
||||
ASSERT_EQ(builder.Add(key, value, &blob_index).code(), Status::kCorruption);
|
||||
ASSERT_TRUE(builder.Add(key, value, &blob_index).IsCorruption());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
+25
-37
@@ -17,10 +17,10 @@
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/status.h"
|
||||
#include "table/format.h"
|
||||
#include "table/multiget_context.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/compression.h"
|
||||
#include "util/crc32c.h"
|
||||
#include "util/stop_watch.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -69,16 +69,9 @@ Status BlobFileReader::Create(
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Decompressor> decompressor;
|
||||
if (compression_type != kNoCompression) {
|
||||
// The blob format has always used compression format 2
|
||||
decompressor = GetBuiltinV2CompressionManager()->GetDecompressorOptimizeFor(
|
||||
compression_type);
|
||||
}
|
||||
|
||||
blob_file_reader->reset(new BlobFileReader(
|
||||
std::move(file_reader), file_size, compression_type,
|
||||
std::move(decompressor), immutable_options.clock, statistics));
|
||||
blob_file_reader->reset(
|
||||
new BlobFileReader(std::move(file_reader), file_size, compression_type,
|
||||
immutable_options.clock, statistics));
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
@@ -289,13 +282,11 @@ Status BlobFileReader::ReadFromFile(const RandomAccessFileReader* file_reader,
|
||||
|
||||
BlobFileReader::BlobFileReader(
|
||||
std::unique_ptr<RandomAccessFileReader>&& file_reader, uint64_t file_size,
|
||||
CompressionType compression_type,
|
||||
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
|
||||
CompressionType compression_type, SystemClock* clock,
|
||||
Statistics* statistics)
|
||||
: file_reader_(std::move(file_reader)),
|
||||
file_size_(file_size),
|
||||
compression_type_(compression_type),
|
||||
decompressor_(std::move(decompressor)),
|
||||
clock_(clock),
|
||||
statistics_(statistics) {
|
||||
assert(file_reader_);
|
||||
@@ -384,9 +375,8 @@ Status BlobFileReader::GetBlob(
|
||||
const Slice value_slice(record_slice.data() + adjustment, value_size);
|
||||
|
||||
{
|
||||
const Status s = UncompressBlobIfNeeded(value_slice, compression_type,
|
||||
decompressor_.get(), allocator,
|
||||
clock_, statistics_, result);
|
||||
const Status s = UncompressBlobIfNeeded(
|
||||
value_slice, compression_type, allocator, clock_, statistics_, result);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -534,9 +524,9 @@ void BlobFileReader::MultiGetBlob(
|
||||
|
||||
// Uncompress blob if needed
|
||||
Slice value_slice(record_slice.data() + adjustments[i], req->len);
|
||||
*req->status = UncompressBlobIfNeeded(
|
||||
value_slice, compression_type_, decompressor_.get(), allocator, clock_,
|
||||
statistics_, &blob_reqs[i].second);
|
||||
*req->status =
|
||||
UncompressBlobIfNeeded(value_slice, compression_type_, allocator,
|
||||
clock_, statistics_, &blob_reqs[i].second);
|
||||
if (req->status->ok()) {
|
||||
total_bytes += record_slice.size();
|
||||
}
|
||||
@@ -593,8 +583,8 @@ Status BlobFileReader::VerifyBlob(const Slice& record_slice,
|
||||
|
||||
Status BlobFileReader::UncompressBlobIfNeeded(
|
||||
const Slice& value_slice, CompressionType compression_type,
|
||||
Decompressor* decompressor, MemoryAllocator* allocator, SystemClock* clock,
|
||||
Statistics* statistics, std::unique_ptr<BlobContents>* result) {
|
||||
MemoryAllocator* allocator, SystemClock* clock, Statistics* statistics,
|
||||
std::unique_ptr<BlobContents>* result) {
|
||||
assert(result);
|
||||
|
||||
if (compression_type == kNoCompression) {
|
||||
@@ -603,33 +593,31 @@ Status BlobFileReader::UncompressBlobIfNeeded(
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
assert(decompressor);
|
||||
UncompressionContext context(compression_type);
|
||||
UncompressionInfo info(context, UncompressionDict::GetEmptyDict(),
|
||||
compression_type);
|
||||
|
||||
Decompressor::Args args;
|
||||
args.compression_type = compression_type;
|
||||
args.compressed_data = value_slice;
|
||||
size_t uncompressed_size = 0;
|
||||
constexpr uint32_t compression_format_version = 2;
|
||||
|
||||
Status s = decompressor->ExtractUncompressedSize(args);
|
||||
if (!s.ok()) {
|
||||
return Status::Corruption(s.ToString());
|
||||
}
|
||||
|
||||
CacheAllocationPtr output = AllocateBlock(args.uncompressed_size, allocator);
|
||||
CacheAllocationPtr output;
|
||||
|
||||
{
|
||||
PERF_TIMER_GUARD(blob_decompress_time);
|
||||
StopWatch stop_watch(clock, statistics, BLOB_DB_DECOMPRESSION_MICROS);
|
||||
s = decompressor->DecompressBlock(args, output.get());
|
||||
output = OLD_UncompressData(info, value_slice.data(), value_slice.size(),
|
||||
&uncompressed_size, compression_format_version,
|
||||
allocator);
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"BlobFileReader::UncompressBlobIfNeeded:TamperWithResult", &s);
|
||||
"BlobFileReader::UncompressBlobIfNeeded:TamperWithResult", &output);
|
||||
|
||||
if (!s.ok()) {
|
||||
return Status::Corruption(s.ToString());
|
||||
if (!output) {
|
||||
return Status::Corruption("Unable to uncompress blob");
|
||||
}
|
||||
|
||||
result->reset(new BlobContents(std::move(output), args.uncompressed_size));
|
||||
result->reset(new BlobContents(std::move(output), uncompressed_size));
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
#include "db/blob/blob_read_request.h"
|
||||
#include "file/random_access_file_reader.h"
|
||||
#include "rocksdb/advanced_compression.h"
|
||||
#include "rocksdb/compression_type.h"
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "util/autovector.h"
|
||||
@@ -65,8 +64,7 @@ class BlobFileReader {
|
||||
private:
|
||||
BlobFileReader(std::unique_ptr<RandomAccessFileReader>&& file_reader,
|
||||
uint64_t file_size, CompressionType compression_type,
|
||||
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
|
||||
Statistics* statistics);
|
||||
SystemClock* clock, Statistics* statistics);
|
||||
|
||||
static Status OpenFile(const ImmutableOptions& immutable_options,
|
||||
const FileOptions& file_opts,
|
||||
@@ -98,7 +96,6 @@ class BlobFileReader {
|
||||
|
||||
static Status UncompressBlobIfNeeded(const Slice& value_slice,
|
||||
CompressionType compression_type,
|
||||
Decompressor* decompressor,
|
||||
MemoryAllocator* allocator,
|
||||
SystemClock* clock,
|
||||
Statistics* statistics,
|
||||
@@ -107,7 +104,6 @@ class BlobFileReader {
|
||||
std::unique_ptr<RandomAccessFileReader> file_reader_;
|
||||
uint64_t file_size_;
|
||||
CompressionType compression_type_;
|
||||
std::shared_ptr<Decompressor> decompressor_;
|
||||
SystemClock* clock_;
|
||||
Statistics* statistics_;
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
||||
|
||||
ASSERT_OK(blob_log_writer.WriteHeader(WriteOptions(), header));
|
||||
|
||||
std::vector<GrowableBuffer> compressed_blobs(num);
|
||||
std::vector<std::string> compressed_blobs(num);
|
||||
std::vector<Slice> blobs_to_write(num);
|
||||
if (kNoCompression == compression) {
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
@@ -73,13 +73,16 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
||||
blob_sizes[i] = blobs[i].size();
|
||||
}
|
||||
} else {
|
||||
auto compressor =
|
||||
GetBuiltinV2CompressionManager()->GetCompressor({}, compression);
|
||||
CompressionOptions opts;
|
||||
CompressionContext context(compression, opts);
|
||||
CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
|
||||
compression);
|
||||
|
||||
constexpr uint32_t compression_format_version = 2;
|
||||
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
ASSERT_OK(LegacyForceBuiltinCompression(*compressor,
|
||||
/*working_area=*/nullptr,
|
||||
blobs[i], &compressed_blobs[i]));
|
||||
ASSERT_TRUE(OLD_CompressData(blobs[i], info, compression_format_version,
|
||||
&compressed_blobs[i]));
|
||||
blobs_to_write[i] = compressed_blobs[i];
|
||||
blob_sizes[i] = compressed_blobs[i].size();
|
||||
}
|
||||
@@ -806,10 +809,11 @@ TEST_F(BlobFileReaderTest, UncompressionError) {
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlobFileReader::UncompressBlobIfNeeded:TamperWithResult", [](void* arg) {
|
||||
auto* result = static_cast<Status*>(arg);
|
||||
assert(result);
|
||||
CacheAllocationPtr* const output =
|
||||
static_cast<CacheAllocationPtr*>(arg);
|
||||
assert(output);
|
||||
|
||||
*result = Status::Corruption("Injected result");
|
||||
output->reset();
|
||||
});
|
||||
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
@@ -820,12 +824,11 @@ TEST_F(BlobFileReaderTest, UncompressionError) {
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
ASSERT_EQ(reader
|
||||
->GetBlob(ReadOptions(), key, blob_offset, blob_size,
|
||||
kSnappyCompression, prefetch_buffer, allocator,
|
||||
&value, &bytes_read)
|
||||
.code(),
|
||||
Status::Code::kCorruption);
|
||||
ASSERT_TRUE(reader
|
||||
->GetBlob(ReadOptions(), key, blob_offset, blob_size,
|
||||
kSnappyCompression, prefetch_buffer, allocator,
|
||||
&value, &bytes_read)
|
||||
.IsCorruption());
|
||||
ASSERT_EQ(value, nullptr);
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
||||
|
||||
ASSERT_OK(blob_log_writer.WriteHeader(WriteOptions(), header));
|
||||
|
||||
std::vector<GrowableBuffer> compressed_blobs(num);
|
||||
std::vector<std::string> compressed_blobs(num);
|
||||
std::vector<Slice> blobs_to_write(num);
|
||||
if (kNoCompression == compression) {
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
@@ -75,13 +75,16 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
||||
blob_sizes[i] = blobs[i].size();
|
||||
}
|
||||
} else {
|
||||
auto compressor =
|
||||
GetBuiltinV2CompressionManager()->GetCompressor({}, compression);
|
||||
CompressionOptions opts;
|
||||
CompressionContext context(compression, opts);
|
||||
CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
|
||||
compression);
|
||||
|
||||
constexpr uint32_t compression_format_version = 2;
|
||||
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
ASSERT_OK(LegacyForceBuiltinCompression(*compressor,
|
||||
/*working_area=*/nullptr,
|
||||
blobs[i], &compressed_blobs[i]));
|
||||
ASSERT_TRUE(OLD_CompressData(blobs[i], info, compression_format_version,
|
||||
&compressed_blobs[i]));
|
||||
blobs_to_write[i] = compressed_blobs[i];
|
||||
blob_sizes[i] = compressed_blobs[i].size();
|
||||
}
|
||||
|
||||
@@ -56,18 +56,6 @@ TableBuilder* NewTableBuilder(const TableBuilderOptions& tboptions,
|
||||
return tboptions.moptions.table_factory->NewTableBuilder(tboptions, file);
|
||||
}
|
||||
|
||||
void ExtractTimestampFromTableProperties(const TableProperties& tp,
|
||||
FileMetaData* meta) {
|
||||
auto min_ts_iter = tp.user_collected_properties.find("rocksdb.timestamp_min");
|
||||
if (min_ts_iter != tp.user_collected_properties.end()) {
|
||||
meta->min_timestamp = min_ts_iter->second;
|
||||
}
|
||||
auto max_ts_iter = tp.user_collected_properties.find("rocksdb.timestamp_max");
|
||||
if (max_ts_iter != tp.user_collected_properties.end()) {
|
||||
meta->max_timestamp = max_ts_iter->second;
|
||||
}
|
||||
}
|
||||
|
||||
Status BuildTable(
|
||||
const std::string& dbname, VersionSet* versions,
|
||||
const ImmutableDBOptions& db_options, const TableBuilderOptions& tboptions,
|
||||
@@ -367,7 +355,6 @@ Status BuildTable(
|
||||
assert(meta->fd.GetFileSize() > 0);
|
||||
tp = builder
|
||||
->GetTableProperties(); // refresh now that builder is finished
|
||||
ExtractTimestampFromTableProperties(tp, meta);
|
||||
if (memtable_payload_bytes != nullptr &&
|
||||
memtable_garbage_bytes != nullptr) {
|
||||
const CompactionIterationStats& ci_stats = c_iter.iter_stats();
|
||||
|
||||
@@ -41,13 +41,6 @@ class BlobFileCompletionCallback;
|
||||
TableBuilder* NewTableBuilder(const TableBuilderOptions& tboptions,
|
||||
WritableFileWriter* file);
|
||||
|
||||
// Extract min/max timestamps from table properties and populate FileMetaData.
|
||||
// This is used by both flush (BuildTable) and compaction (CompactionOutputs)
|
||||
// to populate timestamp range in FileMetaData from the TimestampTableProperties
|
||||
// collector output.
|
||||
void ExtractTimestampFromTableProperties(const TableProperties& tp,
|
||||
FileMetaData* meta);
|
||||
|
||||
// Build a Table file from the contents of *iter. The generated file
|
||||
// will be named according to number specified in meta. On success, the rest of
|
||||
// *meta will be filled with metadata about the generated table.
|
||||
|
||||
@@ -71,11 +71,6 @@ using ROCKSDB_NAMESPACE::CompactionFilterFactory;
|
||||
using ROCKSDB_NAMESPACE::CompactionJobInfo;
|
||||
using ROCKSDB_NAMESPACE::CompactionOptionsFIFO;
|
||||
using ROCKSDB_NAMESPACE::CompactionReason;
|
||||
using ROCKSDB_NAMESPACE::CompactionService;
|
||||
using ROCKSDB_NAMESPACE::CompactionServiceJobInfo;
|
||||
using ROCKSDB_NAMESPACE::CompactionServiceJobStatus;
|
||||
using ROCKSDB_NAMESPACE::CompactionServiceOptionsOverride;
|
||||
using ROCKSDB_NAMESPACE::CompactionServiceScheduleResponse;
|
||||
using ROCKSDB_NAMESPACE::CompactRangeOptions;
|
||||
using ROCKSDB_NAMESPACE::Comparator;
|
||||
using ROCKSDB_NAMESPACE::CompressionType;
|
||||
@@ -89,12 +84,10 @@ using ROCKSDB_NAMESPACE::EnvOptions;
|
||||
using ROCKSDB_NAMESPACE::EventListener;
|
||||
using ROCKSDB_NAMESPACE::ExportImportFilesMetaData;
|
||||
using ROCKSDB_NAMESPACE::ExternalFileIngestionInfo;
|
||||
using ROCKSDB_NAMESPACE::FileChecksumGenFactory;
|
||||
using ROCKSDB_NAMESPACE::FileLock;
|
||||
using ROCKSDB_NAMESPACE::FilterPolicy;
|
||||
using ROCKSDB_NAMESPACE::FlushJobInfo;
|
||||
using ROCKSDB_NAMESPACE::FlushOptions;
|
||||
using ROCKSDB_NAMESPACE::GetFileChecksumGenCrc32cFactory;
|
||||
using ROCKSDB_NAMESPACE::HistogramData;
|
||||
using ROCKSDB_NAMESPACE::HyperClockCacheOptions;
|
||||
using ROCKSDB_NAMESPACE::ImportColumnFamilyOptions;
|
||||
@@ -114,8 +107,6 @@ using ROCKSDB_NAMESPACE::NewCompactOnDeletionCollectorFactory;
|
||||
using ROCKSDB_NAMESPACE::NewGenericRateLimiter;
|
||||
using ROCKSDB_NAMESPACE::NewLRUCache;
|
||||
using ROCKSDB_NAMESPACE::NewRibbonFilterPolicy;
|
||||
using ROCKSDB_NAMESPACE::NewSstPartitionerFixedPrefixFactory;
|
||||
using ROCKSDB_NAMESPACE::OpenAndCompactOptions;
|
||||
using ROCKSDB_NAMESPACE::OptimisticTransactionDB;
|
||||
using ROCKSDB_NAMESPACE::OptimisticTransactionOptions;
|
||||
using ROCKSDB_NAMESPACE::Options;
|
||||
@@ -136,11 +127,9 @@ using ROCKSDB_NAMESPACE::Snapshot;
|
||||
using ROCKSDB_NAMESPACE::SstFileManager;
|
||||
using ROCKSDB_NAMESPACE::SstFileMetaData;
|
||||
using ROCKSDB_NAMESPACE::SstFileWriter;
|
||||
using ROCKSDB_NAMESPACE::SstPartitionerFactory;
|
||||
using ROCKSDB_NAMESPACE::Status;
|
||||
using ROCKSDB_NAMESPACE::StderrLogger;
|
||||
using ROCKSDB_NAMESPACE::SubcompactionJobInfo;
|
||||
using ROCKSDB_NAMESPACE::TableFactory;
|
||||
using ROCKSDB_NAMESPACE::TablePropertiesCollectorFactory;
|
||||
using ROCKSDB_NAMESPACE::Transaction;
|
||||
using ROCKSDB_NAMESPACE::TransactionDB;
|
||||
@@ -240,15 +229,6 @@ struct rocksdb_filelock_t {
|
||||
struct rocksdb_logger_t {
|
||||
std::shared_ptr<Logger> rep;
|
||||
};
|
||||
struct rocksdb_file_checksum_gen_factory_t {
|
||||
std::shared_ptr<FileChecksumGenFactory> rep;
|
||||
};
|
||||
struct rocksdb_sst_partitioner_factory_t {
|
||||
std::shared_ptr<SstPartitionerFactory> rep;
|
||||
};
|
||||
struct rocksdb_table_properties_collector_factory_t {
|
||||
std::shared_ptr<TablePropertiesCollectorFactory> rep;
|
||||
};
|
||||
struct rocksdb_lru_cache_options_t {
|
||||
LRUCacheOptions rep;
|
||||
};
|
||||
@@ -369,22 +349,6 @@ struct rocksdb_statistics_histogram_data_t {
|
||||
HistogramData rep;
|
||||
};
|
||||
|
||||
struct rocksdb_compactionservice_scheduleresponse_t {
|
||||
CompactionServiceScheduleResponse rep;
|
||||
};
|
||||
|
||||
struct rocksdb_compactionservice_jobinfo_t {
|
||||
CompactionServiceJobInfo rep;
|
||||
};
|
||||
|
||||
struct rocksdb_compaction_service_options_override_t {
|
||||
CompactionServiceOptionsOverride rep;
|
||||
};
|
||||
|
||||
struct rocksdb_open_and_compact_options_t {
|
||||
OpenAndCompactOptions rep;
|
||||
};
|
||||
|
||||
struct rocksdb_compactionfilter_t : public CompactionFilter {
|
||||
void* state_;
|
||||
void (*destructor_)(void*);
|
||||
@@ -679,547 +643,6 @@ static inline char* CopyString(const Slice& slice) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* rocksdb_compactionservice_jobinfo_t_get_db_name(
|
||||
const rocksdb_compactionservice_jobinfo_t* info, size_t* len) {
|
||||
*len = info->rep.db_name.size();
|
||||
return info->rep.db_name.data();
|
||||
}
|
||||
|
||||
const char* rocksdb_compactionservice_jobinfo_t_get_db_id(
|
||||
const rocksdb_compactionservice_jobinfo_t* info, size_t* len) {
|
||||
*len = info->rep.db_id.size();
|
||||
return info->rep.db_id.data();
|
||||
}
|
||||
|
||||
const char* rocksdb_compactionservice_jobinfo_t_get_db_session_id(
|
||||
const rocksdb_compactionservice_jobinfo_t* info, size_t* len) {
|
||||
*len = info->rep.db_session_id.size();
|
||||
return info->rep.db_session_id.data();
|
||||
}
|
||||
|
||||
const char* rocksdb_compactionservice_jobinfo_t_get_cf_name(
|
||||
const rocksdb_compactionservice_jobinfo_t* info, size_t* len) {
|
||||
*len = info->rep.cf_name.size();
|
||||
return info->rep.cf_name.data();
|
||||
}
|
||||
|
||||
uint32_t rocksdb_compactionservice_jobinfo_t_get_cf_id(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return info->rep.cf_id;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_compactionservice_jobinfo_t_get_job_id(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return info->rep.job_id;
|
||||
}
|
||||
|
||||
int rocksdb_compactionservice_jobinfo_t_get_priority(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return static_cast<int>(info->rep.priority);
|
||||
}
|
||||
|
||||
int rocksdb_compactionservice_jobinfo_t_get_compaction_reason(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return static_cast<int>(info->rep.compaction_reason);
|
||||
}
|
||||
|
||||
int rocksdb_compactionservice_jobinfo_t_get_base_input_level(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return info->rep.base_input_level;
|
||||
}
|
||||
|
||||
int rocksdb_compactionservice_jobinfo_t_get_output_level(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return info->rep.output_level;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_compactionservice_jobinfo_t_is_full_compaction(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return info->rep.is_full_compaction;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_compactionservice_jobinfo_t_is_manual_compaction(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return info->rep.is_manual_compaction;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_compactionservice_jobinfo_t_is_bottommost_level(
|
||||
const rocksdb_compactionservice_jobinfo_t* info) {
|
||||
return info->rep.bottommost_level;
|
||||
}
|
||||
|
||||
// Helper function to validate compaction service job status
|
||||
static inline bool IsValidCompactionServiceJobStatus(int status) {
|
||||
return status >= rocksdb_compactionservice_jobstatus_success &&
|
||||
status <= rocksdb_compactionservice_jobstatus_use_local;
|
||||
}
|
||||
|
||||
rocksdb_compactionservice_scheduleresponse_t*
|
||||
rocksdb_compactionservice_scheduleresponse_create(const char* scheduled_job_id,
|
||||
int status, char** errptr) {
|
||||
// Validate status is in range [success=0, failure=1, aborted=2, use_local=3]
|
||||
if (!IsValidCompactionServiceJobStatus(status)) {
|
||||
SaveError(errptr,
|
||||
Status::InvalidArgument("Invalid status value. Must be 0-3."));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
rocksdb_compactionservice_scheduleresponse_t* response =
|
||||
new rocksdb_compactionservice_scheduleresponse_t{
|
||||
CompactionServiceScheduleResponse(
|
||||
scheduled_job_id ? std::string(scheduled_job_id) : "",
|
||||
static_cast<CompactionServiceJobStatus>(status))};
|
||||
return response;
|
||||
}
|
||||
|
||||
rocksdb_compactionservice_scheduleresponse_t*
|
||||
rocksdb_compactionservice_scheduleresponse_create_with_status(int status,
|
||||
char** errptr) {
|
||||
// Validate status is in range [success=0, failure=1, aborted=2, use_local=3]
|
||||
if (!IsValidCompactionServiceJobStatus(status)) {
|
||||
SaveError(errptr,
|
||||
Status::InvalidArgument("Invalid status value. Must be 0-3."));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
rocksdb_compactionservice_scheduleresponse_t* response =
|
||||
new rocksdb_compactionservice_scheduleresponse_t{
|
||||
CompactionServiceScheduleResponse(
|
||||
static_cast<CompactionServiceJobStatus>(status))};
|
||||
return response;
|
||||
}
|
||||
|
||||
void rocksdb_compactionservice_scheduleresponse_t_destroy(
|
||||
rocksdb_compactionservice_scheduleresponse_t* response) {
|
||||
if (response) {
|
||||
delete response;
|
||||
}
|
||||
}
|
||||
|
||||
int rocksdb_compactionservice_scheduleresponse_getstatus(
|
||||
const rocksdb_compactionservice_scheduleresponse_t* response) {
|
||||
if (!response) {
|
||||
return rocksdb_compactionservice_jobstatus_failure;
|
||||
}
|
||||
return static_cast<int>(response->rep.status);
|
||||
}
|
||||
|
||||
const char* rocksdb_compactionservice_scheduleresponse_get_scheduled_job_id(
|
||||
const rocksdb_compactionservice_scheduleresponse_t* response, size_t* len) {
|
||||
if (!response || !len) {
|
||||
if (len) {
|
||||
*len = 0;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
*len = response->rep.scheduled_job_id.size();
|
||||
return response->rep.scheduled_job_id.data();
|
||||
}
|
||||
|
||||
struct rocksdb_compactionservice_t : public CompactionService {
|
||||
void* state_;
|
||||
void (*destructor_)(void*);
|
||||
rocksdb_compaction_service_schedule_cb schedule_;
|
||||
std::string 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_;
|
||||
|
||||
rocksdb_compactionservice_t(
|
||||
void* state, void (*destructor)(void*),
|
||||
rocksdb_compaction_service_schedule_cb
|
||||
rocksdb_compaction_service_schedule_ptr,
|
||||
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)
|
||||
: state_(state),
|
||||
destructor_(destructor),
|
||||
schedule_(rocksdb_compaction_service_schedule_ptr),
|
||||
name_(name ? name : "CompactionService"),
|
||||
wait_(wait),
|
||||
cancel_awaiting_jobs_(cancel_awaiting_jobs),
|
||||
on_installation_(on_installation) {}
|
||||
|
||||
~rocksdb_compactionservice_t() override {
|
||||
if (destructor_) {
|
||||
(*destructor_)(state_);
|
||||
}
|
||||
}
|
||||
|
||||
const char* Name() const override { return name_.c_str(); }
|
||||
|
||||
CompactionServiceScheduleResponse Schedule(
|
||||
const CompactionServiceJobInfo& info,
|
||||
const std::string& compaction_service_input) override {
|
||||
if (schedule_ == nullptr) {
|
||||
return CompactionServiceScheduleResponse(
|
||||
CompactionServiceJobStatus::kUseLocal);
|
||||
}
|
||||
|
||||
rocksdb_compactionservice_scheduleresponse_t* c_response = (*schedule_)(
|
||||
state_,
|
||||
reinterpret_cast<const rocksdb_compactionservice_jobinfo_t*>(&info),
|
||||
compaction_service_input.data(), compaction_service_input.size());
|
||||
|
||||
if (c_response == nullptr) {
|
||||
return CompactionServiceScheduleResponse(
|
||||
CompactionServiceJobStatus::kFailure);
|
||||
}
|
||||
|
||||
CompactionServiceScheduleResponse response = std::move(c_response->rep);
|
||||
delete c_response;
|
||||
return response;
|
||||
}
|
||||
|
||||
CompactionServiceJobStatus Wait(const std::string& scheduled_job_id,
|
||||
std::string* result) override {
|
||||
if (wait_ == nullptr) {
|
||||
return CompactionServiceJobStatus::kUseLocal;
|
||||
}
|
||||
|
||||
char* c_result = nullptr;
|
||||
size_t result_len = 0;
|
||||
|
||||
int status =
|
||||
(*wait_)(state_, scheduled_job_id.c_str(), &c_result, &result_len);
|
||||
|
||||
if (c_result != nullptr) {
|
||||
if (result != nullptr) {
|
||||
result->assign(c_result, result_len);
|
||||
}
|
||||
free(c_result);
|
||||
}
|
||||
|
||||
return static_cast<CompactionServiceJobStatus>(status);
|
||||
}
|
||||
|
||||
void CancelAwaitingJobs() override {
|
||||
if (cancel_awaiting_jobs_ != nullptr) {
|
||||
(*cancel_awaiting_jobs_)(state_);
|
||||
}
|
||||
}
|
||||
|
||||
void OnInstallation(const std::string& scheduled_job_id,
|
||||
CompactionServiceJobStatus status) override {
|
||||
if (on_installation_ != nullptr) {
|
||||
(*on_installation_)(state_, scheduled_job_id.c_str(),
|
||||
static_cast<int>(status));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
return new rocksdb_compactionservice_t(state, destructor, schedule, name,
|
||||
wait, cancel_awaiting_jobs,
|
||||
on_installation);
|
||||
}
|
||||
|
||||
void rocksdb_options_set_compaction_service(
|
||||
rocksdb_options_t* opt, rocksdb_compactionservice_t* service) {
|
||||
if (!opt || !service) {
|
||||
return;
|
||||
}
|
||||
|
||||
opt->rep.compaction_service = std::shared_ptr<CompactionService>(service);
|
||||
}
|
||||
|
||||
// CompactionServiceOptionsOverride functions
|
||||
rocksdb_compaction_service_options_override_t*
|
||||
rocksdb_compaction_service_options_override_create() {
|
||||
return new rocksdb_compaction_service_options_override_t;
|
||||
}
|
||||
|
||||
rocksdb_compaction_service_options_override_t*
|
||||
rocksdb_compaction_service_options_override_create_from_options(
|
||||
rocksdb_options_t* options) {
|
||||
if (!options) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
rocksdb_compaction_service_options_override_t* override_opts =
|
||||
new rocksdb_compaction_service_options_override_t;
|
||||
|
||||
// Copy all relevant options from rocksdb_options_t
|
||||
override_opts->rep.env = options->rep.env;
|
||||
override_opts->rep.file_checksum_gen_factory =
|
||||
options->rep.file_checksum_gen_factory;
|
||||
override_opts->rep.comparator = options->rep.comparator;
|
||||
override_opts->rep.merge_operator = options->rep.merge_operator;
|
||||
override_opts->rep.compaction_filter = options->rep.compaction_filter;
|
||||
override_opts->rep.compaction_filter_factory =
|
||||
options->rep.compaction_filter_factory;
|
||||
override_opts->rep.prefix_extractor = options->rep.prefix_extractor;
|
||||
override_opts->rep.table_factory = options->rep.table_factory;
|
||||
override_opts->rep.sst_partitioner_factory =
|
||||
options->rep.sst_partitioner_factory;
|
||||
override_opts->rep.listeners = options->rep.listeners;
|
||||
override_opts->rep.statistics = options->rep.statistics;
|
||||
override_opts->rep.info_log = options->rep.info_log;
|
||||
override_opts->rep.table_properties_collector_factories =
|
||||
options->rep.table_properties_collector_factories;
|
||||
|
||||
return override_opts;
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_destroy(
|
||||
rocksdb_compaction_service_options_override_t* override_options) {
|
||||
if (override_options) {
|
||||
delete override_options;
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_set_env(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_env_t* env) {
|
||||
if (override_options && env) {
|
||||
override_options->rep.env = env->rep;
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_set_comparator(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_comparator_t* comparator) {
|
||||
if (override_options && comparator) {
|
||||
override_options->rep.comparator = comparator;
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_set_merge_operator(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_mergeoperator_t* merge_operator) {
|
||||
if (override_options && merge_operator) {
|
||||
override_options->rep.merge_operator =
|
||||
std::shared_ptr<MergeOperator>(merge_operator);
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_set_compaction_filter(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_compactionfilter_t* compaction_filter) {
|
||||
if (override_options && compaction_filter) {
|
||||
override_options->rep.compaction_filter = compaction_filter;
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_set_compaction_filter_factory(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_compactionfilterfactory_t* compaction_filter_factory) {
|
||||
if (override_options && compaction_filter_factory) {
|
||||
override_options->rep.compaction_filter_factory =
|
||||
std::shared_ptr<CompactionFilterFactory>(compaction_filter_factory);
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_set_prefix_extractor(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_slicetransform_t* prefix_extractor) {
|
||||
if (override_options && prefix_extractor) {
|
||||
override_options->rep.prefix_extractor =
|
||||
std::shared_ptr<const SliceTransform>(prefix_extractor);
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_set_block_based_table_factory(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_block_based_table_options_t* table_options) {
|
||||
if (override_options && table_options) {
|
||||
override_options->rep.table_factory = std::shared_ptr<TableFactory>(
|
||||
NewBlockBasedTableFactory(table_options->rep));
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_set_cuckoo_table_factory(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_cuckoo_table_options_t* table_options) {
|
||||
if (override_options && table_options) {
|
||||
override_options->rep.table_factory = std::shared_ptr<TableFactory>(
|
||||
NewCuckooTableFactory(table_options->rep));
|
||||
}
|
||||
}
|
||||
|
||||
// Note: add_event_listener is defined later after rocksdb_eventlistener_t
|
||||
// struct
|
||||
|
||||
void rocksdb_compaction_service_options_override_set_statistics(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_options_t* options) {
|
||||
if (override_options && options) {
|
||||
override_options->rep.statistics = options->rep.statistics;
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_set_info_log(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_logger_t* logger) {
|
||||
if (override_options && logger) {
|
||||
override_options->rep.info_log = logger->rep;
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_set_option(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
const char* key, const char* value) {
|
||||
if (override_options && key && value) {
|
||||
override_options->rep.options_map[std::string(key)] = std::string(value);
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_set_file_checksum_gen_factory(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_file_checksum_gen_factory_t* factory) {
|
||||
if (override_options && factory) {
|
||||
override_options->rep.file_checksum_gen_factory = factory->rep;
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_set_sst_partitioner_factory(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_sst_partitioner_factory_t* factory) {
|
||||
if (override_options && factory) {
|
||||
override_options->rep.sst_partitioner_factory = factory->rep;
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_add_table_properties_collector_factory(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_table_properties_collector_factory_t* factory) {
|
||||
if (override_options && factory) {
|
||||
override_options->rep.table_properties_collector_factories.push_back(
|
||||
factory->rep);
|
||||
}
|
||||
}
|
||||
|
||||
// Atomic bool management for cancellation
|
||||
unsigned char* rocksdb_open_and_compact_canceled_create() {
|
||||
return reinterpret_cast<unsigned char*>(new std::atomic<bool>(false));
|
||||
}
|
||||
|
||||
void rocksdb_open_and_compact_canceled_destroy(unsigned char* canceled) {
|
||||
if (canceled) {
|
||||
delete reinterpret_cast<std::atomic<bool>*>(canceled);
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_open_and_compact_canceled_set(unsigned char* canceled,
|
||||
unsigned char value) {
|
||||
if (canceled) {
|
||||
reinterpret_cast<std::atomic<bool>*>(canceled)->store(value != 0);
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAndCompactOptions functions
|
||||
rocksdb_open_and_compact_options_t* rocksdb_open_and_compact_options_create() {
|
||||
return new rocksdb_open_and_compact_options_t;
|
||||
}
|
||||
|
||||
void rocksdb_open_and_compact_options_destroy(
|
||||
rocksdb_open_and_compact_options_t* options) {
|
||||
if (options) {
|
||||
delete options;
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_open_and_compact_options_set_canceled(
|
||||
rocksdb_open_and_compact_options_t* options, unsigned char* canceled) {
|
||||
if (options && canceled) {
|
||||
options->rep.canceled = reinterpret_cast<std::atomic<bool>*>(canceled);
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_open_and_compact_options_set_allow_resumption(
|
||||
rocksdb_open_and_compact_options_t* options,
|
||||
unsigned char allow_resumption) {
|
||||
if (options) {
|
||||
options->rep.allow_resumption = allow_resumption != 0;
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAndCompact functions
|
||||
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) {
|
||||
if (!db_path || !output_directory || !input || !override_options) {
|
||||
SaveError(errptr, Status::InvalidArgument("Invalid arguments"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string input_str(input, input_len);
|
||||
std::string output_str;
|
||||
|
||||
Status s = DB::OpenAndCompact(db_path, output_directory, input_str,
|
||||
&output_str, override_options->rep);
|
||||
|
||||
if (!s.ok()) {
|
||||
SaveError(errptr, s);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Allocate +1 for null terminator
|
||||
char* result = static_cast<char*>(malloc(output_str.size() + 1));
|
||||
if (!result) {
|
||||
SaveError(errptr, Status::MemoryLimit("Failed to allocate output buffer"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
memcpy(result, output_str.data(), output_str.size());
|
||||
result[output_str.size()] = '\0';
|
||||
|
||||
// Only set output_len after successful allocation
|
||||
if (output_len) {
|
||||
*output_len = output_str.size();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!options || !db_path || !output_directory || !input ||
|
||||
!override_options) {
|
||||
SaveError(errptr, Status::InvalidArgument("Invalid arguments"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string input_str(input, input_len);
|
||||
std::string output_str;
|
||||
|
||||
Status s = DB::OpenAndCompact(options->rep, db_path, output_directory,
|
||||
input_str, &output_str, override_options->rep);
|
||||
|
||||
if (!s.ok()) {
|
||||
SaveError(errptr, s);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Allocate +1 for null terminator
|
||||
char* result = static_cast<char*>(malloc(output_str.size() + 1));
|
||||
if (!result) {
|
||||
SaveError(errptr, Status::MemoryLimit("Failed to allocate output buffer"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
memcpy(result, output_str.data(), output_str.size());
|
||||
result[output_str.size()] = '\0'; // Null terminate
|
||||
|
||||
// Only set output_len after successful allocation
|
||||
if (output_len) {
|
||||
*output_len = output_str.size();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
rocksdb_t* rocksdb_open(const rocksdb_options_t* options, const char* name,
|
||||
char** errptr) {
|
||||
DB* db;
|
||||
@@ -3763,11 +3186,6 @@ void rocksdb_block_based_options_set_unpartitioned_pinning_tier(
|
||||
static_cast<ROCKSDB_NAMESPACE::PinningTier>(v);
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_block_align(
|
||||
rocksdb_block_based_table_options_t* options, unsigned char v) {
|
||||
options->rep.block_align = v;
|
||||
}
|
||||
|
||||
/* FlushJobInfo */
|
||||
|
||||
const char* rocksdb_flushjobinfo_cf_name(const rocksdb_flushjobinfo_t* info,
|
||||
@@ -4123,15 +3541,6 @@ void rocksdb_options_add_eventlistener(rocksdb_options_t* opt,
|
||||
opt->rep.listeners.emplace_back(std::shared_ptr<EventListener>(t));
|
||||
}
|
||||
|
||||
void rocksdb_compaction_service_options_override_add_event_listener(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_eventlistener_t* event_listener) {
|
||||
if (override_options && event_listener) {
|
||||
override_options->rep.listeners.emplace_back(
|
||||
std::shared_ptr<EventListener>(event_listener));
|
||||
}
|
||||
}
|
||||
|
||||
rocksdb_cuckoo_table_options_t* rocksdb_cuckoo_options_create() {
|
||||
return new rocksdb_cuckoo_table_options_t;
|
||||
}
|
||||
@@ -4343,65 +3752,6 @@ rocksdb_logger_t* rocksdb_logger_create_callback_logger(
|
||||
|
||||
void rocksdb_logger_destroy(rocksdb_logger_t* logger) { delete logger; }
|
||||
|
||||
/* File Checksum Gen Factory */
|
||||
|
||||
rocksdb_file_checksum_gen_factory_t*
|
||||
rocksdb_file_checksum_gen_crc32c_factory_create() {
|
||||
rocksdb_file_checksum_gen_factory_t* factory =
|
||||
new rocksdb_file_checksum_gen_factory_t;
|
||||
factory->rep = GetFileChecksumGenCrc32cFactory();
|
||||
return factory;
|
||||
}
|
||||
|
||||
void rocksdb_file_checksum_gen_factory_destroy(
|
||||
rocksdb_file_checksum_gen_factory_t* factory) {
|
||||
delete factory;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_file_checksum_gen_factory(
|
||||
rocksdb_options_t* opt, rocksdb_file_checksum_gen_factory_t* factory) {
|
||||
if (opt && factory) {
|
||||
opt->rep.file_checksum_gen_factory = factory->rep;
|
||||
}
|
||||
}
|
||||
|
||||
/* SST Partitioner Factory */
|
||||
|
||||
rocksdb_sst_partitioner_factory_t*
|
||||
rocksdb_sst_partitioner_fixed_prefix_factory_create(size_t prefix_len) {
|
||||
rocksdb_sst_partitioner_factory_t* factory =
|
||||
new rocksdb_sst_partitioner_factory_t;
|
||||
factory->rep = NewSstPartitionerFixedPrefixFactory(prefix_len);
|
||||
return factory;
|
||||
}
|
||||
|
||||
void rocksdb_sst_partitioner_factory_destroy(
|
||||
rocksdb_sst_partitioner_factory_t* factory) {
|
||||
delete factory;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_sst_partitioner_factory(
|
||||
rocksdb_options_t* opt, rocksdb_sst_partitioner_factory_t* factory) {
|
||||
if (opt && factory) {
|
||||
opt->rep.sst_partitioner_factory = factory->rep;
|
||||
}
|
||||
}
|
||||
|
||||
/* Table Properties Collector Factory */
|
||||
|
||||
void rocksdb_table_properties_collector_factory_destroy(
|
||||
rocksdb_table_properties_collector_factory_t* factory) {
|
||||
delete factory;
|
||||
}
|
||||
|
||||
void rocksdb_options_add_table_properties_collector_factory(
|
||||
rocksdb_options_t* opt,
|
||||
rocksdb_table_properties_collector_factory_t* factory) {
|
||||
if (opt && factory) {
|
||||
opt->rep.table_properties_collector_factories.push_back(factory->rep);
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_options_set_env(rocksdb_options_t* opt, rocksdb_env_t* env) {
|
||||
opt->rep.env = (env ? env->rep : nullptr);
|
||||
}
|
||||
@@ -8491,7 +7841,7 @@ rocksdb_memory_usage_t* rocksdb_approximate_memory_usage_create(
|
||||
dbs.push_back(db->rep);
|
||||
}
|
||||
|
||||
std::unordered_set<const Cache*> cache_set;
|
||||
unordered_set<const Cache*> cache_set;
|
||||
for (auto cache : consumers->caches) {
|
||||
cache_set.insert(const_cast<const Cache*>(cache->rep.get()));
|
||||
}
|
||||
|
||||
-372
@@ -722,88 +722,6 @@ 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;
|
||||
@@ -4565,296 +4483,6 @@ 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);
|
||||
|
||||
// Test file checksum gen factory
|
||||
rocksdb_file_checksum_gen_factory_t* checksum_factory =
|
||||
rocksdb_file_checksum_gen_crc32c_factory_create();
|
||||
CheckCondition(checksum_factory != NULL);
|
||||
rocksdb_compaction_service_options_override_set_file_checksum_gen_factory(
|
||||
override_opts, checksum_factory);
|
||||
|
||||
// Test SST partitioner factory
|
||||
rocksdb_sst_partitioner_factory_t* partitioner_factory =
|
||||
rocksdb_sst_partitioner_fixed_prefix_factory_create(4);
|
||||
CheckCondition(partitioner_factory != NULL);
|
||||
rocksdb_compaction_service_options_override_set_sst_partitioner_factory(
|
||||
override_opts, partitioner_factory);
|
||||
|
||||
// Test merge operator
|
||||
rocksdb_compaction_service_options_override_set_merge_operator(
|
||||
override_opts, NULL);
|
||||
|
||||
// Test compaction filter
|
||||
rocksdb_compaction_service_options_override_set_compaction_filter(
|
||||
override_opts, NULL);
|
||||
|
||||
// Test prefix extractor
|
||||
rocksdb_compaction_service_options_override_set_prefix_extractor(
|
||||
override_opts, NULL);
|
||||
|
||||
// Test table factory - block based
|
||||
rocksdb_block_based_table_options_t* table_opts =
|
||||
rocksdb_block_based_options_create();
|
||||
rocksdb_compaction_service_options_override_set_block_based_table_factory(
|
||||
override_opts, table_opts);
|
||||
rocksdb_block_based_options_destroy(table_opts);
|
||||
|
||||
// Test statistics via options
|
||||
rocksdb_options_t* stats_opts = rocksdb_options_create();
|
||||
rocksdb_options_enable_statistics(stats_opts);
|
||||
rocksdb_compaction_service_options_override_set_statistics(override_opts,
|
||||
stats_opts);
|
||||
rocksdb_options_destroy(stats_opts);
|
||||
|
||||
// Test info log
|
||||
rocksdb_logger_t* logger =
|
||||
rocksdb_logger_create_stderr_logger(1, "test_prefix");
|
||||
rocksdb_compaction_service_options_override_set_info_log(override_opts,
|
||||
logger);
|
||||
rocksdb_logger_destroy(logger);
|
||||
|
||||
// Test options map
|
||||
rocksdb_compaction_service_options_override_set_option(
|
||||
override_opts, "max_bytes_for_level_base", "67108864");
|
||||
|
||||
// Cleanup
|
||||
rocksdb_file_checksum_gen_factory_destroy(checksum_factory);
|
||||
rocksdb_sst_partitioner_factory_destroy(partitioner_factory);
|
||||
rocksdb_compaction_service_options_override_destroy(override_opts);
|
||||
}
|
||||
|
||||
StartPhase("factory_options_on_regular_options");
|
||||
{
|
||||
// Test that the new factory types work with regular rocksdb_options_t
|
||||
rocksdb_options_t* test_opts = rocksdb_options_create();
|
||||
|
||||
// Test file checksum gen factory on regular options
|
||||
rocksdb_file_checksum_gen_factory_t* checksum_factory =
|
||||
rocksdb_file_checksum_gen_crc32c_factory_create();
|
||||
CheckCondition(checksum_factory != NULL);
|
||||
rocksdb_options_set_file_checksum_gen_factory(test_opts, checksum_factory);
|
||||
|
||||
// Test SST partitioner factory on regular options
|
||||
rocksdb_sst_partitioner_factory_t* partitioner_factory =
|
||||
rocksdb_sst_partitioner_fixed_prefix_factory_create(8);
|
||||
CheckCondition(partitioner_factory != NULL);
|
||||
rocksdb_options_set_sst_partitioner_factory(test_opts, partitioner_factory);
|
||||
|
||||
// Cleanup
|
||||
rocksdb_file_checksum_gen_factory_destroy(checksum_factory);
|
||||
rocksdb_sst_partitioner_factory_destroy(partitioner_factory);
|
||||
rocksdb_options_destroy(test_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;
|
||||
|
||||
+2
-2
@@ -1248,7 +1248,7 @@ Compaction* ColumnFamilyData::PickCompaction(
|
||||
auto* result = compaction_picker_->PickCompaction(
|
||||
GetName(), mutable_options, mutable_db_options, existing_snapshots,
|
||||
snapshot_checker, current_->storage_info(), log_buffer,
|
||||
GetFullHistoryTsLow(), require_max_output_level);
|
||||
require_max_output_level);
|
||||
if (result != nullptr) {
|
||||
result->FinalizeInputInfo(current_);
|
||||
}
|
||||
@@ -1336,7 +1336,7 @@ Compaction* ColumnFamilyData::CompactRange(
|
||||
GetName(), mutable_cf_options, mutable_db_options,
|
||||
current_->storage_info(), input_level, output_level,
|
||||
compact_range_options, begin, end, compaction_end, conflict,
|
||||
max_file_num_to_ignore, trim_ts, GetFullHistoryTsLow());
|
||||
max_file_num_to_ignore, trim_ts);
|
||||
if (result != nullptr) {
|
||||
result->FinalizeInputInfo(current_);
|
||||
}
|
||||
|
||||
@@ -433,9 +433,8 @@ TEST_F(CompactFilesTest, SentinelCompressionType) {
|
||||
ROCKSDB_NAMESPACE::TablePropertiesCollection all_tables_props;
|
||||
ASSERT_OK(db->GetPropertiesOfAllTables(&all_tables_props));
|
||||
for (const auto& name_and_table_props : all_tables_props) {
|
||||
// As of format_version 7, more elaborate information is encoded into the
|
||||
// compression_name property
|
||||
ASSERT_EQ("BuiltinV2;02;", name_and_table_props.second->compression_name);
|
||||
ASSERT_EQ(CompressionTypeToString(CompressionType::kZlibCompression),
|
||||
name_and_table_props.second->compression_name);
|
||||
}
|
||||
delete db;
|
||||
}
|
||||
|
||||
@@ -1310,14 +1310,14 @@ void CompactionIterator::PrepareOutput() {
|
||||
validity_info_.rep);
|
||||
assert(false);
|
||||
}
|
||||
|
||||
bool zeroed_seqno = false;
|
||||
ikey_.sequence = 0;
|
||||
last_key_seq_zeroed_ = true;
|
||||
TEST_SYNC_POINT_CALLBACK("CompactionIterator::PrepareOutput:ZeroingSeq",
|
||||
&ikey_);
|
||||
if (!timestamp_size_) {
|
||||
current_key_.UpdateInternalKey(0, ikey_.type);
|
||||
zeroed_seqno = true;
|
||||
} else if (full_history_ts_low_ && cmp_with_history_ts_low_ < 0) {
|
||||
// For UDT, the seqno and timestamp could only be zeroed out after the
|
||||
// key is below history_ts_low_.
|
||||
// We can also zero out timestamp for better compression.
|
||||
// For the same user key (excluding timestamp), the timestamp-based
|
||||
// history can be collapsed to save some space if the timestamp is
|
||||
// older than *full_history_ts_low_.
|
||||
@@ -1325,14 +1325,6 @@ void CompactionIterator::PrepareOutput() {
|
||||
const Slice ts_slice = kTsMin;
|
||||
ikey_.SetTimestamp(ts_slice);
|
||||
current_key_.UpdateInternalKey(0, ikey_.type, &ts_slice);
|
||||
zeroed_seqno = true;
|
||||
}
|
||||
|
||||
if (zeroed_seqno) {
|
||||
ikey_.sequence = 0;
|
||||
last_key_seq_zeroed_ = true;
|
||||
TEST_SYNC_POINT_CALLBACK("CompactionIterator::PrepareOutput:ZeroingSeq",
|
||||
&ikey_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2438,32 +2438,6 @@ bool CompactionJob::UpdateInternalStatsFromInputFiles(
|
||||
bool has_error = false;
|
||||
const ReadOptions read_options(Env::IOActivity::kCompaction);
|
||||
const auto& input_table_properties = compaction->GetInputTableProperties();
|
||||
|
||||
// Check all input files for old block-based SST format_version. Why? Old
|
||||
// block-based SST files from roughly version 5.0 to 5.18 could produce
|
||||
// inaccurate num_entries counts due to the evolution of its handling along
|
||||
// with num_range_deletions. We have to disable some paranoid checks when
|
||||
// compacting files from such an old release. However, we don't have great
|
||||
// information to identify those files, so we heuristically over-approximate
|
||||
// that set of files using
|
||||
// (a) format_version < 5, which will be true for any files from RocksDB <
|
||||
// 6.6.0 and should not be true for any recent production files
|
||||
// (b) to avoid including non-block-based SST files (which still use older
|
||||
// format_version markers, and do not support DeleteRange), we also require
|
||||
// the presence of the user property "rocksdb.block.based.table.index.type",
|
||||
// which was added in RocksDB 2.8 and is always present in block-based tables.
|
||||
for (const auto& tp_pair : input_table_properties) {
|
||||
if (tp_pair.second && tp_pair.second->format_version < 5) {
|
||||
// Check for block-based table by looking for its index type property
|
||||
const auto& user_props = tp_pair.second->user_collected_properties;
|
||||
if (user_props.find(BlockBasedTablePropertyNames::kIndexType) !=
|
||||
user_props.end()) {
|
||||
job_stats_->has_accurate_num_input_records = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int input_level = 0;
|
||||
input_level < static_cast<int>(compaction->num_input_levels());
|
||||
++input_level) {
|
||||
@@ -2708,10 +2682,7 @@ Status CompactionJob::ReadTablePropertiesDirectly(
|
||||
std::shared_ptr<const TableProperties>* tp) {
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
std::string file_name = GetTableFileName(file_meta->fd.GetNumber());
|
||||
FileOptions fopts = file_options_;
|
||||
fopts.file_checksum = file_meta->file_checksum;
|
||||
fopts.file_checksum_func_name = file_meta->file_checksum_func_name;
|
||||
Status s = ioptions.fs->NewRandomAccessFile(file_name, fopts, &file,
|
||||
Status s = ioptions.fs->NewRandomAccessFile(file_name, file_options_, &file,
|
||||
nullptr /* dbg */);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
|
||||
@@ -250,10 +250,6 @@ class CompactionJob {
|
||||
// @param num_input_range_del if non-null, will be set to the number of range
|
||||
// deletion entries in this compaction input.
|
||||
//
|
||||
// If any input file has potentially unreliable num_entries count (old SST
|
||||
// files - details in implementation),
|
||||
// job_stats_->has_accurate_num_input_records is set to false.
|
||||
//
|
||||
// Returns true iff internal_stats_.output_level_stats.num_input_records and
|
||||
// num_input_range_del are calculated successfully.
|
||||
//
|
||||
|
||||
@@ -49,10 +49,8 @@ Status CompactionOutputs::Finish(
|
||||
meta->fd.file_size = current_bytes;
|
||||
meta->tail_size = builder_->GetTailSize();
|
||||
meta->marked_for_compaction = builder_->NeedCompact();
|
||||
const TableProperties& tp = builder_->GetTableProperties();
|
||||
meta->user_defined_timestamps_persisted =
|
||||
static_cast<bool>(tp.user_defined_timestamps_persisted);
|
||||
ExtractTimestampFromTableProperties(tp, meta);
|
||||
meta->user_defined_timestamps_persisted = static_cast<bool>(
|
||||
builder_->GetTableProperties().user_defined_timestamps_persisted);
|
||||
}
|
||||
current_output().finished = true;
|
||||
stats_.bytes_written += current_bytes;
|
||||
|
||||
@@ -611,8 +611,7 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
|
||||
int input_level, int output_level,
|
||||
const CompactRangeOptions& compact_range_options, const InternalKey* begin,
|
||||
const InternalKey* end, InternalKey** compaction_end, bool* manual_conflict,
|
||||
uint64_t max_file_num_to_ignore, const std::string& trim_ts,
|
||||
const std::string& full_history_ts_low) {
|
||||
uint64_t max_file_num_to_ignore, const std::string& trim_ts) {
|
||||
// CompactionPickerFIFO has its own implementation of compact range
|
||||
assert(ioptions_.compaction_style != kCompactionStyleFIFO);
|
||||
|
||||
@@ -691,8 +690,7 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
|
||||
compact_range_options.blob_garbage_collection_age_cutoff);
|
||||
|
||||
RegisterCompaction(c);
|
||||
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options,
|
||||
full_history_ts_low);
|
||||
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options);
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -889,8 +887,7 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
|
||||
// takes running compactions into account (by skipping files that are already
|
||||
// being compacted). Since we just changed compaction score, we recalculate it
|
||||
// here
|
||||
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options,
|
||||
full_history_ts_low);
|
||||
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options);
|
||||
|
||||
return compaction;
|
||||
}
|
||||
|
||||
@@ -65,8 +65,7 @@ class CompactionPicker {
|
||||
const MutableDBOptions& mutable_db_options,
|
||||
const std::vector<SequenceNumber>& existing_snapshots,
|
||||
const SnapshotChecker* snapshot_checker, VersionStorageInfo* vstorage,
|
||||
LogBuffer* log_buffer, const std::string& full_history_ts_low,
|
||||
bool require_max_output_level = false) = 0;
|
||||
LogBuffer* log_buffer, bool require_max_output_level) = 0;
|
||||
|
||||
// The returned Compaction might not include the whole requested range.
|
||||
// In that case, compaction_end will be set to the next key that needs
|
||||
@@ -83,8 +82,7 @@ class CompactionPicker {
|
||||
const CompactRangeOptions& compact_range_options,
|
||||
const InternalKey* begin, const InternalKey* end,
|
||||
InternalKey** compaction_end, bool* manual_conflict,
|
||||
uint64_t max_file_num_to_ignore, const std::string& trim_ts,
|
||||
const std::string& full_history_ts_low);
|
||||
uint64_t max_file_num_to_ignore, const std::string& trim_ts);
|
||||
|
||||
// The maximum allowed output level. Default value is NumberLevels() - 1.
|
||||
virtual int MaxOutputLevel() const { return NumberLevels() - 1; }
|
||||
@@ -286,8 +284,7 @@ class NullCompactionPicker : public CompactionPicker {
|
||||
const std::vector<SequenceNumber>& /*existing_snapshots*/,
|
||||
const SnapshotChecker* /*snapshot_checker*/,
|
||||
VersionStorageInfo* /*vstorage*/, LogBuffer* /* log_buffer */,
|
||||
const std::string& /*full_history_ts_low*/,
|
||||
bool /*require_max_output_level*/) override {
|
||||
bool /*require_max_output_level*/ = false) override {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -301,8 +298,8 @@ class NullCompactionPicker : public CompactionPicker {
|
||||
const CompactRangeOptions& /*compact_range_options*/,
|
||||
const InternalKey* /*begin*/, const InternalKey* /*end*/,
|
||||
InternalKey** /*compaction_end*/, bool* /*manual_conflict*/,
|
||||
uint64_t /*max_file_num_to_ignore*/, const std::string& /*trim_ts*/,
|
||||
const std::string& /*full_history_ts_low*/) override {
|
||||
uint64_t /*max_file_num_to_ignore*/,
|
||||
const std::string& /*trim_ts*/) override {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -419,19 +419,12 @@ Compaction* FIFOCompactionPicker::PickTemperatureChangeCompaction(
|
||||
return c;
|
||||
}
|
||||
|
||||
// The full_history_ts_low parameter is used to control bottommost file marking
|
||||
// for compaction when user-defined timestamps (UDT) are enabled.
|
||||
|
||||
// TODO leverage full_history_ts_low for FIFO compaction, by trigggerring
|
||||
// compaction early for data that has already expired to achieve the goal of TTL
|
||||
// enforced compliance.
|
||||
Compaction* FIFOCompactionPicker::PickCompaction(
|
||||
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
|
||||
const MutableDBOptions& mutable_db_options,
|
||||
const std::vector<SequenceNumber>& /* existing_snapshots */,
|
||||
const SnapshotChecker* /* snapshot_checker */, VersionStorageInfo* vstorage,
|
||||
LogBuffer* log_buffer, const std::string& /* full_history_ts_low */,
|
||||
bool /* require_max_output_level*/) {
|
||||
LogBuffer* log_buffer, bool /* require_max_output_level*/) {
|
||||
Compaction* c = nullptr;
|
||||
if (mutable_cf_options.ttl > 0) {
|
||||
c = PickTTLCompaction(cf_name, mutable_cf_options, mutable_db_options,
|
||||
@@ -456,8 +449,7 @@ Compaction* FIFOCompactionPicker::PickCompactionForCompactRange(
|
||||
const CompactRangeOptions& /*compact_range_options*/,
|
||||
const InternalKey* /*begin*/, const InternalKey* /*end*/,
|
||||
InternalKey** compaction_end, bool* /*manual_conflict*/,
|
||||
uint64_t /*max_file_num_to_ignore*/, const std::string& /*trim_ts*/,
|
||||
const std::string& full_history_ts_low) {
|
||||
uint64_t /*max_file_num_to_ignore*/, const std::string& /*trim_ts*/) {
|
||||
#ifdef NDEBUG
|
||||
(void)input_level;
|
||||
(void)output_level;
|
||||
@@ -466,10 +458,10 @@ Compaction* FIFOCompactionPicker::PickCompactionForCompactRange(
|
||||
assert(output_level == 0);
|
||||
*compaction_end = nullptr;
|
||||
LogBuffer log_buffer(InfoLogLevel::INFO_LEVEL, ioptions_.logger);
|
||||
Compaction* c = PickCompaction(
|
||||
cf_name, mutable_cf_options, mutable_db_options,
|
||||
/*existing_snapshots*/ {}, /*snapshot_checker*/ nullptr, vstorage,
|
||||
&log_buffer, full_history_ts_low, /* require_max_output_level */ false);
|
||||
Compaction* c =
|
||||
PickCompaction(cf_name, mutable_cf_options, mutable_db_options,
|
||||
/*existing_snapshots*/ {}, /*snapshot_checker*/ nullptr,
|
||||
vstorage, &log_buffer);
|
||||
log_buffer.FlushBufferToLog();
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ class FIFOCompactionPicker : public CompactionPicker {
|
||||
const std::vector<SequenceNumber>& /* existing_snapshots */,
|
||||
const SnapshotChecker* /* snapshot_checker */,
|
||||
VersionStorageInfo* version, LogBuffer* log_buffer,
|
||||
const std::string& /* full_history_ts_low */,
|
||||
bool /* require_max_output_level*/ = false) override;
|
||||
|
||||
Compaction* PickCompactionForCompactRange(
|
||||
@@ -34,8 +33,7 @@ class FIFOCompactionPicker : public CompactionPicker {
|
||||
const CompactRangeOptions& compact_range_options,
|
||||
const InternalKey* begin, const InternalKey* end,
|
||||
InternalKey** compaction_end, bool* manual_conflict,
|
||||
uint64_t max_file_num_to_ignore, const std::string& trim_ts,
|
||||
const std::string& full_history_ts_low) override;
|
||||
uint64_t max_file_num_to_ignore, const std::string& trim_ts) override;
|
||||
|
||||
// The maximum allowed output level. Always returns 0.
|
||||
int MaxOutputLevel() const override { return 0; }
|
||||
|
||||
@@ -61,16 +61,14 @@ class LevelCompactionBuilder {
|
||||
LogBuffer* log_buffer,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
const ImmutableOptions& ioptions,
|
||||
const MutableDBOptions& mutable_db_options,
|
||||
const std::string& full_history_ts_low)
|
||||
const MutableDBOptions& mutable_db_options)
|
||||
: cf_name_(cf_name),
|
||||
vstorage_(vstorage),
|
||||
compaction_picker_(compaction_picker),
|
||||
log_buffer_(log_buffer),
|
||||
mutable_cf_options_(mutable_cf_options),
|
||||
ioptions_(ioptions),
|
||||
mutable_db_options_(mutable_db_options),
|
||||
full_history_ts_low_(full_history_ts_low) {}
|
||||
mutable_db_options_(mutable_db_options) {}
|
||||
|
||||
// Pick and return a compaction.
|
||||
Compaction* PickCompaction();
|
||||
@@ -157,7 +155,6 @@ class LevelCompactionBuilder {
|
||||
const MutableCFOptions& mutable_cf_options_;
|
||||
const ImmutableOptions& ioptions_;
|
||||
const MutableDBOptions& mutable_db_options_;
|
||||
const std::string& full_history_ts_low_;
|
||||
// Pick a path ID to place a newly generated file, with its level
|
||||
static uint32_t GetPathId(const ImmutableCFOptions& ioptions,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
@@ -574,8 +571,7 @@ Compaction* LevelCompactionBuilder::GetCompaction() {
|
||||
// takes running compactions into account (by skipping files that are already
|
||||
// being compacted). Since we just changed compaction score, we recalculate it
|
||||
// here
|
||||
vstorage_->ComputeCompactionScore(ioptions_, mutable_cf_options_,
|
||||
full_history_ts_low_);
|
||||
vstorage_->ComputeCompactionScore(ioptions_, mutable_cf_options_);
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -980,11 +976,10 @@ Compaction* LevelCompactionPicker::PickCompaction(
|
||||
const MutableDBOptions& mutable_db_options,
|
||||
const std::vector<SequenceNumber>& /*existing_snapshots */,
|
||||
const SnapshotChecker* /*snapshot_checker*/, VersionStorageInfo* vstorage,
|
||||
LogBuffer* log_buffer, const std::string& full_history_ts_low,
|
||||
bool /* require_max_output_level*/) {
|
||||
LogBuffer* log_buffer, bool /* require_max_output_level*/) {
|
||||
LevelCompactionBuilder builder(cf_name, vstorage, this, log_buffer,
|
||||
mutable_cf_options, ioptions_,
|
||||
mutable_db_options, full_history_ts_low);
|
||||
mutable_db_options);
|
||||
return builder.PickCompaction();
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -26,7 +26,6 @@ class LevelCompactionPicker : public CompactionPicker {
|
||||
const std::vector<SequenceNumber>& /* existing_snapshots */,
|
||||
const SnapshotChecker* /* snapshot_checker */,
|
||||
VersionStorageInfo* vstorage, LogBuffer* log_buffer,
|
||||
const std::string& full_history_ts_low,
|
||||
bool /*require_max_output_level*/ = false) override;
|
||||
|
||||
bool NeedsCompaction(const VersionStorageInfo* vstorage) const override;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -39,7 +39,7 @@ class UniversalCompactionBuilder {
|
||||
const std::vector<SequenceNumber>& existing_snapshots,
|
||||
const SnapshotChecker* snapshot_checker, VersionStorageInfo* vstorage,
|
||||
UniversalCompactionPicker* picker, LogBuffer* log_buffer,
|
||||
bool require_max_output_level, const std::string& full_history_ts_low)
|
||||
bool require_max_output_level)
|
||||
: ioptions_(ioptions),
|
||||
icmp_(icmp),
|
||||
cf_name_(cf_name),
|
||||
@@ -50,8 +50,7 @@ class UniversalCompactionBuilder {
|
||||
log_buffer_(log_buffer),
|
||||
require_max_output_level_(require_max_output_level),
|
||||
allow_ingest_behind_(ioptions.cf_allow_ingest_behind ||
|
||||
ioptions.allow_ingest_behind),
|
||||
full_history_ts_low_(full_history_ts_low) {
|
||||
ioptions.allow_ingest_behind) {
|
||||
assert(icmp_);
|
||||
const auto* ucmp = icmp_->user_comparator();
|
||||
assert(ucmp);
|
||||
@@ -451,7 +450,6 @@ class UniversalCompactionBuilder {
|
||||
std::map<uint64_t, size_t> file_marked_for_compaction_to_sorted_run_index_;
|
||||
bool require_max_output_level_;
|
||||
bool allow_ingest_behind_;
|
||||
const std::string& full_history_ts_low_;
|
||||
|
||||
std::vector<UniversalCompactionBuilder::SortedRun> CalculateSortedRuns(
|
||||
const VersionStorageInfo& vstorage, int last_level,
|
||||
@@ -597,20 +595,16 @@ bool UniversalCompactionPicker::NeedsCompaction(
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO leverage full_history_ts_low in universal compaction picking. It could
|
||||
// help reduce the same infinite compaction loop issue found in level
|
||||
// compaction.
|
||||
Compaction* UniversalCompactionPicker::PickCompaction(
|
||||
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
|
||||
const MutableDBOptions& mutable_db_options,
|
||||
const std::vector<SequenceNumber>& existing_snapshots,
|
||||
const SnapshotChecker* snapshot_checker, VersionStorageInfo* vstorage,
|
||||
LogBuffer* log_buffer, const std::string& full_history_ts_low,
|
||||
bool require_max_output_level) {
|
||||
LogBuffer* log_buffer, bool require_max_output_level) {
|
||||
UniversalCompactionBuilder builder(
|
||||
ioptions_, icmp_, cf_name, mutable_cf_options, mutable_db_options,
|
||||
existing_snapshots, snapshot_checker, vstorage, this, log_buffer,
|
||||
require_max_output_level, full_history_ts_low);
|
||||
require_max_output_level);
|
||||
return builder.PickCompaction();
|
||||
}
|
||||
|
||||
@@ -831,8 +825,7 @@ Compaction* UniversalCompactionBuilder::PickCompaction() {
|
||||
RecordInHistogram(ioptions_.stats, NUM_FILES_IN_SINGLE_COMPACTION, num_files);
|
||||
|
||||
picker_->RegisterCompaction(c);
|
||||
vstorage_->ComputeCompactionScore(ioptions_, mutable_cf_options_,
|
||||
full_history_ts_low_);
|
||||
vstorage_->ComputeCompactionScore(ioptions_, mutable_cf_options_);
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("UniversalCompactionBuilder::PickCompaction:Return",
|
||||
c);
|
||||
|
||||
@@ -26,8 +26,7 @@ class UniversalCompactionPicker : public CompactionPicker {
|
||||
const MutableDBOptions& mutable_db_options,
|
||||
const std::vector<SequenceNumber>& existing_snapshots,
|
||||
const SnapshotChecker* snapshot_checker, VersionStorageInfo* vstorage,
|
||||
LogBuffer* log_buffer, const std::string& full_history_ts_low,
|
||||
bool require_max_output_level = false) override;
|
||||
LogBuffer* log_buffer, bool require_max_output_level = false) override;
|
||||
int MaxOutputLevel() const override { return NumberLevels() - 1; }
|
||||
|
||||
bool NeedsCompaction(const VersionStorageInfo* vstorage) const override;
|
||||
|
||||
+4
-4
@@ -65,7 +65,7 @@ Status VerifySstFileChecksum(const Options& options,
|
||||
}
|
||||
|
||||
Status VerifySstFileChecksumInternal(const Options& options,
|
||||
const FileOptions& file_options,
|
||||
const EnvOptions& env_options,
|
||||
const ReadOptions& read_options,
|
||||
const std::string& file_path,
|
||||
const SequenceNumber& largest_seqno) {
|
||||
@@ -74,8 +74,8 @@ Status VerifySstFileChecksumInternal(const Options& options,
|
||||
InternalKeyComparator internal_comparator(options.comparator);
|
||||
ImmutableOptions ioptions(options);
|
||||
|
||||
Status s =
|
||||
ioptions.fs->NewRandomAccessFile(file_path, file_options, &file, nullptr);
|
||||
Status s = ioptions.fs->NewRandomAccessFile(
|
||||
file_path, FileOptions(env_options), &file, nullptr);
|
||||
if (s.ok()) {
|
||||
s = ioptions.fs->GetFileSize(file_path, IOOptions(), &file_size, nullptr);
|
||||
} else {
|
||||
@@ -94,7 +94,7 @@ Status VerifySstFileChecksumInternal(const Options& options,
|
||||
const bool kImmortal = true;
|
||||
auto reader_options = TableReaderOptions(
|
||||
ioptions, options.prefix_extractor, options.compression_manager.get(),
|
||||
file_options, internal_comparator, options.block_protection_bytes_per_key,
|
||||
env_options, internal_comparator, options.block_protection_bytes_per_key,
|
||||
false /* skip_filters */, !kImmortal, false /* force_direct_prefetch */,
|
||||
-1 /* level */);
|
||||
reader_options.largest_seqno = largest_seqno;
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
|
||||
#pragma once
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
Status VerifySstFileChecksumInternal(const Options& options,
|
||||
const FileOptions& file_options,
|
||||
const EnvOptions& env_options,
|
||||
const ReadOptions& read_options,
|
||||
const std::string& file_path,
|
||||
const SequenceNumber& largest_seqno = 0);
|
||||
|
||||
@@ -242,43 +242,6 @@ TEST_F(DBBasicTest, ReadOnlyDB) {
|
||||
Status::Code::kNotSupported);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, ReadOnlyDBFlushWAL) {
|
||||
// Test that FlushWAL returns NotSupported on read-only DB, and that
|
||||
// GetLiveFilesStorageInfo works correctly even with manual_wal_flush=true.
|
||||
// This is a regression test for a bug where GetLiveFilesStorageInfo would
|
||||
// crash on read-only DBs with manual_wal_flush=true because FlushWAL
|
||||
// accessed logs_.back() on an empty deque.
|
||||
auto options = CurrentOptions();
|
||||
options.manual_wal_flush = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("foo", "v1"));
|
||||
ASSERT_OK(Put("bar", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("baz", "v3")); // Unflushed data in WAL
|
||||
Close();
|
||||
|
||||
// Reopen as read-only
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
ASSERT_EQ("v1", Get("foo"));
|
||||
ASSERT_EQ("v2", Get("bar"));
|
||||
ASSERT_EQ("v3", Get("baz"));
|
||||
|
||||
// FlushWAL should return NotSupported (not crash)
|
||||
ASSERT_EQ(db_->FlushWAL(/*sync=*/false).code(), Status::Code::kNotSupported);
|
||||
ASSERT_EQ(db_->FlushWAL(/*sync=*/true).code(), Status::Code::kNotSupported);
|
||||
|
||||
// GetLiveFilesStorageInfo should succeed (previously crashed with
|
||||
// manual_wal_flush=true because it called FlushWAL which accessed
|
||||
// logs_.back() on empty deque)
|
||||
LiveFilesStorageInfoOptions lfsi_opts;
|
||||
lfsi_opts.wal_size_for_flush = 0;
|
||||
std::vector<LiveFileStorageInfo> files;
|
||||
ASSERT_OK(db_->GetLiveFilesStorageInfo(lfsi_opts, &files));
|
||||
ASSERT_GT(files.size(), 0);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, ReadOnlyDBWithWriteDBIdToManifestSet) {
|
||||
auto options = CurrentOptions();
|
||||
options.write_dbid_to_manifest = false;
|
||||
@@ -3905,75 +3868,6 @@ 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.
|
||||
@@ -5474,94 +5368,6 @@ INSTANTIATE_TEST_CASE_P(DBBasicTestDeadline, DBBasicTestDeadline,
|
||||
::testing::Values(std::make_tuple(true, false),
|
||||
std::make_tuple(false, true),
|
||||
std::make_tuple(true, true)));
|
||||
|
||||
// FileSystemWrapper that captures FileOptions passed to NewRandomAccessFile
|
||||
// for .sst files, so we can verify file_checksum fields are populated.
|
||||
class ChecksumCapturingFS : public FileSystemWrapper {
|
||||
public:
|
||||
explicit ChecksumCapturingFS(const std::shared_ptr<FileSystem>& base)
|
||||
: FileSystemWrapper(base) {}
|
||||
|
||||
static const char* kClassName() { return "ChecksumCapturingFS"; }
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
IOStatus NewRandomAccessFile(const std::string& fname,
|
||||
const FileOptions& opts,
|
||||
std::unique_ptr<FSRandomAccessFile>* result,
|
||||
IODebugContext* dbg) override {
|
||||
if (fname.find(".sst") != std::string::npos) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
captured_file_checksum_ = opts.file_checksum;
|
||||
captured_file_checksum_func_name_ = opts.file_checksum_func_name;
|
||||
capture_count_++;
|
||||
}
|
||||
return target()->NewRandomAccessFile(fname, opts, result, dbg);
|
||||
}
|
||||
|
||||
std::string GetCapturedFileChecksum() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return captured_file_checksum_;
|
||||
}
|
||||
|
||||
std::string GetCapturedFileChecksumFuncName() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return captured_file_checksum_func_name_;
|
||||
}
|
||||
|
||||
int GetCaptureCount() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return capture_count_;
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
captured_file_checksum_.clear();
|
||||
captured_file_checksum_func_name_.clear();
|
||||
capture_count_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mu_;
|
||||
std::string captured_file_checksum_;
|
||||
std::string captured_file_checksum_func_name_;
|
||||
int capture_count_ = 0;
|
||||
};
|
||||
|
||||
TEST_F(DBBasicTest, FileChecksumInFileOptions) {
|
||||
// Verify that file_checksum and file_checksum_func_name from FileMetaData
|
||||
// are propagated through FileOptions when opening SST files.
|
||||
auto capturing_fs =
|
||||
std::make_shared<ChecksumCapturingFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> env(new CompositeEnvWrapper(env_, capturing_fs));
|
||||
|
||||
Options options = GetDefaultOptions();
|
||||
options.create_if_missing = true;
|
||||
options.env = env.get();
|
||||
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Write data and flush to create an SST with a file checksum.
|
||||
ASSERT_OK(Put("key1", "value1"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Reset captures, then reopen to trigger TableCache SST open.
|
||||
capturing_fs->Reset();
|
||||
Reopen(options);
|
||||
|
||||
// Read to trigger SST open through TableCache::GetTableReader.
|
||||
ASSERT_EQ("value1", Get("key1"));
|
||||
|
||||
// Verify that checksum fields were populated.
|
||||
ASSERT_GT(capturing_fs->GetCaptureCount(), 0);
|
||||
ASSERT_FALSE(capturing_fs->GetCapturedFileChecksum().empty());
|
||||
ASSERT_NE(capturing_fs->GetCapturedFileChecksumFuncName(),
|
||||
capturing_fs->GetCapturedFileChecksum());
|
||||
ASSERT_EQ(capturing_fs->GetCapturedFileChecksumFuncName(),
|
||||
"FileChecksumCrc32c");
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+7
-7
@@ -101,7 +101,7 @@ TEST_F(DBFlushTest, SyncFail) {
|
||||
TEST_SYNC_POINT("DBFlushTest::SyncFail:2");
|
||||
fault_injection_env->SetFilesystemActive(true);
|
||||
// Now the background job will do the flush; wait for it.
|
||||
// Returns the IO error happened during flush.
|
||||
// Returns the IO error happend during flush.
|
||||
ASSERT_NOK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
ASSERT_EQ("", FilesPerLevel()); // flush failed.
|
||||
Destroy(options);
|
||||
@@ -518,11 +518,11 @@ TEST_F(DBFlushTest, StatisticsGarbageInsertAndDeletes) {
|
||||
// Note : one set of delete for KEY1, KEY2, KEY3 is written to
|
||||
// SSTable to propagate the delete operations to K-V pairs
|
||||
// that could have been inserted into the database during past Flush
|
||||
// operations.
|
||||
// opeartions.
|
||||
EXPECTED_MEMTABLE_GARBAGE_BYTES_AT_FLUSH -=
|
||||
KEY1.size() + KEY2.size() + KEY3.size() + 3 * sizeof(uint64_t);
|
||||
|
||||
// Additional useful payload.
|
||||
// Additional useful paylaod.
|
||||
ASSERT_OK(Delete(KEY4));
|
||||
ASSERT_OK(Delete(KEY5));
|
||||
ASSERT_OK(Delete(KEY6));
|
||||
@@ -614,7 +614,7 @@ TEST_F(DBFlushTest, StatisticsGarbageRangeDeletes) {
|
||||
|
||||
// Note : one set of deleteRange for (KEY1, KEY2) and (KEY2, KEY3) is written
|
||||
// to SSTable to propagate the deleteRange operations to K-V pairs that could
|
||||
// have been inserted into the database during past Flush operations.
|
||||
// have been inserted into the database during past Flush opeartions.
|
||||
EXPECTED_MEMTABLE_GARBAGE_BYTES_AT_FLUSH -=
|
||||
(KEY1.size() + KEY2.size() + sizeof(uint64_t)) +
|
||||
(KEY2.size() + KEY3.size() + sizeof(uint64_t));
|
||||
@@ -842,7 +842,7 @@ TEST_F(DBFlushTest, FixFlushReasonRaceFromConcurrentFlushes) {
|
||||
ASSERT_OK(Put(1, Key(idx), std::string(1, 'v')));
|
||||
}
|
||||
|
||||
// To coerce a manual flush happening in the middle of GetLiveFiles's flush,
|
||||
// To coerce a manual flush happenning in the middle of GetLiveFiles's flush,
|
||||
// we need to pause background flush thread and enable it later.
|
||||
std::shared_ptr<test::SleepingBackgroundTask> sleeping_task =
|
||||
std::make_shared<test::SleepingBackgroundTask>();
|
||||
@@ -851,7 +851,7 @@ TEST_F(DBFlushTest, FixFlushReasonRaceFromConcurrentFlushes) {
|
||||
sleeping_task.get(), Env::Priority::HIGH);
|
||||
sleeping_task->WaitUntilSleeping();
|
||||
|
||||
// Coerce a manual flush happening in the middle of GetLiveFiles's flush
|
||||
// Coerce a manual flush happenning in the middle of GetLiveFiles's flush
|
||||
bool get_live_files_paused_at_sync_point = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::AtomicFlushMemTables:AfterScheduleFlush", [&](void* /* arg */) {
|
||||
@@ -1428,7 +1428,7 @@ TEST_F(DBFlushTest, MemPurgeDeleteAndDeleteRange) {
|
||||
Close();
|
||||
}
|
||||
|
||||
// Create a Compaction Filter that will be invoked
|
||||
// Create a Compaction Fitler that will be invoked
|
||||
// at flush time and will update the value of a KV pair
|
||||
// if the key string is "lower" than the filter_key_ string.
|
||||
class ConditionalUpdateFilter : public CompactionFilter {
|
||||
|
||||
+50
-110
@@ -1177,38 +1177,23 @@ FSDirectory* DBImpl::GetDataDir(ColumnFamilyData* cfd, size_t path_id) const {
|
||||
}
|
||||
|
||||
Status DBImpl::SetOptions(
|
||||
const std::unordered_map<ColumnFamilyHandle*,
|
||||
std::unordered_map<std::string, std::string>>&
|
||||
column_families_opts_map) {
|
||||
ColumnFamilyHandle* column_family,
|
||||
const std::unordered_map<std::string, std::string>& options_map) {
|
||||
// TODO: plumb Env::IOActivity, Env::IOPriority
|
||||
const ReadOptions read_options;
|
||||
const WriteOptions write_options;
|
||||
|
||||
if (column_families_opts_map.empty()) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
for (const auto& cf_opts : column_families_opts_map) {
|
||||
if (cf_opts.second.empty()) {
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"SetOptions() on column family [%s], empty input",
|
||||
cf_opts.first->GetName().c_str());
|
||||
return Status::InvalidArgument("empty input");
|
||||
}
|
||||
}
|
||||
|
||||
autovector<std::pair<ColumnFamilyData*,
|
||||
const std::unordered_map<std::string, std::string>*>>
|
||||
column_family_datas;
|
||||
for (const auto& cf_opts : column_families_opts_map) {
|
||||
column_family_datas.push_back(
|
||||
{static_cast_with_check<ColumnFamilyHandleImpl>(cf_opts.first)->cfd(),
|
||||
&cf_opts.second});
|
||||
auto* cfd =
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(column_family)->cfd();
|
||||
if (options_map.empty()) {
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"SetOptions() on column family [%s], empty input",
|
||||
cfd->GetName().c_str());
|
||||
return Status::InvalidArgument("empty input");
|
||||
}
|
||||
|
||||
InstrumentedMutexLock ol(&options_mutex_);
|
||||
autovector<MutableCFOptions>
|
||||
new_options_copy; // For logging outside of DB mutex
|
||||
MutableCFOptions new_options_copy; // For logging outside of DB mutex
|
||||
Status s;
|
||||
Status persist_options_status;
|
||||
SuperVersionContext sv_context(/* create_superversion */ true);
|
||||
@@ -1231,104 +1216,68 @@ Status DBImpl::SetOptions(
|
||||
// Thus aren't releasing the DB mutex from LogAndApply calling pre_cb,
|
||||
// through installing the new Version until the end of this block, after
|
||||
// installing the new SuperVersion.
|
||||
auto pre_cb = [&]() -> Status {
|
||||
Status cb_s = cfd->SetOptions(db_options, options_map);
|
||||
if (cb_s.ok()) {
|
||||
new_options_copy = cfd->GetLatestMutableCFOptions();
|
||||
}
|
||||
return cb_s;
|
||||
};
|
||||
VersionEdit dummy_edit;
|
||||
dummy_edit.MarkNoManifestWriteDummy();
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::SetOptions:dummy_edit", &dummy_edit);
|
||||
for (const auto& cfd_opts : column_family_datas) {
|
||||
auto* cfd = cfd_opts.first;
|
||||
const auto* options_map_ptr = cfd_opts.second;
|
||||
auto pre_cb = [&]() -> Status {
|
||||
Status cb_s = cfd->SetOptions(db_options, *options_map_ptr);
|
||||
if (cb_s.ok()) {
|
||||
new_options_copy.emplace_back(cfd->GetLatestMutableCFOptions());
|
||||
}
|
||||
return cb_s;
|
||||
};
|
||||
|
||||
s = versions_->LogAndApply(
|
||||
cfd, read_options, write_options, &dummy_edit, &mutex_,
|
||||
directories_.GetDbDir(), false /*new_descriptor_log=*/,
|
||||
nullptr /*new_opts*/, {} /*manifest_wcb*/, pre_cb);
|
||||
if (!versions_->io_status().ok()) {
|
||||
assert(!s.ok());
|
||||
error_handler_.SetBGError(versions_->io_status(),
|
||||
BackgroundErrorReason::kManifestWrite);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
break;
|
||||
}
|
||||
s = versions_->LogAndApply(
|
||||
cfd, read_options, write_options, &dummy_edit, &mutex_,
|
||||
directories_.GetDbDir(), false /*new_descriptor_log=*/,
|
||||
nullptr /*new_opts*/, {} /*manifest_wcb*/, pre_cb);
|
||||
if (!versions_->io_status().ok()) {
|
||||
assert(!s.ok());
|
||||
error_handler_.SetBGError(versions_->io_status(),
|
||||
BackgroundErrorReason::kManifestWrite);
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
// Trigger possible flush/compactions. This has to be before we persist
|
||||
// options to file, otherwise there will be a deadlock with writer
|
||||
// thread.
|
||||
for (const auto& cfd_opts : column_family_datas) {
|
||||
InstallSuperVersionForConfigChange(cfd_opts.first, &sv_context);
|
||||
}
|
||||
InstallSuperVersionForConfigChange(cfd, &sv_context);
|
||||
persist_options_status =
|
||||
WriteOptionsFile(write_options, true /*db_mutex_already_held*/);
|
||||
bg_cv_.SignalAll();
|
||||
|
||||
#ifndef NDEBUG
|
||||
for (size_t i = 0; i < column_family_datas.size(); ++i) {
|
||||
auto* cfd = column_family_datas[i].first;
|
||||
assert(new_options_copy[i] == cfd->GetLatestMutableCFOptions());
|
||||
assert(cfd->GetLatestMutableCFOptions() ==
|
||||
cfd->GetCurrentMutableCFOptions());
|
||||
assert(cfd->GetCurrentMutableCFOptions() ==
|
||||
cfd->current()->GetMutableCFOptions());
|
||||
}
|
||||
#endif
|
||||
assert(new_options_copy == cfd->GetLatestMutableCFOptions());
|
||||
assert(cfd->GetLatestMutableCFOptions() ==
|
||||
cfd->GetCurrentMutableCFOptions());
|
||||
assert(cfd->GetCurrentMutableCFOptions() ==
|
||||
cfd->current()->GetMutableCFOptions());
|
||||
}
|
||||
}
|
||||
sv_context.Clean();
|
||||
|
||||
if (s.ok()) {
|
||||
bool needs_seqno_worker = false;
|
||||
for (const auto& cf_opts : column_families_opts_map) {
|
||||
if (cf_opts.second.count("preserve_internal_time_seconds") > 0 ||
|
||||
cf_opts.second.count("preclude_last_level_data_seconds") > 0) {
|
||||
needs_seqno_worker = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (needs_seqno_worker) {
|
||||
s = RegisterRecordSeqnoTimeWorker();
|
||||
}
|
||||
if (s.ok() && (options_map.count("preserve_internal_time_seconds") > 0 ||
|
||||
options_map.count("preclude_last_level_data_seconds") > 0)) {
|
||||
s = RegisterRecordSeqnoTimeWorker();
|
||||
}
|
||||
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"SetOptions() on [%zu] column families, inputs:",
|
||||
column_family_datas.size());
|
||||
for (size_t i = 0; i < column_family_datas.size(); ++i) {
|
||||
const auto* cfd = column_family_datas[i].first;
|
||||
const auto* options_map_ptr = column_family_datas[i].second;
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"Set options on column family [%s] (%zu/%zu), inputs:",
|
||||
cfd->GetName().c_str(), i, column_family_datas.size());
|
||||
for (const auto& o : *options_map_ptr) {
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log, "%s: %s\n",
|
||||
o.first.c_str(), o.second.c_str());
|
||||
}
|
||||
ROCKS_LOG_INFO(
|
||||
immutable_db_options_.info_log,
|
||||
"SetOptions() on column family [%s], inputs:", cfd->GetName().c_str());
|
||||
for (const auto& o : options_map) {
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log, "%s: %s\n", o.first.c_str(),
|
||||
o.second.c_str());
|
||||
}
|
||||
if (s.ok()) {
|
||||
for (size_t i = 0; i < column_family_datas.size(); ++i) {
|
||||
const auto* cfd = column_family_datas[i].first;
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"Set options on column family [%s] (%zu/%zu) succeeded, "
|
||||
"updated CF options:",
|
||||
cfd->GetName().c_str(), i, column_family_datas.size());
|
||||
new_options_copy[i].Dump(immutable_db_options_.info_log.get());
|
||||
}
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"[%s] SetOptions() succeeded", cfd->GetName().c_str());
|
||||
new_options_copy.Dump(immutable_db_options_.info_log.get());
|
||||
if (!persist_options_status.ok()) {
|
||||
// NOTE: WriteOptionsFile already logs on failure
|
||||
s = persist_options_status;
|
||||
}
|
||||
} else {
|
||||
persist_options_status.PermitUncheckedError(); // less important
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log, "SetOptions() failed: %s",
|
||||
s.ToString().c_str());
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log, "[%s] SetOptions() failed",
|
||||
cfd->GetName().c_str());
|
||||
}
|
||||
LogFlush(immutable_db_options_.info_log);
|
||||
return s;
|
||||
@@ -4402,8 +4351,7 @@ void DBImpl::ReleaseSnapshot(const Snapshot* s) {
|
||||
for (auto* cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (!cfd->AllowIngestBehind()) {
|
||||
cfd->current()->storage_info()->UpdateOldestSnapshot(
|
||||
oldest_snapshot, /*allow_ingest_behind=*/false,
|
||||
cfd->ioptions().user_comparator, cfd->GetFullHistoryTsLow());
|
||||
oldest_snapshot, /*allow_ingest_behind=*/false);
|
||||
if (!cfd->current()
|
||||
->storage_info()
|
||||
->BottommostFilesMarkedForCompaction()
|
||||
@@ -5039,8 +4987,7 @@ Status DBImpl::DeleteFilesInRanges(ColumnFamilyHandle* column_family,
|
||||
}
|
||||
if (!deleted_files.empty()) {
|
||||
vstorage->ComputeCompactionScore(cfd->ioptions(),
|
||||
cfd->GetLatestMutableCFOptions(),
|
||||
cfd->GetFullHistoryTsLow());
|
||||
cfd->GetLatestMutableCFOptions());
|
||||
}
|
||||
if (edit.GetDeletedFiles().empty()) {
|
||||
job_context.Clean();
|
||||
@@ -6471,11 +6418,8 @@ Status DBImpl::VerifyChecksumInternal(const ReadOptions& read_options,
|
||||
fmeta->file_checksum_func_name, fname,
|
||||
read_options);
|
||||
} else {
|
||||
FileOptions fopts = file_options_;
|
||||
fopts.file_checksum = fmeta->file_checksum;
|
||||
fopts.file_checksum_func_name = fmeta->file_checksum_func_name;
|
||||
s = ROCKSDB_NAMESPACE::VerifySstFileChecksumInternal(
|
||||
opts, fopts, read_options, fname, fd.largest_seqno);
|
||||
opts, file_options_, read_options, fname, fd.largest_seqno);
|
||||
}
|
||||
RecordTick(stats_, VERIFY_CHECKSUM_READ_BYTES,
|
||||
IOSTATS(bytes_read) - prev_bytes_read);
|
||||
@@ -6543,15 +6487,12 @@ Status DBImpl::VerifyFullFileChecksum(const std::string& file_checksum_expected,
|
||||
}
|
||||
std::string file_checksum;
|
||||
std::string func_name;
|
||||
FileOptions fopts;
|
||||
fopts.file_checksum = file_checksum_expected;
|
||||
fopts.file_checksum_func_name = func_name_expected;
|
||||
s = ROCKSDB_NAMESPACE::GenerateOneFileChecksum(
|
||||
fs_.get(), fname, immutable_db_options_.file_checksum_gen_factory.get(),
|
||||
func_name_expected, &file_checksum, &func_name,
|
||||
read_options.readahead_size, immutable_db_options_.allow_mmap_reads,
|
||||
io_tracer_, immutable_db_options_.rate_limiter.get(), read_options,
|
||||
immutable_db_options_.stats, immutable_db_options_.clock, fopts);
|
||||
immutable_db_options_.stats, immutable_db_options_.clock);
|
||||
if (s.ok()) {
|
||||
assert(func_name_expected == func_name);
|
||||
if (file_checksum != file_checksum_expected) {
|
||||
@@ -6910,8 +6851,7 @@ void DBImpl::TriggerPeriodicCompaction() {
|
||||
if (cfd->GetLatestCFOptions().periodic_compaction_seconds &&
|
||||
!cfd->queued_for_compaction()) {
|
||||
cfd->current()->storage_info()->ComputeCompactionScore(
|
||||
cfd->ioptions(), cfd->GetLatestMutableCFOptions(),
|
||||
cfd->GetFullHistoryTsLow());
|
||||
cfd->ioptions(), cfd->GetLatestMutableCFOptions());
|
||||
EnqueuePendingCompaction(cfd);
|
||||
if (cfd->queued_for_compaction()) {
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
|
||||
@@ -458,9 +458,8 @@ class DBImpl : public DB {
|
||||
|
||||
using DB::SetOptions;
|
||||
Status SetOptions(
|
||||
const std::unordered_map<ColumnFamilyHandle*,
|
||||
std::unordered_map<std::string, std::string>>&
|
||||
column_families_opts_map) override;
|
||||
ColumnFamilyHandle* column_family,
|
||||
const std::unordered_map<std::string, std::string>& options_map) override;
|
||||
|
||||
Status SetDBOptions(
|
||||
const std::unordered_map<std::string, std::string>& options_map) override;
|
||||
|
||||
@@ -1449,8 +1449,7 @@ Status DBImpl::PerformTrivialMove(Compaction& c, LogBuffer* log_buffer,
|
||||
f->file_creation_time, f->epoch_number,
|
||||
f->file_checksum, f->file_checksum_func_name,
|
||||
f->unique_id, f->compensated_range_deletion_size,
|
||||
f->tail_size, f->user_defined_timestamps_persisted,
|
||||
f->min_timestamp, f->max_timestamp);
|
||||
f->tail_size, f->user_defined_timestamps_persisted);
|
||||
moved_bytes += static_cast<size_t>(c.input(l, i)->fd.GetFileSize());
|
||||
ROCKS_LOG_BUFFER(
|
||||
log_buffer, "[%s] Moved #%" PRIu64 " to level-%d %" PRIu64 " bytes\n",
|
||||
@@ -1646,8 +1645,8 @@ Status DBImpl::CompactFilesImpl(
|
||||
// takes running compactions into account (by skipping files that are already
|
||||
// being compacted). Since we just changed compaction score, we recalculate it
|
||||
// here.
|
||||
version->storage_info()->ComputeCompactionScore(
|
||||
cfd->ioptions(), c->mutable_cf_options(), cfd->GetFullHistoryTsLow());
|
||||
version->storage_info()->ComputeCompactionScore(cfd->ioptions(),
|
||||
c->mutable_cf_options());
|
||||
|
||||
compaction_job.Prepare(std::nullopt /*subcompact to be computed*/);
|
||||
|
||||
@@ -1972,8 +1971,7 @@ Status DBImpl::ReFitLevel(ColumnFamilyData* cfd, int level, int target_level) {
|
||||
f->oldest_ancester_time, f->file_creation_time, f->epoch_number,
|
||||
f->file_checksum, f->file_checksum_func_name, f->unique_id,
|
||||
f->compensated_range_deletion_size, f->tail_size,
|
||||
f->user_defined_timestamps_persisted, f->min_timestamp,
|
||||
f->max_timestamp);
|
||||
f->user_defined_timestamps_persisted);
|
||||
}
|
||||
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
|
||||
"[%s] Apply version edit:\n%s", cfd->GetName().c_str(),
|
||||
@@ -2891,8 +2889,16 @@ Status DBImpl::WaitForFlushMemTables(
|
||||
|
||||
Status DBImpl::EnableAutoCompaction(
|
||||
const std::vector<ColumnFamilyHandle*>& column_family_handles) {
|
||||
return SetOptions(column_family_handles,
|
||||
{{"disable_auto_compactions", "false"}});
|
||||
Status s;
|
||||
for (auto cf_ptr : column_family_handles) {
|
||||
Status status =
|
||||
this->SetOptions(cf_ptr, {{"disable_auto_compactions", "false"}});
|
||||
if (!status.ok()) {
|
||||
s = status;
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
// NOTE: Calling DisableManualCompaction() may overwrite the
|
||||
@@ -3833,8 +3839,7 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
->current()
|
||||
->storage_info()
|
||||
->ComputeCompactionScore(c->immutable_options(),
|
||||
c->mutable_cf_options(),
|
||||
cfd->GetFullHistoryTsLow());
|
||||
c->mutable_cf_options());
|
||||
EnqueuePendingCompaction(cfd);
|
||||
|
||||
c.reset();
|
||||
@@ -4091,9 +4096,7 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
in_file->unique_id,
|
||||
in_file->compensated_range_deletion_size,
|
||||
in_file->tail_size,
|
||||
in_file->user_defined_timestamps_persisted,
|
||||
in_file->min_timestamp,
|
||||
in_file->max_timestamp};
|
||||
in_file->user_defined_timestamps_persisted};
|
||||
|
||||
out_files.push_back(std::move(out_file_metadata));
|
||||
}
|
||||
@@ -4398,8 +4401,7 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
->current()
|
||||
->storage_info()
|
||||
->ComputeCompactionScore(c->immutable_options(),
|
||||
c->mutable_cf_options(),
|
||||
cfd->GetFullHistoryTsLow());
|
||||
c->mutable_cf_options());
|
||||
EnqueuePendingCompaction(cfd);
|
||||
}
|
||||
}
|
||||
@@ -4505,7 +4507,7 @@ Compaction* DBImpl::CreateIntendedCompactionForwardedToBottomPriorityPool(
|
||||
nullptr /* snapshot_checker */, c->compaction_reason());
|
||||
|
||||
cfd->compaction_picker()->RegisterCompaction(intended_compaction);
|
||||
vstorage->ComputeCompactionScore(io, mo, cfd->GetFullHistoryTsLow());
|
||||
vstorage->ComputeCompactionScore(io, mo);
|
||||
intended_compaction->FinalizeInputInfo(cfd->current());
|
||||
|
||||
return intended_compaction;
|
||||
@@ -4877,8 +4879,7 @@ void DBImpl::ResetBottomPriCompactionIntent(ColumnFamilyData* cfd,
|
||||
std::unique_ptr<Compaction>& c) {
|
||||
c->ReleaseCompactionFiles(Status::OK());
|
||||
cfd->current()->storage_info()->ComputeCompactionScore(
|
||||
c->immutable_options(), c->mutable_cf_options(),
|
||||
cfd->GetFullHistoryTsLow());
|
||||
c->immutable_options(), c->mutable_cf_options());
|
||||
c.reset();
|
||||
}
|
||||
|
||||
|
||||
@@ -46,8 +46,7 @@ Status DBImpl::SuggestCompactRange(ColumnFamilyHandle* column_family,
|
||||
// Since we have some more files to compact, we should also recompute
|
||||
// compaction score
|
||||
vstorage->ComputeCompactionScore(cfd->ioptions(),
|
||||
cfd->GetLatestMutableCFOptions(),
|
||||
cfd->GetFullHistoryTsLow());
|
||||
cfd->GetLatestMutableCFOptions());
|
||||
EnqueuePendingCompaction(cfd);
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
}
|
||||
@@ -144,8 +143,7 @@ Status DBImpl::PromoteL0(ColumnFamilyHandle* column_family, int target_level) {
|
||||
f->file_creation_time, f->epoch_number, f->file_checksum,
|
||||
f->file_checksum_func_name, f->unique_id,
|
||||
f->compensated_range_deletion_size, f->tail_size,
|
||||
f->user_defined_timestamps_persisted, f->min_timestamp,
|
||||
f->max_timestamp);
|
||||
f->user_defined_timestamps_persisted);
|
||||
}
|
||||
|
||||
status = versions_->LogAndApply(cfd, read_options, write_options, &edit,
|
||||
|
||||
@@ -657,8 +657,7 @@ Status DBImpl::Recover(
|
||||
f->file_creation_time, f->epoch_number,
|
||||
f->file_checksum, f->file_checksum_func_name,
|
||||
f->unique_id, f->compensated_range_deletion_size,
|
||||
f->tail_size, f->user_defined_timestamps_persisted,
|
||||
f->min_timestamp, f->max_timestamp);
|
||||
f->tail_size, f->user_defined_timestamps_persisted);
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"[%s] Moving #%" PRIu64
|
||||
" from from_level-%d to from_level-%d %" PRIu64
|
||||
|
||||
@@ -121,11 +121,6 @@ class DBImplReadOnly : public DBImpl {
|
||||
return Status::NotSupported("Not supported operation in read only mode.");
|
||||
}
|
||||
|
||||
using DBImpl::FlushWAL;
|
||||
Status FlushWAL(const FlushWALOptions& /*options*/) override {
|
||||
return Status::NotSupported("Not supported operation in read only mode.");
|
||||
}
|
||||
|
||||
using DB::IngestExternalFile;
|
||||
Status IngestExternalFile(
|
||||
ColumnFamilyHandle* /*column_family*/,
|
||||
|
||||
@@ -216,9 +216,9 @@ class DBImplSecondary : public DBImpl {
|
||||
|
||||
using DBImpl::SetOptions;
|
||||
Status SetOptions(
|
||||
const std::unordered_map<ColumnFamilyHandle*,
|
||||
std::unordered_map<std::string, std::string>>&
|
||||
/*column_families_opts_map*/) override {
|
||||
ColumnFamilyHandle* /*cfd*/,
|
||||
const std::unordered_map<std::string, std::string>& /*options_map*/)
|
||||
override {
|
||||
// Currently not supported because changing certain options may cause
|
||||
// flush/compaction and/or write to MANIFEST.
|
||||
return Status::NotSupported("Not supported operation in secondary mode.");
|
||||
|
||||
@@ -5027,152 +5027,6 @@ TEST_P(DBMultiScanIteratorTest, AsyncPrefetchWithExternalFileIngestion) {
|
||||
ASSERT_EQ(total_keys, 400);
|
||||
iter.reset();
|
||||
}
|
||||
|
||||
TEST_P(DBMultiScanIteratorTest, StatisticsTest) {
|
||||
// Test that multi scan statistics are properly recorded
|
||||
auto options = CurrentOptions();
|
||||
options.statistics = CreateDBStatistics();
|
||||
// Use small block size to ensure multiple blocks
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.block_size = 256;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Create data across multiple blocks
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
std::stringstream ss;
|
||||
ss << std::setw(3) << std::setfill('0') << i;
|
||||
// Use larger values to ensure multiple blocks
|
||||
ASSERT_OK(Put("k" + ss.str(), std::string(100, 'v')));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Reset stats before multi scan
|
||||
ASSERT_OK(options.statistics->Reset());
|
||||
|
||||
// Set up two scan ranges
|
||||
std::vector<std::string> key_ranges({"k010", "k030", "k060", "k080"});
|
||||
ReadOptions ro;
|
||||
ro.fill_cache = GetParam();
|
||||
MultiScanArgs scan_options(BytewiseComparator());
|
||||
scan_options.insert(key_ranges[0], key_ranges[1]);
|
||||
scan_options.insert(key_ranges[2], key_ranges[3]);
|
||||
|
||||
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
|
||||
std::unique_ptr<MultiScan> iter =
|
||||
dbfull()->NewMultiScan(ro, cfh, scan_options);
|
||||
|
||||
// Iterate through all ranges
|
||||
int count = 0;
|
||||
try {
|
||||
for (auto range : *iter) {
|
||||
for (auto it : range) {
|
||||
(void)it;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
} catch (MultiScanException& ex) {
|
||||
ASSERT_NOK(ex.status());
|
||||
std::cerr << "Iterator returned status " << ex.what();
|
||||
abort();
|
||||
}
|
||||
ASSERT_EQ(count, 40); // 20 keys per range
|
||||
iter.reset();
|
||||
|
||||
// Check statistics
|
||||
// MULTISCAN_PREPARE_CALLS should be at least 1
|
||||
ASSERT_GE(TestGetTickerCount(options, MULTISCAN_PREPARE_CALLS), 1);
|
||||
|
||||
// MULTISCAN_PREPARE_ERRORS should be 0
|
||||
ASSERT_EQ(TestGetTickerCount(options, MULTISCAN_PREPARE_ERRORS), 0);
|
||||
|
||||
// MULTISCAN_SEEK_ERRORS should be 0
|
||||
ASSERT_EQ(TestGetTickerCount(options, MULTISCAN_SEEK_ERRORS), 0);
|
||||
|
||||
// Blocks should be prefetched or from cache
|
||||
uint64_t blocks_prefetched =
|
||||
TestGetTickerCount(options, MULTISCAN_BLOCKS_PREFETCHED);
|
||||
uint64_t blocks_from_cache =
|
||||
TestGetTickerCount(options, MULTISCAN_BLOCKS_FROM_CACHE);
|
||||
ASSERT_GT(blocks_prefetched + blocks_from_cache, 0);
|
||||
|
||||
// If blocks were prefetched, prefetch bytes and IO requests should be > 0
|
||||
if (blocks_prefetched > 0) {
|
||||
ASSERT_GT(TestGetTickerCount(options, MULTISCAN_PREFETCH_BYTES), 0);
|
||||
uint64_t io_requests = TestGetTickerCount(options, MULTISCAN_IO_REQUESTS);
|
||||
ASSERT_GT(io_requests, 0);
|
||||
ASSERT_LE(io_requests, blocks_prefetched);
|
||||
}
|
||||
|
||||
// Wasted blocks should be 0 since we iterated through everything
|
||||
ASSERT_EQ(TestGetTickerCount(options, MULTISCAN_PREFETCH_BLOCKS_WASTED), 0);
|
||||
}
|
||||
|
||||
TEST_P(DBMultiScanIteratorTest, StatisticsWastedBlocksTest) {
|
||||
// Test that wasted blocks are tracked when iteration is abandoned early
|
||||
auto options = CurrentOptions();
|
||||
options.statistics = CreateDBStatistics();
|
||||
// Use small block size to ensure multiple blocks
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.block_size = 256;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Create data across multiple blocks
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
std::stringstream ss;
|
||||
ss << std::setw(3) << std::setfill('0') << i;
|
||||
ASSERT_OK(Put("k" + ss.str(), std::string(100, 'v')));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Reset stats before multi scan
|
||||
ASSERT_OK(options.statistics->Reset());
|
||||
|
||||
// Set up a large scan range
|
||||
ReadOptions ro;
|
||||
ro.fill_cache = GetParam();
|
||||
MultiScanArgs scan_options(BytewiseComparator());
|
||||
scan_options.insert("k000", "k099");
|
||||
|
||||
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
|
||||
std::unique_ptr<MultiScan> iter =
|
||||
dbfull()->NewMultiScan(ro, cfh, scan_options);
|
||||
|
||||
// Only iterate through a few keys, then abandon
|
||||
int count = 0;
|
||||
try {
|
||||
for (auto range : *iter) {
|
||||
for (auto it : range) {
|
||||
(void)it;
|
||||
count++;
|
||||
if (count >= 5) {
|
||||
break; // Abandon iteration early
|
||||
}
|
||||
}
|
||||
if (count >= 5) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (MultiScanException& ex) {
|
||||
ASSERT_NOK(ex.status());
|
||||
std::cerr << "Iterator returned status " << ex.what();
|
||||
abort();
|
||||
}
|
||||
ASSERT_EQ(count, 5);
|
||||
|
||||
// Destroy iterator to trigger wasted blocks counting
|
||||
iter.reset();
|
||||
|
||||
uint64_t blocks_prefetched =
|
||||
TestGetTickerCount(options, MULTISCAN_BLOCKS_PREFETCHED);
|
||||
|
||||
// If blocks were prefetched, some should be wasted since we abandoned early
|
||||
if (blocks_prefetched > 1) {
|
||||
// We only read a few keys, so there should be wasted blocks
|
||||
ASSERT_GT(TestGetTickerCount(options, MULTISCAN_PREFETCH_BLOCKS_WASTED), 0);
|
||||
}
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -1689,46 +1689,6 @@ TEST_F(DBOptionsTest, SetOptionsNoManifestWrite) {
|
||||
ASSERT_EQ(Get("x"), "x");
|
||||
}
|
||||
|
||||
TEST_F(DBOptionsTest, SetOptionsMultipleColumnFamilies) {
|
||||
Options options;
|
||||
options.create_if_missing = true;
|
||||
options.env = CurrentOptions().env;
|
||||
options.disable_auto_compactions = true;
|
||||
Reopen(options);
|
||||
|
||||
// Create two additional column families
|
||||
CreateColumnFamilies({"cf1", "cf2"}, options);
|
||||
ReopenWithColumnFamilies({"default", "cf1", "cf2"}, options);
|
||||
|
||||
// Verify initial state - auto compaction should be disabled
|
||||
ASSERT_TRUE(dbfull()->GetOptions(handles_[0]).disable_auto_compactions);
|
||||
ASSERT_TRUE(dbfull()->GetOptions(handles_[1]).disable_auto_compactions);
|
||||
ASSERT_TRUE(dbfull()->GetOptions(handles_[2]).disable_auto_compactions);
|
||||
|
||||
// Set options on multiple column families at once
|
||||
ASSERT_OK(dbfull()->SetOptions({handles_[1], handles_[2]},
|
||||
{{"disable_auto_compactions", "false"}}));
|
||||
|
||||
ASSERT_TRUE(
|
||||
dbfull()->GetOptions(handles_[0]).disable_auto_compactions); // unchanged
|
||||
ASSERT_FALSE(
|
||||
dbfull()->GetOptions(handles_[1]).disable_auto_compactions); // changed
|
||||
ASSERT_FALSE(
|
||||
dbfull()->GetOptions(handles_[2]).disable_auto_compactions); // changed
|
||||
|
||||
std::unordered_map<ColumnFamilyHandle*,
|
||||
std::unordered_map<std::string, std::string>>
|
||||
options_map;
|
||||
options_map[handles_[0]] = {{"disable_auto_compactions", "false"}};
|
||||
options_map[handles_[1]] = {{"disable_auto_compactions", "true"}};
|
||||
options_map[handles_[2]] = {{"disable_auto_compactions", "true"}};
|
||||
ASSERT_OK(dbfull()->SetOptions(options_map));
|
||||
|
||||
ASSERT_FALSE(dbfull()->GetOptions(handles_[0]).disable_auto_compactions);
|
||||
ASSERT_TRUE(dbfull()->GetOptions(handles_[1]).disable_auto_compactions);
|
||||
ASSERT_TRUE(dbfull()->GetOptions(handles_[2]).disable_auto_compactions);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+10
-29
@@ -1519,14 +1519,16 @@ TEST_F(DBPropertiesTest, NeedCompactHintPersistentTest) {
|
||||
|
||||
// Excluded from RocksDB lite tests due to `GetPropertiesOfAllTables()` usage.
|
||||
TEST_F(DBPropertiesTest, BlockAddForCompressionSampling) {
|
||||
// Sampled compression requires at least one of the following four types.
|
||||
if (!Snappy_Supported() && !Zlib_Supported() && !LZ4_Supported() &&
|
||||
!ZSTD_Supported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.table_properties_collector_factories.emplace_back(
|
||||
std::make_shared<BlockCountingTablePropertiesCollectorFactory>());
|
||||
options.compression = kNoCompression;
|
||||
|
||||
bool fast_sampling_supported = Snappy_Supported() || LZ4_Supported();
|
||||
bool slow_sampling_supported = ZSTD_Supported() || Zlib_Supported();
|
||||
|
||||
for (bool sample_for_compression : {false, true}) {
|
||||
// For simplicity/determinism, sample 100% when enabled, or 0% when disabled
|
||||
@@ -1540,11 +1542,10 @@ TEST_F(DBPropertiesTest, BlockAddForCompressionSampling) {
|
||||
// L1_0 ["a", "b"]
|
||||
//
|
||||
// L0_0 was created by flush. L1_0 was created by compaction. Each file
|
||||
// contains one data block with enough data to be compressible.
|
||||
// contains one data block.
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
for (int j = 0; j < 50; ++j) {
|
||||
ASSERT_OK(Put(std::to_string(j), "thisismyvalue"));
|
||||
}
|
||||
ASSERT_OK(Put("a", "val"));
|
||||
ASSERT_OK(Put("b", "val"));
|
||||
ASSERT_OK(Flush());
|
||||
if (i == 1) {
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
@@ -1557,33 +1558,13 @@ TEST_F(DBPropertiesTest, BlockAddForCompressionSampling) {
|
||||
ASSERT_OK(db_->GetPropertiesOfAllTables(&file_to_props));
|
||||
ASSERT_EQ(2, file_to_props.size());
|
||||
for (const auto& file_and_props : file_to_props) {
|
||||
auto& props = *file_and_props.second;
|
||||
auto& user_props = props.user_collected_properties;
|
||||
auto& user_props = file_and_props.second->user_collected_properties;
|
||||
ASSERT_TRUE(user_props.find(BlockCountingTablePropertiesCollector::
|
||||
kNumSampledBlocksPropertyName) !=
|
||||
user_props.end());
|
||||
ASSERT_EQ(user_props.at(BlockCountingTablePropertiesCollector::
|
||||
kNumSampledBlocksPropertyName),
|
||||
std::to_string(sample_for_compression ? 1 : 0));
|
||||
if (sample_for_compression) {
|
||||
EXPECT_GT(props.fast_compression_estimated_data_size, 0);
|
||||
EXPECT_GT(props.slow_compression_estimated_data_size, 0);
|
||||
if (fast_sampling_supported) {
|
||||
EXPECT_LT(props.fast_compression_estimated_data_size,
|
||||
props.data_size);
|
||||
if (slow_sampling_supported) {
|
||||
EXPECT_LT(props.slow_compression_estimated_data_size,
|
||||
props.fast_compression_estimated_data_size);
|
||||
}
|
||||
}
|
||||
if (slow_sampling_supported) {
|
||||
EXPECT_LT(props.slow_compression_estimated_data_size,
|
||||
props.data_size);
|
||||
}
|
||||
} else {
|
||||
EXPECT_EQ(props.fast_compression_estimated_data_size, 0);
|
||||
EXPECT_EQ(props.slow_compression_estimated_data_size, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6544,9 +6544,6 @@ TEST_F(DBTest2, LastLevelStatistics) {
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
get_iostats_context()->Reset();
|
||||
IOStatsContext* iostats = get_iostats_context();
|
||||
|
||||
// generate 1 sst on level 0
|
||||
ASSERT_OK(Put("foo1", "bar"));
|
||||
ASSERT_OK(Put("bar", "bar"));
|
||||
@@ -6647,87 +6644,9 @@ TEST_F(DBTest2, LastLevelStatistics) {
|
||||
// Control
|
||||
ASSERT_NE(options.statistics->getTickerCount(LAST_LEVEL_READ_COUNT),
|
||||
options.statistics->getTickerCount(NON_LAST_LEVEL_READ_COUNT));
|
||||
|
||||
// Control: unknown temperature iostats should be zero since files have
|
||||
// explicit temperatures (mapped or written)
|
||||
EXPECT_EQ(
|
||||
iostats->file_io_stats_by_temperature.unknown_non_last_level_bytes_read,
|
||||
0);
|
||||
EXPECT_EQ(
|
||||
iostats->file_io_stats_by_temperature.unknown_non_last_level_read_count,
|
||||
0);
|
||||
EXPECT_EQ(
|
||||
iostats->file_io_stats_by_temperature.unknown_last_level_bytes_read, 0);
|
||||
EXPECT_EQ(
|
||||
iostats->file_io_stats_by_temperature.unknown_last_level_read_count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Test the iostats for files with Temperature::kUnknown that is not mapped
|
||||
// to another temperature. These stats are used to indicate which non-tiered
|
||||
// workloads are most promising for tiering (so this test doesn't set
|
||||
// temperatures).
|
||||
TEST_F(DBTest2, UnknownLastLevelStatistics) {
|
||||
Options options = CurrentOptions();
|
||||
options.statistics = CreateDBStatistics();
|
||||
BlockBasedTableOptions bbto;
|
||||
bbto.no_block_cache = true;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(bbto));
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
get_iostats_context()->Reset();
|
||||
IOStatsContext* iostats = get_iostats_context();
|
||||
|
||||
// Generate 1 sst file on level 0 with kUnknown temperature
|
||||
ASSERT_OK(Put("foo", "bar"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Read from the kUnknown file on non-last level
|
||||
ASSERT_EQ("bar", Get("foo"));
|
||||
|
||||
// Verify unknown_non_last_level stats are populated
|
||||
EXPECT_GT(
|
||||
iostats->file_io_stats_by_temperature.unknown_non_last_level_bytes_read,
|
||||
0);
|
||||
EXPECT_GT(
|
||||
iostats->file_io_stats_by_temperature.unknown_non_last_level_read_count,
|
||||
0);
|
||||
// No reads from last level yet
|
||||
EXPECT_EQ(iostats->file_io_stats_by_temperature.unknown_last_level_bytes_read,
|
||||
0);
|
||||
EXPECT_EQ(iostats->file_io_stats_by_temperature.unknown_last_level_read_count,
|
||||
0);
|
||||
|
||||
// Compact to the last level (level 6) explicitly using MoveFilesToLevel
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
MoveFilesToLevel(6);
|
||||
|
||||
// Reopen DB to ensure table cache is cleared and files are re-opened
|
||||
// with correct is_last_level flag
|
||||
Reopen(options);
|
||||
|
||||
// Reset iostats to measure only the following reads
|
||||
get_iostats_context()->Reset();
|
||||
|
||||
// Read from the file now on last level (still kUnknown since
|
||||
// last_level_temperature is not set)
|
||||
ASSERT_EQ("bar", Get("foo"));
|
||||
|
||||
// Verify unknown_last_level stats are populated
|
||||
EXPECT_GT(iostats->file_io_stats_by_temperature.unknown_last_level_bytes_read,
|
||||
0);
|
||||
EXPECT_GT(iostats->file_io_stats_by_temperature.unknown_last_level_read_count,
|
||||
0);
|
||||
// No new reads from non-last level
|
||||
EXPECT_EQ(
|
||||
iostats->file_io_stats_by_temperature.unknown_non_last_level_bytes_read,
|
||||
0);
|
||||
EXPECT_EQ(
|
||||
iostats->file_io_stats_by_temperature.unknown_non_last_level_read_count,
|
||||
0);
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, CheckpointFileTemperature) {
|
||||
class NoLinkTestFS : public FileTemperatureTestFS {
|
||||
using FileTemperatureTestFS::FileTemperatureTestFS;
|
||||
|
||||
+1
-1
@@ -1613,7 +1613,7 @@ TEST_F(DBWALTest, SyncWalPartialFailure) {
|
||||
return s;
|
||||
}
|
||||
|
||||
Atomic<uint32_t> syncs_before_failure_{UINT32_MAX};
|
||||
AcqRelAtomic<uint32_t> syncs_before_failure_{UINT32_MAX};
|
||||
|
||||
protected:
|
||||
class MyTestWritableFile : public FSWritableFileOwnerWrapper {
|
||||
|
||||
@@ -7,13 +7,9 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
|
||||
#include <set>
|
||||
|
||||
#include "db/column_family.h"
|
||||
#include "db/compaction/compaction.h"
|
||||
#include "db/db_test_util.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/sst_file_reader.h"
|
||||
#include "test_util/testutil.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -52,122 +48,6 @@ class TimestampCompatibleCompactionTest : public DBTestBase {
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// Helper to get all files with their level and timestamps
|
||||
std::vector<std::tuple<int, std::string, std::string>>
|
||||
GetAllFileTimestamps() {
|
||||
std::vector<std::tuple<int, std::string, std::string>> results;
|
||||
ColumnFamilyHandle* cfh = db_->DefaultColumnFamily();
|
||||
auto* cfd = static_cast_with_check<ColumnFamilyHandleImpl>(cfh)->cfd();
|
||||
auto* vstorage = cfd->current()->storage_info();
|
||||
|
||||
for (int level = 0; level < cfd->NumberLevels(); level++) {
|
||||
for (auto* file : vstorage->LevelFiles(level)) {
|
||||
results.emplace_back(level, file->min_timestamp, file->max_timestamp);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// Helper to compute overall min/max timestamps across all files
|
||||
// Returns {min_ts, max_ts} as uint64_t values
|
||||
// Asserts that all files have non-empty timestamps
|
||||
std::pair<uint64_t, uint64_t> GetOverallTimestampRange() {
|
||||
auto files = GetAllFileTimestamps();
|
||||
EXPECT_GE(files.size(), 1U);
|
||||
|
||||
uint64_t overall_min = UINT64_MAX;
|
||||
uint64_t overall_max = 0;
|
||||
for (const auto& [level, min_ts, max_ts] : files) {
|
||||
EXPECT_FALSE(min_ts.empty()) << "min_timestamp empty at level " << level;
|
||||
EXPECT_FALSE(max_ts.empty()) << "max_timestamp empty at level " << level;
|
||||
|
||||
if (!min_ts.empty() && !max_ts.empty()) {
|
||||
uint64_t file_min = DecodeFixed64(min_ts.data());
|
||||
uint64_t file_max = DecodeFixed64(max_ts.data());
|
||||
overall_min = std::min(overall_min, file_min);
|
||||
overall_max = std::max(overall_max, file_max);
|
||||
}
|
||||
}
|
||||
return {overall_min, overall_max};
|
||||
}
|
||||
|
||||
// Helper to verify timestamp range matches expected values, including after
|
||||
// reopen
|
||||
void VerifyTimestampRangeWithPersistence(const Options& options,
|
||||
uint64_t expected_min,
|
||||
uint64_t expected_max) {
|
||||
// Verify before reopen
|
||||
auto [min_ts, max_ts] = GetOverallTimestampRange();
|
||||
ASSERT_EQ(expected_min, min_ts);
|
||||
ASSERT_EQ(expected_max, max_ts);
|
||||
|
||||
size_t file_count_before = GetAllFileTimestamps().size();
|
||||
|
||||
// Verify manifest persistence by reopening
|
||||
Reopen(options);
|
||||
|
||||
// Verify after reopen
|
||||
auto [reopened_min_ts, reopened_max_ts] = GetOverallTimestampRange();
|
||||
ASSERT_EQ(expected_min, reopened_min_ts);
|
||||
ASSERT_EQ(expected_max, reopened_max_ts);
|
||||
ASSERT_EQ(file_count_before, GetAllFileTimestamps().size());
|
||||
}
|
||||
|
||||
// Helper to create common options for UDT tests with level compaction
|
||||
Options CreateTimestampOptions(bool disable_auto_compactions = false) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.num_levels = 4;
|
||||
options.persist_user_defined_timestamps = true;
|
||||
options.comparator = test::BytewiseComparatorWithU64TsWrapper();
|
||||
options.disable_auto_compactions = disable_auto_compactions;
|
||||
return options;
|
||||
}
|
||||
|
||||
// Helper to write test data with alternating timestamps in a range
|
||||
// Writes keys [start_key, end_key) with timestamps alternating between
|
||||
// min_ts and max_ts
|
||||
void WriteDataWithTimestampRange(int start_key, int end_key, uint64_t min_ts,
|
||||
uint64_t max_ts) {
|
||||
std::string ts_buf;
|
||||
for (int i = start_key; i < end_key; i++) {
|
||||
ts_buf.clear();
|
||||
uint64_t ts = (i % 2 == 0) ? min_ts : max_ts;
|
||||
PutFixed64(&ts_buf, ts);
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(i), ts_buf,
|
||||
"value" + std::to_string(i)));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to check if any file has the expected timestamp range
|
||||
bool HasFileWithTimestampRange(uint64_t expected_min, uint64_t expected_max) {
|
||||
auto file_timestamps = GetAllFileTimestamps();
|
||||
for (const auto& [level, min_ts, max_ts] : file_timestamps) {
|
||||
if (!min_ts.empty() && !max_ts.empty()) {
|
||||
uint64_t file_min = DecodeFixed64(min_ts.data());
|
||||
uint64_t file_max = DecodeFixed64(max_ts.data());
|
||||
if (file_min == expected_min && file_max == expected_max) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Helper to verify data is readable with a given timestamp
|
||||
void VerifyDataReadable(int key, const std::string& expected_value,
|
||||
uint64_t read_ts) {
|
||||
std::string value;
|
||||
std::string ts_buf;
|
||||
PutFixed64(&ts_buf, read_ts);
|
||||
ReadOptions read_opts;
|
||||
Slice ts_slice(ts_buf);
|
||||
read_opts.timestamp = &ts_slice;
|
||||
ASSERT_OK(db_->Get(read_opts, Key(key), &value));
|
||||
ASSERT_EQ(expected_value, value);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(TimestampCompatibleCompactionTest, UserKeyCrossFileBoundary) {
|
||||
@@ -464,385 +344,6 @@ TEST_F(TimestampCompatibleCompactionTest, EmptyCompactionOutput) {
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
}
|
||||
|
||||
TEST_F(TimestampCompatibleCompactionTest, SeqnoZeroingWithUDT) {
|
||||
// This test validates that seqno is only zeroed when the timestamp is older
|
||||
// than full_history_ts_low_. Before the fix, seqno was incorrectly zeroed
|
||||
// even when UDT was enabled but timestamp wasn't old enough.
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.comparator = test::BytewiseComparatorWithU64TsWrapper();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Track seqno zeroing events and which keys are zeroed
|
||||
std::set<std::string> zeroed_keys;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionIterator::PrepareOutput:ZeroingSeq", [&](void* arg) {
|
||||
auto* ikey = static_cast<ParsedInternalKey*>(arg);
|
||||
ASSERT_EQ(0, ikey->sequence);
|
||||
// Extract user key without timestamp (last 8 bytes)
|
||||
Slice user_key_with_ts = ikey->user_key;
|
||||
std::string user_key =
|
||||
user_key_with_ts.ToString().substr(0, user_key_with_ts.size() - 8);
|
||||
zeroed_keys.insert(user_key);
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Case 1: Test that seqno is NOT zeroed when full_history_ts_low is not set
|
||||
// Write a key with timestamp 100
|
||||
std::string ts_str = Timestamp(100);
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key1", ts_str, "value1"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
zeroed_keys.clear();
|
||||
{
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
}
|
||||
// With UDT enabled and no full_history_ts_low, seqno should NOT be zeroed
|
||||
ASSERT_TRUE(zeroed_keys.empty());
|
||||
|
||||
// Case 2: Test that seqno IS zeroed when timestamp < full_history_ts_low
|
||||
// Write a new key with timestamp 200
|
||||
ts_str = Timestamp(200);
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key2", ts_str, "value2"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
zeroed_keys.clear();
|
||||
{
|
||||
// Set full_history_ts_low to 300, so ts < 300 should be zeroed
|
||||
std::string full_history_ts_low = Timestamp(300);
|
||||
Slice ts_slice = full_history_ts_low;
|
||||
CompactRangeOptions cro;
|
||||
cro.full_history_ts_low = &ts_slice;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
}
|
||||
// key1 (ts=100) and key2 (ts=200) both have ts < 300, so both should be
|
||||
// zeroed
|
||||
ASSERT_EQ(2u, zeroed_keys.size());
|
||||
ASSERT_TRUE(zeroed_keys.count("key1") > 0);
|
||||
ASSERT_TRUE(zeroed_keys.count("key2") > 0);
|
||||
|
||||
// Case 3: Write a new key with timestamp >= full_history_ts_low
|
||||
// and verify it is NOT zeroed while old keys are re-zeroed
|
||||
ts_str = Timestamp(500);
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key3", ts_str, "value3"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
zeroed_keys.clear();
|
||||
{
|
||||
// Set full_history_ts_low to 400
|
||||
// key1 (ts=100) and key2 (ts=200) have ts < 400, will be re-processed
|
||||
// key3 (ts=500) has ts >= 400, should NOT be zeroed
|
||||
std::string full_history_ts_low = Timestamp(400);
|
||||
Slice ts_slice = full_history_ts_low;
|
||||
CompactRangeOptions cro;
|
||||
cro.full_history_ts_low = &ts_slice;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
}
|
||||
// key3 should NOT appear in zeroed_keys since ts=500 >= 400
|
||||
ASSERT_TRUE(zeroed_keys.count("key3") == 0);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// Verify data is still readable
|
||||
std::string value;
|
||||
ts_str = Timestamp(600);
|
||||
Slice read_ts = ts_str;
|
||||
ReadOptions read_opts;
|
||||
read_opts.timestamp = &read_ts;
|
||||
ASSERT_OK(db_->Get(read_opts, "key1", &value));
|
||||
ASSERT_EQ("value1", value);
|
||||
ASSERT_OK(db_->Get(read_opts, "key2", &value));
|
||||
ASSERT_EQ("value2", value);
|
||||
ASSERT_OK(db_->Get(read_opts, "key3", &value));
|
||||
ASSERT_EQ("value3", value);
|
||||
}
|
||||
|
||||
// Test that files with max_timestamp >= full_history_ts_low are not marked
|
||||
// for bottommost compaction, which prevents infinite compaction loops.
|
||||
TEST_F(TimestampCompatibleCompactionTest,
|
||||
BottommostCompactionRespectsFullHistoryTsLow) {
|
||||
Options options = CreateTimestampOptions();
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Write some data with timestamps 100-199
|
||||
std::string ts_buf;
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ts_buf.clear();
|
||||
PutFixed64(&ts_buf, 100 + i);
|
||||
ASSERT_OK(
|
||||
db_->Put(WriteOptions(), Key(i), ts_buf, "value" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Compact to the bottommost level
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
// Set full_history_ts_low to 150 - files with max_ts >= 150 should NOT be
|
||||
// marked for bottommost compaction since seqno cannot be zeroed
|
||||
ts_buf.clear();
|
||||
PutFixed64(&ts_buf, 150);
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(db_->DefaultColumnFamily(), ts_buf));
|
||||
|
||||
// Release a snapshot to potentially trigger bottommost file marking
|
||||
// but files should NOT be marked because max_ts (199) >= full_history_ts_low
|
||||
// (150)
|
||||
const Snapshot* snap = db_->GetSnapshot();
|
||||
db_->ReleaseSnapshot(snap);
|
||||
|
||||
// Wait for any scheduled compactions - should complete without infinite loop
|
||||
// Use a reasonable timeout to detect infinite loops
|
||||
WaitForCompactOptions wfc_options;
|
||||
wfc_options.timeout = std::chrono::microseconds(5000000); // 5 seconds
|
||||
Status s = dbfull()->WaitForCompact(wfc_options);
|
||||
// Should succeed without timeout (no infinite compaction loop)
|
||||
ASSERT_TRUE(s.ok() || s.IsTimedOut());
|
||||
if (s.IsTimedOut()) {
|
||||
// If timeout, the fix is not working - this should not happen
|
||||
FAIL() << "WaitForCompact timed out - possible infinite compaction loop";
|
||||
}
|
||||
|
||||
// Now set full_history_ts_low beyond max timestamp in the file (200+)
|
||||
// This should allow the file to be properly marked and compacted
|
||||
ts_buf.clear();
|
||||
PutFixed64(&ts_buf, 300);
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(db_->DefaultColumnFamily(), ts_buf));
|
||||
|
||||
// Trigger another snapshot release to potentially mark files
|
||||
snap = db_->GetSnapshot();
|
||||
db_->ReleaseSnapshot(snap);
|
||||
|
||||
// Now compaction should clean up the file.
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
}
|
||||
|
||||
// Test that files are NOT marked for bottommost compaction when UDT is enabled
|
||||
// and full_history_ts_low has never been set (empty).
|
||||
TEST_F(TimestampCompatibleCompactionTest,
|
||||
BottommostCompactionSkipsWhenFullHistoryTsLowNotSet) {
|
||||
Options options = CreateTimestampOptions();
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Write some data with timestamps 100-199
|
||||
std::string ts_buf;
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ts_buf.clear();
|
||||
PutFixed64(&ts_buf, 100 + i);
|
||||
ASSERT_OK(
|
||||
db_->Put(WriteOptions(), Key(i), ts_buf, "value" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Compact to the bottommost level without setting full_history_ts_low
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
// Verify files have valid max_timestamp
|
||||
auto file_timestamps = GetAllFileTimestamps();
|
||||
ASSERT_GE(file_timestamps.size(), 1U);
|
||||
for (const auto& [level, min_ts, max_ts] : file_timestamps) {
|
||||
ASSERT_FALSE(max_ts.empty()) << "max_timestamp should not be empty";
|
||||
}
|
||||
|
||||
// full_history_ts_low is NOT set (empty), so files should NOT be marked
|
||||
// for bottommost compaction even after releasing a snapshot.
|
||||
// This tests the branch: if (full_history_ts_low.empty()) { continue; }
|
||||
const Snapshot* snap = db_->GetSnapshot();
|
||||
db_->ReleaseSnapshot(snap);
|
||||
|
||||
// Wait for any scheduled compactions
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
// Now set full_history_ts_low to a value > max_timestamp (199) in the file
|
||||
// This should allow the file to be properly marked and compacted
|
||||
ts_buf.clear();
|
||||
PutFixed64(&ts_buf, 300);
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(db_->DefaultColumnFamily(), ts_buf));
|
||||
|
||||
// Trigger another snapshot release to potentially mark files
|
||||
snap = db_->GetSnapshot();
|
||||
db_->ReleaseSnapshot(snap);
|
||||
|
||||
// Now compaction should be able to proceed since full_history_ts_low is set
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
// Verify data is still readable
|
||||
VerifyDataReadable(0, "value0", 250);
|
||||
}
|
||||
|
||||
// Test that ingested SST files created with UDT have their min/max timestamps
|
||||
// properly extracted from table properties and populated in FileMetaData.
|
||||
// This verifies the fix in external_sst_file_ingestion_job.cc that calls
|
||||
// ExtractTimestampFromTableProperties after creating FileMetaData.
|
||||
TEST_F(TimestampCompatibleCompactionTest,
|
||||
IngestedFileTimestampsExtractedFromTableProperties) {
|
||||
Options options = CreateTimestampOptions();
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Create an SST file WITH timestamps using SstFileWriter
|
||||
std::string sst_file = dbname_ + "/ingested_udt_file.sst";
|
||||
const uint64_t kMinTs = 100;
|
||||
const uint64_t kMaxTs = 200;
|
||||
|
||||
{
|
||||
SstFileWriter sst_file_writer(EnvOptions(), options);
|
||||
ASSERT_OK(sst_file_writer.Open(sst_file));
|
||||
|
||||
std::string ts_buf;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
// Alternate between min and max timestamps
|
||||
uint64_t ts = (i % 2 == 0) ? kMinTs : kMaxTs;
|
||||
ts_buf.clear();
|
||||
PutFixed64(&ts_buf, ts);
|
||||
// SstFileWriter with UDT comparator requires key with timestamp
|
||||
ASSERT_OK(
|
||||
sst_file_writer.Put(Key(i), ts_buf, "value" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(sst_file_writer.Finish());
|
||||
}
|
||||
|
||||
// Verify the SST file has timestamp properties before ingestion
|
||||
{
|
||||
std::unique_ptr<SstFileReader> reader(new SstFileReader(options));
|
||||
ASSERT_OK(reader->Open(sst_file));
|
||||
auto props = reader->GetTableProperties();
|
||||
auto& user_collected = props->user_collected_properties;
|
||||
ASSERT_TRUE(user_collected.find("rocksdb.timestamp_min") !=
|
||||
user_collected.end())
|
||||
<< "SST file should have rocksdb.timestamp_min property";
|
||||
ASSERT_TRUE(user_collected.find("rocksdb.timestamp_max") !=
|
||||
user_collected.end())
|
||||
<< "SST file should have rocksdb.timestamp_max property";
|
||||
}
|
||||
|
||||
// Ingest the SST file
|
||||
IngestExternalFileOptions ifo;
|
||||
ifo.move_files = false;
|
||||
ASSERT_OK(db_->IngestExternalFile({sst_file}, ifo));
|
||||
|
||||
// Verify the ingested file has proper timestamps in FileMetaData
|
||||
ASSERT_TRUE(HasFileWithTimestampRange(kMinTs, kMaxTs))
|
||||
<< "Ingested file should have min_timestamp=" << kMinTs
|
||||
<< " and max_timestamp=" << kMaxTs << " in FileMetaData";
|
||||
|
||||
// Verify timestamps persist after reopen
|
||||
Reopen(options);
|
||||
|
||||
ASSERT_TRUE(HasFileWithTimestampRange(kMinTs, kMaxTs))
|
||||
<< "Ingested file timestamps should persist after reopen";
|
||||
|
||||
// Verify data is readable
|
||||
VerifyDataReadable(0, "value0", kMaxTs);
|
||||
|
||||
// Clean up
|
||||
ASSERT_OK(env_->DeleteFile(sst_file));
|
||||
}
|
||||
|
||||
// Test that min/max timestamps are correctly tracked in FileMetaData and
|
||||
// persisted in the manifest during flush.
|
||||
TEST_F(TimestampCompatibleCompactionTest, TimestampRangePersistenceFlush) {
|
||||
Options options = CreateTimestampOptions();
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Expected timestamp range
|
||||
const uint64_t kMinTs = 100;
|
||||
const uint64_t kMaxTs = 200;
|
||||
|
||||
// Write data with specific timestamp range
|
||||
WriteDataWithTimestampRange(0, 50, kMinTs, kMaxTs);
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// First verify table properties have the timestamps
|
||||
// (this confirms TimestampTablePropertiesCollector is working)
|
||||
TablePropertiesCollection props;
|
||||
ASSERT_OK(db_->GetPropertiesOfAllTables(&props));
|
||||
ASSERT_EQ(1U, props.size());
|
||||
for (const auto& item : props) {
|
||||
auto& user_collected = item.second->user_collected_properties;
|
||||
ASSERT_TRUE(user_collected.find("rocksdb.timestamp_min") !=
|
||||
user_collected.end());
|
||||
ASSERT_TRUE(user_collected.find("rocksdb.timestamp_max") !=
|
||||
user_collected.end());
|
||||
// Verify the collected timestamps match expected values
|
||||
std::string collected_min_ts = user_collected.at("rocksdb.timestamp_min");
|
||||
std::string collected_max_ts = user_collected.at("rocksdb.timestamp_max");
|
||||
ASSERT_EQ(kMinTs, DecodeFixed64(collected_min_ts.data()));
|
||||
ASSERT_EQ(kMaxTs, DecodeFixed64(collected_max_ts.data()));
|
||||
}
|
||||
|
||||
// Verify FileMetaData timestamps and persistence through reopen
|
||||
VerifyTimestampRangeWithPersistence(options, kMinTs, kMaxTs);
|
||||
|
||||
// Verify we can still read the data
|
||||
VerifyDataReadable(0, "value0", kMaxTs);
|
||||
}
|
||||
|
||||
// Test that min/max timestamps are correctly merged during compaction
|
||||
// and persisted in the manifest.
|
||||
TEST_F(TimestampCompatibleCompactionTest, TimestampRangePersistenceCompaction) {
|
||||
Options options = CreateTimestampOptions(true /* disable_auto_compactions */);
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Create multiple L0 files with different timestamp ranges
|
||||
// File 1: timestamps 100-150
|
||||
const uint64_t kFile1MinTs = 100;
|
||||
const uint64_t kFile1MaxTs = 150;
|
||||
WriteDataWithTimestampRange(0, 10, kFile1MinTs, kFile1MaxTs);
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// File 2: timestamps 50-80 (earlier range)
|
||||
const uint64_t kFile2MinTs = 50;
|
||||
const uint64_t kFile2MaxTs = 80;
|
||||
WriteDataWithTimestampRange(10, 20, kFile2MinTs, kFile2MaxTs);
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// File 3: timestamps 200-300 (later range)
|
||||
const uint64_t kFile3MinTs = 200;
|
||||
const uint64_t kFile3MaxTs = 300;
|
||||
WriteDataWithTimestampRange(20, 30, kFile3MinTs, kFile3MaxTs);
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Expected combined range: min=50, max=300
|
||||
const uint64_t kExpectedMinTs = 50;
|
||||
const uint64_t kExpectedMaxTs = 300;
|
||||
|
||||
// Verify we have 3 L0 files before compaction with valid timestamps
|
||||
auto files_before = GetAllFileTimestamps();
|
||||
ASSERT_EQ(3U, files_before.size());
|
||||
for (const auto& [level, min_ts, max_ts] : files_before) {
|
||||
ASSERT_EQ(0, level); // All files should be in L0
|
||||
ASSERT_FALSE(min_ts.empty());
|
||||
ASSERT_FALSE(max_ts.empty());
|
||||
}
|
||||
|
||||
// Trigger compaction
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
// Verify timestamp range and persistence through reopen
|
||||
VerifyTimestampRangeWithPersistence(options, kExpectedMinTs, kExpectedMaxTs);
|
||||
|
||||
// Verify data is still readable
|
||||
VerifyDataReadable(0, "value0", kExpectedMaxTs);
|
||||
VerifyDataReadable(15, "value15", kExpectedMaxTs);
|
||||
VerifyDataReadable(25, "value25", kExpectedMaxTs);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+3
-3
@@ -741,7 +741,7 @@ TEST_P(DBWriteTest, LockWALConcurrentRecursive) {
|
||||
ExternalSstFileInfo external_info;
|
||||
ASSERT_OK(sst_file_writer.Finish(&external_info));
|
||||
}
|
||||
Atomic<bool> parallel_ingest_completed{false};
|
||||
AcqRelAtomic<bool> parallel_ingest_completed{false};
|
||||
port::Thread parallel_ingest{[&]() {
|
||||
IngestExternalFileOptions ingest_opts;
|
||||
ingest_opts.move_files = true; // faster than copy
|
||||
@@ -750,7 +750,7 @@ TEST_P(DBWriteTest, LockWALConcurrentRecursive) {
|
||||
parallel_ingest_completed.Store(true);
|
||||
}};
|
||||
|
||||
Atomic<bool> flush_completed{false};
|
||||
AcqRelAtomic<bool> flush_completed{false};
|
||||
port::Thread parallel_flush{[&]() {
|
||||
FlushOptions flush_opts;
|
||||
// NB: Flush with wait=false case is tested above in LockWALInEffect
|
||||
@@ -762,7 +762,7 @@ TEST_P(DBWriteTest, LockWALConcurrentRecursive) {
|
||||
flush_completed.Store(true);
|
||||
}};
|
||||
|
||||
Atomic<bool> parallel_put_completed{false};
|
||||
AcqRelAtomic<bool> parallel_put_completed{false};
|
||||
port::Thread parallel_put{[&]() {
|
||||
// This can make certain failure scenarios more likely:
|
||||
// sleep(1);
|
||||
|
||||
+9
-11
@@ -158,17 +158,15 @@ Status UpdateManifestForFilesState(
|
||||
// Current state inconsistent with manifest
|
||||
++files_updated;
|
||||
edit.DeleteFile(level, number);
|
||||
edit.AddFile(level, lf->fd.GetNumber(), lf->fd.GetPathId(),
|
||||
lf->fd.GetFileSize(), lf->smallest, lf->largest,
|
||||
lf->fd.smallest_seqno, lf->fd.largest_seqno,
|
||||
lf->marked_for_compaction, temp,
|
||||
lf->oldest_blob_file_number,
|
||||
lf->oldest_ancester_time, lf->file_creation_time,
|
||||
lf->epoch_number, lf->file_checksum,
|
||||
lf->file_checksum_func_name, lf->unique_id,
|
||||
lf->compensated_range_deletion_size, lf->tail_size,
|
||||
lf->user_defined_timestamps_persisted,
|
||||
lf->min_timestamp, lf->max_timestamp);
|
||||
edit.AddFile(
|
||||
level, number, lf->fd.GetPathId(), lf->fd.GetFileSize(),
|
||||
lf->smallest, lf->largest, lf->fd.smallest_seqno,
|
||||
lf->fd.largest_seqno, lf->marked_for_compaction, temp,
|
||||
lf->oldest_blob_file_number, lf->oldest_ancester_time,
|
||||
lf->file_creation_time, lf->epoch_number, lf->file_checksum,
|
||||
lf->file_checksum_func_name, lf->unique_id,
|
||||
lf->compensated_range_deletion_size, lf->tail_size,
|
||||
lf->user_defined_timestamps_persisted);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "db/builder.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/version_edit.h"
|
||||
#include "file/file_util.h"
|
||||
@@ -300,18 +299,6 @@ Status ExternalSstFileIngestionJob::Prepare(
|
||||
// ingestion.
|
||||
// TODO: plumb Env::IOActivity
|
||||
ReadOptions ro;
|
||||
// Pass user-provided checksums through FileOptions when available.
|
||||
// The caller may not have provided checksums at all (empty vectors),
|
||||
// so we guard with a bounds check.
|
||||
FileOptions fopts;
|
||||
if (i < files_checksums.size()) {
|
||||
fopts.file_checksum = files_checksums[i];
|
||||
}
|
||||
if (i < files_checksum_func_names.size()) {
|
||||
fopts.file_checksum_func_name = files_checksum_func_names[i];
|
||||
} else {
|
||||
fopts.file_checksum_func_name = kNoFileChecksumFuncName;
|
||||
}
|
||||
IOStatus io_s = GenerateOneFileChecksum(
|
||||
fs_.get(), files_to_ingest_[i].internal_file_path,
|
||||
db_options_.file_checksum_gen_factory.get(),
|
||||
@@ -320,7 +307,7 @@ Status ExternalSstFileIngestionJob::Prepare(
|
||||
ingestion_options_.verify_checksums_readahead_size,
|
||||
db_options_.allow_mmap_reads, io_tracer_,
|
||||
db_options_.rate_limiter.get(), ro, db_options_.stats,
|
||||
db_options_.clock, fopts);
|
||||
db_options_.clock);
|
||||
if (!io_s.ok()) {
|
||||
status = io_s;
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
@@ -712,13 +699,9 @@ Status ExternalSstFileIngestionJob::AssignLevelsForOneBatch(
|
||||
? kReservedEpochNumberForFileIngestedBehind
|
||||
: cfd_->NewEpochNumber(), // orders files ingested to L0
|
||||
file->file_checksum, file->file_checksum_func_name, file->unique_id, 0,
|
||||
tail_size, file->user_defined_timestamps_persisted, "", "");
|
||||
tail_size, file->user_defined_timestamps_persisted);
|
||||
f_metadata.temperature = file->file_temperature;
|
||||
f_metadata.marked_for_compaction = marked_for_compaction;
|
||||
// Extract min/max timestamps from table properties for UDT support.
|
||||
// This ensures ingested files have proper timestamp ranges in FileMetaData,
|
||||
// similar to files created by flush and compaction.
|
||||
ExtractTimestampFromTableProperties(file->table_properties, &f_metadata);
|
||||
edit_.AddFile(file->picked_level, f_metadata);
|
||||
|
||||
*batch_uppermost_level =
|
||||
@@ -1494,15 +1477,13 @@ IOStatus ExternalSstFileIngestionJob::GenerateChecksumForIngestedFile(
|
||||
// TODO: rate limit file reads for checksum calculation during file ingestion.
|
||||
// TODO: plumb Env::IOActivity
|
||||
ReadOptions ro;
|
||||
FileOptions gen_fopts;
|
||||
gen_fopts.file_checksum_func_name = kNoFileChecksumFuncName;
|
||||
IOStatus io_s = GenerateOneFileChecksum(
|
||||
fs_.get(), file_to_ingest->internal_file_path,
|
||||
db_options_.file_checksum_gen_factory.get(), requested_checksum_func_name,
|
||||
&file_checksum, &file_checksum_func_name,
|
||||
ingestion_options_.verify_checksums_readahead_size,
|
||||
db_options_.allow_mmap_reads, io_tracer_, db_options_.rate_limiter.get(),
|
||||
ro, db_options_.stats, db_options_.clock, gen_fopts);
|
||||
ro, db_options_.stats, db_options_.clock);
|
||||
if (!io_s.ok()) {
|
||||
ROCKS_LOG_WARN(
|
||||
db_options_.info_log, "Failed to generate checksum for %s: %s",
|
||||
|
||||
+1
-2
@@ -1095,8 +1095,7 @@ Status FlushJob::WriteLevel0Table() {
|
||||
meta_.file_creation_time, meta_.epoch_number,
|
||||
meta_.file_checksum, meta_.file_checksum_func_name,
|
||||
meta_.unique_id, meta_.compensated_range_deletion_size,
|
||||
meta_.tail_size, meta_.user_defined_timestamps_persisted,
|
||||
meta_.min_timestamp, meta_.max_timestamp);
|
||||
meta_.tail_size, meta_.user_defined_timestamps_persisted);
|
||||
edit_->SetBlobFileAdditions(std::move(blob_file_additions));
|
||||
}
|
||||
// Piggyback FlushJobInfo on the first first flushed memtable.
|
||||
|
||||
@@ -310,10 +310,8 @@ Status ImportColumnFamilyJob::GetIngestedFileInfo(
|
||||
std::unique_ptr<FSRandomAccessFile> sst_file;
|
||||
std::unique_ptr<RandomAccessFileReader> sst_file_reader;
|
||||
|
||||
FileOptions fo{env_options_};
|
||||
fo.file_checksum = file_meta.file_checksum;
|
||||
fo.file_checksum_func_name = file_meta.file_checksum_func_name;
|
||||
status = fs_->NewRandomAccessFile(external_file, fo, &sst_file, nullptr);
|
||||
status =
|
||||
fs_->NewRandomAccessFile(external_file, env_options_, &sst_file, nullptr);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
+10
-10
@@ -704,17 +704,17 @@ class Repairer {
|
||||
VersionEdit dummy_edit;
|
||||
for (const auto* table : cf_id_and_tables.second) {
|
||||
// TODO(opt): separate out into multiple levels
|
||||
const auto& meta = table->meta;
|
||||
dummy_edit.AddFile(
|
||||
0, meta.fd.GetNumber(), meta.fd.GetPathId(), meta.fd.GetFileSize(),
|
||||
meta.smallest, meta.largest, meta.fd.smallest_seqno,
|
||||
meta.fd.largest_seqno, meta.marked_for_compaction, meta.temperature,
|
||||
meta.oldest_blob_file_number, meta.oldest_ancester_time,
|
||||
meta.file_creation_time, meta.epoch_number, meta.file_checksum,
|
||||
meta.file_checksum_func_name, meta.unique_id,
|
||||
meta.compensated_range_deletion_size, meta.tail_size,
|
||||
meta.user_defined_timestamps_persisted, meta.min_timestamp,
|
||||
meta.max_timestamp);
|
||||
0, table->meta.fd.GetNumber(), table->meta.fd.GetPathId(),
|
||||
table->meta.fd.GetFileSize(), table->meta.smallest,
|
||||
table->meta.largest, table->meta.fd.smallest_seqno,
|
||||
table->meta.fd.largest_seqno, table->meta.marked_for_compaction,
|
||||
table->meta.temperature, table->meta.oldest_blob_file_number,
|
||||
table->meta.oldest_ancester_time, table->meta.file_creation_time,
|
||||
table->meta.epoch_number, table->meta.file_checksum,
|
||||
table->meta.file_checksum_func_name, table->meta.unique_id,
|
||||
table->meta.compensated_range_deletion_size, table->meta.tail_size,
|
||||
table->meta.user_defined_timestamps_persisted);
|
||||
}
|
||||
s = dummy_version_builder.Apply(&dummy_edit);
|
||||
if (s.ok()) {
|
||||
|
||||
+2
-3
@@ -98,8 +98,6 @@ Status TableCache::GetTableReader(
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
FileOptions fopts = file_options;
|
||||
fopts.temperature = file_temperature;
|
||||
fopts.file_checksum = file_meta.file_checksum;
|
||||
fopts.file_checksum_func_name = file_meta.file_checksum_func_name;
|
||||
Status s = PrepareIOFromReadOptions(ro, ioptions_.clock, fopts.io_options);
|
||||
TEST_SYNC_POINT_CALLBACK("TableCache::GetTableReader:BeforeOpenFile",
|
||||
const_cast<Status*>(&s));
|
||||
@@ -115,7 +113,8 @@ Status TableCache::GetTableReader(
|
||||
Status temp_s =
|
||||
PrepareIOFromReadOptions(ro, ioptions_.clock, fopts.io_options);
|
||||
if (temp_s.ok()) {
|
||||
temp_s = ioptions_.fs->NewRandomAccessFile(fname, fopts, &file, nullptr);
|
||||
temp_s = ioptions_.fs->NewRandomAccessFile(fname, file_options, &file,
|
||||
nullptr);
|
||||
}
|
||||
if (temp_s.ok()) {
|
||||
RecordTick(ioptions_.stats, NO_FILE_OPENS);
|
||||
|
||||
@@ -76,8 +76,7 @@ class VersionBuilderTest : public testing::Test {
|
||||
oldest_blob_file_number, kUnknownOldestAncesterTime,
|
||||
kUnknownFileCreationTime, epoch_number, kUnknownFileChecksum,
|
||||
kUnknownFileChecksumFuncName, kNullUniqueId64x2, 0, 0,
|
||||
/* user_defined_timestamps_persisted */ true, /* min timestamp */ "",
|
||||
/* max timestamp */ "");
|
||||
/* user_defined_timestamps_persisted */ true);
|
||||
f->compensated_file_size = file_size;
|
||||
f->num_entries = num_entries;
|
||||
f->num_deletions = num_deletions;
|
||||
|
||||
@@ -304,15 +304,6 @@ void VersionEdit::EncodeToNewFile4(const FileMetaData& f, int level,
|
||||
char p = static_cast<char>(0);
|
||||
PutLengthPrefixedSlice(dst, Slice(&p, 1));
|
||||
}
|
||||
// Encode min/max timestamp if they are non-empty
|
||||
if (!f.min_timestamp.empty()) {
|
||||
PutVarint32(dst, NewFileCustomTag::kMinTimestamp);
|
||||
PutLengthPrefixedSlice(dst, Slice(f.min_timestamp));
|
||||
}
|
||||
if (!f.max_timestamp.empty()) {
|
||||
PutVarint32(dst, NewFileCustomTag::kMaxTimestamp);
|
||||
PutLengthPrefixedSlice(dst, Slice(f.max_timestamp));
|
||||
}
|
||||
TEST_SYNC_POINT_CALLBACK("VersionEdit::EncodeTo:NewFile4:CustomizeFields",
|
||||
dst);
|
||||
|
||||
@@ -452,12 +443,6 @@ const char* VersionEdit::DecodeNewFile4From(Slice* input, int& max_level,
|
||||
}
|
||||
f.user_defined_timestamps_persisted = (field[0] == 1);
|
||||
break;
|
||||
case kMinTimestamp:
|
||||
f.min_timestamp = field.ToString();
|
||||
break;
|
||||
case kMaxTimestamp:
|
||||
f.max_timestamp = field.ToString();
|
||||
break;
|
||||
default:
|
||||
if ((custom_tag & kCustomTagNonSafeIgnoreMask) != 0) {
|
||||
// Should not proceed if cannot understand it
|
||||
|
||||
+11
-26
@@ -276,14 +276,6 @@ struct FileMetaData {
|
||||
// false, it's explicitly written to Manifest.
|
||||
bool user_defined_timestamps_persisted = true;
|
||||
|
||||
// Minimum user-defined timestamp in the file. Empty if no UDT or unknown.
|
||||
// This is populated from the table properties "rocksdb.timestamp_min".
|
||||
std::string min_timestamp;
|
||||
|
||||
// Maximum user-defined timestamp in the file. Empty if no UDT or unknown.
|
||||
// This is populated from the table properties "rocksdb.timestamp_max".
|
||||
std::string max_timestamp;
|
||||
|
||||
FileMetaData() = default;
|
||||
|
||||
FileMetaData(uint64_t file, uint32_t file_path_id, uint64_t file_size,
|
||||
@@ -296,9 +288,7 @@ struct FileMetaData {
|
||||
const std::string& _file_checksum_func_name,
|
||||
UniqueId64x2 _unique_id,
|
||||
const uint64_t _compensated_range_deletion_size,
|
||||
uint64_t _tail_size, bool _user_defined_timestamps_persisted,
|
||||
const std::string& _min_timestamp,
|
||||
const std::string& _max_timestamp)
|
||||
uint64_t _tail_size, bool _user_defined_timestamps_persisted)
|
||||
: fd(file, file_path_id, file_size, smallest_seq, largest_seq),
|
||||
smallest(smallest_key),
|
||||
largest(largest_key),
|
||||
@@ -313,9 +303,7 @@ struct FileMetaData {
|
||||
file_checksum_func_name(_file_checksum_func_name),
|
||||
unique_id(std::move(_unique_id)),
|
||||
tail_size(_tail_size),
|
||||
user_defined_timestamps_persisted(_user_defined_timestamps_persisted),
|
||||
min_timestamp(_min_timestamp),
|
||||
max_timestamp(_max_timestamp) {
|
||||
user_defined_timestamps_persisted(_user_defined_timestamps_persisted) {
|
||||
TEST_SYNC_POINT_CALLBACK("FileMetaData::FileMetaData", this);
|
||||
}
|
||||
|
||||
@@ -398,8 +386,7 @@ struct FileMetaData {
|
||||
usage += sizeof(*this);
|
||||
#endif // ROCKSDB_MALLOC_USABLE_SIZE
|
||||
usage += smallest.size() + largest.size() + file_checksum.size() +
|
||||
file_checksum_func_name.size() + min_timestamp.size() +
|
||||
max_timestamp.size();
|
||||
file_checksum_func_name.size();
|
||||
return usage;
|
||||
}
|
||||
|
||||
@@ -750,19 +737,17 @@ class VersionEdit {
|
||||
const std::string& file_checksum_func_name,
|
||||
const UniqueId64x2& unique_id,
|
||||
const uint64_t compensated_range_deletion_size,
|
||||
uint64_t tail_size, bool user_defined_timestamps_persisted,
|
||||
const std::string& min_timestamp = "",
|
||||
const std::string& max_timestamp = "") {
|
||||
uint64_t tail_size, bool user_defined_timestamps_persisted) {
|
||||
assert(smallest_seqno <= largest_seqno);
|
||||
new_files_.emplace_back(
|
||||
level,
|
||||
FileMetaData(
|
||||
file, file_path_id, file_size, smallest, largest, smallest_seqno,
|
||||
largest_seqno, marked_for_compaction, temperature,
|
||||
oldest_blob_file_number, oldest_ancester_time, file_creation_time,
|
||||
epoch_number, file_checksum, file_checksum_func_name, unique_id,
|
||||
compensated_range_deletion_size, tail_size,
|
||||
user_defined_timestamps_persisted, min_timestamp, max_timestamp));
|
||||
FileMetaData(file, file_path_id, file_size, smallest, largest,
|
||||
smallest_seqno, largest_seqno, marked_for_compaction,
|
||||
temperature, oldest_blob_file_number, oldest_ancester_time,
|
||||
file_creation_time, epoch_number, file_checksum,
|
||||
file_checksum_func_name, unique_id,
|
||||
compensated_range_deletion_size, tail_size,
|
||||
user_defined_timestamps_persisted));
|
||||
files_to_quarantine_.push_back(file);
|
||||
if (!HasLastSequence() || largest_seqno > GetLastSequence()) {
|
||||
SetLastSequence(largest_seqno);
|
||||
|
||||
+12
-56
@@ -1820,10 +1820,8 @@ Status Version::GetTableProperties(const ReadOptions& read_options,
|
||||
file_name = TableFileName(ioptions.cf_paths, file_meta->fd.GetNumber(),
|
||||
file_meta->fd.GetPathId());
|
||||
}
|
||||
FileOptions fopts = file_options_;
|
||||
fopts.file_checksum = file_meta->file_checksum;
|
||||
fopts.file_checksum_func_name = file_meta->file_checksum_func_name;
|
||||
s = ioptions.fs->NewRandomAccessFile(file_name, fopts, &file, nullptr);
|
||||
s = ioptions.fs->NewRandomAccessFile(file_name, file_options_, &file,
|
||||
nullptr);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -3446,7 +3444,7 @@ bool Version::MaybeInitializeFileMetaData(const ReadOptions& read_options,
|
||||
// Ensure new invariants on old files
|
||||
file_meta->num_deletions =
|
||||
std::max(tp->num_deletions, tp->num_range_deletions);
|
||||
file_meta->num_entries = std::max(tp->num_entries, file_meta->num_deletions);
|
||||
file_meta->num_entries = std::max(tp->num_entries, tp->num_deletions);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3739,8 +3737,7 @@ bool ShouldChangeFileTemperature(const ImmutableOptions& ioptions,
|
||||
|
||||
void VersionStorageInfo::ComputeCompactionScore(
|
||||
const ImmutableOptions& immutable_options,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
const std::string& full_history_ts_low) {
|
||||
const MutableCFOptions& mutable_cf_options) {
|
||||
double total_downcompact_bytes = 0.0;
|
||||
// Historically, score is defined as actual bytes in a level divided by
|
||||
// the level's target size, and 1.0 is the threshold for triggering
|
||||
@@ -3939,8 +3936,7 @@ void VersionStorageInfo::ComputeCompactionScore(
|
||||
ComputeFilesMarkedForCompaction(max_output_level);
|
||||
ComputeBottommostFilesMarkedForCompaction(
|
||||
immutable_options.cf_allow_ingest_behind ||
|
||||
immutable_options.allow_ingest_behind,
|
||||
immutable_options.user_comparator, full_history_ts_low);
|
||||
immutable_options.allow_ingest_behind);
|
||||
ComputeExpiredTtlFiles(immutable_options, mutable_cf_options.ttl);
|
||||
ComputeFilesMarkedForPeriodicCompaction(
|
||||
immutable_options, mutable_cf_options.periodic_compaction_seconds,
|
||||
@@ -4531,20 +4527,17 @@ void VersionStorageInfo::GenerateFileLocationIndex() {
|
||||
}
|
||||
}
|
||||
|
||||
void VersionStorageInfo::UpdateOldestSnapshot(
|
||||
SequenceNumber seqnum, bool allow_ingest_behind, const Comparator* ucmp,
|
||||
const std::string& full_history_ts_low) {
|
||||
void VersionStorageInfo::UpdateOldestSnapshot(SequenceNumber seqnum,
|
||||
bool allow_ingest_behind) {
|
||||
assert(seqnum >= oldest_snapshot_seqnum_);
|
||||
oldest_snapshot_seqnum_ = seqnum;
|
||||
if (oldest_snapshot_seqnum_ > bottommost_files_mark_threshold_) {
|
||||
ComputeBottommostFilesMarkedForCompaction(allow_ingest_behind, ucmp,
|
||||
full_history_ts_low);
|
||||
ComputeBottommostFilesMarkedForCompaction(allow_ingest_behind);
|
||||
}
|
||||
}
|
||||
|
||||
void VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction(
|
||||
bool allow_ingest_behind, const Comparator* ucmp,
|
||||
const std::string& full_history_ts_low) {
|
||||
bool allow_ingest_behind) {
|
||||
bottommost_files_marked_for_compaction_.clear();
|
||||
bottommost_files_mark_threshold_ = kMaxSequenceNumber;
|
||||
if (allow_ingest_behind) {
|
||||
@@ -4565,39 +4558,12 @@ void VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction(
|
||||
current_time - static_cast<int64_t>(bottommost_file_compaction_delay_);
|
||||
}
|
||||
|
||||
// For UDT, we need to check if the file's max timestamp is below
|
||||
// full_history_ts_low. If not, the compaction won't be able to collapse the
|
||||
// timestamp to clean up the tombstone , so marking the file would be futile
|
||||
// and could cause an infinite compaction loop.
|
||||
const bool has_udt = ucmp && ucmp->timestamp_size() > 0;
|
||||
|
||||
for (auto& level_and_file : bottommost_files_) {
|
||||
if (!level_and_file.second->being_compacted &&
|
||||
level_and_file.second->fd.largest_seqno != 0) {
|
||||
// largest_seqno might be nonzero due to containing the final key in an
|
||||
// earlier compaction, whose seqnum we didn't zero out.
|
||||
if (level_and_file.second->fd.largest_seqno < oldest_snapshot_seqnum_) {
|
||||
if (has_udt) {
|
||||
const std::string& max_ts = level_and_file.second->max_timestamp;
|
||||
// If max_timestamp is empty, the file could come from very old
|
||||
// version which does not have timestamp. In that case, we should pick
|
||||
// the file for compaction. After compaction, the file will have
|
||||
// max_timestamp set propertly.
|
||||
if (!max_ts.empty()) {
|
||||
// If full_history_ts_low is empty, it means it was never set, which
|
||||
// means its value is 0. Therefore, it would be always smaller than
|
||||
// max_timestamp
|
||||
if (full_history_ts_low.empty()) {
|
||||
continue;
|
||||
}
|
||||
// If max timestamp >= full_history_ts_low, skip this file
|
||||
if (ucmp->CompareTimestamp(Slice(max_ts), full_history_ts_low) >=
|
||||
0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!needs_delay) {
|
||||
bottommost_files_marked_for_compaction_.push_back(level_and_file);
|
||||
} else if (creation_time_ub > 0) {
|
||||
@@ -5605,15 +5571,6 @@ 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
|
||||
@@ -5673,8 +5630,7 @@ void VersionSet::AppendVersion(ColumnFamilyData* column_family_data,
|
||||
// compute new compaction score
|
||||
v->storage_info()->ComputeCompactionScore(
|
||||
column_family_data->ioptions(),
|
||||
column_family_data->GetLatestMutableCFOptions(),
|
||||
column_family_data->GetFullHistoryTsLow());
|
||||
column_family_data->GetLatestMutableCFOptions());
|
||||
|
||||
// Mark v finalized
|
||||
v->storage_info_.SetFinalized();
|
||||
@@ -7137,6 +7093,7 @@ Status VersionSet::WriteCurrentStateToManifest(
|
||||
|
||||
for (const auto& f : level_files) {
|
||||
assert(f);
|
||||
|
||||
edit.AddFile(level, f->fd.GetNumber(), f->fd.GetPathId(),
|
||||
f->fd.GetFileSize(), f->smallest, f->largest,
|
||||
f->fd.smallest_seqno, f->fd.largest_seqno,
|
||||
@@ -7145,8 +7102,7 @@ Status VersionSet::WriteCurrentStateToManifest(
|
||||
f->file_creation_time, f->epoch_number, f->file_checksum,
|
||||
f->file_checksum_func_name, f->unique_id,
|
||||
f->compensated_range_deletion_size, f->tail_size,
|
||||
f->user_defined_timestamps_persisted, f->min_timestamp,
|
||||
f->max_timestamp);
|
||||
f->user_defined_timestamps_persisted);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-12
@@ -200,8 +200,7 @@ class VersionStorageInfo {
|
||||
// REQUIRES: db_mutex held!!
|
||||
// TODO find a better way to pass compaction_options_fifo.
|
||||
void ComputeCompactionScore(const ImmutableOptions& immutable_options,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
const std::string& full_history_ts_low);
|
||||
const MutableCFOptions& mutable_cf_options);
|
||||
|
||||
// Estimate est_comp_needed_bytes_
|
||||
void EstimateCompactionBytesNeeded(
|
||||
@@ -231,15 +230,8 @@ class VersionStorageInfo {
|
||||
// oldest snapshot changes as that is when bottom-level files can become
|
||||
// eligible for compaction.
|
||||
//
|
||||
// For columns with User Defined Timestamps (UDT), also checks that the
|
||||
// file's largest timestamp is below full_history_ts_low before marking,
|
||||
// since compaction can only collapse timestamp when it is below this
|
||||
// threshold.
|
||||
//
|
||||
// REQUIRES: DB mutex held
|
||||
void ComputeBottommostFilesMarkedForCompaction(
|
||||
bool allow_ingest_behind, const Comparator* ucmp,
|
||||
const std::string& full_history_ts_low);
|
||||
void ComputeBottommostFilesMarkedForCompaction(bool allow_ingest_behind);
|
||||
|
||||
// This computes files_marked_for_forced_blob_gc_ and is called by
|
||||
// ComputeCompactionScore()
|
||||
@@ -256,8 +248,7 @@ class VersionStorageInfo {
|
||||
// files marked for compaction.
|
||||
// REQUIRES: DB mutex held
|
||||
void UpdateOldestSnapshot(SequenceNumber oldest_snapshot_seqnum,
|
||||
bool allow_ingest_behind, const Comparator* ucmp,
|
||||
const std::string& full_history_ts_low);
|
||||
bool allow_ingest_behind);
|
||||
|
||||
int MaxInputLevel() const;
|
||||
int MaxOutputLevel(bool allow_ingest_behind) const;
|
||||
|
||||
+9
-18
@@ -56,8 +56,7 @@ class GenerateLevelFilesBriefTest : public testing::Test {
|
||||
kInvalidBlobFileNumber, kUnknownOldestAncesterTime,
|
||||
kUnknownFileCreationTime, kUnknownEpochNumber, kUnknownFileChecksum,
|
||||
kUnknownFileChecksumFuncName, kNullUniqueId64x2, 0, 0,
|
||||
/* user_defined_timestamps_persisted */ true, /* min timestamp */ "",
|
||||
/* max timestamp */ "");
|
||||
/* user_defined_timestamps_persisted */ true);
|
||||
files_.push_back(f);
|
||||
}
|
||||
|
||||
@@ -173,8 +172,7 @@ class VersionStorageInfoTestBase : public testing::Test {
|
||||
kUnknownOldestAncesterTime, kUnknownFileCreationTime,
|
||||
kUnknownEpochNumber, kUnknownFileChecksum, kUnknownFileChecksumFuncName,
|
||||
kNullUniqueId64x2, compensated_range_deletion_size, 0,
|
||||
/* user_defined_timestamps_persisted */ true, /* min timestamp */ "",
|
||||
/* max timestamp */ "");
|
||||
/* user_defined_timestamps_persisted */ true);
|
||||
vstorage_.AddFile(level, f);
|
||||
}
|
||||
|
||||
@@ -393,8 +391,7 @@ TEST_F(VersionStorageInfoTest, MaxBytesForLevelDynamicWithLargeL0_1) {
|
||||
ASSERT_EQ(51450U, vstorage_.MaxBytesForLevel(3));
|
||||
ASSERT_EQ(257250U, vstorage_.MaxBytesForLevel(4));
|
||||
|
||||
vstorage_.ComputeCompactionScore(ioptions_, mutable_cf_options_,
|
||||
/*full_history_ts_low=*/"");
|
||||
vstorage_.ComputeCompactionScore(ioptions_, mutable_cf_options_);
|
||||
// Only L0 hits compaction.
|
||||
ASSERT_EQ(vstorage_.CompactionScoreLevel(0), 0);
|
||||
}
|
||||
@@ -424,8 +421,7 @@ TEST_F(VersionStorageInfoTest, MaxBytesForLevelDynamicWithLargeL0_2) {
|
||||
ASSERT_EQ(51450U, vstorage_.MaxBytesForLevel(3));
|
||||
ASSERT_EQ(257250U, vstorage_.MaxBytesForLevel(4));
|
||||
|
||||
vstorage_.ComputeCompactionScore(ioptions_, mutable_cf_options_,
|
||||
/*full_history_ts_low=*/"");
|
||||
vstorage_.ComputeCompactionScore(ioptions_, mutable_cf_options_);
|
||||
// Although L2 and l3 have higher unadjusted compaction score, considering
|
||||
// a relatively large L0 being compacted down soon, L4 is picked up for
|
||||
// compaction.
|
||||
@@ -457,8 +453,7 @@ TEST_F(VersionStorageInfoTest, MaxBytesForLevelDynamicWithLargeL0_3) {
|
||||
ASSERT_EQ(2, vstorage_.base_level());
|
||||
ASSERT_EQ(20000U, vstorage_.MaxBytesForLevel(2));
|
||||
|
||||
vstorage_.ComputeCompactionScore(ioptions_, mutable_cf_options_,
|
||||
/*full_history_ts_low=*/"");
|
||||
vstorage_.ComputeCompactionScore(ioptions_, mutable_cf_options_);
|
||||
// Although L2 has higher unadjusted compaction score, considering
|
||||
// a relatively large L0 being compacted down soon, L3 is picked up for
|
||||
// compaction.
|
||||
@@ -488,8 +483,7 @@ TEST_F(VersionStorageInfoTest, DrainUnnecessaryLevel) {
|
||||
ASSERT_EQ(1, vstorage_.base_level());
|
||||
ASSERT_EQ(1000, vstorage_.MaxBytesForLevel(1));
|
||||
ASSERT_EQ(10100, vstorage_.MaxBytesForLevel(3));
|
||||
vstorage_.ComputeCompactionScore(ioptions_, mutable_cf_options_,
|
||||
/*full_history_ts_low=*/"");
|
||||
vstorage_.ComputeCompactionScore(ioptions_, mutable_cf_options_);
|
||||
|
||||
// Tests that levels 1 and 3 are eligible for compaction.
|
||||
// Levels 1 and 3 are much smaller than target size,
|
||||
@@ -1352,8 +1346,7 @@ class VersionSetTestBase {
|
||||
Temperature::kUnknown, info.oldest_blob_file_number, 0, 0,
|
||||
info.epoch_number, kUnknownFileChecksum, kUnknownFileChecksumFuncName,
|
||||
kNullUniqueId64x2, 0, 0,
|
||||
/* user_defined_timestamps_persisted */ true, /* min timestamp */ "",
|
||||
/* max timestamp */ "");
|
||||
/* user_defined_timestamps_persisted */ true);
|
||||
if (info.file_missing) {
|
||||
ASSERT_OK(fs_->DeleteFile(fname, IOOptions(), nullptr));
|
||||
}
|
||||
@@ -3947,8 +3940,7 @@ TEST_F(VersionSetTestMissingFiles, ManifestFarBehindSst) {
|
||||
largest_ikey, 0, 0, false, Temperature::kUnknown, 0, 0, 0,
|
||||
file_num /* epoch_number */, kUnknownFileChecksum,
|
||||
kUnknownFileChecksumFuncName, kNullUniqueId64x2, 0, 0,
|
||||
/* user_defined_timestamps_persisted */ true, /* min timestamp */ "",
|
||||
/* max timestamp */ "");
|
||||
/* user_defined_timestamps_persisted */ true);
|
||||
added_files.emplace_back(0, meta);
|
||||
}
|
||||
WriteFileAdditionAndDeletionToManifest(
|
||||
@@ -4009,8 +4001,7 @@ TEST_F(VersionSetTestMissingFiles, ManifestAheadofSst) {
|
||||
largest_ikey, 0, 0, false, Temperature::kUnknown, 0, 0, 0,
|
||||
file_num /* epoch_number */, kUnknownFileChecksum,
|
||||
kUnknownFileChecksumFuncName, kNullUniqueId64x2, 0, 0,
|
||||
/* user_defined_timestamps_persisted */ true, /* min timestamp */ "",
|
||||
/* max timestamp */ "");
|
||||
/* user_defined_timestamps_persisted */ true);
|
||||
added_files.emplace_back(0, meta);
|
||||
}
|
||||
WriteFileAdditionAndDeletionToManifest(
|
||||
|
||||
@@ -877,24 +877,5 @@ Status DestroyUnverifiedSubdir(const std::string& dirname) {
|
||||
return s;
|
||||
}
|
||||
|
||||
Status DbStressDestroyDb(const std::string& db_path) {
|
||||
Status s;
|
||||
Options options;
|
||||
// NOTE: using db_stress_listener_env in order to see obsolete MANIFEST files
|
||||
options.env = db_stress_listener_env;
|
||||
// Remove DB files in a principled way to avoid issues
|
||||
if (FLAGS_use_blob_db) {
|
||||
s = blob_db::DestroyBlobDB(db_path, options, blob_db::BlobDBOptions());
|
||||
} else {
|
||||
s = DestroyDB(db_path, options);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
// Remove everything else recursively, only reporting success if able to
|
||||
// delete everything
|
||||
return DestroyDir(db_stress_listener_env, db_path);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
#endif // GFLAGS
|
||||
@@ -100,8 +100,6 @@ DECLARE_bool(enable_pipelined_write);
|
||||
DECLARE_bool(verify_before_write);
|
||||
DECLARE_bool(histogram);
|
||||
DECLARE_bool(destroy_db_initially);
|
||||
DECLARE_bool(destroy_db_and_exit);
|
||||
DECLARE_string(delete_dir_and_exit);
|
||||
DECLARE_bool(verbose);
|
||||
DECLARE_bool(progress_reports);
|
||||
DECLARE_uint64(db_write_buffer_size);
|
||||
@@ -822,10 +820,5 @@ Status SaveFilesInDirectory(const std::string& src_dirname,
|
||||
const std::string& dst_dirname);
|
||||
Status DestroyUnverifiedSubdir(const std::string& dirname);
|
||||
Status InitUnverifiedSubdir(const std::string& dirname);
|
||||
|
||||
// Destroy the DB at the given path under the env configured for db_stress.
|
||||
// Handles both regular DB and BlobDB, and cleans and removes the entire dir.
|
||||
Status DbStressDestroyDb(const std::string& db_path);
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
#endif // GFLAGS
|
||||
@@ -9,11 +9,8 @@
|
||||
|
||||
#ifdef GFLAGS
|
||||
#pragma once
|
||||
|
||||
#include "db_stress_tool/db_stress_common.h"
|
||||
#include "file/filename.h"
|
||||
#include "monitoring/thread_status_util.h"
|
||||
#include "rocksdb/file_checksum.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
namespace {
|
||||
@@ -176,35 +173,6 @@ class DbStressFSWrapper : public FileSystemWrapper {
|
||||
const FileOptions& file_opts,
|
||||
std::unique_ptr<FSRandomAccessFile>* r,
|
||||
IODebugContext* dbg) override {
|
||||
// verify that file checksums are propagated through FileOptions
|
||||
// for SST file opens.
|
||||
|
||||
std::string basename = f.substr(f.rfind('/') + 1);
|
||||
uint64_t file_number;
|
||||
FileType file_type;
|
||||
if (ParseFileName(basename, &file_number, &file_type) &&
|
||||
file_type == kTableFile) {
|
||||
// file_checksum_func_name must always be populated to be sure each call
|
||||
// site within RocksDB is intentional about populating the fields with the
|
||||
// best available information:
|
||||
// - kNoFileChecksumFuncName: no checksum context available
|
||||
// (e.g., SstFileDumper, SstFileReader, checksum generation),
|
||||
// always paired with empty checksum
|
||||
// - kUnknownFileChecksumFuncName: file created without a
|
||||
// checksum factory (from MANIFEST), always paired with
|
||||
// empty checksum
|
||||
// - a real name (e.g., "FileChecksumCrc32c"): checksum exists
|
||||
assert(!file_opts.file_checksum_func_name.empty());
|
||||
if (file_opts.file_checksum_func_name == kUnknownFileChecksumFuncName ||
|
||||
file_opts.file_checksum_func_name == kNoFileChecksumFuncName) {
|
||||
// No checksum available — checksum value must be empty
|
||||
assert(file_opts.file_checksum.empty());
|
||||
} else {
|
||||
// A real checksum function — checksum value must be present
|
||||
assert(!file_opts.file_checksum.empty());
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
IOStatus s = target()->NewRandomAccessFile(f, file_opts, &file, dbg);
|
||||
if (s.ok()) {
|
||||
|
||||
@@ -135,14 +135,6 @@ DEFINE_bool(histogram, false, "Print histogram of operation timings");
|
||||
DEFINE_bool(destroy_db_initially, true,
|
||||
"Destroys the database dir before start if this is true");
|
||||
|
||||
DEFINE_bool(destroy_db_and_exit, false,
|
||||
"Destroys the database dir and exits. Useful for cleanup without "
|
||||
"running stress test. Other options are mostly ignored.");
|
||||
|
||||
DEFINE_string(delete_dir_and_exit, "",
|
||||
"Recursively deletes the specified directory and exits. "
|
||||
"Useful for cleaning up TEST_TMPDIR after crash tests.");
|
||||
|
||||
DEFINE_bool(verbose, false, "Verbose");
|
||||
|
||||
DEFINE_bool(progress_reports, true,
|
||||
|
||||
@@ -77,7 +77,22 @@ StressTest::StressTest()
|
||||
secondary_db_(nullptr),
|
||||
is_db_stopped_(false) {
|
||||
if (FLAGS_destroy_db_initially) {
|
||||
const Status s = DbStressDestroyDb(FLAGS_db);
|
||||
std::vector<std::string> files;
|
||||
db_stress_env->GetChildren(FLAGS_db, &files);
|
||||
for (unsigned int i = 0; i < files.size(); i++) {
|
||||
if (Slice(files[i]).starts_with("heap-")) {
|
||||
db_stress_env->DeleteFile(FLAGS_db + "/" + files[i]);
|
||||
}
|
||||
}
|
||||
|
||||
Options options;
|
||||
options.env = db_stress_env;
|
||||
// Remove files without preserving manfiest files
|
||||
const Status s = !FLAGS_use_blob_db
|
||||
? DestroyDB(FLAGS_db, options)
|
||||
: blob_db::DestroyBlobDB(FLAGS_db, options,
|
||||
blob_db::BlobDBOptions());
|
||||
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "Cannot destroy original db: %s\n", s.ToString().c_str());
|
||||
exit(1);
|
||||
|
||||
@@ -94,39 +94,10 @@ int db_stress_tool(int argc, char** argv) {
|
||||
raw_env = fault_env_guard.get();
|
||||
}
|
||||
|
||||
auto db_stress_fs =
|
||||
std::make_shared<DbStressFSWrapper>(raw_env->GetFileSystem());
|
||||
env_wrapper_guard =
|
||||
std::make_shared<CompositeEnvWrapper>(raw_env, db_stress_fs);
|
||||
env_wrapper_guard = std::make_shared<CompositeEnvWrapper>(
|
||||
raw_env, std::make_shared<DbStressFSWrapper>(raw_env->GetFileSystem()));
|
||||
db_stress_env = env_wrapper_guard.get();
|
||||
|
||||
// Handle --destroy_db_and_exit early, before other option validation
|
||||
if (FLAGS_destroy_db_and_exit) {
|
||||
s = DbStressDestroyDb(FLAGS_db);
|
||||
if (s.ok()) {
|
||||
fprintf(stdout, "Successfully destroyed db at %s\n", FLAGS_db.c_str());
|
||||
return 0;
|
||||
} else {
|
||||
fprintf(stderr, "Failed to destroy db at %s: %s\n", FLAGS_db.c_str(),
|
||||
s.ToString().c_str());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle --delete_dir_and_exit early, before other option validation
|
||||
if (!FLAGS_delete_dir_and_exit.empty()) {
|
||||
s = DestroyDir(raw_env, FLAGS_delete_dir_and_exit);
|
||||
if (s.ok()) {
|
||||
fprintf(stdout, "Successfully deleted directory %s\n",
|
||||
FLAGS_delete_dir_and_exit.c_str());
|
||||
return 0;
|
||||
} else {
|
||||
fprintf(stderr, "Failed to delete directory %s: %s\n",
|
||||
FLAGS_delete_dir_and_exit.c_str(), s.ToString().c_str());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
FLAGS_rep_factory = StringToRepFactory(FLAGS_memtablerep.c_str());
|
||||
|
||||
// The number of background threads should be at least as much the
|
||||
|
||||
@@ -1904,17 +1904,6 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
} while (!s.ok() && IsErrorInjectedAndRetryable(s) &&
|
||||
initial_wal_write_may_succeed);
|
||||
|
||||
if ((s.IsDeadlock() || s.IsTimedOut()) &&
|
||||
(FLAGS_use_multiget || FLAGS_use_multi_get_entity)) {
|
||||
// Deadlock or timeout is ok, when multi get is tested. Because multi get
|
||||
// tests execute MaybeAddKeyToTxnForRYW function which writes to the
|
||||
// same key space but does not acquire stress test level mutex. So it is
|
||||
// possible RocksDB returns deadlock or timeout. Return OK() for these
|
||||
// cases
|
||||
pending_expected_value.Rollback();
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
pending_expected_value.Rollback();
|
||||
if (IsErrorInjectedAndRetryable(s)) {
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
---
|
||||
title: "BitFields API: Type-Safe Bit Packing for Lock-Free Data Structures"
|
||||
layout: post
|
||||
author: pdillinger
|
||||
category: blog
|
||||
---
|
||||
|
||||
Modern concurrent data structures increasingly rely on [atomic operations](https://en.cppreference.com/w/cpp/atomic/atomic) to avoid the overhead of locking. A valuable but under-utilized technique for maximizing the effectiveness of atomic operations is [bit packing](https://en.wikipedia.org/wiki/Bit_field)---fitting multiple logical fields into a single atomic variable for algorithmic simplicity and efficiency. However, language support for bit packing does not guarantee dense packing, and manually managing bit manipulation quickly becomes error-prone, especially when dealing with complex state machines.
|
||||
|
||||
To address this in RocksDB, we have developed a reusable **BitFields API**, a type-safe, zero-overhead abstraction for bit packing in C++. This works in conjunction with clean wrappers for `std::atomic` for powerful and relatively safe bit-packing of atomic data. For broader use, a [variant of the code](https://github.com/facebook/folly/pull/2549) has been proposed for adding to folly.
|
||||
|
||||
## The Problem: Managing Packed Bit Fields
|
||||
|
||||
Consider HyperClockCache, an essentially lock-free cache implementation in RocksDB, which was [refactored to use this BitFields API](https://github.com/facebook/rocksdb/pull/14154). It is a hash table built on *slots* that can each hold a cache entry and relevant metadata. For atomic simplicity and efficiency, all the essential metadata for each slot is packed into a single 64-bit value:
|
||||
- The reference count and eviction metadata are together encoded into *acquire* and *release* counters, 30 bits each.
|
||||
- The possible states of {*empty*, *under construction/destruction*, *occupied+visible*, and *occupied+invisible*} are encoded into three state bits (instead of two, for easier decoding and manipulation).
|
||||
- A *hit* bit is used for secondary cache integration.
|
||||
|
||||
Traditionally, you might write code like this:
|
||||
|
||||
```cpp
|
||||
// Old approach: manual bit manipulation
|
||||
constexpr uint64_t kAcquireCounterShift = 0;
|
||||
constexpr uint64_t kReleaseCounterShift = 30;
|
||||
constexpr uint64_t kCounterMask = 0x3FFFFFFF;
|
||||
constexpr uint64_t kHitBitShift = 60;
|
||||
constexpr uint64_t kOccupiedShift = 61;
|
||||
constexpr uint64_t kShareableShift = 62;
|
||||
constexpr uint64_t kVisibleShift = 63;
|
||||
constexpr uint64_t kStateShift = kOccupiedShift;
|
||||
|
||||
std::atomic<uint64_t> meta_;
|
||||
|
||||
bool IsUnderConstruction(uint64_t meta) const {
|
||||
return (meta & (uint64_t{1} << kOccupiedShift)) && !(meta & (uint64_t{1} << kShareableShift));
|
||||
}
|
||||
|
||||
// Getting fields
|
||||
uint64_t meta = meta_.load(std::memory_order_acquire);
|
||||
if (IsUnderConstruction(meta)) {
|
||||
// ...
|
||||
} else if ((meta >> kVisibleShift) & 1) {
|
||||
uint32_t refcount =
|
||||
static_cast<uint32_t>(((meta >> kAcquireCounterShift) -
|
||||
(meta >> kReleaseCounterShift)) & kCounterMask);
|
||||
// ...
|
||||
}
|
||||
|
||||
|
||||
// Setting fields
|
||||
|
||||
// Set the hit bit (relaxed)
|
||||
meta_.fetch_or(uint64_t{1} << kHitBitShift, std::memory_order_relaxed);
|
||||
|
||||
// Set both counters to `new_count` (as in eviction processing)
|
||||
uint64_t meta = meta_.load(std::memory_order_relaxed);
|
||||
uint64_t new_meta =
|
||||
(meta & ((uint64_t{1} << kHitBitShift) | (uint64_t{7} << kStateShift))) |
|
||||
(new_count << kReleaseCounterShift) |
|
||||
(new_count << kAcquireCounterShift);
|
||||
bool success = meta_.compare_exchange_strong(meta, new_meta,
|
||||
std::memory_order_acq_rel);
|
||||
|
||||
// Increment acquire counter by initial_countdown
|
||||
old_meta = meta_.fetch_add((uint64_t{1} << kAcquireCounterShift) * initial_countdown,
|
||||
std::memory_order_acq_rel);
|
||||
```
|
||||
|
||||
This approach has several problems:
|
||||
1. **Error-prone**: Easy to get masks and shifts wrong
|
||||
2. **Maintenance burden**: Changes to field sizes require updating multiple constants
|
||||
3. **Abstraction challenges**: Even if writing a full set of well-tested getters and setters to hide all the details, details can leak in to do things like update multiple fields in one non-CAS (compare-and-swap) atomic operation.
|
||||
|
||||
## New Solution: BitFields API
|
||||
|
||||
The BitFields API provides a declarative, type-safe way to define bit-packed structures. Here's how the same example looks with BitFields:
|
||||
|
||||
```cpp
|
||||
// New approach: declarative bit fields. (Each field must reference the
|
||||
// previous, so that the declaration machinery is simply stateless.)
|
||||
struct SlotMeta : public BitFields<uint64_t, SlotMeta> {
|
||||
using AcquireCounter = UnsignedBitField<SlotMeta, 30, NoPrevBitField>;
|
||||
using ReleaseCounter = UnsignedBitField<SlotMeta, 30, AcquireCounter>;
|
||||
using HitFlag = BoolBitField<SlotMeta, ReleaseCounter>;
|
||||
using OccupiedFlag = BoolBitField<SlotMeta, HitFlag>;
|
||||
using ShareableFlag = BoolBitField<SlotMeta, OccupiedFlag>;
|
||||
using VisibleFlag = BoolBitField<SlotMeta, ShareableFlag>;
|
||||
|
||||
// Convenience helpers
|
||||
bool IsUnderConstruction() const {
|
||||
return Get<OccupiedFlag>() && !Get<ShareableFlag>();
|
||||
}
|
||||
};
|
||||
|
||||
BitFieldsAtomic<SlotMeta> meta_;
|
||||
|
||||
// Getting fields
|
||||
SlotMeta state = meta_.Load();
|
||||
if (state.IsUnderConstruction()) {
|
||||
// ...
|
||||
} else if (state.Get<SlotMeta::VisibleFlag>()) {
|
||||
uint32_t refcount = state.Get<SlotMeta::AcquireCounter>() -
|
||||
state.Get<SlotMeta::ReleaseCounter>();
|
||||
// ...
|
||||
}
|
||||
|
||||
// Setting fields
|
||||
|
||||
// Set the hit bit (relaxed)
|
||||
meta_.ApplyRelaxed(SlotMeta::HitFlag::SetTransform());
|
||||
|
||||
// Set both counters to `new_count` (as in eviction processing)
|
||||
SlotMeta meta = meta_.LoadRelaxed();
|
||||
SlotMeta new_meta = meta;
|
||||
new_meta.Set<SlotMeta::ReleaseCounter>(new_count);
|
||||
new_meta.Set<SlotMeta::AcquireCounter>(new_count);
|
||||
meta_.CasStrongRelaxed(meta, new_meta);
|
||||
|
||||
// Increment acquire counter by initial_countdown
|
||||
auto add_acquire =
|
||||
AcquireCounter::PlusTransformPromiseNoOverflow(initial_countdown);
|
||||
meta_.Apply(add_acquire, &old_meta);
|
||||
|
||||
// Bonus: Atomic multi-field updates without compare-exchange
|
||||
auto transform = AcquireCounter::PlusTransformPromiseNoOverflow(1) +
|
||||
ReleaseCounter::PlusTransformPromiseNoOverflow(1);
|
||||
meta_.Apply(transform);
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### Type Safety and Self-Documentation
|
||||
|
||||
Each field has a specific type (`bool` for `BoolBitField`, appropriately-sized unsigned int for `UnsignedBitField`) and clear semantic meaning. The field definitions are self-documenting: you can immediately see how many bits each field occupies and in what order.
|
||||
|
||||
### [Zero Overhead](https://en.cppreference.com/w/cpp/language/Zero-overhead_principle)
|
||||
|
||||
Because of heavy use of templates and constexpr operations and the ability to satisfy multiple field reads or writes from a single atomic operation, we have seen no runtime overhead vs. hand-written bit manipulation, in RocksDB. In one case, we verified the assembly code was identical.
|
||||
|
||||
[For folly's LifoSem](https://github.com/facebook/folly/pull/2550), there was one case where an optimization hack with detected overflow from one field to another couldn't be replicated as efficiently with the BitFields API because it would violate overflow checking. For that case I dove into the underlying representation to bypass the BitFields overflow check.
|
||||
|
||||
### Atomic Operations with Transforms
|
||||
|
||||
One of the most powerful features is the ability to combine multiple field updates into a single atomic operation using "transforms", if they are all either (a) some combination of addition and subtraction, (b) bitwise-and, or (c) bitwise-or. For example:
|
||||
|
||||
```cpp
|
||||
// Clear several but not all fields atomically
|
||||
auto and_transform = Field1::AndTransform(0) +
|
||||
Field2::ClearTransform() +
|
||||
Field4::ClearTransform();
|
||||
atomic_bitfields.Apply(and_transform, &old_state, &new_state);
|
||||
...
|
||||
// Set more than one boolean field atomically
|
||||
auto or_transform = Field2::SetTransform() +
|
||||
Field4::SetTransform();
|
||||
atomic_bitfields.Apply(or_transform, &old_state, &new_state);
|
||||
...
|
||||
auto add_transform = Field1::PlusTransformPromiseNoOverflow(1) +
|
||||
Field3::MinusTransformPromiseNoUnderflow(1);
|
||||
atomic_bitfields.Apply(add_transform, &old_state, &new_state);
|
||||
```
|
||||
|
||||
Each `Apply()` generates a single atomic operation (e.g., `fetch_add` or `fetch_or`) that updates all the specified fields, and optionally returns both the old and new values. This enables a number of hacks for atomic updates without CAS.
|
||||
|
||||
### Overflow Protection
|
||||
|
||||
The API includes built-in overflow detection in debug builds:
|
||||
|
||||
```cpp
|
||||
// An assertion will fail in debug builds if the counter overflows
|
||||
auto transform = Counter::PlusTransformPromiseNoOverflow(value);
|
||||
atomic.Apply(transform);
|
||||
```
|
||||
|
||||
For fields at the top of the underlying representation (where overflow doesn't affect other fields), overflow is explicitly ignored. (A compile time error is generated if you try to use `PlusTransformPromiseNoOverflow` on a field at the top of the representation or `PlusTransformIgnoreOverflow` on a field not at the top of the representation.)
|
||||
|
||||
```cpp
|
||||
// For wraparound counters
|
||||
auto transform = Counter::PlusTransformIgnoreOverflow(value);
|
||||
```
|
||||
|
||||
This capability is used in a folly data structure called LifoSem, which [I have proposed to refactor](https://github.com/facebook/folly/pull/2550) to a proposed BitFields API variant for folly.
|
||||
|
||||
### Compare-and-Swap (CAS) Support
|
||||
|
||||
The atomic wrappers provide full CAS support for lock-free algorithms:
|
||||
|
||||
```cpp
|
||||
SlotMeta expected = current_state;
|
||||
SlotMeta desired = expected.With<Field1>(new_value).With<Field2>(true);
|
||||
if (meta_.CasStrong(expected, desired)) {
|
||||
// Successfully updated
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Atomic wrappers
|
||||
|
||||
The BitFields API includes two atomic wrappers: `RelaxedBitFieldsAtomic` and `BitFieldsAtomic`. However, RocksDB also has versions of these wrappers for regular `std::atomic` variables that help with memory ordering discipline: `RelaxedAtomic` and `Atomic` in `util/atomic.h`.
|
||||
|
||||
These wrappers help in a couple of ways:
|
||||
* **Self-document intended memory order**: An atomic field generally has a single memory order that all or most operations should use, typically either `std::memory_order_relaxed` or `std::memory_order_acq_rel`.
|
||||
* **More intentional memory orders and atomic operations**: The standard library's implicit conversions and default memory ordering (`memory_order_seq_cst`) make it easy to accidentally use sequential consistency with acquire/release ordering or even relaxed, which could hurt performance, and tend to hide where atomic operations are actually happening (e.g. implicit vs. explicit load).
|
||||
|
||||
For example, instead of writing:
|
||||
```cpp
|
||||
std::atomic<uint64_t> stat_counter;
|
||||
stat_counter++; // Uses memory_order_seq_cst implicitly - maybe inefficient
|
||||
```
|
||||
|
||||
You write:
|
||||
```cpp
|
||||
RelaxedAtomic<uint64_t> stat_counter;
|
||||
stat_counter.FetchAddRelaxed(1); // Explicitly relaxed - appropriate for a diagnostic counter
|
||||
```
|
||||
|
||||
Or for data providing synchronization:
|
||||
```cpp
|
||||
Atomic<size_t> refcount;
|
||||
refcount.FetchAdd(1); // Standard acquire-release semantics for coordinating with other threads
|
||||
```
|
||||
|
||||
These wrappers complement the BitFields atomic wrappers by providing the same ordering discipline for non-packed atomic variables throughout much of RocksDB, creating a more readable and less clunky approach to concurrent programming. Migrating remaining uses of `std::atomic` is an ongoing effort.
|
||||
|
||||
## Real-World Usage in RocksDB
|
||||
|
||||
The BitFields API was developed along with the revamped parallel compression in RocksDB, but with the intention to also clean up the HyperClockCache (HCC) implementation. With that migration complete, we can see the benefits. Specifically, **by packing more of the state machine into a single atomic value, the parallel algorithms became both simpler and more efficient.** Concurrent algorithms that could have blown up in their state space with elaborate interleavings between threads trying not to block each other, e.g. because of multi-step consensus on work assignments, were instead able to quickly and more easily make progress, e.g. with atomically clear work assignments.
|
||||
|
||||
### Before: Manual Bit Manipulation
|
||||
|
||||
The old HCC code was difficult to read and maintain. Many of the common read and update operations had manually written helper functions, but it was not practical to develop the full set of functions needed for rare cases. Consider this code that clears the "visible" flag on a slot when an entry is erased from subsequent lookups but might still be referenced:
|
||||
|
||||
```cpp
|
||||
// Old HCC code, without atomic wrappers
|
||||
uint64_t old_meta =
|
||||
h->meta.fetch_and(~(uint64_t{ClockHandle::kStateVisibleBit}
|
||||
<< ClockHandle::kStateShift), std::memory_order_acq_rel);
|
||||
// Apply update to local copy
|
||||
uint64_t new_meta = old_meta & ~(uint64_t{ClockHandle::kStateVisibleBit}
|
||||
<< ClockHandle::kStateShift);
|
||||
|
||||
// New HCC code
|
||||
SlotMeta old_meta, new_meta;
|
||||
h->meta.Apply(SlotMeta::VisibleFlag::ClearTransform(), &old_meta, &new_meta);
|
||||
```
|
||||
|
||||
Or this assertion that the acquire and release counters are different:
|
||||
|
||||
```cpp
|
||||
// Old HCC code
|
||||
uint64_t old_meta = ...;
|
||||
assert(((old_meta >> ClockHandle::kAcquireCounterShift) &
|
||||
ClockHandle::kCounterMask) !=
|
||||
((old_meta >> ClockHandle::kReleaseCounterShift) &
|
||||
ClockHandle::kCounterMask));
|
||||
|
||||
// New HCC code without single-purpose helper functions
|
||||
SlotMeta old_meta = ...;
|
||||
assert(old_meta.Get<SlotMeta::AcquireCounter>() !=
|
||||
old_meta.Get<SlotMeta::ReleaseCounter>());
|
||||
|
||||
// New HCC code, with single-purpose helper functions
|
||||
SlotMeta old_meta = ...;
|
||||
assert(old_meta.GetAcquireCounter() != old_meta.GetReleaseCounter());
|
||||
```
|
||||
|
||||
Some hand-written helper functions or using directives are still useful for brevity, but even without them all the bit manipulation details are hidden in the BitFields implementation.
|
||||
|
||||
## Future Directions
|
||||
|
||||
We hope the proposed folly version is accepted to make the BitFields API available for broader usage. Additionally, some quality-of-life improvements are likely possible, perhaps including easier declaration and usage syntax, hopefully without delving into boost-like macro hell. Better runtime and compile time checks might also be possible.
|
||||
|
||||
## Conclusion
|
||||
|
||||
The BitFields API demonstrates that zero-overhead abstractions can significantly improve code quality without sacrificing performance. By providing type safety, self-documentation, and convenience features around bit manipulation and atomic operations, it makes lock-free programming more accessible and maintainable. Bit-packed atomics are arguably essential for *slaying the complexity dragon* of efficient lock-free and low-lock algorithms, because they reduce explosion in algorithm states.
|
||||
|
||||
For RocksDB specifically, the migration to BitFields has made the HyperClockCache implementation substantially easier to understand and modify, while maintaining the same high-performance characteristics. Combined with the recent [parallel compression revamp](/blog/2025/10/08/parallel-compression-revamp.html), these improvements showcase our ongoing commitment to writing clean, efficient, and maintainable code.
|
||||
|
||||
The BitFields API is available in RocksDB's util/bit_fields.h and can be adapted for use in other projects requiring efficient, type-safe bit packing. For those building high-performance concurrent systems, it offers a compelling alternative to manual bit manipulation—proving that safe abstractions and peak performance are not mutually exclusive.
|
||||
Vendored
+41
-489
@@ -41,9 +41,6 @@
|
||||
#include "env/env_chroot.h"
|
||||
#include "env/env_encryption_ctr.h"
|
||||
#include "env/fs_readonly.h"
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
#include "env/io_posix.h"
|
||||
#endif
|
||||
#include "env/mock_env.h"
|
||||
#include "env/unique_id_gen.h"
|
||||
#include "logging/log_buffer.h"
|
||||
@@ -1658,6 +1655,42 @@ 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;
|
||||
@@ -1673,20 +1706,19 @@ TEST_F(EnvPosixTest, MultiReadIOUringError2) {
|
||||
|
||||
bool io_uring_submit_and_wait_called = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_sq_ready:return1",
|
||||
"PosixRandomAccessFile::MultiRead:io_uring_submit_and_wait:return1",
|
||||
[&](void* arg) {
|
||||
io_uring_submit_and_wait_called = true;
|
||||
unsigned* ret = static_cast<unsigned*>(arg);
|
||||
*ret = 1;
|
||||
ssize_t* ret = static_cast<ssize_t*>(arg);
|
||||
(*ret)--;
|
||||
});
|
||||
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;
|
||||
// CQ should be empty after drain - peek should fail
|
||||
int ret = io_uring_peek_cqe(iu, &cqe);
|
||||
assert(-EAGAIN == ret); // No CQEs available
|
||||
assert(io_uring_wait_cqe(iu, &cqe) == 0);
|
||||
io_uring_cqe_seen(iu, cqe);
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
@@ -3608,486 +3640,6 @@ 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
|
||||
}
|
||||
|
||||
// Helper function to run AbortIO test with parameterized read requests.
|
||||
// Each request is specified as {offset, length}.
|
||||
// use_direct_io: if true, opens the file with O_DIRECT to bypass page cache.
|
||||
// iterations: number of times to repeat the test (useful for race conditions).
|
||||
void TestAbortIOWithRequests(
|
||||
Env* env, size_t file_size,
|
||||
const std::vector<std::pair<uint64_t, size_t>>& read_specs,
|
||||
bool use_direct_io = false, int iterations = 1) {
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
fprintf(stderr,
|
||||
"TestAbortIOWithRequests: file_size=%zu, num_reads=%zu, "
|
||||
"direct_io=%d, iterations=%d\n",
|
||||
file_size, read_specs.size(), use_direct_io, iterations);
|
||||
std::shared_ptr<FileSystem> fs = env->GetFileSystem();
|
||||
std::string fname = test::PerThreadDBPath(env, "testfile_abortio");
|
||||
|
||||
// 1. Create test file once (content doesn't change between iterations)
|
||||
{
|
||||
std::unique_ptr<FSWritableFile> wfile;
|
||||
FileOptions file_opts;
|
||||
file_opts.use_direct_writes = true;
|
||||
ASSERT_OK(fs->NewWritableFile(fname, file_opts, &wfile, nullptr));
|
||||
|
||||
// Query the file's required buffer alignment (logical block size)
|
||||
// instead of hardcoding 4096, to support devices with different
|
||||
// sector sizes.
|
||||
size_t sector_size = wfile->GetRequiredBufferAlignment();
|
||||
|
||||
// Round up to full sectors for direct IO writes
|
||||
size_t num_sectors = (file_size + sector_size - 1) / sector_size;
|
||||
for (size_t i = 0; i < num_sectors; ++i) {
|
||||
auto data = NewAligned(sector_size, static_cast<char>(i + 1));
|
||||
Slice slice(data.get(), sector_size);
|
||||
ASSERT_OK(wfile->Append(slice, IOOptions(), nullptr));
|
||||
}
|
||||
|
||||
// Truncate to exact file size if not aligned to sector boundary
|
||||
if (file_size % sector_size != 0) {
|
||||
ASSERT_OK(wfile->Truncate(file_size, IOOptions(), nullptr));
|
||||
}
|
||||
|
||||
ASSERT_OK(wfile->Close(IOOptions(), nullptr));
|
||||
}
|
||||
|
||||
for (int iter = 0; iter < iterations; iter++) {
|
||||
// 2. Submit ReadAsync requests and immediately abort
|
||||
{
|
||||
FileOptions file_opts;
|
||||
file_opts.use_direct_reads = use_direct_io;
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
ASSERT_OK(fs->NewRandomAccessFile(fname, file_opts, &file, nullptr));
|
||||
|
||||
const size_t num_reads = read_specs.size();
|
||||
IOOptions opts;
|
||||
std::vector<void*> io_handles(num_reads);
|
||||
std::vector<FSReadRequest> reqs(num_reads);
|
||||
std::vector<std::unique_ptr<char, Deleter>> data;
|
||||
std::vector<size_t> vals;
|
||||
IOHandleDeleter del_fn;
|
||||
std::atomic<int> callbacks_invoked{0};
|
||||
|
||||
// Initialize read requests from specs
|
||||
for (size_t i = 0; i < num_reads; i++) {
|
||||
reqs[i].offset = read_specs[i].first;
|
||||
reqs[i].len = read_specs[i].second;
|
||||
data.emplace_back(NewAligned(reqs[i].len, 0));
|
||||
reqs[i].scratch = data.back().get();
|
||||
vals.push_back(i);
|
||||
}
|
||||
|
||||
// Callback
|
||||
std::function<void(FSReadRequest&, void*)> callback =
|
||||
[&](FSReadRequest& req, void* cb_arg) {
|
||||
size_t i = *(reinterpret_cast<size_t*>(cb_arg));
|
||||
reqs[i].status = req.status;
|
||||
callbacks_invoked++;
|
||||
};
|
||||
|
||||
// Submit all ReadAsync requests
|
||||
for (size_t i = 0; i < num_reads; i++) {
|
||||
void* cb_arg = static_cast<void*>(&(vals[i]));
|
||||
IOStatus s = file->ReadAsync(reqs[i], opts, callback, cb_arg,
|
||||
&(io_handles[i]), &del_fn, nullptr);
|
||||
if (s.IsNotSupported()) {
|
||||
// io_uring not supported, clean up and skip
|
||||
fprintf(stderr,
|
||||
"WARNING: io_uring not supported, skipping test: %s\n",
|
||||
s.ToString().c_str());
|
||||
for (size_t j = 0; j < i; j++) {
|
||||
if (io_handles[j]) {
|
||||
del_fn(io_handles[j]);
|
||||
}
|
||||
}
|
||||
ASSERT_OK(fs->DeleteFile(fname, IOOptions(), nullptr));
|
||||
return;
|
||||
}
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
// Immediately call AbortIO - this should NOT hang
|
||||
ASSERT_OK(fs->AbortIO(io_handles));
|
||||
|
||||
// Verify all handles are finished and all callbacks were invoked.
|
||||
// Since all handles are passed to AbortIO, every handle is guaranteed
|
||||
// to be finalized (either completed or cancelled).
|
||||
for (size_t i = 0; i < num_reads; i++) {
|
||||
Posix_IOHandle* h = static_cast<Posix_IOHandle*>(io_handles[i]);
|
||||
ASSERT_TRUE(h->is_finished);
|
||||
}
|
||||
ASSERT_EQ(callbacks_invoked.load(), static_cast<int>(num_reads));
|
||||
|
||||
// Clean up handles
|
||||
for (size_t i = 0; i < num_reads; i++) {
|
||||
if (io_handles[i]) {
|
||||
del_fn(io_handles[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_OK(fs->DeleteFile(fname, IOOptions(), nullptr));
|
||||
|
||||
fprintf(stderr, "TestAbortIOWithRequests: completed %d iterations\n",
|
||||
iterations);
|
||||
#else
|
||||
fprintf(stderr,
|
||||
"TestAbortIOWithRequests: SKIPPED (ROCKSDB_IOURING_PRESENT not "
|
||||
"defined)\n");
|
||||
(void)env;
|
||||
(void)file_size;
|
||||
(void)read_specs;
|
||||
(void)use_direct_io;
|
||||
(void)iterations;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Test overlapping reads at aligned offsets (multiples of 4KB)
|
||||
TEST_F(TestAsyncRead, AbortIOOverlappingAligned) {
|
||||
// 4 reads of 16KB each, overlapping by 8KB, all at 4KB-aligned offsets
|
||||
// Read 0: [0, 16KB), Read 1: [8KB, 24KB), Read 2: [16KB, 32KB), Read 3:
|
||||
// [24KB, 40KB)
|
||||
std::vector<std::pair<uint64_t, size_t>> specs = {
|
||||
{0, 16384},
|
||||
{8192, 16384},
|
||||
{16384, 16384},
|
||||
{24576, 16384},
|
||||
};
|
||||
TestAbortIOWithRequests(env_, 64 * 1024, specs);
|
||||
}
|
||||
|
||||
// Test reads at unaligned offsets (not multiples of 4KB)
|
||||
TEST_F(TestAsyncRead, AbortIOUnalignedOffsets) {
|
||||
// Reads starting at non-4KB-aligned offsets
|
||||
std::vector<std::pair<uint64_t, size_t>> specs = {
|
||||
{1000, 8192}, // starts at 1000 (unaligned)
|
||||
{5000, 12288}, // starts at 5000 (unaligned), spans multiple sectors
|
||||
{15000, 8192}, // starts at 15000 (unaligned)
|
||||
{25500, 16384}, // starts at 25500 (unaligned)
|
||||
};
|
||||
TestAbortIOWithRequests(env_, 64 * 1024, specs);
|
||||
}
|
||||
|
||||
// Test mix of aligned and unaligned, various sizes
|
||||
TEST_F(TestAsyncRead, AbortIOMixedOffsets) {
|
||||
std::vector<std::pair<uint64_t, size_t>> specs = {
|
||||
{0, 4096}, // aligned, 1 sector
|
||||
{1500, 8192}, // unaligned, 2 sectors
|
||||
{4096, 20480}, // aligned, 5 sectors
|
||||
{7000, 4096}, // unaligned, spans 2 sectors
|
||||
{16384, 32768}, // aligned, 8 sectors
|
||||
{50000, 8192}, // unaligned
|
||||
};
|
||||
TestAbortIOWithRequests(env_, 128 * 1024, specs);
|
||||
}
|
||||
|
||||
// Stress test with many concurrent handles
|
||||
TEST_F(TestAsyncRead, AbortIOStress) {
|
||||
std::vector<std::pair<uint64_t, size_t>> specs;
|
||||
// 16 overlapping reads with mixed alignment
|
||||
for (int i = 0; i < 16; i++) {
|
||||
uint64_t offset = i * 4000; // Not aligned to 4KB
|
||||
size_t len = 8192 + (i % 4) * 4096; // 8KB to 20KB
|
||||
specs.emplace_back(offset, len);
|
||||
}
|
||||
TestAbortIOWithRequests(env_, 256 * 1024, specs);
|
||||
}
|
||||
|
||||
// Regression test for a fixed bug in AbortIO where out-of-order io_uring
|
||||
// completions could cause an infinite hang. The bug occurred when completions
|
||||
// for a different handle arrived while waiting for the current handle - the
|
||||
// code would consume those completions but not mark the handle as finished,
|
||||
// causing a hang when later iterating to that handle.
|
||||
//
|
||||
// Uses a large read (1MB) followed by a small read (4KB) with Direct I/O to
|
||||
// maximize the chance of out-of-order completions. Runs 100 iterations to
|
||||
// increase the likelihood of triggering the race condition.
|
||||
TEST_F(TestAsyncRead, AbortIOReversedHandles) {
|
||||
// Request 0: LARGE (1MB) at offset 0
|
||||
// Request 1: SMALL (4KB) at offset 1MB
|
||||
std::vector<std::pair<uint64_t, size_t>> specs = {
|
||||
{0, 1024 * 1024}, // 1MB read
|
||||
{1024 * 1024, 4096}, // 4KB read at 1MB offset
|
||||
};
|
||||
// 2MB file, Direct I/O enabled, 100 iterations
|
||||
TestAbortIOWithRequests(env_, 2 * 1024 * 1024, specs,
|
||||
/*use_direct_io=*/true, /*iterations=*/100);
|
||||
}
|
||||
|
||||
// Test for bug fix: AbortIO with partial handles should correctly handle
|
||||
// completions for non-aborted handles.
|
||||
//
|
||||
// Previously, AbortIO would consume completions for non-aborted handles but
|
||||
// not set is_finished (since it expected req_count==2 for all handles).
|
||||
// This caused subsequent Poll calls to hang forever.
|
||||
//
|
||||
// The fix correctly detects handles not in the abort set and finalizes them
|
||||
// immediately when their completion arrives (at req_count==1).
|
||||
TEST_F(TestAsyncRead, AbortIOPartialHandlesBug) {
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
std::shared_ptr<FileSystem> fs = env_->GetFileSystem();
|
||||
std::string fname = test::PerThreadDBPath(env_, "testfile_abortio_partial");
|
||||
|
||||
constexpr size_t kSectorSize = 4096;
|
||||
constexpr size_t kFileSize = 2 * 1024 * 1024; // 2MB
|
||||
|
||||
// 1. Create test file with direct I/O
|
||||
{
|
||||
std::unique_ptr<FSWritableFile> wfile;
|
||||
FileOptions file_opts;
|
||||
file_opts.use_direct_writes = true;
|
||||
ASSERT_OK(fs->NewWritableFile(fname, file_opts, &wfile, nullptr));
|
||||
|
||||
size_t num_sectors = kFileSize / kSectorSize;
|
||||
for (size_t i = 0; i < num_sectors; ++i) {
|
||||
auto data = NewAligned(kSectorSize, static_cast<char>(i + 1));
|
||||
Slice slice(data.get(), kSectorSize);
|
||||
ASSERT_OK(wfile->Append(slice, IOOptions(), nullptr));
|
||||
}
|
||||
ASSERT_OK(wfile->Close(IOOptions(), nullptr));
|
||||
}
|
||||
|
||||
// 2. Submit 3 ReadAsync requests, abort only the first one, then Poll the
|
||||
// rest
|
||||
{
|
||||
FileOptions file_opts;
|
||||
file_opts.use_direct_reads = true;
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
ASSERT_OK(fs->NewRandomAccessFile(fname, file_opts, &file, nullptr));
|
||||
|
||||
IOOptions opts;
|
||||
constexpr size_t kNumReads = 3;
|
||||
std::vector<void*> io_handles(kNumReads);
|
||||
std::vector<FSReadRequest> reqs(kNumReads);
|
||||
std::vector<std::unique_ptr<char, Deleter>> data;
|
||||
std::vector<size_t> vals;
|
||||
IOHandleDeleter del_fn;
|
||||
std::atomic<int> callbacks_invoked{0};
|
||||
|
||||
// H0: 1MB read, H1: 4KB read, H2: 4KB read
|
||||
std::vector<std::pair<uint64_t, size_t>> read_specs = {
|
||||
{0, 1024 * 1024}, // H0: 1MB at offset 0
|
||||
{1024 * 1024, 4096}, // H1: 4KB at offset 1MB
|
||||
{1024 * 1024 + 4096, 4096}, // H2: 4KB at offset 1MB+4KB
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < kNumReads; i++) {
|
||||
reqs[i].offset = read_specs[i].first;
|
||||
reqs[i].len = read_specs[i].second;
|
||||
data.emplace_back(NewAligned(reqs[i].len, 0));
|
||||
reqs[i].scratch = data.back().get();
|
||||
vals.push_back(i);
|
||||
}
|
||||
|
||||
std::function<void(FSReadRequest&, void*)> callback =
|
||||
[&](FSReadRequest& req, void* cb_arg) {
|
||||
size_t i = *(reinterpret_cast<size_t*>(cb_arg));
|
||||
reqs[i].status = req.status;
|
||||
callbacks_invoked++;
|
||||
};
|
||||
|
||||
// Submit all ReadAsync requests
|
||||
for (size_t i = 0; i < kNumReads; i++) {
|
||||
void* cb_arg = static_cast<void*>(&(vals[i]));
|
||||
IOStatus s = file->ReadAsync(reqs[i], opts, callback, cb_arg,
|
||||
&(io_handles[i]), &del_fn, nullptr);
|
||||
if (s.IsNotSupported()) {
|
||||
// io_uring not supported, clean up and skip
|
||||
for (size_t j = 0; j < i; j++) {
|
||||
if (io_handles[j]) {
|
||||
del_fn(io_handles[j]);
|
||||
}
|
||||
}
|
||||
ASSERT_OK(fs->DeleteFile(fname, IOOptions(), nullptr));
|
||||
return;
|
||||
}
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
// Wait for reads to complete in io_uring (completions in queue but not
|
||||
// consumed). 5 seconds should be plenty for direct I/O reads to complete.
|
||||
std::this_thread::sleep_for(std::chrono::seconds(5));
|
||||
|
||||
// Abort ONLY H0 - this will consume all completions but should correctly
|
||||
// finalize H1 and H2 (since they're not in the abort set).
|
||||
std::vector<void*> abort_handles = {io_handles[0]};
|
||||
ASSERT_OK(fs->AbortIO(abort_handles));
|
||||
|
||||
// Verify H0 is finished (aborted)
|
||||
Posix_IOHandle* h0 = static_cast<Posix_IOHandle*>(io_handles[0]);
|
||||
ASSERT_TRUE(h0->is_finished);
|
||||
ASSERT_EQ(h0->req_count, 2u); // original + cancel
|
||||
|
||||
// Note: H1 and H2 may or may not be finished at this point. AbortIO
|
||||
// finalizes non-aborted handles whose CQEs arrive while waiting for
|
||||
// aborted handles, but CQE ordering is non-deterministic. If H0's
|
||||
// completions arrived first, H1/H2's CQEs are still in the queue.
|
||||
// Poll handles either case correctly.
|
||||
|
||||
// Poll on H1, H2 - completes them if not already finalized by AbortIO
|
||||
std::vector<void*> poll_handles = {io_handles[1], io_handles[2]};
|
||||
|
||||
// Use a watchdog to detect hang (regression test for the original bug
|
||||
// where AbortIO consumed non-aborted CQEs without finalizing them)
|
||||
std::atomic<bool> poll_completed{false};
|
||||
std::thread watchdog([&]() {
|
||||
for (int i = 0; i < 500; i++) { // 5 seconds timeout
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
if (poll_completed) return;
|
||||
}
|
||||
// Bug regression: Poll hung
|
||||
_exit(1);
|
||||
});
|
||||
|
||||
fs->Poll(poll_handles, poll_handles.size());
|
||||
poll_completed = true;
|
||||
watchdog.join();
|
||||
|
||||
// After Poll, H1 and H2 must be finished
|
||||
Posix_IOHandle* h1 = static_cast<Posix_IOHandle*>(io_handles[1]);
|
||||
Posix_IOHandle* h2 = static_cast<Posix_IOHandle*>(io_handles[2]);
|
||||
ASSERT_TRUE(h1->is_finished);
|
||||
ASSERT_TRUE(h2->is_finished);
|
||||
|
||||
// Verify all callbacks were invoked
|
||||
ASSERT_EQ(callbacks_invoked.load(), 3);
|
||||
|
||||
// Clean up handles
|
||||
for (size_t i = 0; i < kNumReads; i++) {
|
||||
if (io_handles[i]) {
|
||||
del_fn(io_handles[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_OK(fs->DeleteFile(fname, IOOptions(), nullptr));
|
||||
#else
|
||||
(void)env_; // Suppress unused variable warning
|
||||
#endif
|
||||
}
|
||||
|
||||
struct StaticDestructionTester {
|
||||
bool activated = false;
|
||||
~StaticDestructionTester() {
|
||||
|
||||
Vendored
+35
-43
@@ -270,10 +270,7 @@ class PosixFileSystem : public FileSystem {
|
||||
options
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
,
|
||||
!IsIOUringEnabled() ? nullptr
|
||||
: thread_local_async_read_io_urings_.get(),
|
||||
!IsIOUringEnabled() ? nullptr
|
||||
: thread_local_multi_read_io_urings_.get()
|
||||
!IsIOUringEnabled() ? nullptr : thread_local_io_urings_.get()
|
||||
#endif
|
||||
));
|
||||
}
|
||||
@@ -1090,9 +1087,8 @@ class PosixFileSystem : public FileSystem {
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
// io_uring_queue_init.
|
||||
struct io_uring* iu = nullptr;
|
||||
if (thread_local_async_read_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(
|
||||
thread_local_async_read_io_urings_->Get());
|
||||
if (thread_local_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(thread_local_io_urings_->Get());
|
||||
}
|
||||
|
||||
// Init failed, platform doesn't support io_uring.
|
||||
@@ -1129,7 +1125,25 @@ class PosixFileSystem : public FileSystem {
|
||||
// Reset cqe data to catch any stray reuse of it
|
||||
static_cast<struct io_uring_cqe*>(cqe)->user_data = 0xd5d5d5d5d5d5d5d5;
|
||||
|
||||
FinalizeAsyncRead(iu, cqe, posix_handle);
|
||||
FSReadRequest req;
|
||||
req.scratch = posix_handle->scratch;
|
||||
req.offset = posix_handle->offset;
|
||||
req.len = posix_handle->len;
|
||||
|
||||
size_t finished_len = 0;
|
||||
size_t bytes_read = 0;
|
||||
bool read_again = false;
|
||||
UpdateResult(cqe, "", req.len, posix_handle->iov.iov_len,
|
||||
true /*async_read*/, posix_handle->use_direct_io,
|
||||
posix_handle->alignment, finished_len, &req, bytes_read,
|
||||
read_again);
|
||||
posix_handle->is_finished = true;
|
||||
io_uring_cqe_seen(iu, cqe);
|
||||
posix_handle->cb(req, posix_handle->cb_arg);
|
||||
|
||||
(void)finished_len;
|
||||
(void)bytes_read;
|
||||
(void)read_again;
|
||||
|
||||
if (static_cast<Posix_IOHandle*>(io_handles[i]) == posix_handle) {
|
||||
break;
|
||||
@@ -1147,9 +1161,8 @@ class PosixFileSystem : public FileSystem {
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
// io_uring_queue_init.
|
||||
struct io_uring* iu = nullptr;
|
||||
if (thread_local_async_read_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(
|
||||
thread_local_async_read_io_urings_->Get());
|
||||
if (thread_local_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(thread_local_io_urings_->Get());
|
||||
}
|
||||
|
||||
// Init failed, platform doesn't support io_uring.
|
||||
@@ -1170,11 +1183,6 @@ class PosixFileSystem : public FileSystem {
|
||||
return IOStatus::IOError("");
|
||||
}
|
||||
|
||||
// Mark this handle as being aborted. This is used when processing
|
||||
// completions to distinguish between aborted handles (expect 2
|
||||
// completions: original + cancel) and non-aborted handles (expect 1).
|
||||
posix_handle->is_being_aborted = true;
|
||||
|
||||
// Prepare the cancel request.
|
||||
struct io_uring_sqe* sqe;
|
||||
sqe = io_uring_get_sqe(iu);
|
||||
@@ -1221,14 +1229,6 @@ class PosixFileSystem : public FileSystem {
|
||||
}
|
||||
posix_handle->req_count++;
|
||||
|
||||
if (!posix_handle->is_being_aborted) {
|
||||
// This is a completion for a handle NOT being aborted.
|
||||
// It only has 1 outstanding request (the original read), so we
|
||||
// should finalize it now.
|
||||
FinalizeAsyncRead(iu, cqe, posix_handle);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset cqe data to catch any stray reuse of it
|
||||
static_cast<struct io_uring_cqe*>(cqe)->user_data = 0xd5d5d5d5d5d5d5d5;
|
||||
io_uring_cqe_seen(iu, cqe);
|
||||
@@ -1242,23 +1242,16 @@ class PosixFileSystem : public FileSystem {
|
||||
// - And finally, if the request to cancel wasn't
|
||||
// found, the cancel request is completed with -ENOENT.
|
||||
//
|
||||
// Every handle being aborted has to wait for 2 requests completion:
|
||||
// original one and the cancel request which is tracked by
|
||||
// PosixHandle::req_count.
|
||||
// Note: We must mark is_finished and invoke the callback for ANY handle
|
||||
// that reaches req_count == 2, not just the one we're currently waiting
|
||||
// for (io_handles[i]). Otherwise, if completions arrive out of order,
|
||||
// we consume another handle's completions without marking it finished,
|
||||
// causing an infinite hang when we later wait for that handle.
|
||||
if (posix_handle->req_count == 2) {
|
||||
// Every handle has to wait for 2 requests completion: original one and
|
||||
// the cancel request which is tracked by PosixHandle::req_count.
|
||||
if (posix_handle->req_count == 2 &&
|
||||
static_cast<Posix_IOHandle*>(io_handles[i]) == posix_handle) {
|
||||
posix_handle->is_finished = true;
|
||||
FSReadRequest req;
|
||||
req.status = IOStatus::Aborted();
|
||||
posix_handle->cb(req, posix_handle->cb_arg);
|
||||
|
||||
if (static_cast<Posix_IOHandle*>(io_handles[i]) == posix_handle) {
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1274,7 +1267,7 @@ class PosixFileSystem : public FileSystem {
|
||||
void SupportedOps(int64_t& supported_ops) override {
|
||||
supported_ops = 0;
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
if (IsIOUringEnabled() && thread_local_async_read_io_urings_) {
|
||||
if (IsIOUringEnabled()) {
|
||||
// Underlying FS supports async_io
|
||||
supported_ops |= (1 << FSSupportedOps::kAsyncIO);
|
||||
}
|
||||
@@ -1284,8 +1277,7 @@ class PosixFileSystem : public FileSystem {
|
||||
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
// io_uring instance
|
||||
std::unique_ptr<ThreadLocalPtr> thread_local_async_read_io_urings_;
|
||||
std::unique_ptr<ThreadLocalPtr> thread_local_multi_read_io_urings_;
|
||||
std::unique_ptr<ThreadLocalPtr> thread_local_io_urings_;
|
||||
#endif
|
||||
|
||||
size_t page_size_;
|
||||
@@ -1340,12 +1332,12 @@ PosixFileSystem::PosixFileSystem()
|
||||
page_size_(getpagesize()),
|
||||
allow_non_owner_access_(true) {
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
// Test whether IOUring is supported with the same flags that ReadAsync and
|
||||
// MultiRead will use at runtime.
|
||||
// Test whether IOUring is supported, and if it does, create a managing
|
||||
// object for thread local point so that in the future thread-local
|
||||
// io_uring can be created.
|
||||
struct io_uring* new_io_uring = CreateIOUring();
|
||||
if (new_io_uring != nullptr) {
|
||||
thread_local_async_read_io_urings_.reset(new ThreadLocalPtr(DeleteIOUring));
|
||||
thread_local_multi_read_io_urings_.reset(new ThreadLocalPtr(DeleteIOUring));
|
||||
thread_local_io_urings_.reset(new ThreadLocalPtr(DeleteIOUring));
|
||||
delete new_io_uring;
|
||||
}
|
||||
#endif
|
||||
|
||||
Vendored
+111
-296
@@ -589,8 +589,7 @@ PosixRandomAccessFile::PosixRandomAccessFile(
|
||||
const EnvOptions& options
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
,
|
||||
ThreadLocalPtr* thread_local_async_read_io_urings,
|
||||
ThreadLocalPtr* thread_local_multi_read_io_urings
|
||||
ThreadLocalPtr* thread_local_io_urings
|
||||
#endif
|
||||
)
|
||||
: filename_(fname),
|
||||
@@ -599,8 +598,7 @@ PosixRandomAccessFile::PosixRandomAccessFile(
|
||||
logical_sector_size_(logical_block_size)
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
,
|
||||
thread_local_async_read_io_urings_(thread_local_async_read_io_urings),
|
||||
thread_local_multi_read_io_urings_(thread_local_multi_read_io_urings)
|
||||
thread_local_io_urings_(thread_local_io_urings)
|
||||
#endif
|
||||
{
|
||||
assert(!options.use_direct_reads || !options.use_mmap_reads);
|
||||
@@ -661,83 +659,6 @@ 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) {
|
||||
@@ -751,13 +672,12 @@ IOStatus PosixRandomAccessFile::MultiRead(FSReadRequest* reqs, size_t num_reqs,
|
||||
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
struct io_uring* iu = nullptr;
|
||||
if (thread_local_multi_read_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(
|
||||
thread_local_multi_read_io_urings_->Get());
|
||||
if (thread_local_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(thread_local_io_urings_->Get());
|
||||
if (iu == nullptr) {
|
||||
iu = CreateIOUring();
|
||||
if (iu != nullptr) {
|
||||
thread_local_multi_read_io_urings_->Reset(iu);
|
||||
thread_local_io_urings_->Reset(iu);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -768,6 +688,8 @@ 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;
|
||||
@@ -776,199 +698,118 @@ IOStatus PosixRandomAccessFile::MultiRead(FSReadRequest* reqs, size_t num_reqs,
|
||||
};
|
||||
|
||||
autovector<WrappedReadRequest, 32> req_wraps;
|
||||
autovector<WrappedReadRequest*, 4> resubmit_rq_list;
|
||||
autovector<WrappedReadRequest*, 4> incomplete_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) || !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];
|
||||
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];
|
||||
} else {
|
||||
req = &req_wraps[reqs_off++];
|
||||
rep_to_submit = &req_wraps[reqs_off++];
|
||||
}
|
||||
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;
|
||||
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;
|
||||
|
||||
struct io_uring_sqe* sqe;
|
||||
sqe = io_uring_get_sqe(iu);
|
||||
// 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);
|
||||
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);
|
||||
}
|
||||
resubmit_rq_list.clear();
|
||||
incomplete_rq_list.clear();
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (0 == ret) {
|
||||
// This scenario is unexpected for any modern kernel!
|
||||
// We deliberately error out to avoid bugs around infinite loops.
|
||||
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);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
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: "
|
||||
"io_uring_submit_and_wait returned 0 submissions!\n");
|
||||
break;
|
||||
"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;
|
||||
}
|
||||
reqs_submitted += static_cast<unsigned int>(ret);
|
||||
};
|
||||
reqs_pending_submission = io_uring_sq_ready(iu);
|
||||
wrap_cache.erase(wrap_check);
|
||||
|
||||
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) {
|
||||
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) {
|
||||
if (read_again) {
|
||||
Slice tmp_slice;
|
||||
req->status =
|
||||
@@ -978,15 +819,14 @@ 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) {
|
||||
resubmit_rq_list.push_back(req_wrap);
|
||||
incomplete_rq_list.push_back(req_wrap);
|
||||
}
|
||||
}
|
||||
io_uring_cqe_seen(iu, cqe);
|
||||
}
|
||||
if (nr > 0) {
|
||||
io_uring_cq_advance(iu, nr);
|
||||
}
|
||||
wrap_cache.clear();
|
||||
}
|
||||
return ios;
|
||||
#else
|
||||
@@ -1083,13 +923,12 @@ IOStatus PosixRandomAccessFile::ReadAsync(
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
// io_uring_queue_init.
|
||||
struct io_uring* iu = nullptr;
|
||||
if (thread_local_async_read_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(
|
||||
thread_local_async_read_io_urings_->Get());
|
||||
if (thread_local_io_urings_) {
|
||||
iu = static_cast<struct io_uring*>(thread_local_io_urings_->Get());
|
||||
if (iu == nullptr) {
|
||||
iu = CreateIOUring();
|
||||
if (iu != nullptr) {
|
||||
thread_local_async_read_io_urings_->Reset(iu);
|
||||
thread_local_io_urings_->Reset(iu);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1127,35 +966,11 @@ IOStatus PosixRandomAccessFile::ReadAsync(
|
||||
io_uring_sqe_set_data(sqe, posix_handle);
|
||||
|
||||
// Step 4: io_uring_submit
|
||||
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);
|
||||
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));
|
||||
}
|
||||
return IOStatus::OK();
|
||||
#else
|
||||
|
||||
Vendored
+3
-44
@@ -11,16 +11,6 @@
|
||||
#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>
|
||||
|
||||
@@ -127,7 +117,6 @@ struct Posix_IOHandle {
|
||||
use_direct_io(_use_direct_io),
|
||||
alignment(_alignment),
|
||||
is_finished(false),
|
||||
is_being_aborted(false),
|
||||
req_count(0) {}
|
||||
|
||||
struct iovec iov;
|
||||
@@ -140,10 +129,6 @@ struct Posix_IOHandle {
|
||||
bool use_direct_io;
|
||||
size_t alignment;
|
||||
bool is_finished;
|
||||
// is_being_aborted is set by AbortIO when a cancel request is submitted.
|
||||
// Used to distinguish between aborted handles (expect 2 completions) and
|
||||
// non-aborted handles (expect 1 completion) when processing completions.
|
||||
bool is_being_aborted;
|
||||
// req_count is used by AbortIO API to keep track of number of requests.
|
||||
uint32_t req_count;
|
||||
};
|
||||
@@ -202,27 +187,6 @@ inline void UpdateResult(struct io_uring_cqe* cqe, const std::string& file_name,
|
||||
(void)len;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Finalize a completed async read request.
|
||||
// Processes the CQE result, marks the handle as finished, and invokes the
|
||||
// callback. This is shared between Poll and AbortIO (for non-aborted handles).
|
||||
inline void FinalizeAsyncRead(struct io_uring* iu, struct io_uring_cqe* cqe,
|
||||
Posix_IOHandle* posix_handle) {
|
||||
FSReadRequest req;
|
||||
req.scratch = posix_handle->scratch;
|
||||
req.offset = posix_handle->offset;
|
||||
req.len = posix_handle->len;
|
||||
|
||||
size_t finished_len = 0;
|
||||
size_t bytes_read = 0;
|
||||
bool read_again = false;
|
||||
UpdateResult(cqe, "", req.len, posix_handle->iov.iov_len, true /*async_read*/,
|
||||
posix_handle->use_direct_io, posix_handle->alignment,
|
||||
finished_len, &req, bytes_read, read_again);
|
||||
posix_handle->is_finished = true;
|
||||
io_uring_cqe_seen(iu, cqe);
|
||||
posix_handle->cb(req, posix_handle->cb_arg);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef OS_LINUX
|
||||
@@ -335,10 +299,7 @@ inline void DeleteIOUring(void* p) {
|
||||
|
||||
inline struct io_uring* CreateIOUring() {
|
||||
struct io_uring* new_io_uring = new struct io_uring;
|
||||
unsigned int flags = 0;
|
||||
flags |= IORING_SETUP_SINGLE_ISSUER;
|
||||
flags |= IORING_SETUP_DEFER_TASKRUN;
|
||||
int ret = io_uring_queue_init(kIoUringDepth, new_io_uring, flags);
|
||||
int ret = io_uring_queue_init(kIoUringDepth, new_io_uring, 0);
|
||||
if (ret) {
|
||||
delete new_io_uring;
|
||||
new_io_uring = nullptr;
|
||||
@@ -354,8 +315,7 @@ class PosixRandomAccessFile : public FSRandomAccessFile {
|
||||
bool use_direct_io_;
|
||||
size_t logical_sector_size_;
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
ThreadLocalPtr* thread_local_async_read_io_urings_;
|
||||
ThreadLocalPtr* thread_local_multi_read_io_urings_;
|
||||
ThreadLocalPtr* thread_local_io_urings_;
|
||||
#endif
|
||||
|
||||
public:
|
||||
@@ -363,8 +323,7 @@ class PosixRandomAccessFile : public FSRandomAccessFile {
|
||||
size_t logical_block_size, const EnvOptions& options
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
,
|
||||
ThreadLocalPtr* thread_local_async_read_io_urings,
|
||||
ThreadLocalPtr* thread_local_multi_read_io_urings
|
||||
ThreadLocalPtr* thread_local_io_urings
|
||||
#endif
|
||||
);
|
||||
virtual ~PosixRandomAccessFile();
|
||||
|
||||
@@ -351,7 +351,7 @@ void FilePrefetchBuffer::ClearOutdatedData(uint64_t offset, size_t length) {
|
||||
assert(IsBufferQueueEmpty() || buf->IsOffsetInBuffer(offset));
|
||||
}
|
||||
|
||||
Status FilePrefetchBuffer::PollIfNeeded(uint64_t offset, size_t length) {
|
||||
void FilePrefetchBuffer::PollIfNeeded(uint64_t offset, size_t length) {
|
||||
BufferInfo* buf = GetFirstBuffer();
|
||||
|
||||
if (buf->async_read_in_progress_ && fs_ != nullptr) {
|
||||
@@ -362,16 +362,7 @@ Status FilePrefetchBuffer::PollIfNeeded(uint64_t offset, size_t length) {
|
||||
std::vector<void*> handles;
|
||||
handles.emplace_back(buf->io_handle_);
|
||||
StopWatch sw(clock_, stats_, POLL_WAIT_MICROS);
|
||||
IOStatus io_s = fs_->Poll(handles, 1);
|
||||
// Allow tests to inject Poll errors
|
||||
TEST_SYNC_POINT_CALLBACK("FilePrefetchBuffer::PollIfNeeded:IOStatus",
|
||||
&io_s);
|
||||
if (!io_s.ok()) {
|
||||
// On Poll failure, clean up the handle and abort.
|
||||
// DestroyAndClearIOHandle also sets async_read_in_progress_ to false.
|
||||
DestroyAndClearIOHandle(buf);
|
||||
return io_s;
|
||||
}
|
||||
fs_->Poll(handles, 1).PermitUncheckedError();
|
||||
}
|
||||
|
||||
// Reset and Release io_handle after the Poll API as request has been
|
||||
@@ -382,7 +373,6 @@ Status FilePrefetchBuffer::PollIfNeeded(uint64_t offset, size_t length) {
|
||||
// Always call outdated data after Poll as Buffers might be out of sync w.r.t
|
||||
// offset and length.
|
||||
ClearOutdatedData(offset, length);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
// ReadAheadSizeTuning API calls readaheadsize_cb_
|
||||
@@ -521,10 +511,7 @@ Status FilePrefetchBuffer::HandleOverlappingAsyncData(
|
||||
// by Seek, but the next access is at another offset.
|
||||
if (buf->async_read_in_progress_ &&
|
||||
buf->IsOffsetInBufferWithAsyncProgress(offset)) {
|
||||
Status poll_status = PollIfNeeded(offset, length);
|
||||
if (!poll_status.ok()) {
|
||||
return poll_status;
|
||||
}
|
||||
PollIfNeeded(offset, length);
|
||||
}
|
||||
|
||||
if (IsBufferQueueEmpty() || NumBuffersAllocated() == 1) {
|
||||
@@ -659,10 +646,7 @@ Status FilePrefetchBuffer::PrefetchInternal(const IOOptions& opts,
|
||||
return s;
|
||||
}
|
||||
} else {
|
||||
Status poll_status = PollIfNeeded(tmp_offset, tmp_length);
|
||||
if (!poll_status.ok()) {
|
||||
return poll_status;
|
||||
}
|
||||
PollIfNeeded(tmp_offset, tmp_length);
|
||||
}
|
||||
|
||||
AllocateBufferIfEmpty();
|
||||
|
||||
@@ -93,8 +93,8 @@ struct BufferInfo {
|
||||
//
|
||||
// For example - if end offset of previous buffer was 100 and because of
|
||||
// readahead_size optimization, end_offset was trimmed to 60. Then for next
|
||||
// prefetch call, start_offset should be initialized to 100 i.e start_offset
|
||||
// = buf->initial_end_offset_.
|
||||
// prefetch call, start_offset should be intialized to 100 i.e start_offset =
|
||||
// buf->initial_end_offset_.
|
||||
uint64_t initial_end_offset_ = 0;
|
||||
|
||||
bool IsDataBlockInBuffer(uint64_t offset, size_t length) {
|
||||
@@ -155,7 +155,7 @@ enum class FilePrefetchBufferUsage {
|
||||
// When reusing the file system allocated buffer, overlap_buf_ is used if the
|
||||
// main buffer only contains part of the requested data. It is returned to
|
||||
// the caller after the remaining data is fetched.
|
||||
// If num_buffers_ > 1, then the data is prefetched asynchronously in the
|
||||
// If num_buffers_ > 1, then the data is prefetched asynchronosuly in the
|
||||
// buffers whenever the data is consumed from the buffers and that buffer is
|
||||
// freed.
|
||||
// If num_buffers > 1, then requested data can be overlapping between 2 buffers.
|
||||
@@ -431,7 +431,7 @@ class FilePrefetchBuffer {
|
||||
void ClearOutdatedData(uint64_t offset, size_t len);
|
||||
|
||||
// It calls Poll API to check for any pending asynchronous request.
|
||||
Status PollIfNeeded(uint64_t offset, size_t len);
|
||||
void PollIfNeeded(uint64_t offset, size_t len);
|
||||
|
||||
Status PrefetchInternal(const IOOptions& opts, RandomAccessFileReader* reader,
|
||||
uint64_t offset, size_t length, size_t readahead_size,
|
||||
|
||||
+2
-8
@@ -178,8 +178,7 @@ IOStatus GenerateOneFileChecksum(
|
||||
std::string* file_checksum_func_name,
|
||||
size_t verify_checksums_readahead_size, bool /*allow_mmap_reads*/,
|
||||
std::shared_ptr<IOTracer>& io_tracer, RateLimiter* rate_limiter,
|
||||
const ReadOptions& read_options, Statistics* stats, SystemClock* clock,
|
||||
const FileOptions& file_options) {
|
||||
const ReadOptions& read_options, Statistics* stats, SystemClock* clock) {
|
||||
if (checksum_factory == nullptr) {
|
||||
return IOStatus::InvalidArgument("Checksum factory is invalid");
|
||||
}
|
||||
@@ -219,12 +218,7 @@ IOStatus GenerateOneFileChecksum(
|
||||
std::unique_ptr<RandomAccessFileReader> reader;
|
||||
{
|
||||
std::unique_ptr<FSRandomAccessFile> r_file;
|
||||
FileOptions fopts = file_options;
|
||||
if (fopts.file_checksum.empty()) {
|
||||
// No expected checksum is known — this is a from-scratch computation.
|
||||
fopts.file_checksum_func_name = kNoFileChecksumFuncName;
|
||||
}
|
||||
io_s = fs->NewRandomAccessFile(file_path, fopts, &r_file, nullptr);
|
||||
io_s = fs->NewRandomAccessFile(file_path, FileOptions(), &r_file, nullptr);
|
||||
if (!io_s.ok()) {
|
||||
return io_s;
|
||||
}
|
||||
|
||||
+1
-2
@@ -83,8 +83,7 @@ IOStatus GenerateOneFileChecksum(
|
||||
std::string* file_checksum_func_name,
|
||||
size_t verify_checksums_readahead_size, bool allow_mmap_reads,
|
||||
std::shared_ptr<IOTracer>& io_tracer, RateLimiter* rate_limiter,
|
||||
const ReadOptions& read_options, Statistics* stats, SystemClock* clock,
|
||||
const FileOptions& file_options);
|
||||
const ReadOptions& read_options, Statistics* stats, SystemClock* clock);
|
||||
|
||||
inline IOStatus PrepareIOFromReadOptions(const ReadOptions& ro,
|
||||
SystemClock* clock, IOOptions& opts,
|
||||
|
||||
+10
-260
@@ -669,7 +669,7 @@ TEST_P(PrefetchTest, ConfigureAutoMaxReadaheadSize) {
|
||||
MoveFilesToLevel(level);
|
||||
}
|
||||
Close();
|
||||
std::vector<int> buff_prefetch_level_count = {0, 0, 0};
|
||||
std::vector<int> buff_prefectch_level_count = {0, 0, 0};
|
||||
ASSERT_OK(TryReopen(options));
|
||||
{
|
||||
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ReadOptions()));
|
||||
@@ -707,7 +707,7 @@ TEST_P(PrefetchTest, ConfigureAutoMaxReadaheadSize) {
|
||||
iter->Next();
|
||||
}
|
||||
|
||||
buff_prefetch_level_count[level] = buff_prefetch_count;
|
||||
buff_prefectch_level_count[level] = buff_prefetch_count;
|
||||
if (support_prefetch && !use_direct_io) {
|
||||
if (level == 0) {
|
||||
ASSERT_FALSE(fs->IsPrefetchCalled());
|
||||
@@ -728,7 +728,7 @@ TEST_P(PrefetchTest, ConfigureAutoMaxReadaheadSize) {
|
||||
}
|
||||
|
||||
if (!support_prefetch) {
|
||||
ASSERT_GT(buff_prefetch_level_count[1], buff_prefetch_level_count[2]);
|
||||
ASSERT_GT(buff_prefectch_level_count[1], buff_prefectch_level_count[2]);
|
||||
}
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
@@ -814,7 +814,7 @@ TEST_P(PrefetchTest, ConfigureInternalAutoReadaheadSize) {
|
||||
"{initial_auto_readahead_size=0;}"}}));
|
||||
break;
|
||||
case 1:
|
||||
// initial_auto_readahead_size and max_auto_readahead_size are set
|
||||
// intial_auto_readahead_size and max_auto_readahead_size are set
|
||||
// same so readahead_size remains same.
|
||||
ASSERT_OK(db_->SetOptions({{"block_based_table_factory",
|
||||
"{initial_auto_readahead_size=4096;max_"
|
||||
@@ -1081,7 +1081,7 @@ TEST_P(PrefetchTest, PrefetchWhenReseek) {
|
||||
}
|
||||
{
|
||||
/*
|
||||
* Reseek keys from Single Data Block.
|
||||
* Reesek keys from Single Data Block.
|
||||
*/
|
||||
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ReadOptions()));
|
||||
iter->Seek(BuildKey(0));
|
||||
@@ -1116,8 +1116,9 @@ TEST_P(PrefetchTest, PrefetchWhenReseek) {
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
iter->Seek(BuildKey(1008));
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
iter->Seek(BuildKey(996)); // Reseek won't prefetch any data and
|
||||
// readahead_size will be initialized to 8*1024.
|
||||
iter->Seek(
|
||||
BuildKey(996)); // Reseek won't prefetch any data and
|
||||
// readahead_size will be initiallized to 8*1024.
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
iter->Seek(BuildKey(992));
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
@@ -1589,7 +1590,7 @@ INSTANTIATE_TEST_CASE_P(
|
||||
::testing::Combine(
|
||||
// Params are as follows -
|
||||
// Param 0 - TableOptions::index_shortening
|
||||
// Param 2 - ReadOptions::auto_readahead_size
|
||||
// Param 2 - ReadOptinos::auto_readahead_size
|
||||
::testing::Values(
|
||||
BlockBasedTableOptions::IndexShorteningMode::kNoShortening,
|
||||
BlockBasedTableOptions::IndexShorteningMode::kShortenSeparators,
|
||||
@@ -2517,187 +2518,6 @@ TEST_P(PrefetchTest1, SeekParallelizationTest) {
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(PrefetchTest1, PollErrorRecoveryDuringIteration) {
|
||||
// This end-to-end test verifies that Poll() errors during async prefetching
|
||||
// are properly propagated to the iterator. When Poll() fails, the iterator
|
||||
// should stop and return an IOError status.
|
||||
//
|
||||
// With error injection on the 3rd Poll call, the iterator reads ~231 keys
|
||||
// (out of 500) before encountering the error.
|
||||
|
||||
if (mem_env_ || encrypted_env_) {
|
||||
ROCKSDB_GTEST_SKIP("Test requires non-mem or non-encrypted environment");
|
||||
return;
|
||||
}
|
||||
|
||||
const int kNumKeys = 500;
|
||||
std::shared_ptr<MockFS> fs = std::make_shared<MockFS>(
|
||||
FileSystem::Default(), /*support_prefetch=*/false);
|
||||
std::unique_ptr<Env> env(new CompositeEnvWrapper(env_, fs));
|
||||
|
||||
bool use_direct_io = GetParam();
|
||||
Options options;
|
||||
SetGenericOptions(env.get(), use_direct_io, options);
|
||||
options.statistics = CreateDBStatistics();
|
||||
BlockBasedTableOptions table_options;
|
||||
SetBlockBasedTableOptions(table_options);
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
|
||||
Status s = TryReopen(options);
|
||||
if (use_direct_io && (s.IsNotSupported() || s.IsInvalidArgument())) {
|
||||
ROCKSDB_GTEST_SKIP("Direct IO not supported");
|
||||
return;
|
||||
}
|
||||
ASSERT_OK(s);
|
||||
|
||||
// Write keys with known values so we can verify correctness
|
||||
std::map<std::string, std::string> expected_data;
|
||||
{
|
||||
WriteBatch batch;
|
||||
for (int i = 0; i < kNumKeys; i++) {
|
||||
std::string key = BuildKey(i);
|
||||
std::string value = "value_" + std::to_string(i) + "_" +
|
||||
std::string(100, 'x'); // Make values ~110 bytes
|
||||
ASSERT_OK(batch.Put(key, value));
|
||||
expected_data[key] = value;
|
||||
}
|
||||
ASSERT_OK(db_->Write(WriteOptions(), &batch));
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
std::string start_key = BuildKey(0);
|
||||
std::string end_key = BuildKey(kNumKeys - 1);
|
||||
Slice least(start_key.data(), start_key.size());
|
||||
Slice greatest(end_key.data(), end_key.size());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &least, &greatest));
|
||||
|
||||
// Set up callbacks to track async IO and inject Poll errors
|
||||
std::atomic<int> poll_call_count{0};
|
||||
std::atomic<int> poll_error_injected_count{0};
|
||||
bool read_async_called = false;
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"FilePrefetchBuffer::PollIfNeeded:IOStatus", [&](void* arg) {
|
||||
poll_call_count++;
|
||||
int current_count = poll_call_count.load();
|
||||
|
||||
// Inject error on the third Poll call to allow some keys to be read
|
||||
// first
|
||||
if (current_count == 3) {
|
||||
IOStatus* io_s = static_cast<IOStatus*>(arg);
|
||||
*io_s = IOStatus::IOError("Injected Poll error for e2e testing");
|
||||
poll_error_injected_count++;
|
||||
std::cout << "PollErrorRecoveryDuringIteration: Injected error on "
|
||||
"Poll call #"
|
||||
<< current_count << std::endl;
|
||||
}
|
||||
});
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"UpdateResults::io_uring_result",
|
||||
[&](void* /*arg*/) { read_async_called = true; });
|
||||
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Iterate through all keys with async IO enabled
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.adaptive_readahead = true;
|
||||
|
||||
int keys_read = 0;
|
||||
int data_mismatches = 0;
|
||||
Status iter_status;
|
||||
{
|
||||
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ro));
|
||||
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
|
||||
std::string key = iter->key().ToString();
|
||||
std::string value = iter->value().ToString();
|
||||
|
||||
auto it = expected_data.find(key);
|
||||
if (it == expected_data.end()) {
|
||||
std::cout << "PollErrorRecoveryDuringIteration: Unexpected key: " << key
|
||||
<< std::endl;
|
||||
data_mismatches++;
|
||||
} else if (it->second != value) {
|
||||
std::cout << "PollErrorRecoveryDuringIteration: Value mismatch for key "
|
||||
<< key << std::endl;
|
||||
data_mismatches++;
|
||||
}
|
||||
keys_read++;
|
||||
}
|
||||
iter_status = iter->status();
|
||||
}
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// Log results
|
||||
std::cout << "PollErrorRecoveryDuringIteration: " << "read_async_called="
|
||||
<< read_async_called << ", poll_calls=" << poll_call_count.load()
|
||||
<< ", poll_errors_injected=" << poll_error_injected_count.load()
|
||||
<< ", keys_read=" << keys_read << ", expected_keys=" << kNumKeys
|
||||
<< ", data_mismatches=" << data_mismatches
|
||||
<< ", iter_status=" << iter_status.ToString() << std::endl;
|
||||
|
||||
// Verify no data mismatches occurred for keys that were read
|
||||
ASSERT_EQ(data_mismatches, 0)
|
||||
<< "Found " << data_mismatches << " data mismatches";
|
||||
|
||||
if (read_async_called) {
|
||||
// Async IO was used - verify Poll error was injected and propagated
|
||||
ASSERT_EQ(poll_call_count.load(), 3)
|
||||
<< "Expected exactly 3 Poll calls when error injected on 3rd call";
|
||||
ASSERT_EQ(poll_error_injected_count.load(), 1)
|
||||
<< "Expected exactly 1 Poll error to be injected";
|
||||
|
||||
// The iterator should have stopped with an error status
|
||||
ASSERT_TRUE(iter_status.IsIOError())
|
||||
<< "Expected iterator to report IOError after Poll failure, got: "
|
||||
<< iter_status.ToString();
|
||||
|
||||
std::cout << "PollErrorRecoveryDuringIteration: Successfully verified "
|
||||
"Poll error was injected and propagated to iterator"
|
||||
<< std::endl;
|
||||
} else {
|
||||
// Async IO not supported - iterator should complete successfully
|
||||
ASSERT_OK(iter_status);
|
||||
ASSERT_EQ(keys_read, kNumKeys);
|
||||
std::cout << "PollErrorRecoveryDuringIteration: Async IO (io_uring) not "
|
||||
"supported on this platform, verified data correctness"
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
// Retry iteration without error injection - verify all data is still readable
|
||||
// This confirms the Poll error didn't corrupt state
|
||||
{
|
||||
int retry_keys_read = 0;
|
||||
int retry_data_mismatches = 0;
|
||||
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ro));
|
||||
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
|
||||
std::string key = iter->key().ToString();
|
||||
std::string value = iter->value().ToString();
|
||||
|
||||
auto it = expected_data.find(key);
|
||||
if (it == expected_data.end()) {
|
||||
retry_data_mismatches++;
|
||||
} else if (it->second != value) {
|
||||
retry_data_mismatches++;
|
||||
}
|
||||
retry_keys_read++;
|
||||
}
|
||||
ASSERT_OK(iter->status())
|
||||
<< "Retry iteration failed: " << iter->status().ToString();
|
||||
ASSERT_EQ(retry_keys_read, kNumKeys)
|
||||
<< "Retry should read all " << kNumKeys << " keys";
|
||||
ASSERT_EQ(retry_data_mismatches, 0)
|
||||
<< "Retry found " << retry_data_mismatches << " data mismatches";
|
||||
std::cout << "PollErrorRecoveryDuringIteration: Retry succeeded, read all "
|
||||
<< retry_keys_read << " keys correctly" << std::endl;
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
namespace {
|
||||
#ifdef GFLAGS
|
||||
const int kMaxArgCount = 100;
|
||||
@@ -3483,7 +3303,7 @@ TEST_F(FilePrefetchBufferTest, SyncReadaheadStats) {
|
||||
ASSERT_EQ(stats->getAndResetTickerCount(PREFETCH_HITS), 1);
|
||||
ASSERT_EQ(stats->getAndResetTickerCount(PREFETCH_BYTES_USEFUL), 8192);
|
||||
|
||||
// Now read some data with length doesn't align with alignment and it needs
|
||||
// Now read some data with length doesn't align with aligment and it needs
|
||||
// prefetching. Read from 16000 with length 10000 (i.e. requested end offset -
|
||||
// 26000).
|
||||
ASSERT_TRUE(
|
||||
@@ -3557,76 +3377,6 @@ TEST_F(FilePrefetchBufferTest, ForCompaction) {
|
||||
0);
|
||||
}
|
||||
|
||||
TEST_F(FilePrefetchBufferTest, PollErrorPropagation) {
|
||||
// This test verifies that Poll() errors in PollIfNeeded are properly
|
||||
// propagated rather than being silently ignored.
|
||||
|
||||
std::string fname = "poll-error-test";
|
||||
Random rand(0);
|
||||
std::string content = rand.RandomString(32768);
|
||||
Write(fname, content);
|
||||
|
||||
FileOptions opts;
|
||||
std::unique_ptr<RandomAccessFileReader> r;
|
||||
Read(fname, opts, &r);
|
||||
|
||||
// Set up readahead params for async prefetching
|
||||
ReadaheadParams readahead_params;
|
||||
readahead_params.initial_readahead_size = 16384;
|
||||
readahead_params.max_readahead_size = 16384;
|
||||
|
||||
FilePrefetchBuffer fpb(readahead_params, /*enable=*/true,
|
||||
/*track_min_offset=*/false, fs());
|
||||
|
||||
Slice result;
|
||||
// Start an async prefetch to set up async_read_in_progress_ state
|
||||
Status s = fpb.PrefetchAsync(IOOptions(), r.get(), 0, 4096, &result);
|
||||
|
||||
// Skip test on platforms that don't support async IO
|
||||
if (s.IsNotSupported()) {
|
||||
ROCKSDB_GTEST_SKIP("Async IO not supported on this platform");
|
||||
return;
|
||||
}
|
||||
ASSERT_TRUE(s.IsTryAgain());
|
||||
std::cout << "PollErrorPropagation: Async IO supported, proceeding with test"
|
||||
<< std::endl;
|
||||
|
||||
// Set up SyncPoint to inject Poll error
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"FilePrefetchBuffer::PollIfNeeded:IOStatus", [&](void* arg) {
|
||||
IOStatus* io_s = static_cast<IOStatus*>(arg);
|
||||
*io_s = IOStatus::IOError("Injected Poll error for testing");
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// TryReadFromCache will call PollIfNeeded to complete the async read
|
||||
IOOptions io_opts;
|
||||
io_opts.rate_limiter_priority = Env::IOPriority::IO_LOW;
|
||||
Status read_status;
|
||||
bool found =
|
||||
fpb.TryReadFromCache(io_opts, r.get(), 0, 4096, &result, &read_status);
|
||||
|
||||
// When PollIfNeeded fails:
|
||||
// 1. PrefetchInternal returns the error status
|
||||
// 2. TryReadFromCacheUntracked sets *status to the error and returns false
|
||||
// Therefore: found should be false, and read_status should contain the error
|
||||
ASSERT_FALSE(found) << "Expected TryReadFromCache to return false on Poll "
|
||||
"error, but it returned true";
|
||||
ASSERT_TRUE(read_status.IsIOError())
|
||||
<< "Expected IOError status, got: " << read_status.ToString();
|
||||
ASSERT_TRUE(read_status.ToString().find("Injected Poll error") !=
|
||||
std::string::npos)
|
||||
<< "Expected error message to contain 'Injected Poll error', got: "
|
||||
<< read_status.ToString();
|
||||
|
||||
std::cout << "PollErrorPropagation: Poll error correctly propagated - "
|
||||
<< "found=" << found << ", status=" << read_status.ToString()
|
||||
<< std::endl;
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
class FSBufferPrefetchTest
|
||||
: public testing::Test,
|
||||
public ::testing::WithParamInterface<std::tuple<bool, bool>> {
|
||||
|
||||
@@ -66,53 +66,41 @@ inline void RecordIOStats(Statistics* stats, Temperature file_temperature,
|
||||
}
|
||||
|
||||
// record for temperature file
|
||||
switch (file_temperature) {
|
||||
case Temperature::kHot:
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.hot_file_bytes_read, size);
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.hot_file_read_count, 1);
|
||||
RecordTick(stats, HOT_FILE_READ_BYTES, size);
|
||||
RecordTick(stats, HOT_FILE_READ_COUNT, 1);
|
||||
break;
|
||||
case Temperature::kWarm:
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.warm_file_bytes_read, size);
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.warm_file_read_count, 1);
|
||||
RecordTick(stats, WARM_FILE_READ_BYTES, size);
|
||||
RecordTick(stats, WARM_FILE_READ_COUNT, 1);
|
||||
break;
|
||||
case Temperature::kCool:
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.cool_file_bytes_read, size);
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.cool_file_read_count, 1);
|
||||
RecordTick(stats, COOL_FILE_READ_BYTES, size);
|
||||
RecordTick(stats, COOL_FILE_READ_COUNT, 1);
|
||||
break;
|
||||
case Temperature::kCold:
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.cold_file_bytes_read, size);
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.cold_file_read_count, 1);
|
||||
RecordTick(stats, COLD_FILE_READ_BYTES, size);
|
||||
RecordTick(stats, COLD_FILE_READ_COUNT, 1);
|
||||
break;
|
||||
case Temperature::kIce:
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.ice_file_bytes_read, size);
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.ice_file_read_count, 1);
|
||||
RecordTick(stats, ICE_FILE_READ_BYTES, size);
|
||||
RecordTick(stats, ICE_FILE_READ_COUNT, 1);
|
||||
break;
|
||||
case Temperature::kUnknown:
|
||||
if (is_last_level) {
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.unknown_last_level_bytes_read,
|
||||
size);
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.unknown_last_level_read_count,
|
||||
1);
|
||||
} else {
|
||||
IOSTATS_ADD(
|
||||
file_io_stats_by_temperature.unknown_non_last_level_bytes_read,
|
||||
size);
|
||||
IOSTATS_ADD(
|
||||
file_io_stats_by_temperature.unknown_non_last_level_read_count, 1);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
if (file_temperature != Temperature::kUnknown) {
|
||||
switch (file_temperature) {
|
||||
case Temperature::kHot:
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.hot_file_bytes_read, size);
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.hot_file_read_count, 1);
|
||||
RecordTick(stats, HOT_FILE_READ_BYTES, size);
|
||||
RecordTick(stats, HOT_FILE_READ_COUNT, 1);
|
||||
break;
|
||||
case Temperature::kWarm:
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.warm_file_bytes_read, size);
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.warm_file_read_count, 1);
|
||||
RecordTick(stats, WARM_FILE_READ_BYTES, size);
|
||||
RecordTick(stats, WARM_FILE_READ_COUNT, 1);
|
||||
break;
|
||||
case Temperature::kCool:
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.cool_file_bytes_read, size);
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.cool_file_read_count, 1);
|
||||
RecordTick(stats, COOL_FILE_READ_BYTES, size);
|
||||
RecordTick(stats, COOL_FILE_READ_COUNT, 1);
|
||||
break;
|
||||
case Temperature::kCold:
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.cold_file_bytes_read, size);
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.cold_file_read_count, 1);
|
||||
RecordTick(stats, COLD_FILE_READ_BYTES, size);
|
||||
RecordTick(stats, COLD_FILE_READ_COUNT, 1);
|
||||
break;
|
||||
case Temperature::kIce:
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.ice_file_bytes_read, size);
|
||||
IOSTATS_ADD(file_io_stats_by_temperature.ice_file_read_count, 1);
|
||||
RecordTick(stats, ICE_FILE_READ_BYTES, size);
|
||||
RecordTick(stats, ICE_FILE_READ_COUNT, 1);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ endif # FMT_SOURCE_PATH
|
||||
PLATFORM_LDFLAGS += -lglog
|
||||
endif
|
||||
|
||||
FOLLY_COMMIT_HASH = 94a8e82cf16a0e229fc4fc89140219434ba78fa2
|
||||
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
|
||||
@@ -116,27 +116,10 @@ checkout_folly:
|
||||
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
|
||||
@# Restore cached downloads and handle unreliable mirrors with fallback
|
||||
@cd third-party/folly && \
|
||||
DOWNLOAD_DIR=`$(PYTHON) build/fbcode_builder/getdeps.py show-inst-dir | sed 's|/installed/.*|/downloads|'` && \
|
||||
mkdir -p "$$DOWNLOAD_DIR" && \
|
||||
CACHE_DIR="/tmp/rocksdb-getdeps-cache" && \
|
||||
mkdir -p "$$CACHE_DIR" && \
|
||||
echo "Restoring cached downloads..." && \
|
||||
if ls "$$CACHE_DIR"/*.tar.gz "$$CACHE_DIR"/*.tar.xz "$$CACHE_DIR"/*.zip >/dev/null 2>&1; then \
|
||||
cp -n "$$CACHE_DIR"/*.tar.gz "$$CACHE_DIR"/*.tar.xz "$$CACHE_DIR"/*.zip "$$DOWNLOAD_DIR/" 2>/dev/null || true; \
|
||||
fi && \
|
||||
echo "Handling known unreliable downloads with fallback mirrors..." && \
|
||||
$(PYTHON) ../../build_tools/getdeps_fallback_mirror.py "$$DOWNLOAD_DIR" "$$CACHE_DIR" build/fbcode_builder/manifests
|
||||
@# 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
|
||||
@# Update cache with any new downloads
|
||||
@cd third-party/folly && \
|
||||
DOWNLOAD_DIR=`$(PYTHON) build/fbcode_builder/getdeps.py show-inst-dir | sed 's|/installed/.*|/downloads|'` && \
|
||||
CACHE_DIR="/tmp/rocksdb-getdeps-cache" && \
|
||||
if ls "$$DOWNLOAD_DIR"/*.tar.gz "$$DOWNLOAD_DIR"/*.tar.xz "$$DOWNLOAD_DIR"/*.zip >/dev/null 2>&1; then \
|
||||
cp -n "$$DOWNLOAD_DIR"/*.tar.gz "$$DOWNLOAD_DIR"/*.tar.xz "$$DOWNLOAD_DIR"/*.zip "$$CACHE_DIR/" 2>/dev/null || true; \
|
||||
fi
|
||||
|
||||
CXX_M_FLAGS = $(filter -m%, $(CXXFLAGS))
|
||||
|
||||
|
||||
@@ -318,7 +318,7 @@ class Cache : public Customizable {
|
||||
// REQUIRES: handle must have been returned by a method on *this.
|
||||
virtual bool Release(Handle* handle, bool erase_if_last_ref = false) = 0;
|
||||
|
||||
// Return the object associated with a handle returned by a successful
|
||||
// Return the object assiciated with a handle returned by a successful
|
||||
// Lookup(). For historical reasons, this is also known at the "value"
|
||||
// associated with the key.
|
||||
// REQUIRES: handle must not have been released yet.
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <variant>
|
||||
|
||||
#include "rocksdb/cache.h"
|
||||
#include "rocksdb/compression_type.h"
|
||||
#include "rocksdb/data_structure.h"
|
||||
@@ -58,64 +56,7 @@ class Decompressor;
|
||||
// because RocksDB is not exception-safe. This could cause undefined behavior
|
||||
// including data loss, unreported corruption, deadlocks, and more.
|
||||
class Compressor {
|
||||
public: // Auxiliary types
|
||||
// No dictionary should be used (for a given block type).
|
||||
struct DictDisabled {};
|
||||
|
||||
// A recommendation for dictionary compression by collecting samples from
|
||||
// blocks. The caller should collect up to `max_sample_bytes` of sample data
|
||||
// and pass it to MaybeCloneSpecialized() to create a specialized compressor.
|
||||
struct DictSampling {
|
||||
// Maximum total bytes of sample data to collect from blocks.
|
||||
// This controls how much data is buffered before dictionary training.
|
||||
size_t max_sample_bytes = 0;
|
||||
};
|
||||
|
||||
// A pre-defined dictionary that is recommended or specified for direct use
|
||||
// with MaybeCloneSpecialized(), without any sampling.
|
||||
struct DictPreDefined {
|
||||
// The owned raw/serialized dictionary bytes. Recommend std::move to
|
||||
// MaybeCloneSpecialized()
|
||||
std::string dict_data;
|
||||
};
|
||||
|
||||
// The result type for GetDictGuidance() - indicates how dictionary
|
||||
// compression should be configured for a given block type.
|
||||
using DictConfig = std::variant<DictDisabled, DictSampling, DictPreDefined>;
|
||||
|
||||
// Sample data collected from blocks for dictionary training.
|
||||
struct DictSamples {
|
||||
// All the sample input blocks stored contiguously
|
||||
std::string sample_data;
|
||||
// The lengths of each of the sample blocks in `sample_data`
|
||||
std::vector<size_t> sample_lens;
|
||||
|
||||
bool empty() const { return sample_data.empty(); }
|
||||
bool Verify() const {
|
||||
size_t total_len = 0;
|
||||
for (auto len : sample_lens) {
|
||||
total_len += len;
|
||||
}
|
||||
return total_len == sample_data.size();
|
||||
}
|
||||
};
|
||||
|
||||
// Arguments for MaybeCloneSpecialized() - provides either samples, a
|
||||
// pre-defined dictionary, or indicates no dictionary should be used.
|
||||
// NOTE: DictPreDefined here is the same type as above, allowing the
|
||||
// pre-defined dictionary from GetDictGuidance() to be passed through.
|
||||
using DictConfigArgs =
|
||||
std::variant<DictDisabled, DictSamples, DictPreDefined>;
|
||||
|
||||
// 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.
|
||||
// This enables use of ZSTD context, for example.
|
||||
//
|
||||
// EXTENSIBLE or reinterpret_cast-able by custom Compressor implementations
|
||||
struct WorkingArea {};
|
||||
|
||||
public: // Functions
|
||||
public:
|
||||
Compressor() = default;
|
||||
virtual ~Compressor() = default;
|
||||
|
||||
@@ -128,17 +69,15 @@ class Compressor {
|
||||
return id;
|
||||
}
|
||||
|
||||
// Returns the recommended dictionary configuration for the given block type.
|
||||
// See the comments on DictConfig and variants for details.
|
||||
//
|
||||
// NOTE: This may be called on the "base" Compressor returned by
|
||||
// CompressionManager, which is not yet configured with a dictionary,
|
||||
// or it can be skipped by callers not intending to handle dictionary
|
||||
// compression.
|
||||
virtual DictConfig GetDictGuidance(CacheEntryRole block_type) const {
|
||||
// Returns the max total bytes of for all sampled blocks for creating the data
|
||||
// dictionary, or zero indicating dictionary compression should not be
|
||||
// used/configured. This will typically be called after
|
||||
// CompressionManager::GetCompressor() to see if samples should be accumulated
|
||||
// and passed to MaybeCloneSpecialized().
|
||||
virtual size_t GetMaxSampleSizeIfWantDict(CacheEntryRole block_type) const {
|
||||
// Default implementation: no dictionary
|
||||
(void)block_type;
|
||||
return DictDisabled{};
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Returns the serialized form of the data dictionary associated with this
|
||||
@@ -155,32 +94,52 @@ class Compressor {
|
||||
// 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 {
|
||||
// All the sample input blocks stored contiguously
|
||||
std::string sample_data;
|
||||
// The lengths of each of the sample blocks in `sample_data`
|
||||
std::vector<size_t> sample_lens;
|
||||
|
||||
bool empty() { return sample_data.empty(); }
|
||||
bool Verify() {
|
||||
size_t total_len = 0;
|
||||
for (auto len : sample_lens) {
|
||||
total_len += len;
|
||||
}
|
||||
return total_len == sample_data.size();
|
||||
}
|
||||
};
|
||||
|
||||
// Create potential variants of the same Compressor that might be
|
||||
// (a) optimized for a particular block type (does not affect correct
|
||||
// decompression), and/or
|
||||
// (b) configured to use a compression dictionary based on the provided
|
||||
// configuration (samples or pre-defined dictionary). See the comments on
|
||||
// DictConfigArgs and its variants for detail.
|
||||
//
|
||||
// (b) configured to use a compression dictionary, based on the given
|
||||
// samples (decompression must provide the dictionary from
|
||||
// GetSerializedDict())
|
||||
// Return of nullptr indicates no specialization exists or was attempted
|
||||
// and the caller should use the current Compressor for the desired scenario.
|
||||
// Using CacheEntryRole::kMisc for block_type generally means "unspecified".
|
||||
//
|
||||
// The exact dictionary associated with a returned compressor must be read
|
||||
// from GetSerializedDict().
|
||||
// and the caller is best to use the current Compressor for the desired
|
||||
// scenario. Using CacheEntryRole:kMisc for block_type generally means
|
||||
// "unspecified", and both parameters are merely suggestions. The exact
|
||||
// dictionary associated with a returned compressor must be read from
|
||||
// GetSerializedDict().
|
||||
virtual std::unique_ptr<Compressor> MaybeCloneSpecialized(
|
||||
CacheEntryRole block_type, DictConfigArgs&& dict_config) const {
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const {
|
||||
// Default implementation: no specialization
|
||||
(void)block_type;
|
||||
(void)dict_config;
|
||||
(void)dict_samples;
|
||||
// Caller should have checked GetMaxSampleSizeIfWantDict before attempting
|
||||
// to provide dictionary samples
|
||||
assert(dict_samples.empty());
|
||||
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, DictConfigArgs&& dict_config) const {
|
||||
auto clone = MaybeCloneSpecialized(block_type, std::move(dict_config));
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const {
|
||||
auto clone = MaybeCloneSpecialized(block_type, std::move(dict_samples));
|
||||
if (clone == nullptr) {
|
||||
clone = Clone();
|
||||
assert(clone != nullptr);
|
||||
@@ -188,6 +147,14 @@ class Compressor {
|
||||
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.
|
||||
// This enables use of ZSTD context, for example.
|
||||
//
|
||||
// EXTENSIBLE or reinterpret_cast-able by custom Compressor implementations
|
||||
struct WorkingArea {};
|
||||
|
||||
// To allow for flexible re-use / reclaimation, we have explicit Get and
|
||||
// Release functions, and usually wrap in a special RAII smart pointer.
|
||||
// For example, a WorkingArea could be saved/recycled in thread-local or
|
||||
@@ -456,12 +423,6 @@ class CompressionManager
|
||||
// which is valid at the discretion of the CompressionManager. Returning
|
||||
// nullptr should normally be the result if preferred == kNoCompression.
|
||||
//
|
||||
// Compressors returned here are configured WITHOUT a dictionary, so that
|
||||
// it's always possible to get correct compression->decompression results
|
||||
// if not opting-in to dictionary handling. The compressors may recommend
|
||||
// dictionary usage via GetDictGuidance() and creating a modified Compressor
|
||||
// for that. See Compressor::GetDictGuidance() etc. for details.
|
||||
//
|
||||
// These functions must be thread-safe.
|
||||
|
||||
// Get a compressor for an SST file.
|
||||
@@ -516,8 +477,8 @@ class CompressorWrapper : public Compressor {
|
||||
CompressorWrapper(const CompressorWrapper&) = delete;
|
||||
CompressorWrapper& operator=(const CompressorWrapper&) = delete;
|
||||
|
||||
DictConfig GetDictGuidance(CacheEntryRole block_type) const override {
|
||||
return wrapped_->GetDictGuidance(block_type);
|
||||
size_t GetMaxSampleSizeIfWantDict(CacheEntryRole block_type) const override {
|
||||
return wrapped_->GetMaxSampleSizeIfWantDict(block_type);
|
||||
}
|
||||
|
||||
Slice GetSerializedDict() const override {
|
||||
@@ -535,9 +496,9 @@ class CompressorWrapper : public Compressor {
|
||||
// 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, DictConfigArgs&& dict_config) const override {
|
||||
CacheEntryRole block_type, DictSampleArgs&& dict_samples) const override {
|
||||
auto clone =
|
||||
wrapped_->MaybeCloneSpecialized(block_type, std::move(dict_config));
|
||||
wrapped_->MaybeCloneSpecialized(block_type, std::move(dict_samples));
|
||||
// Assert default no-op MaybeCloneSpecialized()
|
||||
assert(clone == nullptr);
|
||||
return clone;
|
||||
@@ -631,14 +592,7 @@ class CompressionManagerWrapper : public CompressionManager {
|
||||
|
||||
std::shared_ptr<CompressionManager> FindCompatibleCompressionManager(
|
||||
Slice compatibility_name) override {
|
||||
// NOTE: We expect that the wrapped CompressionManager will generally
|
||||
// be preferred if compatible, so the default implementation here does
|
||||
// not purely defer to the wrapped instance
|
||||
if (compatibility_name == CompatibilityName()) {
|
||||
return shared_from_this();
|
||||
} else {
|
||||
return wrapped_->FindCompatibleCompressionManager(compatibility_name);
|
||||
}
|
||||
return wrapped_->FindCompatibleCompressionManager(compatibility_name);
|
||||
}
|
||||
|
||||
bool SupportsCompressionType(CompressionType type) const override {
|
||||
|
||||
@@ -86,12 +86,6 @@ typedef struct rocksdb_compactionfiltercontext_t
|
||||
rocksdb_compactionfiltercontext_t;
|
||||
typedef struct rocksdb_compactionfilterfactory_t
|
||||
rocksdb_compactionfilterfactory_t;
|
||||
typedef struct rocksdb_file_checksum_gen_factory_t
|
||||
rocksdb_file_checksum_gen_factory_t;
|
||||
typedef struct rocksdb_sst_partitioner_factory_t
|
||||
rocksdb_sst_partitioner_factory_t;
|
||||
typedef struct rocksdb_table_properties_collector_factory_t
|
||||
rocksdb_table_properties_collector_factory_t;
|
||||
typedef struct rocksdb_comparator_t rocksdb_comparator_t;
|
||||
typedef struct rocksdb_dbpath_t rocksdb_dbpath_t;
|
||||
typedef struct rocksdb_env_t rocksdb_env_t;
|
||||
@@ -172,31 +166,6 @@ 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(
|
||||
@@ -1231,8 +1200,6 @@ rocksdb_block_based_options_set_partition_pinning_tier(
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_block_based_options_set_unpartitioned_pinning_tier(
|
||||
rocksdb_block_based_table_options_t*, int);
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_block_based_options_set_block_align(
|
||||
rocksdb_block_based_table_options_t*, unsigned char);
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_write_buffer_manager(
|
||||
rocksdb_options_t* opt, rocksdb_write_buffer_manager_t* wbm);
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_sst_file_manager(
|
||||
@@ -1479,31 +1446,6 @@ rocksdb_logger_create_callback_logger(int log_level,
|
||||
void* priv);
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_logger_destroy(
|
||||
rocksdb_logger_t* logger);
|
||||
|
||||
/* File Checksum Gen Factory */
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_file_checksum_gen_factory_t*
|
||||
rocksdb_file_checksum_gen_crc32c_factory_create(void);
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_file_checksum_gen_factory_destroy(
|
||||
rocksdb_file_checksum_gen_factory_t* factory);
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_file_checksum_gen_factory(
|
||||
rocksdb_options_t*, rocksdb_file_checksum_gen_factory_t*);
|
||||
|
||||
/* SST Partitioner Factory */
|
||||
extern ROCKSDB_LIBRARY_API rocksdb_sst_partitioner_factory_t*
|
||||
rocksdb_sst_partitioner_fixed_prefix_factory_create(size_t prefix_len);
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_sst_partitioner_factory_destroy(
|
||||
rocksdb_sst_partitioner_factory_t* factory);
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_sst_partitioner_factory(
|
||||
rocksdb_options_t*, rocksdb_sst_partitioner_factory_t*);
|
||||
|
||||
/* Table Properties Collector Factory */
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_table_properties_collector_factory_destroy(
|
||||
rocksdb_table_properties_collector_factory_t* factory);
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_options_add_table_properties_collector_factory(
|
||||
rocksdb_options_t*, rocksdb_table_properties_collector_factory_t*);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_write_buffer_size(
|
||||
rocksdb_options_t*, size_t);
|
||||
extern ROCKSDB_LIBRARY_API size_t
|
||||
@@ -3570,224 +3512,6 @@ extern ROCKSDB_LIBRARY_API unsigned char rocksdb_get_into_buffer_cf(
|
||||
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 rocksdb_compaction_service_options_override_t*
|
||||
rocksdb_compaction_service_options_override_create_from_options(
|
||||
rocksdb_options_t* option);
|
||||
|
||||
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);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_set_merge_operator(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_mergeoperator_t* merge_operator);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_set_compaction_filter(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_compactionfilter_t* compaction_filter);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_set_compaction_filter_factory(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_compactionfilterfactory_t* compaction_filter_factory);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_set_prefix_extractor(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_slicetransform_t* prefix_extractor);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_set_block_based_table_factory(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_block_based_table_options_t* table_options);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_set_cuckoo_table_factory(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_cuckoo_table_options_t* table_options);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_add_event_listener(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_eventlistener_t* event_listener);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_set_statistics(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_options_t* options);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_set_info_log(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_logger_t* logger);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_set_option(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
const char* key, const char* value);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_set_file_checksum_gen_factory(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_file_checksum_gen_factory_t* factory);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_set_sst_partitioner_factory(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_sst_partitioner_factory_t* factory);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_compaction_service_options_override_add_table_properties_collector_factory(
|
||||
rocksdb_compaction_service_options_override_t* override_options,
|
||||
rocksdb_table_properties_collector_factory_t* factory);
|
||||
|
||||
// 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
|
||||
+6
-33
@@ -1105,12 +1105,9 @@ class DB {
|
||||
// details. For optimal performance, ensure that either all entries in
|
||||
// scan_opts specify the range limit, or none of them do.
|
||||
//
|
||||
// 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.
|
||||
// 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")},
|
||||
@@ -1628,39 +1625,15 @@ class DB {
|
||||
// s = db->SetOptions(cfh, {{"block_based_table_factory",
|
||||
// "{prepopulate_block_cache=kDisable;}"}});
|
||||
virtual Status SetOptions(
|
||||
ColumnFamilyHandle* column_family,
|
||||
const std::unordered_map<std::string, std::string>& opts_map) {
|
||||
return SetOptions(std::vector<ColumnFamilyHandle*>{column_family},
|
||||
opts_map);
|
||||
ColumnFamilyHandle* /*column_family*/,
|
||||
const std::unordered_map<std::string, std::string>& /*opts_map*/) {
|
||||
return Status::NotSupported("Not implemented");
|
||||
}
|
||||
// Shortcut for SetOptions on the default column family handle.
|
||||
virtual Status SetOptions(
|
||||
const std::unordered_map<std::string, std::string>& new_options) {
|
||||
return SetOptions(DefaultColumnFamily(), new_options);
|
||||
}
|
||||
// Shortcut where you want to apply the same options to multiple column
|
||||
// families. Beneficial for avoiding reserialization of OPTIONS file.
|
||||
virtual Status SetOptions(
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
const std::unordered_map<std::string, std::string>& opts_map) {
|
||||
std::unordered_map<ColumnFamilyHandle*,
|
||||
std::unordered_map<std::string, std::string>>
|
||||
column_families_opts_map;
|
||||
column_families_opts_map.reserve(column_families.size());
|
||||
for (auto* cf : column_families) {
|
||||
column_families_opts_map[cf] = opts_map;
|
||||
}
|
||||
return SetOptions(column_families_opts_map);
|
||||
}
|
||||
// SetOptions with potentially different options per column family. It is
|
||||
// typically better to batch all option changes together as the OPTIONS file
|
||||
// is written to once per SetOptions call.
|
||||
virtual Status SetOptions(
|
||||
const std::unordered_map<ColumnFamilyHandle*,
|
||||
std::unordered_map<std::string, std::string>>&
|
||||
/*column_families_opts_map*/) {
|
||||
return Status::NotSupported("Not implemented");
|
||||
}
|
||||
|
||||
// Like SetOptions but for DBOptions, including the same caveats for
|
||||
// usability, reliability, and performance. See GetDBOptionsFromMap() (and
|
||||
|
||||
@@ -86,7 +86,7 @@ Status UpdateManifestForFilesState(
|
||||
// keys in a category to return an empty sequence of segments.
|
||||
//
|
||||
// To eliminate a confusing distinction between a segment that is empty vs.
|
||||
// "not present" for a particular key, each key is logically associated with
|
||||
// "not present" for a particular key, each key is logically assiciated with
|
||||
// an infinite sequence of segments, including some infinite tail of 0-length
|
||||
// segments. In practice, we only represent a finite sequence that (at least)
|
||||
// covers the non-trivial segments.
|
||||
@@ -220,7 +220,7 @@ Status UpdateManifestForFilesState(
|
||||
// whole key.
|
||||
// * Range query - Whether there {definitely isn't, might be} any entries
|
||||
// within a lower and upper key bound, in an SST file (or partition, etc.).
|
||||
// NOTE: For this discussion, we ignore the detail of inclusive vs.
|
||||
// NOTE: For this disucssion, we ignore the detail of inclusive vs.
|
||||
// exclusive bounds by assuming a generalized notion of "bound" (vs. key)
|
||||
// that conveniently represents spaces between keys. For details, see
|
||||
// https://github.com/facebook/rocksdb/pull/11434
|
||||
@@ -300,7 +300,7 @@ Status UpdateManifestForFilesState(
|
||||
// * Keys x and z are in categories in category set s, and
|
||||
// * Key y is ordered x < y < z according to the CF comparator,
|
||||
// then both
|
||||
// * The common segment prefix property is satisfied through ordinal i-1
|
||||
// * The common segment prefix property is satisifed through ordinal i-1
|
||||
// and with category set s
|
||||
// * x_i..j <= y_i..j <= z_i..j according to segment comparator c, where
|
||||
// x_i..j is the concatenation of segments i through j of key x (etc.).
|
||||
|
||||
@@ -22,12 +22,7 @@ namespace ROCKSDB_NAMESPACE {
|
||||
// The unknown file checksum.
|
||||
constexpr char kUnknownFileChecksum[] = "";
|
||||
// The unknown sst file checksum function name.
|
||||
// Indicates that the file metadata says that no checksum factory was configured
|
||||
// when the file was written.
|
||||
constexpr char kUnknownFileChecksumFuncName[] = "Unknown";
|
||||
// Used when opening a file and there is no file checksum metadata to propagate
|
||||
// at all.
|
||||
constexpr char kNoFileChecksumFuncName[] = "Unavailable";
|
||||
// The standard DB file checksum function name.
|
||||
// This is the name of the checksum function returned by
|
||||
// GetFileChecksumGenCrc32cFactory();
|
||||
|
||||
Loaded 100 of 170 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user