mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-07 03:22:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26432d79f7 | ||
|
|
557960bd9f | ||
|
|
6720218931 | ||
|
|
7ced0aaac9 | ||
|
|
96012d9954 | ||
|
|
626d471544 | ||
|
|
1129d3510f |
No files matched your search
@@ -268,7 +268,6 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"util/compression.cc",
|
||||
"util/compression_context_cache.cc",
|
||||
"util/concurrent_task_limiter_impl.cc",
|
||||
"util/coro_stats_util.cc",
|
||||
"util/crc32c.cc",
|
||||
"util/crc32c_arm64.cc",
|
||||
"util/data_structure.cc",
|
||||
@@ -307,7 +306,6 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"utilities/compaction_filters.cc",
|
||||
"utilities/compaction_filters/remove_emptyvalue_compactionfilter.cc",
|
||||
"utilities/convenience/info_log_finder.cc",
|
||||
"utilities/copy_engine/copy_engine.cc",
|
||||
"utilities/counted_fs.cc",
|
||||
"utilities/debug.cc",
|
||||
"utilities/env_mirror.cc",
|
||||
@@ -381,21 +379,11 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"utilities/write_batch_with_index/write_batch_with_index_internal.cc",
|
||||
], deps=[
|
||||
"//folly/container:f14_hash",
|
||||
"//folly/coro:baton",
|
||||
"//folly/coro:blocking_wait",
|
||||
"//folly/coro:collect",
|
||||
"//folly/coro:coroutine",
|
||||
"//folly/coro:nothrow",
|
||||
"//folly/coro:task",
|
||||
"//folly/executors/thread_factory:named_thread_factory",
|
||||
"//folly/executors:io_executor",
|
||||
"//folly/executors:io_thread_pool_executor",
|
||||
"//folly/io/async:async_base",
|
||||
"//folly/io/async:event_base_manager",
|
||||
"//folly/io/async:io_uring_backend",
|
||||
"//folly/io/async:request_context",
|
||||
"//folly/synchronization:distributed_mutex",
|
||||
"//folly:executor",
|
||||
], headers=glob(["**/*.h"]), link_whole=False, extra_test_libs=False)
|
||||
|
||||
cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[], deps=[":rocksdb_lib"], headers=[], link_whole=True, extra_test_libs=False)
|
||||
@@ -4755,12 +4743,6 @@ cpp_unittest_wrapper(name="configurable_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="copy_engine_test",
|
||||
srcs=["utilities/copy_engine/copy_engine_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="corruption_test",
|
||||
srcs=["db/corruption_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
|
||||
@@ -8,7 +8,7 @@ This document provides guidance for generating and reviewing code in the RocksDB
|
||||
|
||||
### Code Quality and Maintainability
|
||||
|
||||
**Clarity and Readability:** Write clear, self-documenting code. Use meaningful variable names, add comments for complex logic, and structure code to minimize cognitive load. Avoid clever tricks that sacrifice readability for marginal performance gains unless absolutely necessary. Avoid static_cast, reinterpret_cast, and C-style casts; static_cast_with_check, up_cast, and lossless_cast from cast_util.h are preferred.
|
||||
**Clarity and Readability:** Write clear, self-documenting code. Use meaningful variable names, add comments for complex logic, and structure code to minimize cognitive load. Avoid clever tricks that sacrifice readability for marginal performance gains unless absolutely necessary.
|
||||
|
||||
**Consistent Style:** Follow existing code style conventions. RocksDB uses `.clang-format` for formatting, specific naming conventions, and structural patterns. Deviations from these patterns are frequently flagged in reviews.
|
||||
|
||||
@@ -346,9 +346,6 @@ architectures.
|
||||
* For CI-style flaky tests that do not reproduce with `gtest_parallel.py`,
|
||||
`--gtest_repeat`, or normal coerce-mode runs, inspect
|
||||
`tools/gtest_parallel_repro.py --help`.
|
||||
* Each unit test file has overheads, so avoid creating new unit test files
|
||||
for random minor features. Consider adding to slice_test, db_etc3_test, or
|
||||
others.
|
||||
|
||||
### Unit test dedup guidelines
|
||||
* Extract helper functions for repeated patterns such as object
|
||||
@@ -396,8 +393,6 @@ architectures.
|
||||
|
||||
### Adding release note
|
||||
* Release note should be kept short at high level for external user consumption.
|
||||
* If more than single markdown line, consider how their formatting will be
|
||||
integrated into HISTORY.md.
|
||||
|
||||
### Blog posts (docs/_posts)
|
||||
* Blog post authors must be defined in `docs/_data/authors.yml` to be displayed
|
||||
|
||||
+1
-18
@@ -1002,7 +1002,6 @@ set(SOURCES
|
||||
util/simple_mixed_compressor.cc
|
||||
util/compression_context_cache.cc
|
||||
util/concurrent_task_limiter_impl.cc
|
||||
util/coro_stats_util.cc
|
||||
util/crc32c.cc
|
||||
util/data_structure.cc
|
||||
util/dynamic_bloom.cc
|
||||
@@ -1024,7 +1023,6 @@ set(SOURCES
|
||||
util/xxhash.cc
|
||||
utilities/agg_merge/agg_merge.cc
|
||||
utilities/backup/backup_engine.cc
|
||||
utilities/copy_engine/copy_engine.cc
|
||||
utilities/blob_db/blob_compaction_filter.cc
|
||||
utilities/blob_db/blob_db.cc
|
||||
utilities/blob_db/blob_db_impl.cc
|
||||
@@ -1642,7 +1640,6 @@ if(WITH_TESTS)
|
||||
utilities/cassandra/cassandra_row_merge_test.cc
|
||||
utilities/cassandra/cassandra_serialize_test.cc
|
||||
utilities/checkpoint/checkpoint_test.cc
|
||||
utilities/copy_engine/copy_engine_test.cc
|
||||
utilities/sorted_run_builder/sorted_run_builder_test.cc
|
||||
utilities/env_timed_test.cc
|
||||
utilities/fault_injection_fs_test.cc
|
||||
@@ -1710,21 +1707,7 @@ if(WITH_TESTS)
|
||||
)
|
||||
target_link_libraries(${exename}${ARTIFACT_SUFFIX} testutillib${ARTIFACT_SUFFIX} testharness gtest ${THIRDPARTY_LIBS} ${ROCKSDB_LIB})
|
||||
if(NOT "${exename}" MATCHES "db_sanity_test")
|
||||
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.18)
|
||||
# Discover tests at ctest time instead of during the parallel build.
|
||||
# POST_BUILD discovery (the default) runs each freshly-built test
|
||||
# binary while the -j build is still compiling; on resource-constrained
|
||||
# CI (notably the arm64 macOS runners) that discovery run can yield
|
||||
# empty output, and CMake's JSON parser then aborts the whole build
|
||||
# with "Syntax error: value, object or array expected". PRE_TEST moves
|
||||
# discovery out of the build phase so a slow/empty enumeration no
|
||||
# longer fails the build.
|
||||
gtest_discover_tests(${exename}
|
||||
DISCOVERY_TIMEOUT 120
|
||||
DISCOVERY_MODE PRE_TEST)
|
||||
else()
|
||||
gtest_discover_tests(${exename} DISCOVERY_TIMEOUT 120)
|
||||
endif()
|
||||
gtest_discover_tests(${exename} DISCOVERY_TIMEOUT 120)
|
||||
add_dependencies(rocksdb_check ${exename}${ARTIFACT_SUFFIX})
|
||||
endif()
|
||||
endforeach(sourcefile ${TESTS})
|
||||
|
||||
-19
@@ -1,25 +1,6 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 11.8.1 (07/28/2026)
|
||||
### Public API Changes
|
||||
* Add `DBOptions::read_io_executor_threads` to increase the maximum thread count of the shared filesystem read I/O executor used for asynchronous reads when opening a DB. Opening a DB never reduces the shared executor thread count. Change the experimental `FSRandomAccessFile::SubmitReadAsync()` API to return whether the asynchronous path was used, and add the `FILE_SUBMIT_ASYNC_READ_FALLBACK` ticker to count calls that fall back to synchronous reads.
|
||||
|
||||
### Performance Improvements
|
||||
* When `DBOptions::avoid_unnecessary_blocking_io` is true, obsolete `OPTIONS-*` files found during DB open are deleted by background purge instead of synchronously on the opening thread.
|
||||
|
||||
## 11.8.0 (07/28/2026)
|
||||
### Public API Changes
|
||||
* Add callback-based asynchronous read APIs, `DB::GetAsync()` and `DB::MultiGetAsync()`. The idea is that when IO is required, RocksDB can suspend its internal coroutine read path, allowing the read executor thread to do other work. When the IO is complete, RocksDB invokes the user callback. This requires filesystem support for full performance benefits. A new filesystem API `FSRandomAccessFile::SubmitReadAsync()` is introduced for this. Unlike `ReadAsync`, the filesystem is responsible for eventually calling the callback. `FileSystem::GetReadExecutor()` returns the IO executor whose EventBases run coroutine read processing. In the Posix filesystem, an IO-uring based event loop is used to complete the IO.
|
||||
* Added `FlushOptions::listener_wait` (default `false`). When set together with `FlushOptions::wait == true`, `DB::Flush()` will not return until the registered `EventListener::OnFlushCompleted` callbacks for the flushed memtables have finished running. By default (`false`), `Flush(wait=true)` may return as soon as the flush result is committed, which can be before (or while) the `OnFlushCompleted` callbacks execute on the background flush thread. Also added the corresponding C API `rocksdb_flushoptions_set_listener_wait()` / `rocksdb_flushoptions_get_listener_wait()`.
|
||||
* Deprecated `PinnableWideColumns::serialized_size()` in favor of the new `PinnableWideColumns::payload_size()`, which returns the total size of the columns' names and values (for a plain value this equals the value size). Relatedly, wide-column read accounting changed subtly: read statistics for entities (`BYTES_READ`, `BYTES_PER_READ`, and the per-key byte counts for `GetEntity`/`MultiGetEntity`) and the `ReadOptions::value_size_soft_limit` threshold now measure entities by this payload size instead of the serialized entity size. This drops serialization framing bytes from those measurements (so reported/limited sizes for entity reads are slightly smaller than before) and makes plain-value and wide-column accounting consistent.
|
||||
|
||||
### Behavior Changes
|
||||
* `ReadOptions::value_size_soft_limit` now also bounds the blob values resolved from pre-flush blob direct write references during `MultiGet`/`MultiGetEntity` (previously these were resolved after the limit check and read without bound). Enforcement is "always make progress": at least one key is read even if its value alone exceeds the limit, and only once the returned size exceeds the limit do subsequent keys get `Status::Aborted` -- so a caller retrying the aborted keys cannot loop forever on a single value that by itself exceeds the limit.
|
||||
|
||||
### Performance Improvements
|
||||
* Reduced copying when reading wide-column entities that have blob-referenced columns, whether stored in separate blob files or embedded (same-file) as blob records in the same SST. On the point-lookup (`GetEntity`/`MultiGetEntity`) and blob-backed memtable/direct-write read paths, resolving these columns no longer re-serializes the whole entity into a fresh buffer: inline columns are referenced in place in the pinned entity and each resolved blob value is referenced in place from its fetched buffer. This is enabled by an internal change to `PinnableWideColumns` that lets its columns reference multiple backing buffers.
|
||||
|
||||
## 11.7.0 (07/16/2026)
|
||||
### Public API Changes
|
||||
* Added `RangeLockManagerHandle::SetIsKilledCallback()`, a `std::function<bool()>` predicate polled during range-lock waits. When it returns true, a blocked `GetRangeLock()` wait returns promptly with `Status::Aborted()` instead of waiting for the lock timeout. Range-lock users that install no callback keep the original wait-until-grant-or-timeout behavior.
|
||||
|
||||
@@ -1174,43 +1174,30 @@ check-c-api-gen:
|
||||
echo "Skipping C API backward-compatibility check ($(API_COMPAT_REF) not found; set API_COMPAT_REF)"; \
|
||||
fi
|
||||
# Staleness: regenerate and confirm the checked-in output is current. Needs a
|
||||
# clang++ (the generator parses C++ ASTs). Use CLANG_CXX, which
|
||||
# build_tools/build_detect_platform resolves once (the fbcode clang++ in
|
||||
# fbcode builds, else a versioned clang++ on PATH) and passes explicitly, so
|
||||
# the check and the generator agree on the same binary without heuristics.
|
||||
# clang++ (the generator parses C++ ASTs); detect one the way the generator
|
||||
# does (a clang in $(CXX) -- which may be ccache-prefixed/versioned -- else a
|
||||
# bare/versioned clang++ on PATH) rather than testing $(CXX) verbatim.
|
||||
@strict=""; case "$(CHECK_C_API_GEN_STRICT)" in ""|0|no|NO|false|FALSE) ;; *) strict=1 ;; esac; \
|
||||
cf_arg=""; \
|
||||
if [ -n "$(CLANG_FORMAT_BINARY)" ]; then cf_arg="--clang-format $(CLANG_FORMAT_BINARY)"; fi; \
|
||||
if [ -n "$(CLANG_CXX)" ] && command -v "$(CLANG_CXX)" >/dev/null 2>&1; then \
|
||||
$(PYTHON) tools/c_api_gen/verify_generated_up_to_date.py --clang "$(CLANG_CXX)" $$cf_arg; \
|
||||
have_clang=""; \
|
||||
for c in $$(printf '%s\n' $(CXX) | grep -i clang) clang++ clang++-21 clang++-20 clang++-19 clang++-18 clang++-17 clang++-16 clang++-15 clang++-14 clang++-13; do \
|
||||
if command -v "$$c" >/dev/null 2>&1; then have_clang=1; break; fi; \
|
||||
done; \
|
||||
if [ -n "$$have_clang" ]; then \
|
||||
$(PYTHON) tools/c_api_gen/verify_generated_up_to_date.py $$cf_arg; \
|
||||
elif [ -n "$$strict" ]; then \
|
||||
echo "ERROR: no clang++ found (CLANG_CXX unset) and CHECK_C_API_GEN_STRICT is set; cannot run the C API staleness check" >&2; exit 1; \
|
||||
echo "ERROR: no clang++ found and CHECK_C_API_GEN_STRICT is set; cannot run the C API staleness check" >&2; exit 1; \
|
||||
else \
|
||||
echo "Skipping C API codegen staleness check (no clang++ found; CLANG_CXX unset, install clang++ to enable)"; \
|
||||
echo "Skipping C API codegen staleness check (no clang++ found; install clang++ or set CXX to a clang to enable)"; \
|
||||
fi
|
||||
|
||||
# Regenerate the checked-in C API generated files (fragments under c_api_gen/,
|
||||
# the inlined include/rocksdb/c.h and db/c.cc, and the round-trip tests). Uses
|
||||
# CLANG_CXX (resolved by build_tools/build_detect_platform) so it works in a gcc
|
||||
# build too. Aborts up front if no usable clang++ is available, so a bad
|
||||
# toolchain never leaves the tree in a partially regenerated state. Pin the
|
||||
# formatter to match CI with CLANG_FORMAT_BINARY, e.g.:
|
||||
# make regen-c-api CLANG_FORMAT_BINARY=clang-format-21
|
||||
.PHONY: regen-c-api
|
||||
regen-c-api:
|
||||
@if [ -z "$(CLANG_CXX)" ] || ! command -v "$(CLANG_CXX)" >/dev/null 2>&1; then \
|
||||
echo "ERROR: no clang++ found (CLANG_CXX unset); build on a host with clang++ or the fbcode toolchain to regenerate the C API" >&2; exit 1; \
|
||||
fi
|
||||
@cf_arg=""; \
|
||||
if [ -n "$(CLANG_FORMAT_BINARY)" ]; then cf_arg="--clang-format $(CLANG_FORMAT_BINARY)"; fi; \
|
||||
$(PYTHON) tools/c_api_gen/regen_all.py --clang "$(CLANG_CXX)" $$cf_arg
|
||||
|
||||
# Quick local validation for C API generation plus the focused C API test.
|
||||
# This verifies the checked-in generated fragments as well as the inlined
|
||||
# include/rocksdb/c.h and db/c.cc outputs, then runs c_test in an isolated
|
||||
# TEST_TMPDIR to avoid stale-state failures.
|
||||
check-c-api-c_test:
|
||||
$(PYTHON) tools/c_api_gen/verify_generated_up_to_date.py --clang "$(CLANG_CXX)"
|
||||
$(PYTHON) tools/c_api_gen/verify_generated_up_to_date.py
|
||||
$(MAKE) c_test
|
||||
@tmpdir=$$(mktemp -d); \
|
||||
trap 'rm -rf "$$tmpdir"' EXIT; \
|
||||
@@ -1829,9 +1816,6 @@ backup_engine_test: $(OBJ_DIR)/utilities/backup/backup_engine_test.o $(TEST_LIBR
|
||||
checkpoint_test: $(OBJ_DIR)/utilities/checkpoint/checkpoint_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
copy_engine_test: $(OBJ_DIR)/utilities/copy_engine/copy_engine_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
sorted_run_builder_test: $(OBJ_DIR)/utilities/sorted_run_builder/sorted_run_builder_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
|
||||
@@ -145,21 +145,11 @@ def generate_buck(repo_path, deps_map):
|
||||
# always add range_tree, it's only excluded on ppc64, which we don't use internally
|
||||
src_mk["RANGE_TREE_SOURCES"] + src_mk["TOOL_LIB_SOURCES"],
|
||||
deps=[
|
||||
"//folly:executor",
|
||||
"//folly/container:f14_hash",
|
||||
"//folly/coro:baton",
|
||||
"//folly/coro:blocking_wait",
|
||||
"//folly/coro:collect",
|
||||
"//folly/coro:coroutine",
|
||||
"//folly/coro:nothrow",
|
||||
"//folly/coro:task",
|
||||
"//folly/executors:io_executor",
|
||||
"//folly/executors:io_thread_pool_executor",
|
||||
"//folly/executors/thread_factory:named_thread_factory",
|
||||
"//folly/io/async:async_base",
|
||||
"//folly/io/async:event_base_manager",
|
||||
"//folly/io/async:io_uring_backend",
|
||||
"//folly/io/async:request_context",
|
||||
"//folly/synchronization:distributed_mutex",
|
||||
],
|
||||
headers=LiteralValue("glob([\"**/*.h\"])")
|
||||
|
||||
@@ -89,36 +89,13 @@ if test -z "$CC"; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# CLANG_CXX is the single clang++ detection point for the whole script: it is
|
||||
# used for CXX when building with clang, for CLANG_ANALYZER (scan-build), and by
|
||||
# tooling that needs clang even in a gcc build (e.g. the C API codegen, which
|
||||
# parses C++ ASTs). The fbcode config above already sets CLANG_CXX; otherwise
|
||||
# resolve it here.
|
||||
if test -z "$CLANG_CXX"; then
|
||||
if [ -n "$CXX" ] && echo | $CXX -dM -E -x c++ - 2>/dev/null | grep -q '\b__clang__\b'; then
|
||||
# Honor an already-configured clang compiler (e.g. CI sets
|
||||
# CXX=clang++-21) so codegen parses headers with the same clang the
|
||||
# build uses, not an unrelated clang++ that merely happens to come first
|
||||
# on PATH (an image may ship both a bare clang++ and clang++-NN).
|
||||
CLANG_CXX="$CXX"
|
||||
else
|
||||
# Otherwise take the first clang++ on PATH. Candidates cover a bare
|
||||
# clang++, versioned names, and clang++.par (a Meta-internal wrapper
|
||||
# often present on dev machines).
|
||||
CLANG_CXX=$(command -v clang++ || command -v clang++-21 || command -v clang++-20 \
|
||||
|| command -v clang++-19 || command -v clang++-18 || command -v clang++-17 \
|
||||
|| command -v clang++-16 || command -v clang++-15 || command -v clang++-14 \
|
||||
|| command -v clang++-13 || command -v clang++.par || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
if test -z "$CXX"; then
|
||||
if [ "$USE_CLANG" ] && [ -n "$CLANG_CXX" ]; then
|
||||
CXX="$CLANG_CXX"
|
||||
if [ "$USE_CLANG" -a -x "$(command -v clang++)" ]; then
|
||||
CXX=clang++
|
||||
elif [ -x "$(command -v g++)" ]; then
|
||||
CXX=g++
|
||||
elif [ -n "$CLANG_CXX" ]; then
|
||||
CXX="$CLANG_CXX"
|
||||
elif [ -x "$(command -v clang++)" ]; then
|
||||
CXX=clang++
|
||||
else
|
||||
CXX=g++
|
||||
fi
|
||||
@@ -136,17 +113,6 @@ if test -z "$AR"; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# If the selected C++ compiler is actually clang (e.g. clang was the only
|
||||
# compiler available), reflect that as USE_CLANG so clang-specific logic in the
|
||||
# Makefile applies consistently. Detect it via the compiler's own __clang__
|
||||
# predefined macro rather than matching the binary path, which can false-positive
|
||||
# (e.g. a home directory containing "clang").
|
||||
if test -z "$USE_CLANG"; then
|
||||
if echo | $CXX -dM -E -x c++ - 2>/dev/null | grep -q '\b__clang__\b'; then
|
||||
USE_CLANG=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Detect OS
|
||||
if test -z "$TARGET_OS"; then
|
||||
TARGET_OS=`uname -s`
|
||||
@@ -161,7 +127,7 @@ if test -z "$CLANG_SCAN_BUILD"; then
|
||||
fi
|
||||
|
||||
if test -z "$CLANG_ANALYZER"; then
|
||||
CLANG_ANALYZER="$CLANG_CXX"
|
||||
CLANG_ANALYZER=$(command -v clang++ 2> /dev/null)
|
||||
fi
|
||||
|
||||
if test -z "$FIND"; then
|
||||
@@ -852,10 +818,6 @@ echo "ROCKSDB_MINOR=$ROCKSDB_MINOR" >> "$OUTPUT"
|
||||
echo "ROCKSDB_PATCH=$ROCKSDB_PATCH" >> "$OUTPUT"
|
||||
echo "CLANG_SCAN_BUILD=$CLANG_SCAN_BUILD" >> "$OUTPUT"
|
||||
echo "CLANG_ANALYZER=$CLANG_ANALYZER" >> "$OUTPUT"
|
||||
echo "CLANG_CXX=$CLANG_CXX" >> "$OUTPUT"
|
||||
if test -n "$USE_CLANG"; then
|
||||
echo "USE_CLANG=$USE_CLANG" >> "$OUTPUT"
|
||||
fi
|
||||
echo "PROFILING_FLAGS=$PROFILING_FLAGS" >> "$OUTPUT"
|
||||
echo "FIND=$FIND" >> "$OUTPUT"
|
||||
echo "WATCH=$WATCH" >> "$OUTPUT"
|
||||
|
||||
@@ -104,10 +104,6 @@ CLANG_BIN="$CLANG_BASE/bin"
|
||||
CLANG_LIB="$CLANG_BASE/lib"
|
||||
CLANG_SRC="$CLANG_BASE/../../src"
|
||||
|
||||
# A clang++ is always available in the fbcode toolchain regardless of whether
|
||||
# the build itself uses gcc or clang. CLANG_CXX exposes it for tooling (e.g. the
|
||||
# C API codegen, which parses C++ ASTs) that needs clang even in a gcc build.
|
||||
CLANG_CXX="$CLANG_BIN/clang++"
|
||||
CLANG_ANALYZER="$CLANG_BIN/clang++"
|
||||
CLANG_SCAN_BUILD="$CLANG_BIN/scan-build"
|
||||
|
||||
@@ -171,4 +167,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_CXX 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
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
|
||||
@@ -482,17 +478,6 @@ uint64_t rocksdb_compaction_file_info_oldest_blob_file_number(
|
||||
|
||||
/* BlobFileAdditionInfo */
|
||||
|
||||
const char* rocksdb_blob_file_addition_info_blob_file_path(
|
||||
const rocksdb_blob_file_addition_info_t* info, size_t* size) {
|
||||
*size = info->rep.blob_file_path.size();
|
||||
return info->rep.blob_file_path.data();
|
||||
}
|
||||
|
||||
uint64_t rocksdb_blob_file_addition_info_blob_file_number(
|
||||
const rocksdb_blob_file_addition_info_t* info) {
|
||||
return info->rep.blob_file_number;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_blob_file_addition_info_total_blob_count(
|
||||
const rocksdb_blob_file_addition_info_t* info) {
|
||||
return info->rep.total_blob_count;
|
||||
@@ -505,17 +490,6 @@ uint64_t rocksdb_blob_file_addition_info_total_blob_bytes(
|
||||
|
||||
/* BlobFileGarbageInfo */
|
||||
|
||||
const char* rocksdb_blob_file_garbage_info_blob_file_path(
|
||||
const rocksdb_blob_file_garbage_info_t* info, size_t* size) {
|
||||
*size = info->rep.blob_file_path.size();
|
||||
return info->rep.blob_file_path.data();
|
||||
}
|
||||
|
||||
uint64_t rocksdb_blob_file_garbage_info_blob_file_number(
|
||||
const rocksdb_blob_file_garbage_info_t* info) {
|
||||
return info->rep.blob_file_number;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_blob_file_garbage_info_garbage_blob_count(
|
||||
const rocksdb_blob_file_garbage_info_t* info) {
|
||||
return info->rep.garbage_blob_count;
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
|
||||
@@ -346,14 +342,6 @@ rocksdb_compaction_file_info_oldest_blob_file_number(
|
||||
|
||||
/* BlobFileAdditionInfo */
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_blob_file_addition_info_blob_file_path(
|
||||
const rocksdb_blob_file_addition_info_t* info, size_t* size);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_blob_file_addition_info_blob_file_number(
|
||||
const rocksdb_blob_file_addition_info_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_blob_file_addition_info_total_blob_count(
|
||||
const rocksdb_blob_file_addition_info_t* info);
|
||||
@@ -364,14 +352,6 @@ rocksdb_blob_file_addition_info_total_blob_bytes(
|
||||
|
||||
/* BlobFileGarbageInfo */
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_blob_file_garbage_info_blob_file_path(
|
||||
const rocksdb_blob_file_garbage_info_t* info, size_t* size);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_blob_file_garbage_info_blob_file_number(
|
||||
const rocksdb_blob_file_garbage_info_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_blob_file_garbage_info_garbage_blob_count(
|
||||
const rocksdb_blob_file_garbage_info_t* info);
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
|
||||
@@ -141,15 +137,6 @@ int rocksdb_options_get_max_file_opening_threads(rocksdb_options_t* opt) {
|
||||
return opt->rep.max_file_opening_threads;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_read_io_executor_threads(rocksdb_options_t* opt,
|
||||
int v) {
|
||||
opt->rep.read_io_executor_threads = v;
|
||||
}
|
||||
|
||||
int rocksdb_options_get_read_io_executor_threads(rocksdb_options_t* opt) {
|
||||
return opt->rep.read_io_executor_threads;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_open_files_async(rocksdb_options_t* opt,
|
||||
unsigned char v) {
|
||||
opt->rep.open_files_async = v;
|
||||
@@ -1687,16 +1674,6 @@ unsigned char rocksdb_flushoptions_get_force_atomic_flush(
|
||||
return opt->rep.force_atomic_flush;
|
||||
}
|
||||
|
||||
void rocksdb_flushoptions_set_listener_wait(rocksdb_flushoptions_t* opt,
|
||||
unsigned char v) {
|
||||
opt->rep.listener_wait = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_flushoptions_get_listener_wait(
|
||||
rocksdb_flushoptions_t* opt) {
|
||||
return opt->rep.listener_wait;
|
||||
}
|
||||
|
||||
/* FlushWALOptions */
|
||||
|
||||
void rocksdb_flushwaloptions_set_sync(rocksdb_flushwaloptions_t* opt,
|
||||
@@ -2726,36 +2703,6 @@ uint64_t rocksdb_fifo_compaction_options_get_trivial_copy_buffer_size(
|
||||
|
||||
/* BackupEngineOptions */
|
||||
|
||||
void rocksdb_backup_engine_options_set_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.io_buffer_size = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_backup_engine_options_get_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.io_buffer_size;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.backup_rate_limit = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_backup_engine_options_get_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.backup_rate_limit;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt, int v) {
|
||||
opt->rep.max_background_operations = v;
|
||||
}
|
||||
|
||||
int rocksdb_backup_engine_options_get_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.max_background_operations;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_backup_dir(
|
||||
rocksdb_backup_engine_options_t* opt, const char* v) {
|
||||
opt->rep.backup_dir = v;
|
||||
@@ -2807,6 +2754,26 @@ unsigned char rocksdb_backup_engine_options_get_backup_log_files(
|
||||
return opt->rep.backup_log_files;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.io_buffer_size = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_backup_engine_options_get_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.io_buffer_size;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.backup_rate_limit = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_backup_engine_options_get_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.backup_rate_limit;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_restore_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.restore_rate_limit = v;
|
||||
@@ -2827,6 +2794,16 @@ unsigned char rocksdb_backup_engine_options_get_share_files_with_checksum(
|
||||
return opt->rep.share_files_with_checksum;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt, int v) {
|
||||
opt->rep.max_background_operations = v;
|
||||
}
|
||||
|
||||
int rocksdb_backup_engine_options_get_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.max_background_operations;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_callback_trigger_interval_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.callback_trigger_interval_size = v;
|
||||
@@ -2881,6 +2858,16 @@ rocksdb_backup_engine_options_get_current_temperatures_override_manifest(
|
||||
|
||||
/* CreateBackupOptions */
|
||||
|
||||
void rocksdb_create_backup_options_set_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v) {
|
||||
opt->rep.flush_before_backup = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_create_backup_options_get_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt) {
|
||||
return opt->rep.flush_before_backup;
|
||||
}
|
||||
|
||||
void rocksdb_create_backup_options_set_decrease_background_thread_cpu_priority(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v) {
|
||||
opt->rep.decrease_background_thread_cpu_priority = v;
|
||||
@@ -2903,16 +2890,6 @@ int rocksdb_create_backup_options_get_background_thread_cpu_priority(
|
||||
return static_cast<int>(opt->rep.background_thread_cpu_priority);
|
||||
}
|
||||
|
||||
void rocksdb_create_backup_options_set_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v) {
|
||||
opt->rep.flush_before_backup = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_create_backup_options_get_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt) {
|
||||
return opt->rep.flush_before_backup;
|
||||
}
|
||||
|
||||
void rocksdb_create_backup_options_set_atomic_flush(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v) {
|
||||
opt->rep.atomic_flush = v;
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
|
||||
@@ -55,12 +51,6 @@ rocksdb_options_set_verify_sst_unique_id_in_manifest(rocksdb_options_t* opt,
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_options_get_verify_sst_unique_id_in_manifest(rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_read_io_executor_threads(
|
||||
rocksdb_options_t* opt, int v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int rocksdb_options_get_read_io_executor_threads(
|
||||
rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char* rocksdb_options_get_db_log_dir(
|
||||
rocksdb_options_t* opt, size_t* size);
|
||||
|
||||
@@ -551,12 +541,6 @@ extern ROCKSDB_LIBRARY_API void rocksdb_flushoptions_set_force_atomic_flush(
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_flushoptions_get_force_atomic_flush(rocksdb_flushoptions_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_flushoptions_set_listener_wait(
|
||||
rocksdb_flushoptions_t* opt, unsigned char v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char rocksdb_flushoptions_get_listener_wait(
|
||||
rocksdb_flushoptions_t* opt);
|
||||
|
||||
/* FlushWALOptions */
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_flushwaloptions_set_sync(
|
||||
@@ -1329,30 +1313,6 @@ rocksdb_fifo_compaction_options_get_trivial_copy_buffer_size(
|
||||
|
||||
/* BackupEngineOptions */
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_backup_engine_options_set_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_backup_engine_options_get_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_backup_engine_options_set_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_backup_engine_options_get_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_backup_engine_options_set_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt, int v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_backup_engine_options_get_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_backup_engine_options_set_backup_dir(
|
||||
rocksdb_backup_engine_options_t* opt, const char* v);
|
||||
|
||||
@@ -1390,6 +1350,22 @@ extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_backup_engine_options_get_backup_log_files(
|
||||
rocksdb_backup_engine_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_backup_engine_options_set_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_backup_engine_options_get_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_backup_engine_options_set_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_backup_engine_options_get_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_backup_engine_options_set_restore_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v);
|
||||
@@ -1406,6 +1382,14 @@ extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_backup_engine_options_get_share_files_with_checksum(
|
||||
rocksdb_backup_engine_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_backup_engine_options_set_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt, int v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_backup_engine_options_get_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_backup_engine_options_set_callback_trigger_interval_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v);
|
||||
@@ -1447,6 +1431,14 @@ rocksdb_backup_engine_options_get_current_temperatures_override_manifest(
|
||||
|
||||
/* CreateBackupOptions */
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_create_backup_options_set_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_create_backup_options_get_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_create_backup_options_set_decrease_background_thread_cpu_priority(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v);
|
||||
@@ -1463,14 +1455,6 @@ extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_create_backup_options_get_background_thread_cpu_priority(
|
||||
rocksdb_create_backup_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_create_backup_options_set_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_create_backup_options_get_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_create_backup_options_set_atomic_flush(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v);
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/gen_roundtrip_tests.py.
|
||||
// DO NOT EDIT THIS FILE DIRECTLY.
|
||||
// Round-trip (set -> get -> assert) coverage for generated option wrappers.
|
||||
// Rerun `make regen-c-api`
|
||||
// Rerun: python3 tools/c_api_gen/regen_all.py
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
static void rocksdb_roundtrip_test_rocksdb_block_based_options(void) {
|
||||
@@ -463,10 +459,6 @@ static void rocksdb_roundtrip_test_rocksdb_flushoptions(void) {
|
||||
CheckCondition(rocksdb_flushoptions_get_force_atomic_flush(obj) == 1);
|
||||
rocksdb_flushoptions_set_force_atomic_flush(obj, 0);
|
||||
CheckCondition(rocksdb_flushoptions_get_force_atomic_flush(obj) == 0);
|
||||
rocksdb_flushoptions_set_listener_wait(obj, 1);
|
||||
CheckCondition(rocksdb_flushoptions_get_listener_wait(obj) == 1);
|
||||
rocksdb_flushoptions_set_listener_wait(obj, 0);
|
||||
CheckCondition(rocksdb_flushoptions_get_listener_wait(obj) == 0);
|
||||
rocksdb_flushoptions_set_wait(obj, 1);
|
||||
CheckCondition(rocksdb_flushoptions_get_wait(obj) == 1);
|
||||
rocksdb_flushoptions_set_wait(obj, 0);
|
||||
@@ -849,10 +841,6 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
|
||||
CheckCondition(rocksdb_options_get_preserve_internal_time_seconds(obj) == 1);
|
||||
rocksdb_options_set_preserve_internal_time_seconds(obj, 0);
|
||||
CheckCondition(rocksdb_options_get_preserve_internal_time_seconds(obj) == 0);
|
||||
rocksdb_options_set_read_io_executor_threads(obj, 1);
|
||||
CheckCondition(rocksdb_options_get_read_io_executor_threads(obj) == 1);
|
||||
rocksdb_options_set_read_io_executor_threads(obj, 0);
|
||||
CheckCondition(rocksdb_options_get_read_io_executor_threads(obj) == 0);
|
||||
rocksdb_options_set_reuse_manifest_on_open(obj, 1);
|
||||
CheckCondition(rocksdb_options_get_reuse_manifest_on_open(obj) == 1);
|
||||
rocksdb_options_set_reuse_manifest_on_open(obj, 0);
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
+7
-34
@@ -7,7 +7,6 @@
|
||||
|
||||
#include "db/blob/blob_file_partition_manager.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/blob/same_file_blob_reader.h"
|
||||
#include "db/version_set.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -26,47 +25,21 @@ Status BlobFetcher::FetchBlob(const Slice& user_key,
|
||||
return status;
|
||||
}
|
||||
|
||||
Status VersionBlobFetcherBase::FetchBlob(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
const ReadOptions& read_options = this->read_options();
|
||||
Status BlobFetcher::FetchBlob(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
if (!allow_write_path_fallback_) {
|
||||
assert(version_);
|
||||
|
||||
return version_->GetBlob(read_options, user_key, blob_index,
|
||||
return version_->GetBlob(read_options_, user_key, blob_index,
|
||||
prefetch_buffer, blob_value, bytes_read);
|
||||
}
|
||||
|
||||
return BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
|
||||
read_options, user_key, blob_index, version_, blob_file_cache_,
|
||||
read_options_, user_key, blob_index, version_, blob_file_cache_,
|
||||
prefetch_buffer, blob_value, bytes_read);
|
||||
}
|
||||
|
||||
Status EmbeddedAwareBlobFetcher::FetchBlob(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
// Only an enabled decorator is ever routed to (see EffectiveFetcher()).
|
||||
assert(same_file_reader_ != nullptr);
|
||||
if (blob_index.IsSameFile()) {
|
||||
// Same-file ("embedded") blob records live in the current SST and are read
|
||||
// by the SameFileBlobReader (the BlockBasedTable). A separate blob-file
|
||||
// prefetch buffer and bytes_read counter do not apply here (embedded reads
|
||||
// account their own BLOB_DB_* stats via BlobSource).
|
||||
return same_file_reader_->GetSameFileBlob(read_options(), blob_index,
|
||||
blob_value);
|
||||
}
|
||||
|
||||
if (base_ == nullptr) {
|
||||
return Status::Corruption(
|
||||
"Cannot resolve non-same-file blob reference without a base blob "
|
||||
"fetcher");
|
||||
}
|
||||
return base_->FetchBlob(user_key, blob_index, prefetch_buffer, blob_value,
|
||||
bytes_read);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+13
-157
@@ -5,8 +5,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
@@ -18,174 +16,32 @@ class Slice;
|
||||
class FilePrefetchBuffer;
|
||||
class PinnableSlice;
|
||||
class BlobIndex;
|
||||
class SameFileBlobReader;
|
||||
|
||||
// Abstract interface for blob retrieval on the read path. An implementation
|
||||
// resolves a BlobIndex to its value, pinning it into a PinnableSlice. Callers
|
||||
// hold a plain const BlobFetcher* so resolution can be intercepted by a
|
||||
// decorator (e.g. EmbeddedAwareBlobFetcher) without their knowledge.
|
||||
// A thin wrapper around blob retrieval. By default it reads through Version,
|
||||
// and it can optionally fall back to direct-write blob files that are not yet
|
||||
// manifest-visible.
|
||||
class BlobFetcher {
|
||||
public:
|
||||
virtual ~BlobFetcher() = default;
|
||||
BlobFetcher(const Version* version, const ReadOptions& read_options,
|
||||
BlobFileCache* blob_file_cache = nullptr,
|
||||
bool allow_write_path_fallback = false)
|
||||
: version_(version),
|
||||
read_options_(read_options),
|
||||
blob_file_cache_(blob_file_cache),
|
||||
allow_write_path_fallback_(allow_write_path_fallback) {}
|
||||
|
||||
// Convenience overload: decodes `blob_index_slice` into a BlobIndex and
|
||||
// dispatches to the virtual overload below. Non-virtual; shared by all
|
||||
// implementations.
|
||||
Status FetchBlob(const Slice& user_key, const Slice& blob_index_slice,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value, uint64_t* bytes_read) const;
|
||||
|
||||
virtual Status FetchBlob(const Slice& user_key, const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const = 0;
|
||||
|
||||
// The read options this fetcher operates under. Used (via an
|
||||
// EmbeddedAwareBlobFetcher decorator) to resolve same-file blob references,
|
||||
// which need the current read_tier.
|
||||
virtual const ReadOptions& read_options() const = 0;
|
||||
};
|
||||
|
||||
// Shared implementation of the two Version-backed BlobFetchers below. Reads
|
||||
// through a Version, optionally falling back to direct-write blob files that
|
||||
// are not yet manifest-visible. Everything except the ReadOptions is stored
|
||||
// here; the concrete subclass decides whether it references a caller-owned
|
||||
// ReadOptions (VersionBlobFetcher) or owns its own copy
|
||||
// (OwningVersionBlobFetcher). FetchBlob reads the options via the virtual
|
||||
// read_options() so both storage strategies share one implementation.
|
||||
class VersionBlobFetcherBase : public BlobFetcher {
|
||||
public:
|
||||
// Un-hide the base's Slice-based convenience overload.
|
||||
using BlobFetcher::FetchBlob;
|
||||
|
||||
Status FetchBlob(const Slice& user_key, const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const override;
|
||||
|
||||
// True if this fetcher has enough context to resolve a (non-inlined,
|
||||
// non-same-file) blob reference: either a Version to read through, or the
|
||||
// direct-write fallback (a blob file cache) enabled. Callers that may hold a
|
||||
// null Version (e.g. DBIter) can check this to surface a clean Corruption on
|
||||
// an unexpected blob index instead of dereferencing null in FetchBlob.
|
||||
bool CanResolve() const {
|
||||
return version_ != nullptr ||
|
||||
(allow_write_path_fallback_ && blob_file_cache_ != nullptr);
|
||||
}
|
||||
|
||||
protected:
|
||||
VersionBlobFetcherBase(const Version* version, BlobFileCache* blob_file_cache,
|
||||
bool allow_write_path_fallback)
|
||||
: version_(version),
|
||||
blob_file_cache_(blob_file_cache),
|
||||
allow_write_path_fallback_(allow_write_path_fallback) {}
|
||||
PinnableSlice* blob_value, uint64_t* bytes_read) const;
|
||||
|
||||
private:
|
||||
const Version* version_;
|
||||
ReadOptions read_options_;
|
||||
BlobFileCache* blob_file_cache_;
|
||||
bool allow_write_path_fallback_;
|
||||
};
|
||||
|
||||
// The default BlobFetcher for transient/stack use: holds a reference to a
|
||||
// caller-owned ReadOptions rather than a copy, avoiding a per-fetcher copy of
|
||||
// the (growing) ReadOptions on the hot path. The referenced ReadOptions must
|
||||
// outlive the fetcher, so this is only appropriate for fetchers whose lifetime
|
||||
// is bounded by the caller's (e.g. a stack local per Get / per resolution).
|
||||
// Use OwningVersionBlobFetcher for lifetime-independent fetchers.
|
||||
class VersionBlobFetcher : public VersionBlobFetcherBase {
|
||||
public:
|
||||
VersionBlobFetcher(const Version* version, const ReadOptions& read_options,
|
||||
BlobFileCache* blob_file_cache = nullptr,
|
||||
bool allow_write_path_fallback = false)
|
||||
: VersionBlobFetcherBase(version, blob_file_cache,
|
||||
allow_write_path_fallback),
|
||||
read_options_(read_options) {}
|
||||
|
||||
// Reject binding to a temporary ReadOptions, which would leave a dangling
|
||||
// reference. Callers that cannot guarantee the ReadOptions outlives the
|
||||
// fetcher must use OwningVersionBlobFetcher instead.
|
||||
VersionBlobFetcher(const Version* version, ReadOptions&& read_options,
|
||||
BlobFileCache* blob_file_cache = nullptr,
|
||||
bool allow_write_path_fallback = false) = delete;
|
||||
|
||||
const ReadOptions& read_options() const override { return read_options_; }
|
||||
|
||||
private:
|
||||
const ReadOptions& read_options_;
|
||||
};
|
||||
|
||||
// A BlobFetcher that owns its ReadOptions, for fetchers whose lifetime is
|
||||
// independent of the caller that created them (the lazy read-path resolver and
|
||||
// compaction). Takes ownership of the ReadOptions by rvalue so ownership
|
||||
// transfer is explicit and no hidden copy is made on the way in; later reads
|
||||
// never touch caller state. Callers holding a borrowed ReadOptions they cannot
|
||||
// move must make the copy explicit (e.g. ReadOptions(read_options)).
|
||||
class OwningVersionBlobFetcher : public VersionBlobFetcherBase {
|
||||
public:
|
||||
OwningVersionBlobFetcher(const Version* version, ReadOptions&& read_options,
|
||||
BlobFileCache* blob_file_cache = nullptr,
|
||||
bool allow_write_path_fallback = false)
|
||||
: VersionBlobFetcherBase(version, blob_file_cache,
|
||||
allow_write_path_fallback),
|
||||
read_options_(std::move(read_options)) {}
|
||||
|
||||
const ReadOptions& read_options() const override { return read_options_; }
|
||||
|
||||
private:
|
||||
ReadOptions read_options_;
|
||||
};
|
||||
|
||||
// A BlobFetcher decorator that resolves same-file ("embedded") blob references
|
||||
// against a SameFileBlobReader (the current SST) and delegates all other
|
||||
// references to a base BlobFetcher. It is cheap to construct as a stack local
|
||||
// per resolution: the same-file target is per-SST while the base fetcher is one
|
||||
// per-Get object shared across SSTs, so an immutable per-SST decorator avoids
|
||||
// shared-mutable-state hazards under async MultiGet.
|
||||
//
|
||||
// A decorator is "enabled" when it has a same-file reader and "disabled"
|
||||
// (unusable) when constructed with a null `same_file_reader` (e.g. via the
|
||||
// default constructor). A disabled decorator must not be routed through
|
||||
// directly; callers construct one unconditionally and use EffectiveFetcher(),
|
||||
// which returns this decorator when enabled or the undecorated base fetcher
|
||||
// otherwise. This avoids std::optional / branching at call sites while adding
|
||||
// no indirection on the common (non-embedded) path.
|
||||
//
|
||||
// The base fetcher may be null for an entity whose blob columns are all
|
||||
// same-file (no separate-file blob support wired, e.g. SstFileReader); a
|
||||
// non-same-file reference then surfaces as a Corruption rather than silently.
|
||||
class EmbeddedAwareBlobFetcher : public BlobFetcher {
|
||||
public:
|
||||
explicit EmbeddedAwareBlobFetcher(
|
||||
const BlobFetcher* base = nullptr,
|
||||
const SameFileBlobReader* same_file_reader = nullptr)
|
||||
: base_(base), same_file_reader_(same_file_reader) {}
|
||||
|
||||
// The fetcher to route resolution through: this decorator when it has a
|
||||
// same-file reader, otherwise the (undecorated) base fetcher. Only an enabled
|
||||
// decorator's FetchBlob is ever invoked.
|
||||
const BlobFetcher* EffectiveFetcher() const {
|
||||
return same_file_reader_ != nullptr ? this : base_;
|
||||
}
|
||||
|
||||
// Un-hide the base's Slice-based convenience overload.
|
||||
using BlobFetcher::FetchBlob;
|
||||
|
||||
Status FetchBlob(const Slice& user_key, const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const override;
|
||||
|
||||
const ReadOptions& read_options() const override {
|
||||
// Same-file resolution uses the base fetcher's read options; when there is
|
||||
// no base (e.g. SstFileReader) a default (kReadAllTier) suffices.
|
||||
static const ReadOptions kDefaultReadOptions;
|
||||
return base_ != nullptr ? base_->read_options() : kDefaultReadOptions;
|
||||
}
|
||||
|
||||
private:
|
||||
const BlobFetcher* base_;
|
||||
// Null iff this decorator is disabled (see class comment); otherwise the
|
||||
// same-file blob reader for the current SST.
|
||||
const SameFileBlobReader* same_file_reader_;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -215,10 +215,11 @@ Status BlobWriteBatchTransformer::PutEntityCF(uint32_t column_family_id,
|
||||
output_batch_, column_family_id, key, entity);
|
||||
}
|
||||
|
||||
Slice entity_ref = entity;
|
||||
std::vector<WideColumn> columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> existing_blob_columns;
|
||||
Status status = WideColumnSerialization::Deserialize(entity, columns,
|
||||
&existing_blob_columns);
|
||||
Status status = WideColumnSerialization::DeserializeV2(entity_ref, columns,
|
||||
existing_blob_columns);
|
||||
if (status.ok()) {
|
||||
// Reject pre-serialized entities that already contain blob references.
|
||||
// Passing them through would preserve references to blob files outside this
|
||||
|
||||
+13
-306
@@ -143,24 +143,14 @@ class DBBlobIndexTest : public DBTestBase {
|
||||
columns, s, is_blob_index, value_found);
|
||||
}
|
||||
|
||||
Status MaybeResolveMemtableBlobValueForTest(
|
||||
const Slice& key, const BlobFetcher* blob_fetcher, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, bool* did_resolve, bool* is_blob_index,
|
||||
bool* value_found = nullptr) {
|
||||
return DBImpl::MaybeResolveMemtableBlobValue(key, blob_fetcher, value,
|
||||
columns, did_resolve,
|
||||
is_blob_index, value_found);
|
||||
}
|
||||
|
||||
Status PostprocessMemtableValueReadForTest(
|
||||
const Slice& key, bool resolve_blob_backed_memtable_value,
|
||||
const BlobFetcher* blob_fetcher, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, Status memtable_read_status,
|
||||
bool* is_blob_index, bool* value_found = nullptr) {
|
||||
return DBImpl::PostprocessMemtableValueRead(
|
||||
key, /*timestamp=*/nullptr, resolve_blob_backed_memtable_value,
|
||||
blob_fetcher, value, columns, std::move(memtable_read_status),
|
||||
is_blob_index, value_found);
|
||||
bool MaybeResolveMemtableBlobValueForTest(const Slice& key,
|
||||
const BlobFetcher* blob_fetcher,
|
||||
PinnableSlice* value,
|
||||
PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index,
|
||||
bool* value_found = nullptr) {
|
||||
return DBImpl::MaybeResolveMemtableBlobValue(
|
||||
key, blob_fetcher, value, columns, s, is_blob_index, value_found);
|
||||
}
|
||||
|
||||
Options GetTestOptions() {
|
||||
@@ -319,65 +309,17 @@ TEST_F(DBBlobIndexTest,
|
||||
value.GetSelf()->assign(blob_index.data(), blob_index.size());
|
||||
value.PinSelf();
|
||||
|
||||
bool did_resolve = false;
|
||||
Status s = Status::OK();
|
||||
bool is_blob_index = true;
|
||||
Status s = MaybeResolveMemtableBlobValueForTest(
|
||||
Slice("key"), /*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr,
|
||||
&did_resolve, &is_blob_index);
|
||||
ASSERT_TRUE(MaybeResolveMemtableBlobValueForTest(
|
||||
Slice("key"), /*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr, &s,
|
||||
&is_blob_index));
|
||||
|
||||
ASSERT_TRUE(did_resolve);
|
||||
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
|
||||
ASSERT_TRUE(s.IsNotSupported()) << s.ToString();
|
||||
ASSERT_TRUE(value.empty());
|
||||
ASSERT_FALSE(is_blob_index);
|
||||
}
|
||||
|
||||
TEST_F(DBBlobIndexTest, PostprocessMemtableValueReadClearsOutputsOnError) {
|
||||
// Regression test for the "swallowed status" FIXME: on any error,
|
||||
// PostprocessMemtableValueRead must clear the outputs and return the status,
|
||||
// never leaving a half-populated result behind a non-OK status.
|
||||
|
||||
// Case 1: the incoming memtable read status is already non-OK. The resolve
|
||||
// path is skipped; the finalization block clears outputs and returns it.
|
||||
{
|
||||
PinnableSlice value;
|
||||
value.GetSelf()->assign("stale", 5);
|
||||
value.PinSelf();
|
||||
|
||||
bool is_blob_index = false;
|
||||
Status s = PostprocessMemtableValueReadForTest(
|
||||
Slice("key"), /*resolve_blob_backed_memtable_value=*/true,
|
||||
/*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr,
|
||||
Status::Corruption("read failed"), &is_blob_index);
|
||||
|
||||
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
|
||||
ASSERT_TRUE(value.empty());
|
||||
}
|
||||
|
||||
// Case 2: the incoming status is OK but the blob-resolve path itself fails (a
|
||||
// blob-backed value with no fetcher). This is the actual scenario the FIXME
|
||||
// referred to -- previously the non-OK status from the resolve path was
|
||||
// dropped. The failure must propagate and the raw blob-index bytes must be
|
||||
// cleared rather than handed back as if they were the value.
|
||||
{
|
||||
std::string blob_index;
|
||||
BlobIndex::EncodeBlob(&blob_index, /*file_number=*/123, /*offset=*/456,
|
||||
/*size=*/789, kNoCompression);
|
||||
|
||||
PinnableSlice value;
|
||||
value.GetSelf()->assign(blob_index.data(), blob_index.size());
|
||||
value.PinSelf();
|
||||
|
||||
bool is_blob_index = true;
|
||||
Status s = PostprocessMemtableValueReadForTest(
|
||||
Slice("key"), /*resolve_blob_backed_memtable_value=*/true,
|
||||
/*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr, Status::OK(),
|
||||
&is_blob_index);
|
||||
|
||||
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
|
||||
ASSERT_TRUE(value.empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBBlobIndexTest, ReadOnlyGetImplReturnsBlobIndexWhenRequested) {
|
||||
// Goal: cover the internal read-only GetImpl contract when the caller
|
||||
// explicitly asks for raw blob-index bytes via `is_blob_index`. Recovery
|
||||
@@ -833,241 +775,6 @@ TEST_F(DBBlobIndexTest, EmbeddedBlobSharedBlockAndBlobCache) {
|
||||
ASSERT_OK(iter->status());
|
||||
}
|
||||
|
||||
// Point GetEntity / MultiGetEntity of a wide-column entity whose non-default
|
||||
// columns are embedded (same-file) blobs must return the correct columns and
|
||||
// resolve them zero-copy: each resolved blob column value points straight at
|
||||
// its pinned blob-cache buffer (identical across reads, and in a separate
|
||||
// buffer per column), rather than being copied into one re-serialized entity.
|
||||
TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnGetEntityZeroCopy) {
|
||||
Options options = GetTestOptions();
|
||||
options.create_if_missing = true;
|
||||
options.statistics = CreateDBStatistics();
|
||||
LRUCacheOptions co;
|
||||
co.capacity = 8 << 20;
|
||||
options.blob_cache = NewLRUCache(co);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string sst_path = dbname_ + "/embedded_wc_zerocopy.sst";
|
||||
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
|
||||
embedded_blob_options.min_blob_size = 8;
|
||||
|
||||
const std::string small_default = "d"; // inline default column
|
||||
const std::string big1(1024, 'a'); // same-file blob column
|
||||
const std::string big2(2048, 'b'); // same-file blob column
|
||||
const WideColumns columns{
|
||||
{kDefaultWideColumnName, small_default}, {"big1", big1}, {"big2", big2}};
|
||||
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
|
||||
ASSERT_OK(writer.PutEntity("k", columns));
|
||||
ASSERT_OK(writer.Finish());
|
||||
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
|
||||
|
||||
const char* big1_ptr = nullptr;
|
||||
const char* big2_ptr = nullptr;
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k",
|
||||
&result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
|
||||
// columns() is sorted by name -> [default(""), big1, big2].
|
||||
big1_ptr = result.columns()[1].value().data();
|
||||
big2_ptr = result.columns()[2].value().data();
|
||||
// Multi-buffer: each blob column lives in its own backing buffer, so the
|
||||
// two values are not laid out contiguously (as they would be in a single
|
||||
// re-serialized V1 entity).
|
||||
ASSERT_NE(big1_ptr + big1.size(), big2_ptr);
|
||||
|
||||
// Pointer stability across move (address-stable backing nodes).
|
||||
PinnableWideColumns moved(std::move(result));
|
||||
ASSERT_EQ(moved.columns(), columns);
|
||||
ASSERT_EQ(moved.columns()[1].value().data(), big1_ptr);
|
||||
ASSERT_EQ(moved.columns()[2].value().data(), big2_ptr);
|
||||
}
|
||||
|
||||
// A second read serves the same pinned blob-cache buffers (zero-copy): the
|
||||
// resolved column values point at the very same addresses, which a per-read
|
||||
// re-serialization would not.
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k",
|
||||
&result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
ASSERT_EQ(result.columns()[1].value().data(), big1_ptr);
|
||||
ASSERT_EQ(result.columns()[2].value().data(), big2_ptr);
|
||||
}
|
||||
|
||||
// MultiGetEntity: same correctness and zero-copy (cache-hit) guarantee.
|
||||
{
|
||||
const std::array<Slice, 1> keys{Slice("k")};
|
||||
std::array<PinnableWideColumns, 1> results;
|
||||
std::array<Status, 1> statuses;
|
||||
db_->MultiGetEntity(ReadOptions(), db_->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), results.data(), statuses.data());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(results[0].columns(), columns);
|
||||
ASSERT_EQ(results[0].columns()[1].value().data(), big1_ptr);
|
||||
ASSERT_EQ(results[0].columns()[2].value().data(), big2_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// A wide-column entity whose default column is itself an embedded (same-file)
|
||||
// blob must resolve correctly on the plain Get path (default-column
|
||||
// extraction) as well as GetEntity, matching the values written.
|
||||
TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnGetDefaultColumn) {
|
||||
Options options = GetTestOptions();
|
||||
options.create_if_missing = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string sst_path = dbname_ + "/embedded_wc_default.sst";
|
||||
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
|
||||
embedded_blob_options.min_blob_size = 8;
|
||||
|
||||
const std::string big_default(1024, 'x'); // same-file blob default column
|
||||
const std::string small_attr = "s"; // inline non-default column
|
||||
const WideColumns columns{{kDefaultWideColumnName, big_default},
|
||||
{"attr", small_attr}};
|
||||
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
|
||||
ASSERT_OK(writer.PutEntity("k", columns));
|
||||
ASSERT_OK(writer.Finish());
|
||||
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
|
||||
|
||||
// Plain Get returns the (blob-backed) default column value.
|
||||
PinnableSlice value;
|
||||
ASSERT_OK(db_->Get(ReadOptions(), db_->DefaultColumnFamily(), "k", &value));
|
||||
ASSERT_EQ(value, big_default);
|
||||
|
||||
// GetEntity returns all columns.
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(
|
||||
db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k", &result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
}
|
||||
|
||||
// Merge over an embedded-blob wide-column base entity must resolve the base
|
||||
// entity's same-file default column before merging, and GetMergeOperands must
|
||||
// return the resolved default column as the base operand.
|
||||
TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnMergeBase) {
|
||||
Options options = GetTestOptions(); // merge_operator = StringAppend(',')
|
||||
options.create_if_missing = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string sst_path = dbname_ + "/embedded_wc_merge.sst";
|
||||
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
|
||||
embedded_blob_options.min_blob_size = 8;
|
||||
|
||||
const std::string big_default(1024, 'D'); // same-file blob default column
|
||||
const WideColumns columns{{kDefaultWideColumnName, big_default},
|
||||
{"attr", "s"}};
|
||||
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
|
||||
ASSERT_OK(writer.PutEntity("k", columns));
|
||||
ASSERT_OK(writer.Finish());
|
||||
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
|
||||
|
||||
// Newer merge operand lives in the memtable; the embedded entity base lives
|
||||
// in the ingested SST, so the merge resolves the base via the same-file path.
|
||||
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), "k", "op1"));
|
||||
|
||||
PinnableSlice value;
|
||||
ASSERT_OK(db_->Get(ReadOptions(), db_->DefaultColumnFamily(), "k", &value));
|
||||
ASSERT_EQ(value, big_default + "," + "op1");
|
||||
|
||||
// GetMergeOperands returns the resolved base default column plus the operand.
|
||||
std::array<PinnableSlice, 8> operands;
|
||||
GetMergeOperandsOptions get_merge_opts;
|
||||
get_merge_opts.expected_max_number_of_operands =
|
||||
static_cast<int>(operands.size());
|
||||
int num_operands = 0;
|
||||
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
"k", operands.data(), &get_merge_opts,
|
||||
&num_operands));
|
||||
ASSERT_EQ(num_operands, 2);
|
||||
ASSERT_EQ(operands[0], big_default);
|
||||
ASSERT_EQ(operands[1], "op1");
|
||||
}
|
||||
|
||||
// With read_tier == kBlockCacheTier, an unresolved same-file blob column of a
|
||||
// wide-column entity must surface Incomplete (never a raw same-file BlobIndex).
|
||||
TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnGetEntityBlockCacheTier) {
|
||||
Options options = GetTestOptions();
|
||||
options.create_if_missing = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string sst_path = dbname_ + "/embedded_wc_blockcache.sst";
|
||||
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
|
||||
embedded_blob_options.min_blob_size = 8;
|
||||
|
||||
const std::string big1(1024, 'a');
|
||||
const WideColumns columns{{kDefaultWideColumnName, "d"}, {"big", big1}};
|
||||
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
|
||||
ASSERT_OK(writer.PutEntity("k", columns));
|
||||
ASSERT_OK(writer.Finish());
|
||||
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
|
||||
|
||||
// Warm the block cache so the data block read succeeds under kBlockCacheTier
|
||||
// and the lookup actually reaches same-file blob column resolution.
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k",
|
||||
&result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
}
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.read_tier = kBlockCacheTier;
|
||||
PinnableWideColumns result;
|
||||
Status s =
|
||||
db_->GetEntity(read_options, db_->DefaultColumnFamily(), "k", &result);
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
}
|
||||
|
||||
// A corrupt embedded blob record backing a wide-column column must surface an
|
||||
// error on GetEntity, not expose the raw same-file BlobIndex.
|
||||
// A failure resolving an embedded blob record backing a wide-column column
|
||||
// (e.g. a bad record or blob-region read fault) must surface on GetEntity, not
|
||||
// expose the raw same-file BlobIndex. The failure is injected via a sync point
|
||||
// so the test is independent of the on-disk byte layout (e.g. encrypted env).
|
||||
TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnGetEntityCorruptionSurfaced) {
|
||||
Options options = GetTestOptions();
|
||||
options.create_if_missing = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string sst_path = dbname_ + "/embedded_wc_corrupt.sst";
|
||||
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
|
||||
embedded_blob_options.min_blob_size = 8;
|
||||
|
||||
const std::string big_col(1024, 'Z');
|
||||
const WideColumns columns{{kDefaultWideColumnName, "d"}, {"big", big_col}};
|
||||
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
|
||||
ASSERT_OK(writer.PutEntity("k", columns));
|
||||
ASSERT_OK(writer.Finish());
|
||||
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
|
||||
|
||||
const Status kInjected = Status::Corruption("injected embedded blob error");
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlockBasedTable::MaybeResolveEmbeddedValue:InjectError",
|
||||
[&](void* arg) { *static_cast<Status*>(arg) = kInjected; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
PinnableWideColumns result;
|
||||
const Status s =
|
||||
db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k", &result);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
|
||||
}
|
||||
|
||||
class PlainBlobValueFilterV3 : public CompactionFilter {
|
||||
public:
|
||||
PlainBlobValueFilterV3(std::atomic<int>* filter_call_count,
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class BlobIndex;
|
||||
class PinnableSlice;
|
||||
struct ReadOptions;
|
||||
|
||||
// Narrow interface for resolving a same-file ("embedded") blob reference
|
||||
// against the physical file that currently holds the table entry. Implemented
|
||||
// by BlockBasedTable, which owns the reader context (file, footer, blob cache)
|
||||
// needed to read an embedded blob record.
|
||||
//
|
||||
// Kept in its own low-level header, free of any table-layer dependency, so that
|
||||
// db/blob (EmbeddedAwareBlobFetcher) and table/get_context can depend on just
|
||||
// this interface without pulling in BlockBasedTable. ReadPathBlobResolver (the
|
||||
// lazy-resolution read path) can likewise hold a const SameFileBlobReader*.
|
||||
class SameFileBlobReader {
|
||||
public:
|
||||
virtual ~SameFileBlobReader() = default;
|
||||
|
||||
// Resolves a same-file blob reference (one for which blob_index.IsSameFile()
|
||||
// is true) into `value`, pinning the payload zero-copy (from the blob cache
|
||||
// or an owned buffer). read_tier == kBlockCacheTier without a cached record
|
||||
// yields Status::Incomplete; a bad record yields Status::Corruption.
|
||||
virtual Status GetSameFileBlob(const ReadOptions& read_options,
|
||||
const BlobIndex& blob_index,
|
||||
PinnableSlice* value) const = 0;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -7,7 +7,7 @@
|
||||
// Auto-generated by tools/c_api_gen/regen_all.py.
|
||||
// DO NOT EDIT THIS FILE DIRECTLY.
|
||||
// Edit tools/c_api_gen/c_base.cc or the inputs under tools/c_api_gen/,
|
||||
// then rerun `make regen-c-api`
|
||||
// then rerun: python3 tools/c_api_gen/regen_all.py
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
#include "rocksdb/c.h"
|
||||
@@ -5289,17 +5289,6 @@ uint64_t rocksdb_compaction_file_info_oldest_blob_file_number(
|
||||
|
||||
/* BlobFileAdditionInfo */
|
||||
|
||||
const char* rocksdb_blob_file_addition_info_blob_file_path(
|
||||
const rocksdb_blob_file_addition_info_t* info, size_t* size) {
|
||||
*size = info->rep.blob_file_path.size();
|
||||
return info->rep.blob_file_path.data();
|
||||
}
|
||||
|
||||
uint64_t rocksdb_blob_file_addition_info_blob_file_number(
|
||||
const rocksdb_blob_file_addition_info_t* info) {
|
||||
return info->rep.blob_file_number;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_blob_file_addition_info_total_blob_count(
|
||||
const rocksdb_blob_file_addition_info_t* info) {
|
||||
return info->rep.total_blob_count;
|
||||
@@ -5312,17 +5301,6 @@ uint64_t rocksdb_blob_file_addition_info_total_blob_bytes(
|
||||
|
||||
/* BlobFileGarbageInfo */
|
||||
|
||||
const char* rocksdb_blob_file_garbage_info_blob_file_path(
|
||||
const rocksdb_blob_file_garbage_info_t* info, size_t* size) {
|
||||
*size = info->rep.blob_file_path.size();
|
||||
return info->rep.blob_file_path.data();
|
||||
}
|
||||
|
||||
uint64_t rocksdb_blob_file_garbage_info_blob_file_number(
|
||||
const rocksdb_blob_file_garbage_info_t* info) {
|
||||
return info->rep.blob_file_number;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_blob_file_garbage_info_garbage_blob_count(
|
||||
const rocksdb_blob_file_garbage_info_t* info) {
|
||||
return info->rep.garbage_blob_count;
|
||||
@@ -9107,15 +9085,6 @@ int rocksdb_options_get_max_file_opening_threads(rocksdb_options_t* opt) {
|
||||
return opt->rep.max_file_opening_threads;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_read_io_executor_threads(rocksdb_options_t* opt,
|
||||
int v) {
|
||||
opt->rep.read_io_executor_threads = v;
|
||||
}
|
||||
|
||||
int rocksdb_options_get_read_io_executor_threads(rocksdb_options_t* opt) {
|
||||
return opt->rep.read_io_executor_threads;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_open_files_async(rocksdb_options_t* opt,
|
||||
unsigned char v) {
|
||||
opt->rep.open_files_async = v;
|
||||
@@ -10653,16 +10622,6 @@ unsigned char rocksdb_flushoptions_get_force_atomic_flush(
|
||||
return opt->rep.force_atomic_flush;
|
||||
}
|
||||
|
||||
void rocksdb_flushoptions_set_listener_wait(rocksdb_flushoptions_t* opt,
|
||||
unsigned char v) {
|
||||
opt->rep.listener_wait = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_flushoptions_get_listener_wait(
|
||||
rocksdb_flushoptions_t* opt) {
|
||||
return opt->rep.listener_wait;
|
||||
}
|
||||
|
||||
/* FlushWALOptions */
|
||||
|
||||
void rocksdb_flushwaloptions_set_sync(rocksdb_flushwaloptions_t* opt,
|
||||
@@ -11692,36 +11651,6 @@ uint64_t rocksdb_fifo_compaction_options_get_trivial_copy_buffer_size(
|
||||
|
||||
/* BackupEngineOptions */
|
||||
|
||||
void rocksdb_backup_engine_options_set_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.io_buffer_size = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_backup_engine_options_get_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.io_buffer_size;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.backup_rate_limit = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_backup_engine_options_get_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.backup_rate_limit;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt, int v) {
|
||||
opt->rep.max_background_operations = v;
|
||||
}
|
||||
|
||||
int rocksdb_backup_engine_options_get_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.max_background_operations;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_backup_dir(
|
||||
rocksdb_backup_engine_options_t* opt, const char* v) {
|
||||
opt->rep.backup_dir = v;
|
||||
@@ -11773,6 +11702,26 @@ unsigned char rocksdb_backup_engine_options_get_backup_log_files(
|
||||
return opt->rep.backup_log_files;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.io_buffer_size = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_backup_engine_options_get_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.io_buffer_size;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.backup_rate_limit = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_backup_engine_options_get_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.backup_rate_limit;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_restore_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.restore_rate_limit = v;
|
||||
@@ -11793,6 +11742,16 @@ unsigned char rocksdb_backup_engine_options_get_share_files_with_checksum(
|
||||
return opt->rep.share_files_with_checksum;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt, int v) {
|
||||
opt->rep.max_background_operations = v;
|
||||
}
|
||||
|
||||
int rocksdb_backup_engine_options_get_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.max_background_operations;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_callback_trigger_interval_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.callback_trigger_interval_size = v;
|
||||
@@ -11847,6 +11806,16 @@ rocksdb_backup_engine_options_get_current_temperatures_override_manifest(
|
||||
|
||||
/* CreateBackupOptions */
|
||||
|
||||
void rocksdb_create_backup_options_set_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v) {
|
||||
opt->rep.flush_before_backup = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_create_backup_options_get_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt) {
|
||||
return opt->rep.flush_before_backup;
|
||||
}
|
||||
|
||||
void rocksdb_create_backup_options_set_decrease_background_thread_cpu_priority(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v) {
|
||||
opt->rep.decrease_background_thread_cpu_priority = v;
|
||||
@@ -11869,16 +11838,6 @@ int rocksdb_create_backup_options_get_background_thread_cpu_priority(
|
||||
return static_cast<int>(opt->rep.background_thread_cpu_priority);
|
||||
}
|
||||
|
||||
void rocksdb_create_backup_options_set_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v) {
|
||||
opt->rep.flush_before_backup = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_create_backup_options_get_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt) {
|
||||
return opt->rep.flush_before_backup;
|
||||
}
|
||||
|
||||
void rocksdb_create_backup_options_set_atomic_flush(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v) {
|
||||
opt->rep.atomic_flush = v;
|
||||
|
||||
@@ -4531,11 +4531,6 @@ int main(int argc, char** argv) {
|
||||
CheckNoError(err);
|
||||
|
||||
rocksdb_flushoptions_set_wait(flush_options, 1);
|
||||
// Ensure the OnFlushCompleted callbacks (which update listener_state) have
|
||||
// finished by the time rocksdb_flush() returns, so the flush_count /
|
||||
// saw_flush_entries_* assertions below are not racy.
|
||||
rocksdb_flushoptions_set_listener_wait(flush_options, 1);
|
||||
CheckCondition(1 == rocksdb_flushoptions_get_listener_wait(flush_options));
|
||||
|
||||
rocksdb_put(local_db, local_woptions, "a", 1, "1", 1, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
@@ -251,9 +251,8 @@ void CompactionIterator::SetBlobFetcher(const Version* version,
|
||||
read_options.io_activity = io_activity;
|
||||
read_options.fill_cache = false;
|
||||
|
||||
blob_fetcher_ = std::make_unique<OwningVersionBlobFetcher>(
|
||||
version, std::move(read_options), blob_file_cache,
|
||||
allow_write_path_fallback);
|
||||
blob_fetcher_ = std::make_unique<BlobFetcher>(
|
||||
version, read_options, blob_file_cache, allow_write_path_fallback);
|
||||
blob_resolver_.Init(blob_fetcher_.get(), prefetch_buffers_.get(),
|
||||
&iter_stats_);
|
||||
}
|
||||
@@ -468,14 +467,14 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
}
|
||||
}
|
||||
if (UNLIKELY(has_blob_columns)) {
|
||||
// Entity has blob columns. Deserialize() populates
|
||||
// Entity has blob columns. DeserializeV2() populates
|
||||
// entity_columns_ with the full column set; blob-backed entries still
|
||||
// carry the serialized BlobIndex bytes in value(). The companion
|
||||
// entity_blob_columns_ side list identifies which column indexes are
|
||||
// blob references and provides the decoded BlobIndex objects.
|
||||
Slice input_copy = value_;
|
||||
Status s = WideColumnSerialization::Deserialize(
|
||||
input_copy, entity_columns_, &entity_blob_columns_);
|
||||
Status s = WideColumnSerialization::DeserializeV2(
|
||||
input_copy, entity_columns_, entity_blob_columns_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
@@ -542,7 +541,7 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
} else {
|
||||
// No blob columns, use fast path
|
||||
Slice value_copy = value_;
|
||||
const Status s = WideColumnSerialization::DeserializeSimple(
|
||||
const Status s = WideColumnSerialization::Deserialize(
|
||||
value_copy, filter_existing_columns_);
|
||||
|
||||
if (!s.ok()) {
|
||||
@@ -1551,8 +1550,8 @@ void CompactionIterator::ExtractLargeColumnValuesIfNeeded() {
|
||||
Slice entity_slice = value_;
|
||||
entity_columns_.clear();
|
||||
entity_blob_columns_.clear();
|
||||
Status s = WideColumnSerialization::Deserialize(
|
||||
entity_slice, entity_columns_, &entity_blob_columns_);
|
||||
Status s = WideColumnSerialization::DeserializeV2(
|
||||
entity_slice, entity_columns_, entity_blob_columns_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
@@ -1670,7 +1669,7 @@ bool CompactionIterator::FetchBlobsNeedingGC(
|
||||
Status s = blob_fetcher_->FetchBlob(user_key(), blob_index, prefetch_buffer,
|
||||
&blob_value, &bytes_read);
|
||||
if (!s.ok()) {
|
||||
status_ = std::move(s);
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
@@ -1818,8 +1817,8 @@ void CompactionIterator::GarbageCollectEntityBlobsIfNeeded() {
|
||||
Slice entity_slice = value_;
|
||||
entity_columns_.clear();
|
||||
entity_blob_columns_.clear();
|
||||
Status s = WideColumnSerialization::Deserialize(
|
||||
entity_slice, entity_columns_, &entity_blob_columns_);
|
||||
Status s = WideColumnSerialization::DeserializeV2(
|
||||
entity_slice, entity_columns_, entity_blob_columns_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
@@ -1870,13 +1869,13 @@ void CompactionIterator::PrepareOutput() {
|
||||
"CompactionIterator::PrepareOutput:DeserializeEntity");
|
||||
// Deserialize entity once into member variables, then decide between
|
||||
// blob GC and extraction based on whether blob columns exist.
|
||||
// This avoids the double parse of HasBlobColumns() + Deserialize().
|
||||
// This avoids the double parse of HasBlobColumns() + DeserializeV2().
|
||||
entity_columns_.clear();
|
||||
entity_blob_columns_.clear();
|
||||
Slice entity_slice = value_;
|
||||
{
|
||||
Status s_deser = WideColumnSerialization::Deserialize(
|
||||
entity_slice, entity_columns_, &entity_blob_columns_);
|
||||
Status s_deser = WideColumnSerialization::DeserializeV2(
|
||||
entity_slice, entity_columns_, entity_blob_columns_);
|
||||
if (!s_deser.ok()) {
|
||||
status_ = s_deser;
|
||||
validity_info_.Invalidate();
|
||||
@@ -2065,9 +2064,8 @@ std::unique_ptr<BlobFetcher> CompactionIterator::CreateBlobFetcherIfNeeded(
|
||||
}
|
||||
}
|
||||
|
||||
return std::unique_ptr<BlobFetcher>(
|
||||
new OwningVersionBlobFetcher(version, std::move(read_options),
|
||||
blob_file_cache, allow_write_path_fallback));
|
||||
return std::unique_ptr<BlobFetcher>(new BlobFetcher(
|
||||
version, read_options, blob_file_cache, allow_write_path_fallback));
|
||||
}
|
||||
|
||||
std::unique_ptr<PrefetchBufferCollection>
|
||||
|
||||
@@ -1896,8 +1896,8 @@ TEST_F(WideColumnEntityBlobExtractionTest,
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns));
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns));
|
||||
|
||||
// The large column should now be a blob reference
|
||||
ASSERT_EQ(1, blob_columns.size());
|
||||
@@ -1941,8 +1941,8 @@ TEST_F(WideColumnEntityBlobExtractionTest,
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns));
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns));
|
||||
|
||||
// All three columns should be blob references
|
||||
ASSERT_EQ(3, blob_columns.size());
|
||||
@@ -2014,8 +2014,8 @@ TEST_F(WideColumnEntityBlobExtractionTest, WideColumnEntityMixedSizes) {
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns));
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns));
|
||||
|
||||
// Only the large column should be a blob reference
|
||||
ASSERT_EQ(1, blob_columns.size());
|
||||
@@ -2058,8 +2058,8 @@ TEST_F(WideColumnEntityBlobExtractionTest,
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns));
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns));
|
||||
|
||||
// The default column should be a blob reference
|
||||
ASSERT_EQ(1, blob_columns.size());
|
||||
@@ -2078,14 +2078,11 @@ TEST_F(WideColumnEntityBlobExtractionTest,
|
||||
// Mock BlobFetcher for testing blob GC without a real Version
|
||||
class MockBlobFetcher : public BlobFetcher {
|
||||
public:
|
||||
MockBlobFetcher() = default;
|
||||
|
||||
using BlobFetcher::FetchBlob;
|
||||
MockBlobFetcher() : BlobFetcher(nullptr /* version */, ReadOptions()) {}
|
||||
|
||||
Status FetchBlob(const Slice& /* user_key */, const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* /* prefetch_buffer */,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const override {
|
||||
PinnableSlice* blob_value, uint64_t* bytes_read) const {
|
||||
auto it = blob_data_.find(
|
||||
std::make_pair(blob_index.file_number(), blob_index.offset()));
|
||||
if (it == blob_data_.end()) {
|
||||
@@ -2099,8 +2096,6 @@ class MockBlobFetcher : public BlobFetcher {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
const ReadOptions& read_options() const override { return read_options_; }
|
||||
|
||||
void AddBlob(uint64_t file_number, uint64_t offset,
|
||||
const std::string& value) {
|
||||
blob_data_[std::make_pair(file_number, offset)] = value;
|
||||
@@ -2108,7 +2103,6 @@ class MockBlobFetcher : public BlobFetcher {
|
||||
|
||||
private:
|
||||
std::map<std::pair<uint64_t, uint64_t>, std::string> blob_data_;
|
||||
ReadOptions read_options_;
|
||||
};
|
||||
|
||||
// FakeCompaction that supports blob garbage collection
|
||||
@@ -2378,8 +2372,8 @@ TEST_F(WideColumnEntityBlobGCTest, WideColumnEntityBlobGCRelocate) {
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns_out;
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns_out));
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns_out));
|
||||
|
||||
// Entity structure should be preserved
|
||||
ASSERT_EQ(2, output_columns.size());
|
||||
@@ -2437,8 +2431,8 @@ TEST_F(WideColumnEntityBlobGCTest, WideColumnEntityBlobGCNoRelocateNewerFile) {
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns_out;
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns_out));
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns_out));
|
||||
|
||||
// Blob column should still exist and reference the same file
|
||||
ASSERT_EQ(1, blob_columns_out.size());
|
||||
@@ -2497,8 +2491,8 @@ TEST_F(WideColumnEntityBlobGCTest, WideColumnEntityBlobGCPartialRelocation) {
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns_out;
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns_out));
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns_out));
|
||||
|
||||
// All three columns should exist
|
||||
ASSERT_EQ(3, output_columns.size());
|
||||
@@ -2569,8 +2563,8 @@ TEST_F(WideColumnEntityBlobGCTest, WideColumnEntityBlobGCToInline) {
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns_out;
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns_out));
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns_out));
|
||||
|
||||
// Both columns should exist
|
||||
ASSERT_EQ(2, output_columns.size());
|
||||
@@ -2794,8 +2788,8 @@ TEST_F(WideColumnEntityBlobGCTest,
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> output_blob_columns;
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&output_blob_columns));
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
output_blob_columns));
|
||||
|
||||
ASSERT_EQ(2, output_columns.size());
|
||||
ASSERT_EQ("alpha", output_columns[0].name().ToString());
|
||||
|
||||
+265
-303
@@ -10,9 +10,6 @@
|
||||
#include <cstring>
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/log_writer.h"
|
||||
#include "db/version_edit.h"
|
||||
#include "file/writable_file_writer.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/filter_policy.h"
|
||||
@@ -81,7 +78,6 @@ class MyFlushBlockPolicyFactory : public FlushBlockPolicyFactory {
|
||||
private:
|
||||
const int num_keys_in_block_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
static bool enable_io_uring = true;
|
||||
@@ -92,15 +88,6 @@ class DBBasicTest : public DBTestBase {
|
||||
DBBasicTest() : DBTestBase("db_basic_test", /*env_do_fsync=*/false) {}
|
||||
};
|
||||
|
||||
class DBBasicGetWithParam : public DBBasicTest,
|
||||
public testing::WithParamInterface<bool> {
|
||||
public:
|
||||
DBBasicGetWithParam() : DBBasicTest(), use_coroutine_(GetParam()) {}
|
||||
|
||||
protected:
|
||||
const bool use_coroutine_;
|
||||
};
|
||||
|
||||
TEST_F(DBBasicTest, OpenWhenOpen) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
@@ -1021,92 +1008,6 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// Regression test for corrupted atomic group when reuse_manifest_on_open
|
||||
// appends after an incomplete atomic group at the MANIFEST tail.
|
||||
// Before the fix, last_valid_record_end_ included records from an incomplete
|
||||
// trailing atomic group, causing ReopenManifestForAppend to append new records
|
||||
// after them. On subsequent recovery the incomplete group followed by new
|
||||
// records triggers "corrupted atomic group".
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenIncompleteAtomicGroupAtTail) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
// Find the MANIFEST file.
|
||||
std::string manifest_path;
|
||||
{
|
||||
std::vector<std::string> files;
|
||||
ASSERT_OK(env_->GetChildren(dbname_, &files));
|
||||
for (const auto& f : files) {
|
||||
uint64_t number;
|
||||
FileType type;
|
||||
if (ParseFileName(f, &number, &type) && type == kDescriptorFile) {
|
||||
manifest_path = dbname_ + "/" + f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_FALSE(manifest_path.empty());
|
||||
|
||||
// Append an incomplete atomic group (2 records of a 3-member group) to the
|
||||
// MANIFEST. These are valid log records with correct CRCs but the atomic
|
||||
// group is not complete (missing the third record with remaining_entries=0).
|
||||
{
|
||||
uint64_t file_size;
|
||||
ASSERT_OK(env_->GetFileSize(manifest_path, &file_size));
|
||||
const auto& fs = env_->GetFileSystem();
|
||||
std::unique_ptr<FSWritableFile> fs_file;
|
||||
ASSERT_OK(fs->ReopenWritableFile(manifest_path, FileOptions(), &fs_file,
|
||||
nullptr));
|
||||
std::unique_ptr<WritableFileWriter> file_writer(new WritableFileWriter(
|
||||
std::move(fs_file), manifest_path, FileOptions()));
|
||||
log::Writer log_writer(std::move(file_writer), 0, false, false,
|
||||
kNoCompression, false, file_size % log::kBlockSize);
|
||||
|
||||
// Write 2 records of a 3-member atomic group
|
||||
VersionEdit edit1;
|
||||
edit1.SetLogNumber(0);
|
||||
edit1.SetNextFile(100);
|
||||
edit1.SetLastSequence(100);
|
||||
edit1.MarkAtomicGroup(2); // remaining_entries=2, expects 3 total
|
||||
std::string record1;
|
||||
ASSERT_TRUE(edit1.EncodeTo(&record1, 0));
|
||||
ASSERT_OK(log_writer.AddRecord(WriteOptions(), record1));
|
||||
|
||||
VersionEdit edit2;
|
||||
edit2.SetLogNumber(0);
|
||||
edit2.SetNextFile(100);
|
||||
edit2.SetLastSequence(100);
|
||||
edit2.MarkAtomicGroup(1); // remaining_entries=1
|
||||
std::string record2;
|
||||
ASSERT_TRUE(edit2.EncodeTo(&record2, 0));
|
||||
ASSERT_OK(log_writer.AddRecord(WriteOptions(), record2));
|
||||
// Deliberately NOT writing the third record (remaining_entries=0)
|
||||
}
|
||||
|
||||
// Reopen: recovery should succeed (incomplete trailing group is ignored).
|
||||
// With the fix, ReopenManifestForAppend will NOT reuse the MANIFEST because
|
||||
// manifest_last_valid_record_end_ < physical_size (the incomplete group
|
||||
// records are excluded from the valid end).
|
||||
Reopen(options);
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
|
||||
// Write new data to create new MANIFEST records.
|
||||
ASSERT_OK(Put("k2", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
// Final reopen: must NOT fail with "corrupted atomic group".
|
||||
Reopen(options);
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
ASSERT_EQ("v2", Get("k2"));
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, EnableDirectIOWithZeroBuf) {
|
||||
if (!IsDirectIOSupported()) {
|
||||
ROCKSDB_GTEST_BYPASS("Direct IO not supported");
|
||||
@@ -1469,36 +1370,27 @@ TEST_F(DBBasicTest, LevelLimitReopen) {
|
||||
ASSERT_OK(TryReopenWithColumnFamilies({"default", "pikachu"}, options));
|
||||
}
|
||||
|
||||
TEST_P(DBBasicGetWithParam, PutDeleteGet) {
|
||||
TEST_F(DBBasicTest, PutDeleteGet) {
|
||||
do {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
ASSERT_OK(Put(1, "foo", "v1"));
|
||||
ASSERT_EQ("v1", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("v1", Get(1, "foo"));
|
||||
ASSERT_OK(Put(1, "foo", "v2"));
|
||||
ASSERT_EQ("v2", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("v2", Get(1, "foo"));
|
||||
ASSERT_OK(Delete(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "foo", nullptr, use_coroutine_));
|
||||
|
||||
ASSERT_OK(Put("default_foo", "default_v1"));
|
||||
ASSERT_EQ("default_v1",
|
||||
Get("default_foo", static_cast<const Snapshot*>(nullptr),
|
||||
use_coroutine_));
|
||||
|
||||
PinnableSlice default_pinnable_value;
|
||||
ASSERT_OK(Get("default_foo", &default_pinnable_value, use_coroutine_));
|
||||
ASSERT_EQ("default_v1", default_pinnable_value.ToString());
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "foo"));
|
||||
} while (ChangeOptions());
|
||||
}
|
||||
|
||||
TEST_P(DBBasicGetWithParam, PutSingleDeleteGet) {
|
||||
TEST_F(DBBasicTest, PutSingleDeleteGet) {
|
||||
do {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
ASSERT_OK(Put(1, "foo", "v1"));
|
||||
ASSERT_EQ("v1", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("v1", Get(1, "foo"));
|
||||
ASSERT_OK(Put(1, "foo2", "v2"));
|
||||
ASSERT_EQ("v2", Get(1, "foo2", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("v2", Get(1, "foo2"));
|
||||
ASSERT_OK(SingleDelete(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "foo"));
|
||||
// Ski FIFO and universal compaction because they do not apply to the test
|
||||
// case. Skip MergePut because single delete does not get removed when it
|
||||
// encounters a merge.
|
||||
@@ -1506,40 +1398,40 @@ TEST_P(DBBasicGetWithParam, PutSingleDeleteGet) {
|
||||
kSkipMergePut));
|
||||
}
|
||||
|
||||
TEST_P(DBBasicGetWithParam, TimedPutBasic) {
|
||||
TEST_F(DBBasicTest, TimedPutBasic) {
|
||||
do {
|
||||
Options options = CurrentOptions();
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
ASSERT_OK(TimedPut(1, "foo", "v1", /*write_unix_time=*/0));
|
||||
// Read from memtable
|
||||
ASSERT_EQ("v1", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("v1", Get(1, "foo"));
|
||||
ASSERT_OK(TimedPut(1, "foo", "v2.1", /*write_unix_time=*/3));
|
||||
ASSERT_EQ("v2.1", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("v2.1", Get(1, "foo"));
|
||||
|
||||
// Read from sst file
|
||||
ASSERT_OK(db_->Flush(FlushOptions(), handles_[1]));
|
||||
ASSERT_OK(Merge(1, "foo", "v2.2"));
|
||||
ASSERT_EQ("v2.1,v2.2", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("v2.1,v2.2", Get(1, "foo"));
|
||||
ASSERT_OK(Delete(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "foo"));
|
||||
|
||||
ASSERT_OK(TimedPut(1, "bar", "bv1", /*write_unix_time=*/0));
|
||||
ASSERT_EQ("bv1", Get(1, "bar", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("bv1", Get(1, "bar"));
|
||||
ASSERT_OK(TimedPut(1, "baz", "bzv1", /*write_unix_time=*/0));
|
||||
ASSERT_EQ("bzv1", Get(1, "baz", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("bzv1", Get(1, "baz"));
|
||||
if (option_config_ != kRowCache) {
|
||||
std::string range_del_begin = "b";
|
||||
std::string range_del_end = "baz";
|
||||
Slice begin_rdel = range_del_begin, end_rdel = range_del_end;
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), handles_[1], begin_rdel, end_rdel));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "bar", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "bar"));
|
||||
}
|
||||
|
||||
ASSERT_EQ("bzv1", Get(1, "baz", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("bzv1", Get(1, "baz"));
|
||||
ASSERT_OK(SingleDelete(1, "baz"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "baz", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "baz"));
|
||||
} while (ChangeOptions(kSkipPlainTable));
|
||||
}
|
||||
|
||||
@@ -1565,17 +1457,17 @@ TEST_F(DBBasicTest, EmptyFlush) {
|
||||
kSkipMergePut));
|
||||
}
|
||||
|
||||
TEST_P(DBBasicGetWithParam, GetFromVersions) {
|
||||
TEST_F(DBBasicTest, GetFromVersions) {
|
||||
do {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
ASSERT_OK(Put(1, "foo", "v1"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_EQ("v1", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("NOT_FOUND", Get(0, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("v1", Get(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(0, "foo"));
|
||||
} while (ChangeOptions());
|
||||
}
|
||||
|
||||
TEST_P(DBBasicGetWithParam, GetSnapshot) {
|
||||
TEST_F(DBBasicTest, GetSnapshot) {
|
||||
anon::OptionsOverride options_override;
|
||||
options_override.skip_policy = kSkipNoSnapshot;
|
||||
do {
|
||||
@@ -1586,11 +1478,11 @@ TEST_P(DBBasicGetWithParam, GetSnapshot) {
|
||||
ASSERT_OK(Put(1, key, "v1"));
|
||||
const Snapshot* s1 = db_->GetSnapshot();
|
||||
ASSERT_OK(Put(1, key, "v2"));
|
||||
ASSERT_EQ("v2", Get(1, key, nullptr, use_coroutine_));
|
||||
ASSERT_EQ("v1", Get(1, key, s1, use_coroutine_));
|
||||
ASSERT_EQ("v2", Get(1, key));
|
||||
ASSERT_EQ("v1", Get(1, key, s1));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_EQ("v2", Get(1, key, nullptr, use_coroutine_));
|
||||
ASSERT_EQ("v1", Get(1, key, s1, use_coroutine_));
|
||||
ASSERT_EQ("v2", Get(1, key));
|
||||
ASSERT_EQ("v1", Get(1, key, s1));
|
||||
db_->ReleaseSnapshot(s1);
|
||||
}
|
||||
} while (ChangeOptions());
|
||||
@@ -1847,7 +1739,7 @@ TEST_F(DBBasicTest, LockFileRecovery) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DBBasicGetWithParam, Snapshot) {
|
||||
TEST_F(DBBasicTest, Snapshot) {
|
||||
env_->SetMockSleep();
|
||||
anon::OptionsOverride options_override;
|
||||
options_override.skip_policy = kSkipNoSnapshot;
|
||||
@@ -1881,31 +1773,31 @@ TEST_P(DBBasicGetWithParam, Snapshot) {
|
||||
|
||||
ASSERT_OK(Put(0, "foo", "0v4"));
|
||||
ASSERT_OK(Put(1, "foo", "1v4"));
|
||||
ASSERT_EQ("0v1", Get(0, "foo", s1, use_coroutine_));
|
||||
ASSERT_EQ("1v1", Get(1, "foo", s1, use_coroutine_));
|
||||
ASSERT_EQ("0v2", Get(0, "foo", s2, use_coroutine_));
|
||||
ASSERT_EQ("1v2", Get(1, "foo", s2, use_coroutine_));
|
||||
ASSERT_EQ("0v3", Get(0, "foo", s3.snapshot(), use_coroutine_));
|
||||
ASSERT_EQ("1v3", Get(1, "foo", s3.snapshot(), use_coroutine_));
|
||||
ASSERT_EQ("0v4", Get(0, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("1v4", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("0v1", Get(0, "foo", s1));
|
||||
ASSERT_EQ("1v1", Get(1, "foo", s1));
|
||||
ASSERT_EQ("0v2", Get(0, "foo", s2));
|
||||
ASSERT_EQ("1v2", Get(1, "foo", s2));
|
||||
ASSERT_EQ("0v3", Get(0, "foo", s3.snapshot()));
|
||||
ASSERT_EQ("1v3", Get(1, "foo", s3.snapshot()));
|
||||
ASSERT_EQ("0v4", Get(0, "foo"));
|
||||
ASSERT_EQ("1v4", Get(1, "foo"));
|
||||
}
|
||||
|
||||
ASSERT_EQ(2U, GetNumSnapshots());
|
||||
ASSERT_EQ(time_snap1, GetTimeOldestSnapshots());
|
||||
ASSERT_EQ(GetSequenceOldestSnapshots(), s1->GetSequenceNumber());
|
||||
ASSERT_EQ("0v1", Get(0, "foo", s1, use_coroutine_));
|
||||
ASSERT_EQ("1v1", Get(1, "foo", s1, use_coroutine_));
|
||||
ASSERT_EQ("0v2", Get(0, "foo", s2, use_coroutine_));
|
||||
ASSERT_EQ("1v2", Get(1, "foo", s2, use_coroutine_));
|
||||
ASSERT_EQ("0v4", Get(0, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("1v4", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("0v1", Get(0, "foo", s1));
|
||||
ASSERT_EQ("1v1", Get(1, "foo", s1));
|
||||
ASSERT_EQ("0v2", Get(0, "foo", s2));
|
||||
ASSERT_EQ("1v2", Get(1, "foo", s2));
|
||||
ASSERT_EQ("0v4", Get(0, "foo"));
|
||||
ASSERT_EQ("1v4", Get(1, "foo"));
|
||||
|
||||
db_->ReleaseSnapshot(s1);
|
||||
ASSERT_EQ("0v2", Get(0, "foo", s2, use_coroutine_));
|
||||
ASSERT_EQ("1v2", Get(1, "foo", s2, use_coroutine_));
|
||||
ASSERT_EQ("0v4", Get(0, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("1v4", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("0v2", Get(0, "foo", s2));
|
||||
ASSERT_EQ("1v2", Get(1, "foo", s2));
|
||||
ASSERT_EQ("0v4", Get(0, "foo"));
|
||||
ASSERT_EQ("1v4", Get(1, "foo"));
|
||||
ASSERT_EQ(1U, GetNumSnapshots());
|
||||
ASSERT_LT(time_snap1, GetTimeOldestSnapshots());
|
||||
ASSERT_EQ(GetSequenceOldestSnapshots(), s2->GetSequenceNumber());
|
||||
@@ -1913,19 +1805,11 @@ TEST_P(DBBasicGetWithParam, Snapshot) {
|
||||
db_->ReleaseSnapshot(s2);
|
||||
ASSERT_EQ(0U, GetNumSnapshots());
|
||||
ASSERT_EQ(GetSequenceOldestSnapshots(), 0);
|
||||
ASSERT_EQ("0v4", Get(0, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("1v4", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("0v4", Get(0, "foo"));
|
||||
ASSERT_EQ("1v4", Get(1, "foo"));
|
||||
} while (ChangeOptions());
|
||||
}
|
||||
|
||||
#if USE_COROUTINES
|
||||
INSTANTIATE_TEST_CASE_P(DBBasicGetWithParam, DBBasicGetWithParam,
|
||||
testing::Bool());
|
||||
#else
|
||||
INSTANTIATE_TEST_CASE_P(DBBasicGetWithParam, DBBasicGetWithParam,
|
||||
testing::Values(false));
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
class DBBasicMultiConfigs : public DBBasicTest,
|
||||
public ::testing::WithParamInterface<int> {
|
||||
public:
|
||||
@@ -3874,15 +3758,11 @@ INSTANTIATE_TEST_CASE_P(DBMultiGetTestWithParam, DBMultiGetTestWithParam,
|
||||
testing::Combine(testing::Bool(), testing::Bool()));
|
||||
|
||||
#if USE_COROUTINES
|
||||
class DBMultiGetAsyncIOTest
|
||||
: public DBBasicTest,
|
||||
public ::testing::WithParamInterface<std::tuple<bool, bool>> {
|
||||
class DBMultiGetAsyncIOTest : public DBBasicTest,
|
||||
public ::testing::WithParamInterface<bool> {
|
||||
public:
|
||||
DBMultiGetAsyncIOTest()
|
||||
: DBBasicTest(),
|
||||
optimize_multiget_for_io_(std::get<0>(GetParam())),
|
||||
use_coroutine_(std::get<1>(GetParam())),
|
||||
statistics_(ROCKSDB_NAMESPACE::CreateDBStatistics()) {
|
||||
: DBBasicTest(), statistics_(ROCKSDB_NAMESPACE::CreateDBStatistics()) {
|
||||
BlockBasedTableOptions bbto;
|
||||
bbto.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
options_ = CurrentOptions();
|
||||
@@ -3956,20 +3836,6 @@ class DBMultiGetAsyncIOTest
|
||||
const std::shared_ptr<Statistics>& statistics() { return statistics_; }
|
||||
|
||||
protected:
|
||||
void AssertMultiGetIOBatchSize(uint64_t expected_count,
|
||||
uint64_t expected_max) {
|
||||
HistogramData multiget_io_batch_size;
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE,
|
||||
&multiget_io_batch_size);
|
||||
ASSERT_EQ(multiget_io_batch_size.count, expected_count);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, expected_max);
|
||||
}
|
||||
|
||||
bool UseCoroutineRead() const {
|
||||
return use_coroutine_ &&
|
||||
options_.env->GetFileSystem()->GetReadExecutor() != nullptr;
|
||||
}
|
||||
|
||||
void PrepareDBForTest() {
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
Reopen(options_);
|
||||
@@ -3985,9 +3851,6 @@ class DBMultiGetAsyncIOTest
|
||||
|
||||
void ReopenDB() { Reopen(options_); }
|
||||
|
||||
const bool optimize_multiget_for_io_;
|
||||
const bool use_coroutine_;
|
||||
|
||||
private:
|
||||
std::shared_ptr<Statistics> statistics_;
|
||||
Options options_;
|
||||
@@ -3996,28 +3859,39 @@ class DBMultiGetAsyncIOTest
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL0) {
|
||||
// All 3 keys in L0. The L0 files should be read serially.
|
||||
std::vector<std::string> key_strs{Key(0), Key(40), Key(80)};
|
||||
std::vector<Slice> keys{key_strs[0], key_strs[1], key_strs[2]};
|
||||
std::vector<PinnableSlice> values(key_strs.size());
|
||||
std::vector<Status> statuses(key_strs.size());
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
ASSERT_EQ(values.size(), 3);
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_OK(statuses[2]);
|
||||
ASSERT_EQ(values[0], "val_l0_" + std::to_string(0));
|
||||
ASSERT_EQ(values[1], "val_l0_" + std::to_string(40));
|
||||
ASSERT_EQ(values[2], "val_l0_" + std::to_string(80));
|
||||
|
||||
HistogramData multiget_io_batch_size;
|
||||
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
|
||||
|
||||
// With async IO, lookups will happen in parallel for each key
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
if (UseCoroutineRead()) {
|
||||
AssertMultiGetIOBatchSize(3, 1);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
|
||||
} else if (optimize_multiget_for_io_) {
|
||||
AssertMultiGetIOBatchSize(1, 3);
|
||||
if (GetParam()) {
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 1);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 3);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
|
||||
} else {
|
||||
// Without Async IO, MultiGet will call MultiRead 3 times, once for each
|
||||
// L0 file
|
||||
AssertMultiGetIOBatchSize(3, 1);
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 3);
|
||||
}
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
@@ -4025,58 +3899,64 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL0) {
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL1) {
|
||||
std::vector<std::string> key_strs{Key(33), Key(54), Key(102)};
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
key_strs.push_back(Key(33));
|
||||
key_strs.push_back(Key(54));
|
||||
key_strs.push_back(Key(102));
|
||||
keys.emplace_back(key_strs[0]);
|
||||
keys.emplace_back(key_strs[1]);
|
||||
keys.emplace_back(key_strs[2]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
ASSERT_EQ(values.size(), 3);
|
||||
ASSERT_EQ(statuses[0], Status::OK());
|
||||
ASSERT_EQ(statuses[1], Status::OK());
|
||||
ASSERT_EQ(statuses[2], Status::OK());
|
||||
ASSERT_EQ(values[0], "val_l1_" + std::to_string(33));
|
||||
ASSERT_EQ(values[1], "val_l1_" + std::to_string(54));
|
||||
ASSERT_EQ(values[2], "val_l1_" + std::to_string(102));
|
||||
|
||||
HistogramData multiget_io_batch_size;
|
||||
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
if (UseCoroutineRead()) {
|
||||
AssertMultiGetIOBatchSize(3, 1);
|
||||
} else {
|
||||
// A batch of 3 async IOs is expected, one for each overlapping file in L1.
|
||||
AssertMultiGetIOBatchSize(1, 3);
|
||||
}
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 6 : 3);
|
||||
// A batch of 3 async IOs is expected, one for each overlapping file in L1
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 1);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 3);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, SingleGetFromL1UsesCoroutineRead) {
|
||||
int coroutine_read_count = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"RandomAccessFileReader::ReadCoroutine:SubmitReadAsync",
|
||||
[&](void*) { ++coroutine_read_count; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ASSERT_EQ(
|
||||
"val_l1_" + std::to_string(33),
|
||||
Get(Key(33), static_cast<const Snapshot*>(nullptr), use_coroutine_));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
if (UseCoroutineRead()) {
|
||||
ASSERT_GT(coroutine_read_count, 0);
|
||||
} else {
|
||||
ASSERT_EQ(coroutine_read_count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL1Error) {
|
||||
std::vector<std::string> key_strs{Key(33), Key(54), Key(102)};
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
key_strs.push_back(Key(33));
|
||||
key_strs.push_back(Key(54));
|
||||
key_strs.push_back(Key(102));
|
||||
keys.emplace_back(key_strs[0]);
|
||||
keys.emplace_back(key_strs[1]);
|
||||
keys.emplace_back(key_strs[2]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
|
||||
int count = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
@@ -4105,100 +3985,147 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1Error) {
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
ASSERT_EQ(values.size(), 3);
|
||||
ASSERT_EQ(values[0], "val_l1_" + std::to_string(33));
|
||||
ASSERT_EQ(values[1], "val_l1_" + std::to_string(54));
|
||||
ASSERT_EQ(values[2], Status::IOError().ToString());
|
||||
ASSERT_EQ(statuses[0], Status::OK());
|
||||
ASSERT_EQ(statuses[1], Status::OK());
|
||||
ASSERT_EQ(statuses[2], Status::IOError());
|
||||
|
||||
if (UseCoroutineRead()) {
|
||||
AssertMultiGetIOBatchSize(2, 1);
|
||||
} else {
|
||||
// A batch of 3 async IOs is expected, one for each overlapping file in L1.
|
||||
AssertMultiGetIOBatchSize(1, 2);
|
||||
}
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 5 : 2);
|
||||
HistogramData multiget_io_batch_size;
|
||||
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
|
||||
|
||||
// A batch of 3 async IOs is expected, one for each overlapping file in L1
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 1);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 2);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 2);
|
||||
}
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, LastKeyInFile) {
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
// 21 is the last key in the first L1 file
|
||||
std::vector<std::string> key_strs{Key(21), Key(54), Key(102)};
|
||||
key_strs.push_back(Key(21));
|
||||
key_strs.push_back(Key(54));
|
||||
key_strs.push_back(Key(102));
|
||||
keys.emplace_back(key_strs[0]);
|
||||
keys.emplace_back(key_strs[1]);
|
||||
keys.emplace_back(key_strs[2]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
ASSERT_EQ(values.size(), 3);
|
||||
ASSERT_EQ(statuses[0], Status::OK());
|
||||
ASSERT_EQ(statuses[1], Status::OK());
|
||||
ASSERT_EQ(statuses[2], Status::OK());
|
||||
ASSERT_EQ(values[0], "val_l1_" + std::to_string(21));
|
||||
ASSERT_EQ(values[1], "val_l1_" + std::to_string(54));
|
||||
ASSERT_EQ(values[2], "val_l1_" + std::to_string(102));
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
HistogramData multiget_io_batch_size;
|
||||
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
|
||||
|
||||
// Since the first MultiGet key is the last key in a file, the MultiGet is
|
||||
// expected to lookup in that file first, before moving on to other files.
|
||||
// So the first file lookup will issue one async read, and the next lookup
|
||||
// will lookup 2 files in parallel and issue 2 async reads
|
||||
if (UseCoroutineRead()) {
|
||||
AssertMultiGetIOBatchSize(3, 1);
|
||||
} else {
|
||||
AssertMultiGetIOBatchSize(2, 2);
|
||||
}
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 2);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 2);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2) {
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
// 33 and 102 are in L1, and 56 is in L2
|
||||
std::vector<std::string> key_strs{Key(33), Key(56), Key(102)};
|
||||
key_strs.push_back(Key(33));
|
||||
key_strs.push_back(Key(56));
|
||||
key_strs.push_back(Key(102));
|
||||
keys.emplace_back(key_strs[0]);
|
||||
keys.emplace_back(key_strs[1]);
|
||||
keys.emplace_back(key_strs[2]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
ASSERT_EQ(values.size(), 3);
|
||||
ASSERT_EQ(statuses[0], Status::OK());
|
||||
ASSERT_EQ(statuses[1], Status::OK());
|
||||
ASSERT_EQ(statuses[2], Status::OK());
|
||||
ASSERT_EQ(values[0], "val_l1_" + std::to_string(33));
|
||||
ASSERT_EQ(values[1], "val_l2_" + std::to_string(56));
|
||||
ASSERT_EQ(values[2], "val_l1_" + std::to_string(102));
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
HistogramData multiget_io_batch_size;
|
||||
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
|
||||
|
||||
// There are 2 keys in L1 in twp separate files, and 1 in L2. With
|
||||
// optimize_multiget_for_io, all three lookups will happen in parallel.
|
||||
// Otherwise, the L2 lookup will happen after L1.
|
||||
if (UseCoroutineRead()) {
|
||||
AssertMultiGetIOBatchSize(3, 1);
|
||||
} else {
|
||||
AssertMultiGetIOBatchSize(optimize_multiget_for_io_ ? 1 : 2,
|
||||
optimize_multiget_for_io_ ? 3 : 2);
|
||||
}
|
||||
ASSERT_EQ(multiget_io_batch_size.count, GetParam() ? 1 : 2);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, GetParam() ? 3 : 2);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL2WithRangeOverlapL0L1) {
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
// 19 and 26 are in L2, but overlap with L0 and L1 file ranges
|
||||
std::vector<std::string> key_strs{Key(19), Key(26)};
|
||||
key_strs.push_back(Key(19));
|
||||
key_strs.push_back(Key(26));
|
||||
keys.emplace_back(key_strs[0]);
|
||||
keys.emplace_back(key_strs[1]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
ASSERT_EQ(values.size(), 2);
|
||||
ASSERT_EQ(statuses[0], Status::OK());
|
||||
ASSERT_EQ(statuses[1], Status::OK());
|
||||
ASSERT_EQ(values[0], "val_l2_" + std::to_string(19));
|
||||
ASSERT_EQ(values[1], "val_l2_" + std::to_string(26));
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
// Bloom filters in L0/L1 avoid the coroutine calls in those levels. The
|
||||
// coroutine path counts every per-file MultiGetFromSST coroutine (including
|
||||
// serial reads), whereas the async_io path counts only the parallel overlap
|
||||
// batch, so the coroutine path observes one more.
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 3 : 2);
|
||||
// Bloom filters in L0/L1 will avoid the coroutine calls in those levels
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 2);
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
@@ -4206,35 +4133,62 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL2WithRangeOverlapL0L1) {
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL2WithRangeDelInL1) {
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
// 139 and 163 are in L2, but overlap with a range deletes in L1
|
||||
std::vector<std::string> key_strs{Key(139), Key(163)};
|
||||
key_strs.push_back(Key(139));
|
||||
key_strs.push_back(Key(163));
|
||||
keys.emplace_back(key_strs[0]);
|
||||
keys.emplace_back(key_strs[1]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
ASSERT_EQ(values.size(), 2);
|
||||
ASSERT_EQ(values[0], "NOT_FOUND");
|
||||
ASSERT_EQ(values[1], "NOT_FOUND");
|
||||
ASSERT_EQ(statuses[0], Status::NotFound());
|
||||
ASSERT_EQ(statuses[1], Status::NotFound());
|
||||
|
||||
// Bloom filters in L0/L1 will avoid the coroutine calls in those levels
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 2);
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2WithRangeDelInL1) {
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
// 139 and 163 are in L2, but overlap with a range deletes in L1
|
||||
std::vector<std::string> key_strs{Key(139), Key(144), Key(163)};
|
||||
key_strs.push_back(Key(139));
|
||||
key_strs.push_back(Key(144));
|
||||
key_strs.push_back(Key(163));
|
||||
keys.emplace_back(key_strs[0]);
|
||||
keys.emplace_back(key_strs[1]);
|
||||
keys.emplace_back(key_strs[2]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ASSERT_EQ(values.size(), 3);
|
||||
ASSERT_EQ(values[0], "NOT_FOUND");
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
ASSERT_EQ(values.size(), keys.size());
|
||||
ASSERT_EQ(statuses[0], Status::NotFound());
|
||||
ASSERT_EQ(statuses[1], Status::OK());
|
||||
ASSERT_EQ(values[1], "val_l1_" + std::to_string(144));
|
||||
ASSERT_EQ(values[2], "NOT_FOUND");
|
||||
ASSERT_EQ(statuses[2], Status::NotFound());
|
||||
|
||||
// Bloom filters in L0/L1 will avoid the coroutine calls in those levels
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
|
||||
@@ -4242,18 +4196,32 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2WithRangeDelInL1) {
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetNoIOUring) {
|
||||
std::vector<std::string> key_strs{Key(33), Key(54), Key(102)};
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
key_strs.push_back(Key(33));
|
||||
key_strs.push_back(Key(54));
|
||||
key_strs.push_back(Key(102));
|
||||
keys.emplace_back(key_strs[0]);
|
||||
keys.emplace_back(key_strs[1]);
|
||||
keys.emplace_back(key_strs[2]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
|
||||
enable_io_uring = false;
|
||||
ReopenDB();
|
||||
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
ASSERT_EQ(values.size(), 3);
|
||||
ASSERT_EQ(values[0], "val_l1_" + std::to_string(33));
|
||||
ASSERT_EQ(values[1], "val_l1_" + std::to_string(54));
|
||||
ASSERT_EQ(values[2], "val_l1_" + std::to_string(102));
|
||||
ASSERT_EQ(statuses[0], Status::OK());
|
||||
ASSERT_EQ(statuses[1], Status::OK());
|
||||
ASSERT_EQ(statuses[2], Status::OK());
|
||||
|
||||
HistogramData async_read_bytes;
|
||||
|
||||
@@ -4261,17 +4229,11 @@ TEST_P(DBMultiGetAsyncIOTest, GetNoIOUring) {
|
||||
|
||||
// A batch of 3 async IOs is expected, one for each overlapping file in L1
|
||||
ASSERT_EQ(async_read_bytes.count, 0);
|
||||
const uint64_t coroutine_count =
|
||||
statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT);
|
||||
if (UseCoroutineRead()) {
|
||||
ASSERT_GT(coroutine_count, 0);
|
||||
} else {
|
||||
ASSERT_EQ(coroutine_count, 0);
|
||||
}
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(DBMultiGetAsyncIOTest, DBMultiGetAsyncIOTest,
|
||||
testing::Combine(testing::Bool(), testing::Bool()));
|
||||
testing::Bool());
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetStats) {
|
||||
|
||||
+1
-124
@@ -28,7 +28,6 @@
|
||||
#include "rocksdb/wal_filter.h"
|
||||
#include "test_util/testutil.h"
|
||||
#include "util/defer.h"
|
||||
#include "util/mutexlock.h"
|
||||
#include "util/random.h"
|
||||
#include "utilities/fault_injection_env.h"
|
||||
|
||||
@@ -7903,10 +7902,7 @@ class FastOpenTestFS : public FileSystemWrapper {
|
||||
IODebugContext* dbg) override {
|
||||
if (file_opts.file_metadata != nullptr) {
|
||||
metadata_passed_on_open_.fetch_add(1, std::memory_order_relaxed);
|
||||
{
|
||||
MutexLock l(&last_metadata_mu_);
|
||||
last_metadata_received_ = *file_opts.file_metadata;
|
||||
}
|
||||
last_metadata_received_ = *file_opts.file_metadata;
|
||||
}
|
||||
IOStatus s = target()->NewRandomAccessFile(fname, file_opts, result, dbg);
|
||||
if (s.ok()) {
|
||||
@@ -7925,21 +7921,18 @@ class FastOpenTestFS : public FileSystemWrapper {
|
||||
}
|
||||
|
||||
std::string GetLastMetadataReceived() const {
|
||||
MutexLock l(&last_metadata_mu_);
|
||||
return last_metadata_received_;
|
||||
}
|
||||
|
||||
void ResetCounters() {
|
||||
metadata_retrieved_count_.store(0, std::memory_order_relaxed);
|
||||
metadata_passed_on_open_.store(0, std::memory_order_relaxed);
|
||||
MutexLock l(&last_metadata_mu_);
|
||||
last_metadata_received_.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<int> metadata_retrieved_count_{0};
|
||||
std::atomic<int> metadata_passed_on_open_{0};
|
||||
mutable port::Mutex last_metadata_mu_;
|
||||
std::string last_metadata_received_;
|
||||
};
|
||||
|
||||
@@ -8281,122 +8274,6 @@ TEST_F(DBTest2, FastSstOpenDisableAfterMetadataPersisted) {
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, FastSstOpenMetadataPreservedAcrossManifestRotation) {
|
||||
// Regression test: FileMetaData::file_open_metadata for unchanged live
|
||||
// SSTs must survive a MANIFEST rotation. WriteCurrentStateToManifest
|
||||
// snapshots live SSTs into a new MANIFEST; if it drops file_open_metadata
|
||||
// for those unchanged SSTs, the fast_sst_open optimization is silently
|
||||
// disabled for them until they are rewritten by flush/compaction/ingest.
|
||||
auto test_fs = std::make_shared<FastOpenTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> test_env(new CompositeEnvWrapper(env_, test_fs));
|
||||
|
||||
Options options;
|
||||
options.env = test_env.get();
|
||||
options.fast_sst_open = true;
|
||||
options.create_if_missing = true;
|
||||
// Keep the pre-rotation SST from being rewritten by compaction.
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
std::string test_dbname = test::PerThreadDBPath("fast_open_manifest_rotate");
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
|
||||
// Flush an SST; metadata is retrieved and persisted in the MANIFEST.
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key1", "value1"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
|
||||
|
||||
// Shrink max_manifest_file_size so the next MANIFEST write rotates and
|
||||
// exercises WriteCurrentStateToManifest.
|
||||
ASSERT_OK(db->SetDBOptions(
|
||||
{{"max_manifest_file_size", "1"}, {"max_manifest_space_amp_pct", "0"}}));
|
||||
|
||||
uint64_t manifest_before =
|
||||
static_cast<DBImpl*>(db.get())->TEST_Current_Manifest_FileNo();
|
||||
|
||||
// Flush a second SST to trigger the rotation. SST #1 is unchanged, so
|
||||
// the rotated MANIFEST snapshot must carry SST #1's file_open_metadata.
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key2", "value2"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
|
||||
uint64_t manifest_after =
|
||||
static_cast<DBImpl*>(db.get())->TEST_Current_Manifest_FileNo();
|
||||
ASSERT_GT(manifest_after, manifest_before);
|
||||
|
||||
db.reset();
|
||||
test_fs->ResetCounters();
|
||||
|
||||
// Reopen. Every live SST should be opened with file_open_metadata.
|
||||
// Pre-fix: only SST #2 (added by the rotating edit) has metadata; SST #1
|
||||
// was stripped by the rotation snapshot, so the count would be 1.
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
|
||||
std::string value;
|
||||
ASSERT_OK(db->Get(ReadOptions(), "key1", &value));
|
||||
ASSERT_EQ("value1", value);
|
||||
ASSERT_OK(db->Get(ReadOptions(), "key2", &value));
|
||||
ASSERT_EQ("value2", value);
|
||||
|
||||
ASSERT_EQ(2, test_fs->GetMetadataPassedOnOpenCount());
|
||||
|
||||
db.reset();
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, FastSstOpenMetadataPreservedAcrossTrivialMove) {
|
||||
// Regression test: FileMetaData::file_open_metadata must survive a trivial
|
||||
// move. Trivial move re-records an SST at a new level via DeleteFile +
|
||||
// AddFile without physically rewriting the file. If AddFile drops
|
||||
// file_open_metadata, the file loses its fast_sst_open advantage until
|
||||
// it is next rewritten by real compaction/flush/ingest.
|
||||
auto test_fs = std::make_shared<FastOpenTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> test_env(new CompositeEnvWrapper(env_, test_fs));
|
||||
|
||||
Options options;
|
||||
options.env = test_env.get();
|
||||
options.fast_sst_open = true;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
std::string test_dbname = test::PerThreadDBPath("fast_open_trivial_move");
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
|
||||
// Flush a single SST at L0; metadata is retrieved once and persisted.
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key1", "value1"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
|
||||
|
||||
// Manual compaction. With a single L0 file and empty lower levels, the
|
||||
// compaction picker converts this into a trivial move (no physical
|
||||
// rewrite), exercising PrepareTrivialMoveEdit.
|
||||
ASSERT_OK(db->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
// If the SST had been rewritten, metadata retrieval would have re-run on
|
||||
// the new file. Unchanged count confirms this was a trivial move.
|
||||
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
|
||||
|
||||
db.reset();
|
||||
test_fs->ResetCounters();
|
||||
|
||||
// Reopen. The trivially-moved SST should still carry its
|
||||
// file_open_metadata and be opened via the fast_sst_open fast path.
|
||||
// Pre-fix, the trivial move stripped it and this count would be 0.
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
|
||||
std::string value;
|
||||
ASSERT_OK(db->Get(ReadOptions(), "key1", &value));
|
||||
ASSERT_EQ("value1", value);
|
||||
|
||||
ASSERT_EQ(1, test_fs->GetMetadataPassedOnOpenCount());
|
||||
|
||||
db.reset();
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+5
-138
@@ -8,9 +8,7 @@
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/db_test_util.h"
|
||||
@@ -2221,12 +2219,8 @@ TEST_F(DBFlushTest, FireOnFlushCompletedAfterCommittedResult) {
|
||||
listener->seq1 = db_->GetLatestSequenceNumber();
|
||||
// t1 will wait for the second flush complete before committing flush result.
|
||||
auto t1 = port::Thread([&]() {
|
||||
// flush_opts.wait = true, and listener_wait = true so that Flush() does not
|
||||
// return until the OnFlushCompleted callbacks (which set completed1 and
|
||||
// completed2) have finished running.
|
||||
FlushOptions flush_opts;
|
||||
flush_opts.listener_wait = true;
|
||||
ASSERT_OK(db_->Flush(flush_opts));
|
||||
// flush_opts.wait = true
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
});
|
||||
// Wait for first flush started.
|
||||
TEST_SYNC_POINT(
|
||||
@@ -2239,142 +2233,15 @@ TEST_F(DBFlushTest, FireOnFlushCompletedAfterCommittedResult) {
|
||||
flush_opts.wait = false;
|
||||
ASSERT_OK(db_->Flush(flush_opts));
|
||||
t1.join();
|
||||
// listener_wait on t1's flush above guarantees both OnFlushCompleted
|
||||
// callbacks have finished by the time Flush() returned; no need to
|
||||
// additionally wait for background work here.
|
||||
// Ensure background work is fully finished including listener callbacks
|
||||
// before accessing listener state.
|
||||
ASSERT_OK(dbfull()->TEST_WaitForBackgroundWork());
|
||||
ASSERT_TRUE(listener->completed1);
|
||||
ASSERT_TRUE(listener->completed2);
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
namespace {
|
||||
// EventListener whose OnFlushCompleted blocks until explicitly released, used
|
||||
// to verify FlushOptions::listener_wait. Optionally only blocks on the first
|
||||
// callback (useful for atomic flush, which fires one callback per CF).
|
||||
class BlockingFlushListener : public EventListener {
|
||||
public:
|
||||
explicit BlockingFlushListener(bool block_only_first)
|
||||
: block_only_first_(block_only_first) {}
|
||||
|
||||
void OnFlushCompleted(DB* /*db*/, const FlushJobInfo& /*info*/) override {
|
||||
if (block_only_first_ && num_completed_.fetch_add(1) != 0) {
|
||||
return;
|
||||
}
|
||||
std::unique_lock<std::mutex> lk(mu_);
|
||||
entered_ = true;
|
||||
cv_.notify_all();
|
||||
cv_.wait(lk, [this] { return release_; });
|
||||
returned_ = true;
|
||||
}
|
||||
|
||||
void WaitUntilEntered() {
|
||||
std::unique_lock<std::mutex> lk(mu_);
|
||||
cv_.wait(lk, [this] { return entered_; });
|
||||
}
|
||||
|
||||
void Release() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
release_ = true;
|
||||
}
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
bool CallbackReturned() {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
return returned_;
|
||||
}
|
||||
|
||||
private:
|
||||
const bool block_only_first_;
|
||||
std::atomic<int> num_completed_{0};
|
||||
std::mutex mu_;
|
||||
std::condition_variable cv_;
|
||||
bool entered_ = false;
|
||||
bool release_ = false;
|
||||
bool returned_ = false;
|
||||
};
|
||||
} // anonymous namespace
|
||||
|
||||
TEST_F(DBFlushTest, ListenerWaitWaitsForOnFlushCompleted) {
|
||||
// With FlushOptions::listener_wait, Flush(wait=true) must not return until
|
||||
// the OnFlushCompleted callback has finished, even if the flush result is
|
||||
// already committed and bg_cv_ is signalled by unrelated background work.
|
||||
auto listener = std::make_shared<BlockingFlushListener>(
|
||||
/*block_only_first=*/false);
|
||||
Options options = CurrentOptions();
|
||||
options.listeners.push_back(listener);
|
||||
Reopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
|
||||
std::atomic<bool> flush_returned{false};
|
||||
port::Thread flush_thread([&] {
|
||||
// Exercises the TEST_ wrapper, which flushes with wait=true and
|
||||
// listener_wait=true.
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTableWithListenerWait());
|
||||
flush_returned.store(true);
|
||||
});
|
||||
|
||||
// Wait until we are inside OnFlushCompleted. At this point the memtable has
|
||||
// already been committed/removed, which is what unblocks a plain
|
||||
// Flush(wait=true).
|
||||
listener->WaitUntilEntered();
|
||||
|
||||
// Pump spurious bg_cv_ wakeups. A waiter that ignored listener_wait would
|
||||
// wake, observe the memtable already gone, and return here.
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
db_->DisableManualCompaction();
|
||||
db_->EnableManualCompaction();
|
||||
}
|
||||
|
||||
// The OnFlushCompleted callback has not returned, so neither must Flush().
|
||||
ASSERT_FALSE(listener->CallbackReturned());
|
||||
ASSERT_FALSE(flush_returned.load());
|
||||
|
||||
listener->Release();
|
||||
flush_thread.join();
|
||||
ASSERT_TRUE(flush_returned.load());
|
||||
ASSERT_TRUE(listener->CallbackReturned());
|
||||
}
|
||||
|
||||
TEST_F(DBFlushTest, ListenerWaitAtomicFlushWaitsForAllOnFlushCompleted) {
|
||||
// Same guarantee for atomic flush across multiple column families. The atomic
|
||||
// flush removes all CFs' memtables together and then fires OnFlushCompleted
|
||||
// per CF, releasing the DB mutex between CFs; listener_wait must still block
|
||||
// Flush() until the (first, still-running) callback has finished.
|
||||
auto listener = std::make_shared<BlockingFlushListener>(
|
||||
/*block_only_first=*/true);
|
||||
Options options = CurrentOptions();
|
||||
options.atomic_flush = true;
|
||||
options.listeners.push_back(listener);
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
ASSERT_OK(Put(0, "k0", "v0"));
|
||||
ASSERT_OK(Put(1, "k1", "v1"));
|
||||
|
||||
std::atomic<bool> flush_returned{false};
|
||||
port::Thread flush_thread([&] {
|
||||
FlushOptions fo;
|
||||
fo.wait = true;
|
||||
fo.listener_wait = true;
|
||||
ASSERT_OK(db_->Flush(fo, {handles_[0], handles_[1]}));
|
||||
flush_returned.store(true);
|
||||
});
|
||||
|
||||
listener->WaitUntilEntered();
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
db_->DisableManualCompaction();
|
||||
db_->EnableManualCompaction();
|
||||
}
|
||||
ASSERT_FALSE(listener->CallbackReturned());
|
||||
ASSERT_FALSE(flush_returned.load());
|
||||
|
||||
listener->Release();
|
||||
flush_thread.join();
|
||||
ASSERT_TRUE(flush_returned.load());
|
||||
ASSERT_TRUE(listener->CallbackReturned());
|
||||
}
|
||||
|
||||
TEST_F(DBFlushTest, FlushWithBlob) {
|
||||
constexpr uint64_t min_blob_size = 10;
|
||||
|
||||
|
||||
+182
-13
@@ -34,6 +34,188 @@ size_t CompactedDBImpl::FindFile(const Slice& key) {
|
||||
files_.files);
|
||||
}
|
||||
|
||||
Status CompactedDBImpl::Get(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle*, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGet) {
|
||||
return Status::InvalidArgument(
|
||||
"Can only call Get with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGet`");
|
||||
}
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kGet;
|
||||
}
|
||||
|
||||
assert(user_comparator_);
|
||||
if (read_options.timestamp) {
|
||||
Status s =
|
||||
FailIfTsMismatchCf(DefaultColumnFamily(), *(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd_, cfd_->GetSuperVersion(),
|
||||
*(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const Status s = FailIfCfHasTs(DefaultColumnFamily());
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (timestamp) {
|
||||
timestamp->clear();
|
||||
}
|
||||
|
||||
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
|
||||
BlobFetcher blob_fetcher(version_, read_options);
|
||||
std::string* ts =
|
||||
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr;
|
||||
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
|
||||
GetContext get_context(user_comparator_, nullptr, nullptr, nullptr,
|
||||
GetContext::kNotFound, lkey.user_key(), value,
|
||||
/*columns=*/nullptr, ts, nullptr, nullptr, true,
|
||||
nullptr, nullptr, nullptr, nullptr, &read_cb,
|
||||
nullptr /* is_blob_index */, 0 /* tracing_get_id */,
|
||||
&blob_fetcher);
|
||||
|
||||
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
|
||||
if (user_comparator_->CompareWithoutTimestamp(
|
||||
key, /*a_has_ts=*/false,
|
||||
ExtractUserKeyAndStripTimestamp(f.smallest_key,
|
||||
user_comparator_->timestamp_size()),
|
||||
/*b_has_ts=*/false) < 0) {
|
||||
return Status::NotFound();
|
||||
}
|
||||
TableReader* t = nullptr;
|
||||
TableCache::TypedHandle* handle = nullptr;
|
||||
Status s = cfd_->table_cache()->FindTable(
|
||||
read_options, cfd_->table_cache()->file_options(),
|
||||
cfd_->internal_comparator(), *f.file_metadata, &handle,
|
||||
version_->GetMutableCFOptions(), &t, false /* no_io */,
|
||||
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
|
||||
true /* prefetch_index_and_filter_in_cache */,
|
||||
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
|
||||
true /* pin_table_handle */);
|
||||
if (s.ok()) {
|
||||
assert(handle == nullptr);
|
||||
// Use TableReader directly to avoid extra table_cache->Get() overheads
|
||||
s = t->Get(read_options, lkey.internal_key(), &get_context, nullptr);
|
||||
}
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
return s;
|
||||
}
|
||||
if (get_context.State() == GetContext::kFound) {
|
||||
return Status::OK();
|
||||
}
|
||||
return Status::NotFound();
|
||||
}
|
||||
|
||||
void CompactedDBImpl::MultiGet(const ReadOptions& _read_options,
|
||||
size_t num_keys,
|
||||
ColumnFamilyHandle** /*column_families*/,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
std::string* timestamps, Status* statuses,
|
||||
const bool /*sorted_input*/) {
|
||||
assert(user_comparator_);
|
||||
Status s;
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kMultiGet) {
|
||||
s = Status::InvalidArgument(
|
||||
"Can only call MultiGet with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kMultiGet`");
|
||||
}
|
||||
|
||||
ReadOptions read_options(_read_options);
|
||||
if (s.ok()) {
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kMultiGet;
|
||||
}
|
||||
|
||||
if (read_options.timestamp) {
|
||||
s = FailIfTsMismatchCf(DefaultColumnFamily(), *(read_options.timestamp));
|
||||
if (s.ok()) {
|
||||
if (read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd_, cfd_->GetSuperVersion(),
|
||||
*(read_options.timestamp));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s = FailIfCfHasTs(DefaultColumnFamily());
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] = s;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (timestamps) {
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
timestamps[i].clear();
|
||||
}
|
||||
}
|
||||
|
||||
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
|
||||
BlobFetcher blob_fetcher(version_, read_options);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
const Slice& key = keys[i];
|
||||
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
|
||||
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
|
||||
if (user_comparator_->CompareWithoutTimestamp(
|
||||
key, /*a_has_ts=*/false,
|
||||
ExtractUserKeyAndStripTimestamp(f.smallest_key,
|
||||
user_comparator_->timestamp_size()),
|
||||
/*b_has_ts=*/false) < 0) {
|
||||
statuses[i] = Status::NotFound();
|
||||
continue;
|
||||
}
|
||||
|
||||
PinnableSlice& pinnable_val = values[i];
|
||||
std::string* timestamp = timestamps ? ×tamps[i] : nullptr;
|
||||
GetContext get_context(
|
||||
user_comparator_, nullptr, nullptr, nullptr, GetContext::kNotFound,
|
||||
lkey.user_key(), &pinnable_val, /*columns=*/nullptr,
|
||||
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr, nullptr,
|
||||
nullptr, true, nullptr, nullptr, nullptr, nullptr, &read_cb,
|
||||
nullptr /* is_blob_index */, 0 /* tracing_get_id */, &blob_fetcher);
|
||||
TableReader* t = nullptr;
|
||||
TableCache::TypedHandle* handle = nullptr;
|
||||
Status status = cfd_->table_cache()->FindTable(
|
||||
read_options, cfd_->table_cache()->file_options(),
|
||||
cfd_->internal_comparator(), *f.file_metadata, &handle,
|
||||
version_->GetMutableCFOptions(), &t, false /* no_io */,
|
||||
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
|
||||
true /* prefetch_index_and_filter_in_cache */,
|
||||
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
|
||||
true /* pin_table_handle */);
|
||||
if (status.ok()) {
|
||||
assert(handle == nullptr);
|
||||
// Use TableReader directly to avoid extra table_cache->Get() overheads
|
||||
status = t->Get(read_options, lkey.internal_key(), &get_context, nullptr);
|
||||
}
|
||||
if (!status.ok() && !status.IsNotFound()) {
|
||||
statuses[i] = status;
|
||||
} else if (get_context.State() == GetContext::kFound) {
|
||||
statuses[i] = Status::OK();
|
||||
} else {
|
||||
statuses[i] = Status::NotFound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Status CompactedDBImpl::Init(const Options& options) {
|
||||
SuperVersionContext sv_context(/* create_superversion */ true);
|
||||
mutex_.Lock();
|
||||
@@ -119,16 +301,3 @@ Status CompactedDBImpl::Open(const Options& options, const std::string& dbname,
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
// Generate the regular and coroutine versions of the functions defined in
|
||||
// compacted_db_impl_sync_and_async.h by including it twice. The macros expand
|
||||
// differently based on whether WITH_COROUTINES or WITHOUT_COROUTINES is
|
||||
// defined.
|
||||
// clang-format off
|
||||
#define WITHOUT_COROUTINES
|
||||
#include "db/db_impl/compacted_db_impl_sync_and_async.h"
|
||||
#undef WITHOUT_COROUTINES
|
||||
#define WITH_COROUTINES
|
||||
#include "db/db_impl/compacted_db_impl_sync_and_async.h"
|
||||
#undef WITH_COROUTINES
|
||||
// clang-format on
|
||||
@@ -26,21 +26,18 @@ class CompactedDBImpl : public DBImpl {
|
||||
|
||||
// Implementations of the DB interface
|
||||
using DB::Get;
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, Get, const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp);
|
||||
Status Get(const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp) override;
|
||||
|
||||
using DB::MultiGet;
|
||||
// Note that CompactedDBImpl::MultiGet is not the optimized version of
|
||||
// MultiGet to use.
|
||||
// TODO: optimize CompactedDBImpl::MultiGet, see DBImpl::MultiGet for details.
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(void, MultiGet, const ReadOptions& options,
|
||||
size_t num_keys,
|
||||
ColumnFamilyHandle** column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input);
|
||||
void MultiGet(const ReadOptions& options, size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps,
|
||||
Status* statuses, const bool sorted_input) override;
|
||||
|
||||
using DBImpl::Put;
|
||||
Status Put(const WriteOptions& /*options*/,
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
//
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "util/coro_utils.h"
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, CompactedDBImpl::Get)
|
||||
(const ReadOptions& _read_options, ColumnFamilyHandle*, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGet) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
"Can only call Get with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGet`");
|
||||
}
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kGet;
|
||||
}
|
||||
|
||||
assert(user_comparator_);
|
||||
if (read_options.timestamp) {
|
||||
Status s =
|
||||
FailIfTsMismatchCf(DefaultColumnFamily(), *(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
if (read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd_, cfd_->GetSuperVersion(),
|
||||
*(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const Status s = FailIfCfHasTs(DefaultColumnFamily());
|
||||
if (!s.ok()) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (timestamp) {
|
||||
timestamp->clear();
|
||||
}
|
||||
|
||||
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
|
||||
VersionBlobFetcher blob_fetcher(version_, read_options);
|
||||
std::string* ts =
|
||||
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr;
|
||||
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
|
||||
GetContext get_context(user_comparator_, nullptr, nullptr, nullptr,
|
||||
GetContext::kNotFound, lkey.user_key(), value,
|
||||
/*columns=*/nullptr, ts, nullptr, nullptr, true,
|
||||
nullptr, nullptr, nullptr, nullptr, &read_cb,
|
||||
nullptr /* is_blob_index */, 0 /* tracing_get_id */,
|
||||
&blob_fetcher);
|
||||
|
||||
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
|
||||
if (user_comparator_->CompareWithoutTimestamp(
|
||||
key, /*a_has_ts=*/false,
|
||||
ExtractUserKeyAndStripTimestamp(f.smallest_key,
|
||||
user_comparator_->timestamp_size()),
|
||||
/*b_has_ts=*/false) < 0) {
|
||||
CO_RETURN Status::NotFound();
|
||||
}
|
||||
TableReader* t = nullptr;
|
||||
TableCache::TypedHandle* handle = nullptr;
|
||||
Status s = cfd_->table_cache()->FindTable(
|
||||
read_options, cfd_->table_cache()->file_options(),
|
||||
cfd_->internal_comparator(), *f.file_metadata, &handle,
|
||||
version_->GetMutableCFOptions(), &t, false /* no_io */,
|
||||
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
|
||||
true /* prefetch_index_and_filter_in_cache */,
|
||||
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
|
||||
true /* pin_table_handle */);
|
||||
if (s.ok()) {
|
||||
assert(handle == nullptr);
|
||||
// Use TableReader directly to avoid extra table_cache->Get() overheads
|
||||
s = CO_AWAIT(t->Get, read_options, lkey.internal_key(), &get_context,
|
||||
nullptr);
|
||||
}
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
if (get_context.State() == GetContext::kFound) {
|
||||
CO_RETURN Status::OK();
|
||||
}
|
||||
CO_RETURN Status::NotFound();
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(void, CompactedDBImpl::MultiGet)
|
||||
(const ReadOptions& _read_options, size_t num_keys,
|
||||
ColumnFamilyHandle** /*column_families*/, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps, Status* statuses,
|
||||
const bool /*sorted_input*/) {
|
||||
assert(user_comparator_);
|
||||
Status s;
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kMultiGet) {
|
||||
s = Status::InvalidArgument(
|
||||
"Can only call MultiGet with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kMultiGet`");
|
||||
}
|
||||
|
||||
ReadOptions read_options(_read_options);
|
||||
if (s.ok()) {
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kMultiGet;
|
||||
}
|
||||
|
||||
if (read_options.timestamp) {
|
||||
s = FailIfTsMismatchCf(DefaultColumnFamily(), *(read_options.timestamp));
|
||||
if (s.ok()) {
|
||||
if (read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd_, cfd_->GetSuperVersion(),
|
||||
*(read_options.timestamp));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s = FailIfCfHasTs(DefaultColumnFamily());
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] = s;
|
||||
}
|
||||
CO_RETURN;
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (timestamps) {
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
timestamps[i].clear();
|
||||
}
|
||||
}
|
||||
|
||||
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
|
||||
VersionBlobFetcher blob_fetcher(version_, read_options);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
const Slice& key = keys[i];
|
||||
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
|
||||
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
|
||||
if (user_comparator_->CompareWithoutTimestamp(
|
||||
key, /*a_has_ts=*/false,
|
||||
ExtractUserKeyAndStripTimestamp(f.smallest_key,
|
||||
user_comparator_->timestamp_size()),
|
||||
/*b_has_ts=*/false) < 0) {
|
||||
statuses[i] = Status::NotFound();
|
||||
continue;
|
||||
}
|
||||
|
||||
PinnableSlice& pinnable_val = values[i];
|
||||
std::string* timestamp = timestamps ? ×tamps[i] : nullptr;
|
||||
GetContext get_context(
|
||||
user_comparator_, nullptr, nullptr, nullptr, GetContext::kNotFound,
|
||||
lkey.user_key(), &pinnable_val, /*columns=*/nullptr,
|
||||
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr, nullptr,
|
||||
nullptr, true, nullptr, nullptr, nullptr, nullptr, &read_cb,
|
||||
nullptr /* is_blob_index */, 0 /* tracing_get_id */, &blob_fetcher);
|
||||
TableReader* t = nullptr;
|
||||
TableCache::TypedHandle* handle = nullptr;
|
||||
Status status = cfd_->table_cache()->FindTable(
|
||||
read_options, cfd_->table_cache()->file_options(),
|
||||
cfd_->internal_comparator(), *f.file_metadata, &handle,
|
||||
version_->GetMutableCFOptions(), &t, false /* no_io */,
|
||||
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
|
||||
true /* prefetch_index_and_filter_in_cache */,
|
||||
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
|
||||
true /* pin_table_handle */);
|
||||
if (status.ok()) {
|
||||
assert(handle == nullptr);
|
||||
// Use TableReader directly to avoid extra table_cache->Get() overheads
|
||||
status = CO_AWAIT(t->Get, read_options, lkey.internal_key(), &get_context,
|
||||
nullptr);
|
||||
}
|
||||
if (!status.ok() && !status.IsNotFound()) {
|
||||
statuses[i] = status;
|
||||
} else if (get_context.State() == GetContext::kFound) {
|
||||
statuses[i] = Status::OK();
|
||||
} else {
|
||||
statuses[i] = Status::NotFound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
#endif
|
||||
+848
-386
File diff suppressed because it is too large.
Load diff
+44
-87
@@ -67,7 +67,6 @@
|
||||
#include "rocksdb/write_buffer_manager.h"
|
||||
#include "table/merging_iterator.h"
|
||||
#include "util/autovector.h"
|
||||
#include "util/coro_utils.h"
|
||||
#include "util/hash.h"
|
||||
#include "util/repeatable_thread.h"
|
||||
#include "util/stop_watch.h"
|
||||
@@ -269,14 +268,9 @@ class DBImpl : public DB {
|
||||
bool HasAnyBlobDirectWriteColumnFamily();
|
||||
|
||||
using DB::Get;
|
||||
using DB::GetAsync;
|
||||
DECLARE_SYNC_ASYNC_AND_CALLBACK(
|
||||
Status, Get, GetAsync,
|
||||
(const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value, std::string* timestamp,
|
||||
Status& status, AsyncCallback& callback),
|
||||
const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value, std::string* timestamp);
|
||||
Status Get(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp) override;
|
||||
|
||||
using DB::GetEntity;
|
||||
Status GetEntity(const ReadOptions& options,
|
||||
@@ -301,7 +295,6 @@ class DBImpl : public DB {
|
||||
}
|
||||
|
||||
using DB::MultiGet;
|
||||
using DB::MultiGetAsync;
|
||||
// This MultiGet is a batched version, which may be faster than calling Get
|
||||
// multiple times, especially if the keys have some spatial locality that
|
||||
// enables them to be queried in the same SST files/set of files. The larger
|
||||
@@ -309,16 +302,10 @@ class DBImpl : public DB {
|
||||
// The values and statuses parameters are arrays with number of elements
|
||||
// equal to keys.size(). This allows the storage for those to be alloacted
|
||||
// by the caller on the stack for small batches
|
||||
DECLARE_SYNC_ASYNC_AND_CALLBACK(
|
||||
void, MultiGet, MultiGetAsync,
|
||||
(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input, AsyncCallback& callback),
|
||||
const ReadOptions& _read_options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input = false);
|
||||
void MultiGet(const ReadOptions& _read_options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps,
|
||||
Status* statuses, const bool sorted_input = false) override;
|
||||
|
||||
void MultiGetWithCallback(
|
||||
const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
@@ -731,13 +718,13 @@ class DBImpl : public DB {
|
||||
int* number_of_operands = nullptr;
|
||||
};
|
||||
|
||||
DECLARE_SYNC_AND_ASYNC(Status, GetImpl, const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value);
|
||||
Status GetImpl(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value);
|
||||
|
||||
DECLARE_SYNC_AND_ASYNC(Status, GetImpl, const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp);
|
||||
Status GetImpl(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp);
|
||||
|
||||
// Function that Get and KeyMayExist call with no_io true or false
|
||||
// Note: 'value_found' from KeyMayExist propagates here
|
||||
@@ -746,9 +733,8 @@ class DBImpl : public DB {
|
||||
// get_impl_options.key via get_impl_options.value
|
||||
// If get_impl_options.get_value = false get merge operands associated with
|
||||
// get_impl_options.key via get_impl_options.merge_operands
|
||||
DECLARE_SYNC_AND_ASYNC_VIRTUAL(Status, GetImpl, const ReadOptions& options,
|
||||
const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
virtual Status GetImpl(const ReadOptions& options, const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
|
||||
// If `snapshot` == kMaxSequenceNumber, set a recent one inside the file.
|
||||
ArenaWrappedDBIter* NewIteratorImpl(const ReadOptions& options,
|
||||
@@ -1203,13 +1189,6 @@ class DBImpl : public DB {
|
||||
Status TEST_FlushMemTable(ColumnFamilyData* cfd,
|
||||
const FlushOptions& flush_opts);
|
||||
|
||||
// Like TEST_FlushMemTable(bool wait, ...), but also sets
|
||||
// FlushOptions::listener_wait so the flush does not return until the
|
||||
// registered EventListener::OnFlushCompleted callbacks for the flushed
|
||||
// memtables have finished running.
|
||||
Status TEST_FlushMemTableWithListenerWait(bool allow_write_stall = false,
|
||||
ColumnFamilyHandle* cfh = nullptr);
|
||||
|
||||
// Flush (multiple) ColumnFamilyData without using ColumnFamilyHandle. This
|
||||
// is because in certain cases, we can flush column families, wait for the
|
||||
// flush to complete, but delete the column family handle before the wait
|
||||
@@ -1562,7 +1541,7 @@ class DBImpl : public DB {
|
||||
// 2. db_mutex is NOT held
|
||||
Status RenameTempFileToOptionsFile(const std::string& file_name,
|
||||
bool is_remote_compaction_enabled);
|
||||
Status DeleteObsoleteOptionsFiles(bool schedule_only);
|
||||
Status DeleteObsoleteOptionsFiles();
|
||||
|
||||
void NotifyOnManualFlushScheduled(autovector<ColumnFamilyData*> cfds,
|
||||
FlushReason flush_reason);
|
||||
@@ -2240,10 +2219,7 @@ class DBImpl : public DB {
|
||||
// Required: DB mutex held
|
||||
Status PersistentStatsProcessFormatVersion();
|
||||
|
||||
// `io_activity` is used to construct the ReadOptions/WriteOptions for the
|
||||
// internal recovery work (e.g. the MANIFEST write in LogAndApply). Recovery
|
||||
// is driven by flush operations, so callers pass Env::IOActivity::kFlush.
|
||||
Status ResumeImpl(DBRecoverContext context, Env::IOActivity io_activity);
|
||||
Status ResumeImpl(DBRecoverContext context);
|
||||
|
||||
void MaybeIgnoreError(Status* s) const;
|
||||
|
||||
@@ -2525,24 +2501,18 @@ class DBImpl : public DB {
|
||||
// memtable pending flush.
|
||||
// resuming_from_bg_err indicates whether the caller is attempting to resume
|
||||
// from background error.
|
||||
// If wait_for_listener_notifications is true, also wait until the
|
||||
// EventListener::OnFlushCompleted callbacks for the flushed memtables have
|
||||
// finished running (see FlushOptions::listener_wait).
|
||||
Status WaitForFlushMemTable(
|
||||
ColumnFamilyData* cfd, const uint64_t* flush_memtable_id = nullptr,
|
||||
bool resuming_from_bg_err = false,
|
||||
std::optional<FlushReason> flush_reason = std::nullopt,
|
||||
bool wait_for_listener_notifications = false) {
|
||||
std::optional<FlushReason> flush_reason = std::nullopt) {
|
||||
return WaitForFlushMemTables({cfd}, {flush_memtable_id},
|
||||
resuming_from_bg_err, flush_reason,
|
||||
wait_for_listener_notifications);
|
||||
resuming_from_bg_err, flush_reason);
|
||||
}
|
||||
// Wait for memtables to be flushed for multiple column families.
|
||||
Status WaitForFlushMemTables(
|
||||
const autovector<ColumnFamilyData*>& cfds,
|
||||
const autovector<const uint64_t*>& flush_memtable_ids,
|
||||
bool resuming_from_bg_err, std::optional<FlushReason> flush_reason,
|
||||
bool wait_for_listener_notifications = false);
|
||||
bool resuming_from_bg_err, std::optional<FlushReason> flush_reason);
|
||||
|
||||
inline void WaitForPendingWrites() {
|
||||
mutex_.AssertHeld();
|
||||
@@ -2984,12 +2954,11 @@ class DBImpl : public DB {
|
||||
PinnableWideColumns* columns, std::string* timestamps,
|
||||
Status* statuses, bool sorted_input);
|
||||
|
||||
DECLARE_SYNC_AND_ASYNC(void, MultiGetCommon, const ReadOptions& options,
|
||||
const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
PinnableWideColumns* columns, std::string* timestamps,
|
||||
Status* statuses, bool sorted_input);
|
||||
void MultiGetCommon(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, PinnableWideColumns* columns,
|
||||
std::string* timestamps, Status* statuses,
|
||||
bool sorted_input);
|
||||
|
||||
// A structure to hold the information required to process MultiGet of keys
|
||||
// belonging to one column family. For a multi column family MultiGet, there
|
||||
@@ -3064,9 +3033,8 @@ class DBImpl : public DB {
|
||||
// to have acquired the SuperVersion and pass in a snapshot sequence number
|
||||
// in order to construct the LookupKeys. The start_key and num_keys specify
|
||||
// the range of keys in the sorted_keys vector for a single column family.
|
||||
DECLARE_SYNC_AND_ASYNC(
|
||||
Status, MultiGetImpl, const ReadOptions& read_options, size_t start_key,
|
||||
size_t num_keys,
|
||||
Status MultiGetImpl(
|
||||
const ReadOptions& read_options, size_t start_key, size_t num_keys,
|
||||
autovector<KeyContext*, MultiGetContext::MAX_BATCH_SIZE>* sorted_keys,
|
||||
SuperVersion* sv, SequenceNumber snap_seqnum, ReadCallback* callback);
|
||||
|
||||
@@ -3105,40 +3073,29 @@ class DBImpl : public DB {
|
||||
bool resolve_direct_write_value, const Version* current,
|
||||
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index, bool* value_found = nullptr);
|
||||
// Completes primary memtable hits that can still carry direct-write blob
|
||||
// references before the blob file is visible in Version metadata.
|
||||
static void PostprocessDirectWriteValueRead(
|
||||
const ReadOptions& read_options, const Slice& key,
|
||||
const std::string* timestamp, bool resolve_direct_write_value,
|
||||
const Version* current, ColumnFamilyData* cfd, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
|
||||
bool* value_found = nullptr);
|
||||
// Resolves memtable read results that still carry blob references through
|
||||
// either a raw blob-index payload in `value` or unresolved blob columns in
|
||||
// `columns`. Unlike the direct-write helper above, this path only depends on
|
||||
// a BlobFetcher and therefore works for read-only/secondary DBs. Sets
|
||||
// *did_resolve to true iff there was a reference to resolve -- in which case
|
||||
// this function has finalized `value`/`columns` (populated on success,
|
||||
// cleared on error) -- and false iff there was nothing to resolve (the caller
|
||||
// finalizes the plain value). Only call with an OK incoming status.
|
||||
static Status MaybeResolveMemtableBlobValue(
|
||||
const Slice& key, const BlobFetcher* blob_fetcher, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, bool* did_resolve, bool* is_blob_index,
|
||||
bool* value_found = nullptr);
|
||||
// Completes read-only/secondary memtable Get()/GetEntity() hits. Given the
|
||||
// status from the memtable read, resolves blob-backed payloads when
|
||||
// `resolve_blob_backed_memtable_value` is true, self-pins a plain value on
|
||||
// success, and clears outputs on any error (never leaving a half-populated
|
||||
// result). When the caller explicitly requested raw blob indices via
|
||||
// `GetImplOptions::is_blob_index`, this helper leaves that payload untouched.
|
||||
// `memtable_blob_fetcher` may be null when blob support is disabled for the
|
||||
// column family. Returns the finalized status.
|
||||
static Status PostprocessMemtableValueRead(
|
||||
// a BlobFetcher and therefore works for read-only/secondary DBs.
|
||||
static bool MaybeResolveMemtableBlobValue(const Slice& key,
|
||||
const BlobFetcher* blob_fetcher,
|
||||
PinnableSlice* value,
|
||||
PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index,
|
||||
bool* value_found = nullptr);
|
||||
// Completes read-only/secondary memtable Get()/GetEntity() hits by resolving
|
||||
// blob-backed payloads when `resolve_blob_backed_memtable_value` is true,
|
||||
// pinning plain values on success, and clearing outputs on error. When the
|
||||
// caller explicitly requested raw blob indices via
|
||||
// `GetImplOptions::is_blob_index`, this helper leaves that payload
|
||||
// untouched. `memtable_blob_fetcher` may be null when blob support is
|
||||
// disabled for the column family.
|
||||
static void PostprocessMemtableValueRead(
|
||||
const Slice& key, const std::string* timestamp,
|
||||
bool resolve_blob_backed_memtable_value,
|
||||
const BlobFetcher* memtable_blob_fetcher, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, Status memtable_read_status,
|
||||
bool* is_blob_index, bool* value_found = nullptr);
|
||||
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
|
||||
bool* value_found = nullptr);
|
||||
|
||||
template <typename IterType, typename ImplType,
|
||||
typename ErrorIteratorFuncType>
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
#include "util/cast_util.h"
|
||||
#include "util/coding.h"
|
||||
#include "util/concurrent_task_limiter_impl.h"
|
||||
#include "util/defer.h"
|
||||
#include "util/udt_util.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -359,34 +358,6 @@ Status DBImpl::FlushMemTableToOutputFile(
|
||||
need_cancel = false;
|
||||
}
|
||||
|
||||
// Reserve a pending flush-completion notification before the DB mutex can be
|
||||
// released, so that a Flush(wait=true, listener_wait=true) caller cannot
|
||||
// observe this flush as finished until NotifyOnFlushCompleted() below has
|
||||
// run. flush_job.Run() removed the memtables from the immutable list (which
|
||||
// is what unblocks the waiter) and the DB mutex has been held continuously
|
||||
// since, so there is no window in which the waiter could see the flush
|
||||
// committed with a zero count. Reserved only when a notification will
|
||||
// actually fire (matches the NotifyOnFlushCompleted call below): flush
|
||||
// committed successfully, not a mempurge, listeners are registered, and this
|
||||
// job committed at least one flush result to report.
|
||||
const bool reserved_flush_notification =
|
||||
s.ok() && !switched_to_mempurge &&
|
||||
!immutable_db_options_.listeners.empty() &&
|
||||
!flush_job.GetCommittedFlushJobsInfo()->empty();
|
||||
if (reserved_flush_notification) {
|
||||
cfd->imm()->AddPendingFlushNotifications(1);
|
||||
}
|
||||
// Release the reservation on every exit path (RAII) so a future early return
|
||||
// cannot leak the count and hang listener_wait waiters. The DB mutex is held
|
||||
// throughout this function, including at scope exit, which
|
||||
// SubPendingFlushNotifications() requires. Runs after
|
||||
// NotifyOnFlushCompleted() below has returned.
|
||||
Defer release_flush_notification([&] {
|
||||
if (reserved_flush_notification) {
|
||||
cfd->imm()->SubPendingFlushNotifications(1);
|
||||
}
|
||||
});
|
||||
|
||||
if (cfd->blob_partition_manager() != nullptr &&
|
||||
prepared_blob_generations > 0) {
|
||||
auto unconsumed_additions = flush_job.TakeExternalBlobFileAdditions();
|
||||
@@ -929,42 +900,6 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
|
||||
directories_.GetDbDir(), log_buffer);
|
||||
}
|
||||
|
||||
// Reserve a pending flush-completion notification for every CF that the
|
||||
// NotifyOnFlushCompleted loop below will service, while the DB mutex is still
|
||||
// held continuously since InstallMemtableAtomicFlushResults removed the
|
||||
// memtables. This closes the window where a
|
||||
// Flush(wait=true, listener_wait=true) waiter could observe an atomic flush
|
||||
// as finished before a later CF's OnFlushCompleted has run, since the notify
|
||||
// loop releases and re-acquires the mutex between CFs. The reservations are
|
||||
// released by a matching pass after the notify loop.
|
||||
autovector<uint8_t> reserved_flush_notification;
|
||||
reserved_flush_notification.reserve(num_cfs);
|
||||
{
|
||||
const bool has_listeners = !immutable_db_options_.listeners.empty();
|
||||
for (int i = 0; i != num_cfs; ++i) {
|
||||
const bool reserve = s.ok() && has_listeners &&
|
||||
!switched_to_mempurge[i] && !cfds[i]->IsDropped() &&
|
||||
!jobs[i]->GetCommittedFlushJobsInfo()->empty();
|
||||
if (reserve) {
|
||||
cfds[i]->imm()->AddPendingFlushNotifications(1);
|
||||
}
|
||||
reserved_flush_notification.push_back(reserve ? 1 : 0);
|
||||
}
|
||||
}
|
||||
// Release the reservations on every exit path (RAII) so a future early return
|
||||
// cannot leak the count and hang listener_wait waiters. Done independently of
|
||||
// the notify loop's skips so the count stays balanced even if a CF was
|
||||
// dropped after it was reserved. The DB mutex is held throughout this
|
||||
// function, including at scope exit, which SubPendingFlushNotifications()
|
||||
// requires.
|
||||
Defer release_flush_notifications([&] {
|
||||
for (int i = 0; i != num_cfs; ++i) {
|
||||
if (reserved_flush_notification[i]) {
|
||||
cfds[i]->imm()->SubPendingFlushNotifications(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!s.ok()) {
|
||||
// If the atomic flush's combined MANIFEST write failed, the output files
|
||||
// are cached in the table cache (added by BuildTable during FlushJob::Run)
|
||||
@@ -1688,16 +1623,16 @@ void DBImpl::PrepareTrivialMoveEdit(Compaction& c, LogBuffer* log_buffer,
|
||||
for (size_t i = 0; i < c.num_input_files(l); i++) {
|
||||
FileMetaData* f = c.input(l, i);
|
||||
c.edit()->DeleteFile(c.level(l), f->fd.GetNumber());
|
||||
c.edit()->AddFile(
|
||||
c.output_level(), f->fd.GetNumber(), f->fd.GetPathId(),
|
||||
f->fd.GetFileSize(), f->smallest, f->largest, f->fd.smallest_seqno,
|
||||
f->fd.largest_seqno, f->marked_for_compaction, f->temperature,
|
||||
f->oldest_blob_file_number, 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->file_open_metadata);
|
||||
c.edit()->AddFile(c.output_level(), f->fd.GetNumber(), f->fd.GetPathId(),
|
||||
f->fd.GetFileSize(), f->smallest, f->largest,
|
||||
f->fd.smallest_seqno, f->fd.largest_seqno,
|
||||
f->marked_for_compaction, f->temperature,
|
||||
f->oldest_blob_file_number, 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);
|
||||
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",
|
||||
@@ -2274,7 +2209,7 @@ Status DBImpl::ReFitLevel(ColumnFamilyData* cfd, int level, int target_level) {
|
||||
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->file_open_metadata);
|
||||
f->max_timestamp);
|
||||
}
|
||||
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
|
||||
"[%s] Apply version edit:\n%s", cfd->GetName().c_str(),
|
||||
@@ -2859,7 +2794,7 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
|
||||
s = WaitForFlushMemTables(
|
||||
cfds, flush_memtable_ids,
|
||||
flush_reason == FlushReason::kErrorRecovery /* resuming_from_bg_err */,
|
||||
flush_reason, flush_options.listener_wait);
|
||||
flush_reason);
|
||||
InstrumentedMutexLock lock_guard(&mutex_);
|
||||
for (auto* tmp_cfd : cfds) {
|
||||
tmp_cfd->UnrefAndTryDelete();
|
||||
@@ -3014,7 +2949,7 @@ Status DBImpl::AtomicFlushMemTables(
|
||||
s = WaitForFlushMemTables(
|
||||
cfds, flush_memtable_ids,
|
||||
flush_reason == FlushReason::kErrorRecovery /* resuming_from_bg_err */,
|
||||
flush_reason, flush_options.listener_wait);
|
||||
flush_reason);
|
||||
InstrumentedMutexLock lock_guard(&mutex_);
|
||||
for (auto* cfd : cfds) {
|
||||
cfd->UnrefAndTryDelete();
|
||||
@@ -3183,8 +3118,7 @@ Status DBImpl::WaitUntilFlushWouldNotStallWrites(ColumnFamilyData* cfd,
|
||||
Status DBImpl::WaitForFlushMemTables(
|
||||
const autovector<ColumnFamilyData*>& cfds,
|
||||
const autovector<const uint64_t*>& flush_memtable_ids,
|
||||
bool resuming_from_bg_err, std::optional<FlushReason> flush_reason,
|
||||
bool wait_for_listener_notifications) {
|
||||
bool resuming_from_bg_err, std::optional<FlushReason> flush_reason) {
|
||||
int num = static_cast<int>(cfds.size());
|
||||
// Wait until the compaction completes
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
@@ -3229,13 +3163,7 @@ Status DBImpl::WaitForFlushMemTables(
|
||||
flush_reason.value() != FlushReason::kExternalFileIngestion ||
|
||||
cfds[i]->GetSuperVersion()->imm->GetID() ==
|
||||
cfds[i]->imm()->current()->GetID()) {
|
||||
// When the caller requested it, also wait for the flush-completion
|
||||
// listener callbacks for this CF to finish before counting it as
|
||||
// finished (see FlushOptions::listener_wait).
|
||||
if (!wait_for_listener_notifications ||
|
||||
cfds[i]->imm()->NumPendingFlushNotifications() == 0) {
|
||||
++num_finished;
|
||||
}
|
||||
++num_finished;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ Status DBImpl::TEST_SwitchWAL() {
|
||||
|
||||
Status DBImpl::TEST_ResumeImpl(DBRecoverContext context) {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
return ResumeImpl(context, Env::IOActivity::kFlush);
|
||||
return ResumeImpl(context);
|
||||
}
|
||||
|
||||
uint64_t DBImpl::TEST_MaxNextLevelOverlappingBytes(
|
||||
@@ -164,22 +164,6 @@ Status DBImpl::TEST_FlushMemTable(ColumnFamilyData* cfd,
|
||||
return FlushMemTable(cfd, flush_opts, FlushReason::kTest);
|
||||
}
|
||||
|
||||
Status DBImpl::TEST_FlushMemTableWithListenerWait(bool allow_write_stall,
|
||||
ColumnFamilyHandle* cfh) {
|
||||
FlushOptions fo;
|
||||
fo.wait = true;
|
||||
fo.listener_wait = true;
|
||||
fo.allow_write_stall = allow_write_stall;
|
||||
ColumnFamilyData* cfd;
|
||||
if (cfh == nullptr) {
|
||||
cfd = default_cf_handle_->cfd();
|
||||
} else {
|
||||
auto cfhi = static_cast_with_check<ColumnFamilyHandleImpl>(cfh);
|
||||
cfd = cfhi->cfd();
|
||||
}
|
||||
return FlushMemTable(cfd, fo, FlushReason::kTest);
|
||||
}
|
||||
|
||||
Status DBImpl::TEST_AtomicFlushMemTables(
|
||||
const autovector<ColumnFamilyData*>& provided_candidate_cfds,
|
||||
const FlushOptions& flush_opts) {
|
||||
|
||||
@@ -145,7 +145,7 @@ Status DBImpl::PromoteL0(ColumnFamilyHandle* column_family, int target_level) {
|
||||
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->file_open_metadata);
|
||||
f->max_timestamp);
|
||||
}
|
||||
|
||||
status = versions_->LogAndApply(cfd, read_options, write_options, &edit,
|
||||
|
||||
@@ -243,11 +243,6 @@ Status DBImpl::ValidateOptions(
|
||||
}
|
||||
|
||||
Status DBImpl::ValidateOptions(const DBOptions& db_options) {
|
||||
if (db_options.read_io_executor_threads <= 0) {
|
||||
return Status::InvalidArgument(
|
||||
"read_io_executor_threads must be greater than zero");
|
||||
}
|
||||
|
||||
if (db_options.db_paths.size() > 4) {
|
||||
return Status::NotSupported(
|
||||
"More than four DB paths are not supported yet. ");
|
||||
@@ -688,8 +683,7 @@ Status DBImpl::Recover(
|
||||
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->file_open_metadata);
|
||||
f->min_timestamp, f->max_timestamp);
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"[%s] Moving #%" PRIu64
|
||||
" from from_level-%d to from_level-%d %" PRIu64
|
||||
@@ -2877,16 +2871,11 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::Open:Opened");
|
||||
Status persist_options_status;
|
||||
bool cleanup_obsolete_options_files = false;
|
||||
if (s.ok()) {
|
||||
// Persist RocksDB Options before scheduling the compaction.
|
||||
// The WriteOptionsFile() will release and lock the mutex internally.
|
||||
persist_options_status =
|
||||
impl->WriteOptionsFile(write_options, true /*db_mutex_already_held*/);
|
||||
cleanup_obsolete_options_files =
|
||||
impl->immutable_db_options_.avoid_unnecessary_blocking_io &&
|
||||
impl->immutable_db_options_.compaction_service == nullptr &&
|
||||
!impl->disable_delete_obsolete_files_;
|
||||
impl->opened_successfully_ = true;
|
||||
} else {
|
||||
persist_options_status.PermitUncheckedError();
|
||||
@@ -2976,17 +2965,6 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
s = impl->RegisterRecordSeqnoTimeWorker();
|
||||
}
|
||||
impl->options_mutex_.Unlock();
|
||||
if (cleanup_obsolete_options_files) {
|
||||
Status obsolete_options_status =
|
||||
impl->DeleteObsoleteOptionsFiles(/*schedule_only=*/s.ok());
|
||||
if (!obsolete_options_status.ok()) {
|
||||
ROCKS_LOG_WARN(impl->immutable_db_options_.info_log,
|
||||
"Unable to %s obsolete OPTIONS files%s: %s",
|
||||
s.ok() ? "schedule deletion of" : "delete",
|
||||
s.ok() ? "" : " after DB open failure",
|
||||
obsolete_options_status.ToString().c_str());
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
*dbptr = std::move(impl);
|
||||
} else {
|
||||
|
||||
+141
-13
@@ -30,6 +30,147 @@ DBImplReadOnly::DBImplReadOnly(const DBOptions& db_options,
|
||||
|
||||
DBImplReadOnly::~DBImplReadOnly() = default;
|
||||
|
||||
// Implementations of the DB interface
|
||||
Status DBImplReadOnly::GetImpl(const ReadOptions& read_options,
|
||||
const Slice& key,
|
||||
GetImplOptions& get_impl_options) {
|
||||
assert(get_impl_options.value != nullptr ||
|
||||
get_impl_options.columns != nullptr ||
|
||||
get_impl_options.merge_operands != nullptr);
|
||||
assert(get_impl_options.column_family);
|
||||
|
||||
Status s;
|
||||
|
||||
if (read_options.timestamp) {
|
||||
s = FailIfTsMismatchCf(get_impl_options.column_family,
|
||||
*(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
} else {
|
||||
s = FailIfCfHasTs(get_impl_options.column_family);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (get_impl_options.timestamp) {
|
||||
get_impl_options.timestamp->clear();
|
||||
}
|
||||
|
||||
PERF_CPU_TIMER_GUARD(get_cpu_nanos, immutable_db_options_.clock);
|
||||
StopWatch sw(immutable_db_options_.clock, stats_, DB_GET);
|
||||
PERF_TIMER_GUARD(get_snapshot_time);
|
||||
|
||||
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
SequenceNumber snapshot = versions_->LastSequence();
|
||||
GetWithTimestampReadCallback read_cb(snapshot);
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
get_impl_options.column_family);
|
||||
auto cfd = cfh->cfd();
|
||||
bool is_blob_index = false;
|
||||
bool* is_blob_ptr = get_impl_options.is_blob_index;
|
||||
std::string timestamp_storage;
|
||||
std::string* ts = nullptr;
|
||||
if (ucmp->timestamp_size() > 0) {
|
||||
ts = get_impl_options.timestamp != nullptr
|
||||
? get_impl_options.timestamp
|
||||
: (get_impl_options.get_value ? ×tamp_storage : nullptr);
|
||||
}
|
||||
if (!is_blob_ptr && get_impl_options.get_value) {
|
||||
is_blob_ptr = &is_blob_index;
|
||||
}
|
||||
const bool resolve_blob_backed_memtable_value =
|
||||
get_impl_options.get_value && (is_blob_ptr == &is_blob_index);
|
||||
if (tracer_) {
|
||||
InstrumentedMutexLock lock(&trace_mutex_);
|
||||
if (tracer_) {
|
||||
tracer_->Get(get_impl_options.column_family, key);
|
||||
}
|
||||
}
|
||||
|
||||
// In read-only mode Get(), no super version operation is needed (i.e.
|
||||
// GetAndRefSuperVersion and ReturnAndCleanupSuperVersion)
|
||||
SuperVersion* super_version = cfd->GetSuperVersion();
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd, super_version,
|
||||
*(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
// Prepare to store a list of merge operations if merge occurs.
|
||||
MergeContext merge_context;
|
||||
// TODO - Large Result Optimization for Read Only DB
|
||||
// (https://github.com/facebook/rocksdb/pull/10458)
|
||||
|
||||
SequenceNumber max_covering_tombstone_seq = 0;
|
||||
LookupKey lkey(key, snapshot, read_options.timestamp);
|
||||
PERF_TIMER_STOP(get_snapshot_time);
|
||||
std::optional<BlobFetcher> memtable_blob_fetcher;
|
||||
if (cfd->ioptions().enable_blob_direct_write ||
|
||||
cfd->GetLatestMutableCFOptions().enable_blob_files) {
|
||||
// Recovered memtables can still contain older blob references after
|
||||
// mutable blob-file settings change, so keep blob resolution available
|
||||
// whenever either blob knob indicates it may be needed.
|
||||
memtable_blob_fetcher.emplace(super_version->current, read_options,
|
||||
cfd->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/true);
|
||||
}
|
||||
const BlobFetcher* memtable_blob_fetcher_ptr =
|
||||
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
|
||||
|
||||
// Look up starts here
|
||||
if (super_version->mem->Get(
|
||||
lkey,
|
||||
get_impl_options.value ? get_impl_options.value->GetSelf() : nullptr,
|
||||
get_impl_options.columns, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */, &read_cb, is_blob_ptr,
|
||||
/*do_merge=*/get_impl_options.get_value, memtable_blob_fetcher_ptr)) {
|
||||
DBImpl::PostprocessMemtableValueRead(
|
||||
key, ts, resolve_blob_backed_memtable_value, memtable_blob_fetcher_ptr,
|
||||
get_impl_options.value, get_impl_options.columns, &s, &is_blob_index,
|
||||
get_impl_options.value_found);
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else {
|
||||
PERF_TIMER_GUARD(get_from_output_files_time);
|
||||
PinnedIteratorsManager pinned_iters_mgr;
|
||||
super_version->current->Get(
|
||||
read_options, lkey, get_impl_options.value, get_impl_options.columns,
|
||||
ts, &s, &merge_context, &max_covering_tombstone_seq, &pinned_iters_mgr,
|
||||
/*value_found*/ nullptr,
|
||||
/*key_exists*/ nullptr, /*seq*/ nullptr, &read_cb,
|
||||
/*is_blob*/ nullptr,
|
||||
/*do_merge=*/get_impl_options.get_value);
|
||||
RecordTick(stats_, MEMTABLE_MISS);
|
||||
}
|
||||
{
|
||||
RecordTick(stats_, NUMBER_KEYS_READ);
|
||||
size_t size = 0;
|
||||
if (get_impl_options.value) {
|
||||
size = get_impl_options.value->size();
|
||||
} else if (get_impl_options.columns) {
|
||||
size = get_impl_options.columns->serialized_size();
|
||||
} else if (get_impl_options.merge_operands) {
|
||||
*get_impl_options.number_of_operands =
|
||||
static_cast<int>(merge_context.GetNumOperands());
|
||||
for (const Slice& sl : merge_context.GetOperands()) {
|
||||
size += sl.size();
|
||||
get_impl_options.merge_operands->PinSelf(sl);
|
||||
get_impl_options.merge_operands++;
|
||||
}
|
||||
}
|
||||
RecordTick(stats_, BYTES_READ, size);
|
||||
RecordInHistogram(stats_, BYTES_PER_READ, size);
|
||||
PERF_COUNTER_ADD(get_read_bytes, size);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Iterator* DBImplReadOnly::NewIterator(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
@@ -270,16 +411,3 @@ Status DBImplReadOnly::OpenForReadOnlyWithoutCheck(
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
// Generate the regular and coroutine versions of the functions defined in
|
||||
// db_impl_readonly_sync_and_async.h by including it twice. The macros expand
|
||||
// differently based on whether WITH_COROUTINES or WITHOUT_COROUTINES is
|
||||
// defined.
|
||||
// clang-format off
|
||||
#define WITHOUT_COROUTINES
|
||||
#include "db/db_impl/db_impl_readonly_sync_and_async.h"
|
||||
#undef WITHOUT_COROUTINES
|
||||
#define WITH_COROUTINES
|
||||
#include "db/db_impl/db_impl_readonly_sync_and_async.h"
|
||||
#undef WITH_COROUTINES
|
||||
// clang-format on
|
||||
@@ -24,9 +24,8 @@ class DBImplReadOnly : public DBImpl {
|
||||
|
||||
// Implementations of the DB interface
|
||||
using DBImpl::GetImpl;
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, GetImpl, const ReadOptions& options,
|
||||
const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
Status GetImpl(const ReadOptions& options, const Slice& key,
|
||||
GetImplOptions& get_impl_options) override;
|
||||
|
||||
// TODO: Implement ReadOnly MultiGet?
|
||||
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
//
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "util/coro_utils.h"
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// Implementations of the DB interface
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
(const ReadOptions& read_options, const Slice& key,
|
||||
GetImplOptions& get_impl_options) {
|
||||
assert(get_impl_options.value != nullptr ||
|
||||
get_impl_options.columns != nullptr ||
|
||||
get_impl_options.merge_operands != nullptr);
|
||||
assert(get_impl_options.column_family);
|
||||
|
||||
Status s;
|
||||
|
||||
if (read_options.timestamp) {
|
||||
s = FailIfTsMismatchCf(get_impl_options.column_family,
|
||||
*(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
} else {
|
||||
s = FailIfCfHasTs(get_impl_options.column_family);
|
||||
if (!s.ok()) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (get_impl_options.timestamp) {
|
||||
get_impl_options.timestamp->clear();
|
||||
}
|
||||
|
||||
#if defined(WITHOUT_COROUTINES)
|
||||
PERF_CPU_TIMER_GUARD(get_cpu_nanos, immutable_db_options_.clock);
|
||||
#endif
|
||||
StopWatch sw(immutable_db_options_.clock, stats_, DB_GET);
|
||||
PERF_TIMER_GUARD(get_snapshot_time);
|
||||
|
||||
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
SequenceNumber snapshot = versions_->LastSequence();
|
||||
GetWithTimestampReadCallback read_cb(snapshot);
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
get_impl_options.column_family);
|
||||
auto cfd = cfh->cfd();
|
||||
bool is_blob_index = false;
|
||||
bool* is_blob_ptr = get_impl_options.is_blob_index;
|
||||
std::string timestamp_storage;
|
||||
std::string* ts = nullptr;
|
||||
if (ucmp->timestamp_size() > 0) {
|
||||
ts = get_impl_options.timestamp != nullptr
|
||||
? get_impl_options.timestamp
|
||||
: (get_impl_options.get_value ? ×tamp_storage : nullptr);
|
||||
}
|
||||
if (!is_blob_ptr && get_impl_options.get_value) {
|
||||
is_blob_ptr = &is_blob_index;
|
||||
}
|
||||
const bool resolve_blob_backed_memtable_value =
|
||||
get_impl_options.get_value && (is_blob_ptr == &is_blob_index);
|
||||
if (tracer_) {
|
||||
InstrumentedMutexLock lock(&trace_mutex_);
|
||||
if (tracer_) {
|
||||
tracer_->Get(get_impl_options.column_family, key);
|
||||
}
|
||||
}
|
||||
|
||||
// In read-only mode Get(), no super version operation is needed (i.e.
|
||||
// GetAndRefSuperVersion and ReturnAndCleanupSuperVersion)
|
||||
SuperVersion* super_version = cfd->GetSuperVersion();
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd, super_version,
|
||||
*(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
}
|
||||
// Prepare to store a list of merge operations if merge occurs.
|
||||
MergeContext merge_context;
|
||||
// TODO - Large Result Optimization for Read Only DB
|
||||
// (https://github.com/facebook/rocksdb/pull/10458)
|
||||
|
||||
SequenceNumber max_covering_tombstone_seq = 0;
|
||||
LookupKey lkey(key, snapshot, read_options.timestamp);
|
||||
PERF_TIMER_STOP(get_snapshot_time);
|
||||
std::optional<VersionBlobFetcher> memtable_blob_fetcher;
|
||||
if (cfd->ioptions().enable_blob_direct_write ||
|
||||
cfd->GetLatestMutableCFOptions().enable_blob_files) {
|
||||
// Recovered memtables can still contain older blob references after
|
||||
// mutable blob-file settings change, so keep blob resolution available
|
||||
// whenever either blob knob indicates it may be needed.
|
||||
memtable_blob_fetcher.emplace(super_version->current, read_options,
|
||||
cfd->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/true);
|
||||
}
|
||||
const BlobFetcher* memtable_blob_fetcher_ptr =
|
||||
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
|
||||
|
||||
// Look up starts here
|
||||
if (super_version->mem->Get(
|
||||
lkey,
|
||||
get_impl_options.value ? get_impl_options.value->GetSelf() : nullptr,
|
||||
get_impl_options.columns, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */, &read_cb, is_blob_ptr,
|
||||
/*do_merge=*/get_impl_options.get_value, memtable_blob_fetcher_ptr)) {
|
||||
s = DBImpl::PostprocessMemtableValueRead(
|
||||
key, ts, resolve_blob_backed_memtable_value, memtable_blob_fetcher_ptr,
|
||||
get_impl_options.value, get_impl_options.columns, std::move(s),
|
||||
&is_blob_index, get_impl_options.value_found);
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else {
|
||||
PERF_TIMER_GUARD(get_from_output_files_time);
|
||||
PinnedIteratorsManager pinned_iters_mgr;
|
||||
CO_AWAIT(super_version->current->Get, read_options, lkey,
|
||||
get_impl_options.value, get_impl_options.columns, ts, &s,
|
||||
&merge_context, &max_covering_tombstone_seq, &pinned_iters_mgr,
|
||||
/*value_found*/ nullptr,
|
||||
/*key_exists*/ nullptr, /*seq*/ nullptr, &read_cb,
|
||||
/*is_blob*/ nullptr,
|
||||
/*do_merge=*/get_impl_options.get_value);
|
||||
RecordTick(stats_, MEMTABLE_MISS);
|
||||
}
|
||||
{
|
||||
RecordTick(stats_, NUMBER_KEYS_READ);
|
||||
size_t size = 0;
|
||||
// Mirror DBImpl::GetImpl: only produce merge-operand output and count bytes
|
||||
// read on success. A non-OK status leaves outputs cleared (see
|
||||
// PostprocessMemtableValueRead) and records no read throughput.
|
||||
if (s.ok()) {
|
||||
if (get_impl_options.value) {
|
||||
size = get_impl_options.value->size();
|
||||
} else if (get_impl_options.columns) {
|
||||
size = get_impl_options.columns->payload_size();
|
||||
} else if (get_impl_options.merge_operands) {
|
||||
*get_impl_options.number_of_operands =
|
||||
static_cast<int>(merge_context.GetNumOperands());
|
||||
for (const Slice& sl : merge_context.GetOperands()) {
|
||||
size += sl.size();
|
||||
get_impl_options.merge_operands->PinSelf(sl);
|
||||
get_impl_options.merge_operands++;
|
||||
}
|
||||
}
|
||||
RecordTick(stats_, BYTES_READ, size);
|
||||
RecordInHistogram(stats_, BYTES_PER_READ, size);
|
||||
PERF_COUNTER_ADD(get_read_bytes, size);
|
||||
}
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
#endif // defined(WITHOUT_COROUTINES) ||
|
||||
// (defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
+187
-17
@@ -336,6 +336,193 @@ Status DBImplSecondary::RecoverLogFiles(
|
||||
return status;
|
||||
}
|
||||
|
||||
Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
||||
const Slice& key,
|
||||
GetImplOptions& get_impl_options) {
|
||||
assert(get_impl_options.value != nullptr ||
|
||||
get_impl_options.columns != nullptr ||
|
||||
get_impl_options.merge_operands != nullptr);
|
||||
assert(get_impl_options.column_family);
|
||||
|
||||
Status s;
|
||||
|
||||
if (read_options.timestamp) {
|
||||
s = FailIfTsMismatchCf(get_impl_options.column_family,
|
||||
*(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
} else {
|
||||
s = FailIfCfHasTs(get_impl_options.column_family);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (get_impl_options.timestamp) {
|
||||
get_impl_options.timestamp->clear();
|
||||
}
|
||||
|
||||
PERF_CPU_TIMER_GUARD(get_cpu_nanos, immutable_db_options_.clock);
|
||||
StopWatch sw(immutable_db_options_.clock, stats_, DB_GET);
|
||||
PERF_TIMER_GUARD(get_snapshot_time);
|
||||
|
||||
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
SequenceNumber snapshot = versions_->LastSequence();
|
||||
GetWithTimestampReadCallback read_cb(snapshot);
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
get_impl_options.column_family);
|
||||
auto cfd = cfh->cfd();
|
||||
bool is_blob_index = false;
|
||||
bool* is_blob_ptr = get_impl_options.is_blob_index;
|
||||
std::string timestamp_storage;
|
||||
std::string* ts = nullptr;
|
||||
if (ucmp->timestamp_size() > 0) {
|
||||
ts = get_impl_options.timestamp != nullptr
|
||||
? get_impl_options.timestamp
|
||||
: (get_impl_options.get_value ? ×tamp_storage : nullptr);
|
||||
}
|
||||
if (!is_blob_ptr && get_impl_options.get_value) {
|
||||
is_blob_ptr = &is_blob_index;
|
||||
}
|
||||
const bool resolve_blob_backed_memtable_value =
|
||||
get_impl_options.get_value && (is_blob_ptr == &is_blob_index);
|
||||
if (tracer_) {
|
||||
InstrumentedMutexLock lock(&trace_mutex_);
|
||||
if (tracer_) {
|
||||
tracer_->Get(get_impl_options.column_family, key);
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire SuperVersion
|
||||
SuperVersion* super_version = GetAndRefSuperVersion(cfd);
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd, super_version,
|
||||
*(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
return s;
|
||||
}
|
||||
}
|
||||
MergeContext merge_context;
|
||||
// TODO - Large Result Optimization for Secondary DB
|
||||
// (https://github.com/facebook/rocksdb/pull/10458)
|
||||
|
||||
SequenceNumber max_covering_tombstone_seq = 0;
|
||||
LookupKey lkey(key, snapshot, read_options.timestamp);
|
||||
PERF_TIMER_STOP(get_snapshot_time);
|
||||
bool done = false;
|
||||
std::optional<BlobFetcher> memtable_blob_fetcher;
|
||||
if (cfd->ioptions().enable_blob_direct_write ||
|
||||
cfd->GetLatestMutableCFOptions().enable_blob_files) {
|
||||
// Catch-up can rebuild older blob references into memtables after mutable
|
||||
// blob-file settings change, so keep blob resolution available whenever
|
||||
// either blob knob indicates it may be needed.
|
||||
memtable_blob_fetcher.emplace(super_version->current, read_options,
|
||||
cfd->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/true);
|
||||
}
|
||||
const BlobFetcher* memtable_blob_fetcher_ptr =
|
||||
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
|
||||
|
||||
// Look up starts here
|
||||
if (get_impl_options.get_value) {
|
||||
if (super_version->mem->Get(
|
||||
lkey,
|
||||
get_impl_options.value ? get_impl_options.value->GetSelf()
|
||||
: nullptr,
|
||||
get_impl_options.columns, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */, &read_cb, is_blob_ptr,
|
||||
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
DBImpl::PostprocessMemtableValueRead(
|
||||
key, ts, resolve_blob_backed_memtable_value,
|
||||
memtable_blob_fetcher_ptr, get_impl_options.value,
|
||||
get_impl_options.columns, &s, &is_blob_index,
|
||||
get_impl_options.value_found);
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
super_version->imm->Get(
|
||||
lkey,
|
||||
get_impl_options.value ? get_impl_options.value->GetSelf()
|
||||
: nullptr,
|
||||
get_impl_options.columns, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options, &read_cb,
|
||||
is_blob_ptr, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
DBImpl::PostprocessMemtableValueRead(
|
||||
key, ts, resolve_blob_backed_memtable_value,
|
||||
memtable_blob_fetcher_ptr, get_impl_options.value,
|
||||
get_impl_options.columns, &s, &is_blob_index,
|
||||
get_impl_options.value_found);
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
}
|
||||
} else {
|
||||
// GetMergeOperands
|
||||
if (super_version->mem->Get(
|
||||
lkey,
|
||||
get_impl_options.value ? get_impl_options.value->GetSelf()
|
||||
: nullptr,
|
||||
get_impl_options.columns, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */, &read_cb,
|
||||
/*is_blob_index=*/nullptr, /*do_merge=*/false,
|
||||
memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
super_version->imm->GetMergeOperands(
|
||||
lkey, &s, &merge_context, &max_covering_tombstone_seq,
|
||||
read_options, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
}
|
||||
}
|
||||
if (!s.ok() && !s.IsMergeInProgress() && !s.IsNotFound()) {
|
||||
assert(done);
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
return s;
|
||||
}
|
||||
if (!done) {
|
||||
PERF_TIMER_GUARD(get_from_output_files_time);
|
||||
PinnedIteratorsManager pinned_iters_mgr;
|
||||
super_version->current->Get(
|
||||
read_options, lkey, get_impl_options.value, get_impl_options.columns,
|
||||
ts, &s, &merge_context, &max_covering_tombstone_seq, &pinned_iters_mgr,
|
||||
/*value_found*/ nullptr,
|
||||
/*key_exists*/ nullptr, /*seq*/ nullptr, &read_cb, /*is_blob*/ nullptr,
|
||||
/*do_merge=*/get_impl_options.get_value);
|
||||
RecordTick(stats_, MEMTABLE_MISS);
|
||||
}
|
||||
{
|
||||
PERF_TIMER_GUARD(get_post_process_time);
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
RecordTick(stats_, NUMBER_KEYS_READ);
|
||||
size_t size = 0;
|
||||
if (get_impl_options.value) {
|
||||
size = get_impl_options.value->size();
|
||||
} else if (get_impl_options.columns) {
|
||||
size = get_impl_options.columns->serialized_size();
|
||||
} else if (get_impl_options.merge_operands) {
|
||||
*get_impl_options.number_of_operands =
|
||||
static_cast<int>(merge_context.GetNumOperands());
|
||||
for (const Slice& sl : merge_context.GetOperands()) {
|
||||
size += sl.size();
|
||||
get_impl_options.merge_operands->PinSelf(sl);
|
||||
get_impl_options.merge_operands++;
|
||||
}
|
||||
}
|
||||
RecordTick(stats_, BYTES_READ, size);
|
||||
RecordTimeToHistogram(stats_, BYTES_PER_READ, size);
|
||||
PERF_COUNTER_ADD(get_read_bytes, size);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Iterator* DBImplSecondary::NewIterator(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
@@ -1409,13 +1596,9 @@ Status DB::OpenAndCompact(
|
||||
// needs LSM state from MANIFEST, not memtable data from WAL replay)
|
||||
std::unique_ptr<DB> db;
|
||||
std::vector<ColumnFamilyHandle*> handles;
|
||||
const uint64_t db_open_start_micros = db_options.env->NowMicros();
|
||||
s = DBImplSecondary::OpenAsSecondaryImpl(db_options, name, output_directory,
|
||||
column_families, &handles, &db,
|
||||
/*recover_wal=*/false);
|
||||
RecordTimeToHistogram(db_options.statistics.get(),
|
||||
OPEN_AND_COMPACT_DB_OPEN_MICROS,
|
||||
db_options.env->NowMicros() - db_open_start_micros);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -1616,16 +1799,3 @@ Status DBImplSecondary::FinalizeCompactionProgressWriter(
|
||||
return Status::OK();
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
// Generate the regular and coroutine versions of the functions defined in
|
||||
// db_impl_secondary_sync_and_async.h by including it twice. The macros expand
|
||||
// differently based on whether WITH_COROUTINES or WITHOUT_COROUTINES is
|
||||
// defined.
|
||||
// clang-format off
|
||||
#define WITHOUT_COROUTINES
|
||||
#include "db/db_impl/db_impl_secondary_sync_and_async.h"
|
||||
#undef WITHOUT_COROUTINES
|
||||
#define WITH_COROUTINES
|
||||
#include "db/db_impl/db_impl_secondary_sync_and_async.h"
|
||||
#undef WITH_COROUTINES
|
||||
// clang-format on
|
||||
@@ -97,9 +97,8 @@ class DBImplSecondary : public DBImpl {
|
||||
// it eagerly keeps all talbe files open and is able to access the contents of
|
||||
// deleted files via prior open fd.
|
||||
using DBImpl::GetImpl;
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, GetImpl, const ReadOptions& options,
|
||||
const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
Status GetImpl(const ReadOptions& options, const Slice& key,
|
||||
GetImplOptions& get_impl_options) override;
|
||||
|
||||
using DBImpl::NewIterator;
|
||||
// Operations on the created iterators can return IOError due to files being
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
//
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "util/coro_utils.h"
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
(const ReadOptions& read_options, const Slice& key,
|
||||
GetImplOptions& get_impl_options) {
|
||||
assert(get_impl_options.value != nullptr ||
|
||||
get_impl_options.columns != nullptr ||
|
||||
get_impl_options.merge_operands != nullptr);
|
||||
assert(get_impl_options.column_family);
|
||||
|
||||
Status s;
|
||||
|
||||
if (read_options.timestamp) {
|
||||
s = FailIfTsMismatchCf(get_impl_options.column_family,
|
||||
*(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
} else {
|
||||
s = FailIfCfHasTs(get_impl_options.column_family);
|
||||
if (!s.ok()) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (get_impl_options.timestamp) {
|
||||
get_impl_options.timestamp->clear();
|
||||
}
|
||||
|
||||
#if defined(WITHOUT_COROUTINES)
|
||||
PERF_CPU_TIMER_GUARD(get_cpu_nanos, immutable_db_options_.clock);
|
||||
#endif
|
||||
StopWatch sw(immutable_db_options_.clock, stats_, DB_GET);
|
||||
PERF_TIMER_GUARD(get_snapshot_time);
|
||||
|
||||
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
SequenceNumber snapshot = versions_->LastSequence();
|
||||
GetWithTimestampReadCallback read_cb(snapshot);
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
get_impl_options.column_family);
|
||||
auto cfd = cfh->cfd();
|
||||
bool is_blob_index = false;
|
||||
bool* is_blob_ptr = get_impl_options.is_blob_index;
|
||||
std::string timestamp_storage;
|
||||
std::string* ts = nullptr;
|
||||
if (ucmp->timestamp_size() > 0) {
|
||||
ts = get_impl_options.timestamp != nullptr
|
||||
? get_impl_options.timestamp
|
||||
: (get_impl_options.get_value ? ×tamp_storage : nullptr);
|
||||
}
|
||||
if (!is_blob_ptr && get_impl_options.get_value) {
|
||||
is_blob_ptr = &is_blob_index;
|
||||
}
|
||||
const bool resolve_blob_backed_memtable_value =
|
||||
get_impl_options.get_value && (is_blob_ptr == &is_blob_index);
|
||||
if (tracer_) {
|
||||
InstrumentedMutexLock lock(&trace_mutex_);
|
||||
if (tracer_) {
|
||||
tracer_->Get(get_impl_options.column_family, key);
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire SuperVersion
|
||||
#if defined(WITH_COROUTINES)
|
||||
SuperVersion* super_version = cfd->GetReferencedSuperVersion(this);
|
||||
#else
|
||||
SuperVersion* super_version = GetAndRefSuperVersion(cfd);
|
||||
#endif
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd, super_version,
|
||||
*(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
#if defined(WITH_COROUTINES)
|
||||
CleanupSuperVersion(super_version);
|
||||
#else
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
#endif
|
||||
CO_RETURN s;
|
||||
}
|
||||
}
|
||||
MergeContext merge_context;
|
||||
// TODO - Large Result Optimization for Secondary DB
|
||||
// (https://github.com/facebook/rocksdb/pull/10458)
|
||||
|
||||
SequenceNumber max_covering_tombstone_seq = 0;
|
||||
LookupKey lkey(key, snapshot, read_options.timestamp);
|
||||
PERF_TIMER_STOP(get_snapshot_time);
|
||||
bool done = false;
|
||||
std::optional<VersionBlobFetcher> memtable_blob_fetcher;
|
||||
if (cfd->ioptions().enable_blob_direct_write ||
|
||||
cfd->GetLatestMutableCFOptions().enable_blob_files) {
|
||||
// Catch-up can rebuild older blob references into memtables after mutable
|
||||
// blob-file settings change, so keep blob resolution available whenever
|
||||
// either blob knob indicates it may be needed.
|
||||
memtable_blob_fetcher.emplace(super_version->current, read_options,
|
||||
cfd->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/true);
|
||||
}
|
||||
const BlobFetcher* memtable_blob_fetcher_ptr =
|
||||
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
|
||||
|
||||
// Look up starts here
|
||||
if (get_impl_options.get_value) {
|
||||
if (super_version->mem->Get(
|
||||
lkey,
|
||||
get_impl_options.value ? get_impl_options.value->GetSelf()
|
||||
: nullptr,
|
||||
get_impl_options.columns, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */, &read_cb, is_blob_ptr,
|
||||
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
s = DBImpl::PostprocessMemtableValueRead(
|
||||
key, ts, resolve_blob_backed_memtable_value,
|
||||
memtable_blob_fetcher_ptr, get_impl_options.value,
|
||||
get_impl_options.columns, std::move(s), &is_blob_index,
|
||||
get_impl_options.value_found);
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
super_version->imm->Get(
|
||||
lkey,
|
||||
get_impl_options.value ? get_impl_options.value->GetSelf()
|
||||
: nullptr,
|
||||
get_impl_options.columns, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options, &read_cb,
|
||||
is_blob_ptr, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
s = DBImpl::PostprocessMemtableValueRead(
|
||||
key, ts, resolve_blob_backed_memtable_value,
|
||||
memtable_blob_fetcher_ptr, get_impl_options.value,
|
||||
get_impl_options.columns, std::move(s), &is_blob_index,
|
||||
get_impl_options.value_found);
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
}
|
||||
} else {
|
||||
// GetMergeOperands
|
||||
if (super_version->mem->Get(
|
||||
lkey,
|
||||
get_impl_options.value ? get_impl_options.value->GetSelf()
|
||||
: nullptr,
|
||||
get_impl_options.columns, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */, &read_cb,
|
||||
/*is_blob_index=*/nullptr, /*do_merge=*/false,
|
||||
memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
super_version->imm->GetMergeOperands(
|
||||
lkey, &s, &merge_context, &max_covering_tombstone_seq,
|
||||
read_options, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
}
|
||||
}
|
||||
if (!s.ok() && !s.IsMergeInProgress() && !s.IsNotFound()) {
|
||||
assert(done);
|
||||
#if defined(WITH_COROUTINES)
|
||||
CleanupSuperVersion(super_version);
|
||||
#else
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
#endif
|
||||
CO_RETURN s;
|
||||
}
|
||||
if (!done) {
|
||||
PERF_TIMER_GUARD(get_from_output_files_time);
|
||||
PinnedIteratorsManager pinned_iters_mgr;
|
||||
CO_AWAIT(super_version->current->Get, read_options, lkey,
|
||||
get_impl_options.value, get_impl_options.columns, ts, &s,
|
||||
&merge_context, &max_covering_tombstone_seq, &pinned_iters_mgr,
|
||||
/*value_found*/ nullptr,
|
||||
/*key_exists*/ nullptr, /*seq*/ nullptr, &read_cb,
|
||||
/*is_blob*/ nullptr,
|
||||
/*do_merge=*/get_impl_options.get_value);
|
||||
RecordTick(stats_, MEMTABLE_MISS);
|
||||
}
|
||||
{
|
||||
PERF_TIMER_GUARD(get_post_process_time);
|
||||
#if defined(WITH_COROUTINES)
|
||||
CleanupSuperVersion(super_version);
|
||||
#else
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
#endif
|
||||
RecordTick(stats_, NUMBER_KEYS_READ);
|
||||
size_t size = 0;
|
||||
// Mirror DBImpl::GetImpl: only produce merge-operand output and count bytes
|
||||
// read on success. A non-OK status leaves outputs cleared (see
|
||||
// PostprocessMemtableValueRead) and records no read throughput.
|
||||
if (s.ok()) {
|
||||
if (get_impl_options.value) {
|
||||
size = get_impl_options.value->size();
|
||||
} else if (get_impl_options.columns) {
|
||||
size = get_impl_options.columns->payload_size();
|
||||
} else if (get_impl_options.merge_operands) {
|
||||
*get_impl_options.number_of_operands =
|
||||
static_cast<int>(merge_context.GetNumOperands());
|
||||
for (const Slice& sl : merge_context.GetOperands()) {
|
||||
size += sl.size();
|
||||
get_impl_options.merge_operands->PinSelf(sl);
|
||||
get_impl_options.merge_operands++;
|
||||
}
|
||||
}
|
||||
RecordTick(stats_, BYTES_READ, size);
|
||||
RecordTimeToHistogram(stats_, BYTES_PER_READ, size);
|
||||
PERF_COUNTER_ADD(get_read_bytes, size);
|
||||
}
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
#endif // defined(WITHOUT_COROUTINES) ||
|
||||
// (defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
@@ -1,828 +0,0 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
//
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "util/coro_utils.h"
|
||||
|
||||
#if defined(USE_COROUTINES) && defined(WITH_COROUTINES)
|
||||
#include "folly/executors/IOExecutor.h"
|
||||
#include "folly/io/async/EventBase.h"
|
||||
#endif // USE_COROUTINES && WITH_COROUTINES
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
(const ReadOptions& read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value) {
|
||||
CO_RETURN CO_AWAIT(GetImpl, read_options, column_family, key, value,
|
||||
/*timestamp=*/nullptr);
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::Get)
|
||||
(const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value, std::string* timestamp) {
|
||||
assert(value != nullptr);
|
||||
value->Reset();
|
||||
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGet) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
"Can only call Get with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGet`");
|
||||
}
|
||||
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kGet;
|
||||
}
|
||||
|
||||
#ifdef WITH_COROUTINES
|
||||
auto* read_executor = immutable_db_options_.fs->GetReadExecutor();
|
||||
if (read_executor != nullptr) {
|
||||
auto* read_event_base = read_executor->getEventBase();
|
||||
assert(read_event_base != nullptr);
|
||||
Status s = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base),
|
||||
GetImplCoroutine(read_options, column_family, key, value, timestamp)));
|
||||
co_return s;
|
||||
}
|
||||
#endif
|
||||
CO_RETURN GetImpl(read_options, column_family, key, value, timestamp);
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
(const ReadOptions& read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value, std::string* timestamp) {
|
||||
GetImplOptions get_impl_options;
|
||||
get_impl_options.column_family = column_family;
|
||||
get_impl_options.value = value;
|
||||
get_impl_options.timestamp = timestamp;
|
||||
|
||||
Status s = CO_AWAIT(GetImpl, read_options, key, get_impl_options);
|
||||
CO_RETURN s;
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
(const ReadOptions& read_options, const Slice& key,
|
||||
GetImplOptions& get_impl_options) {
|
||||
assert(get_impl_options.value != nullptr ||
|
||||
get_impl_options.merge_operands != nullptr ||
|
||||
get_impl_options.columns != nullptr);
|
||||
|
||||
assert(get_impl_options.column_family);
|
||||
|
||||
if (read_options.timestamp) {
|
||||
const Status s = FailIfTsMismatchCf(get_impl_options.column_family,
|
||||
*(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
} else {
|
||||
const Status s = FailIfCfHasTs(get_impl_options.column_family);
|
||||
if (!s.ok()) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (get_impl_options.timestamp) {
|
||||
get_impl_options.timestamp->clear();
|
||||
}
|
||||
|
||||
GetWithTimestampReadCallback read_cb(0); // Will call Refresh
|
||||
|
||||
#if defined(WITHOUT_COROUTINES)
|
||||
PERF_CPU_TIMER_GUARD(get_cpu_nanos, immutable_db_options_.clock);
|
||||
#endif
|
||||
StopWatch sw(immutable_db_options_.clock, stats_, DB_GET);
|
||||
PERF_TIMER_GUARD(get_snapshot_time);
|
||||
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
get_impl_options.column_family);
|
||||
auto cfd = cfh->cfd();
|
||||
|
||||
if (tracer_) {
|
||||
// TODO: This mutex should be removed later, to improve performance when
|
||||
// tracing is enabled.
|
||||
InstrumentedMutexLock lock(&trace_mutex_);
|
||||
if (tracer_) {
|
||||
// TODO: maybe handle the tracing status?
|
||||
tracer_->Get(get_impl_options.column_family, key).PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
|
||||
if (get_impl_options.get_merge_operands_options != nullptr) {
|
||||
for (int i = 0; i < get_impl_options.get_merge_operands_options
|
||||
->expected_max_number_of_operands;
|
||||
++i) {
|
||||
get_impl_options.merge_operands[i].Reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire SuperVersion
|
||||
#if defined(WITH_COROUTINES)
|
||||
SuperVersion* sv = cfd->GetReferencedSuperVersion(this);
|
||||
Defer sv_cleanup([&] { CleanupSuperVersion(sv); });
|
||||
#else
|
||||
SuperVersion* sv = GetAndRefSuperVersion(cfd);
|
||||
Defer sv_cleanup([&] { ReturnAndCleanupSuperVersion(cfd, sv); });
|
||||
#endif
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
const Status s =
|
||||
FailIfReadCollapsedHistory(cfd, sv, *(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::GetImpl:AfterAcquireSv", nullptr);
|
||||
TEST_SYNC_POINT("DBImpl::GetImpl:1");
|
||||
TEST_SYNC_POINT("DBImpl::GetImpl:2");
|
||||
|
||||
SequenceNumber snapshot;
|
||||
if (read_options.snapshot != nullptr) {
|
||||
if (get_impl_options.callback) {
|
||||
// Already calculated based on read_options.snapshot
|
||||
snapshot = get_impl_options.callback->max_visible_seq();
|
||||
} else {
|
||||
snapshot =
|
||||
static_cast<const SnapshotImpl*>(read_options.snapshot)->number_;
|
||||
}
|
||||
} else {
|
||||
// Note that the snapshot is assigned AFTER referencing the super
|
||||
// version because otherwise a flush happening in between may compact away
|
||||
// data for the snapshot, so the reader would see neither data that was be
|
||||
// visible to the snapshot before compaction nor the newer data inserted
|
||||
// afterwards.
|
||||
snapshot = GetLastPublishedSequence();
|
||||
if (get_impl_options.callback) {
|
||||
// The unprep_seqs are not published for write unprepared, so it could be
|
||||
// that max_visible_seq is larger. Seek to the std::max of the two.
|
||||
// However, we still want our callback to contain the actual snapshot so
|
||||
// that it can do the correct visibility filtering.
|
||||
get_impl_options.callback->Refresh(snapshot);
|
||||
|
||||
// Internally, WriteUnpreparedTxnReadCallback::Refresh would set
|
||||
// max_visible_seq = max(max_visible_seq, snapshot)
|
||||
//
|
||||
// Currently, the commented out assert is broken by
|
||||
// InvalidSnapshotReadCallback, but if write unprepared recovery followed
|
||||
// the regular transaction flow, then this special read callback would not
|
||||
// be needed.
|
||||
//
|
||||
// assert(callback->max_visible_seq() >= snapshot);
|
||||
snapshot = get_impl_options.callback->max_visible_seq();
|
||||
}
|
||||
}
|
||||
// If timestamp is used, we use read callback to ensure <key,t,s> is returned
|
||||
// only if t <= read_opts.timestamp and s <= snapshot.
|
||||
// HACK: temporarily overwrite input struct field but restore
|
||||
SaveAndRestore<ReadCallback*> restore_callback(&get_impl_options.callback);
|
||||
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
if (ucmp->timestamp_size() > 0) {
|
||||
assert(!get_impl_options
|
||||
.callback); // timestamp with callback is not supported
|
||||
read_cb.Refresh(snapshot);
|
||||
get_impl_options.callback = &read_cb;
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::GetImpl:3");
|
||||
TEST_SYNC_POINT("DBImpl::GetImpl:4");
|
||||
|
||||
// Prepare to store a list of merge operations if merge occurs.
|
||||
MergeContext merge_context;
|
||||
merge_context.get_merge_operands_options =
|
||||
get_impl_options.get_merge_operands_options;
|
||||
SequenceNumber max_covering_tombstone_seq = 0;
|
||||
|
||||
Status s;
|
||||
// First look in the memtable, then in the immutable memtable (if any).
|
||||
// s is both in/out. When in, s could either be OK or MergeInProgress.
|
||||
// merge_operands will contain the sequence of merges in the latter case.
|
||||
LookupKey lkey(key, snapshot, read_options.timestamp);
|
||||
PERF_TIMER_STOP(get_snapshot_time);
|
||||
|
||||
bool skip_memtable = (read_options.read_tier == kPersistedTier &&
|
||||
has_unpersisted_data_.load(std::memory_order_relaxed));
|
||||
bool done = false;
|
||||
bool is_blob_index = false;
|
||||
bool* is_blob_ptr = get_impl_options.is_blob_index;
|
||||
auto* partition_mgr = cfd->blob_partition_manager();
|
||||
std::string timestamp_storage;
|
||||
std::string* timestamp = nullptr;
|
||||
if (ucmp->timestamp_size() > 0) {
|
||||
timestamp = get_impl_options.timestamp != nullptr
|
||||
? get_impl_options.timestamp
|
||||
: (partition_mgr != nullptr ? ×tamp_storage : nullptr);
|
||||
}
|
||||
if (partition_mgr != nullptr && !is_blob_ptr && get_impl_options.get_value) {
|
||||
is_blob_ptr = &is_blob_index;
|
||||
}
|
||||
const bool resolve_direct_write_value =
|
||||
partition_mgr != nullptr && (is_blob_ptr == &is_blob_index);
|
||||
std::optional<VersionBlobFetcher> memtable_blob_fetcher;
|
||||
if (partition_mgr != nullptr) {
|
||||
memtable_blob_fetcher.emplace(sv->current, read_options,
|
||||
cfd->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/true);
|
||||
}
|
||||
const BlobFetcher* memtable_blob_fetcher_ptr =
|
||||
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
|
||||
if (!skip_memtable) {
|
||||
// Get value associated with key
|
||||
if (get_impl_options.get_value) {
|
||||
if (sv->mem->Get(lkey,
|
||||
get_impl_options.value
|
||||
? get_impl_options.value->GetSelf()
|
||||
: nullptr,
|
||||
get_impl_options.columns, timestamp, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */,
|
||||
get_impl_options.callback, is_blob_ptr,
|
||||
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
PostprocessDirectWriteValueRead(
|
||||
read_options, key, timestamp, resolve_direct_write_value,
|
||||
sv->current, cfd, get_impl_options.value, get_impl_options.columns,
|
||||
&s, &is_blob_index, get_impl_options.value_found);
|
||||
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
sv->imm->Get(lkey,
|
||||
get_impl_options.value
|
||||
? get_impl_options.value->GetSelf()
|
||||
: nullptr,
|
||||
get_impl_options.columns, timestamp, &s,
|
||||
&merge_context, &max_covering_tombstone_seq,
|
||||
read_options, get_impl_options.callback,
|
||||
is_blob_ptr, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
PostprocessDirectWriteValueRead(
|
||||
read_options, key, timestamp, resolve_direct_write_value,
|
||||
sv->current, cfd, get_impl_options.value, get_impl_options.columns,
|
||||
&s, &is_blob_index, get_impl_options.value_found);
|
||||
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
}
|
||||
} else {
|
||||
// Get Merge Operands associated with key, Merge Operands should not be
|
||||
// merged and raw values should be returned to the user.
|
||||
if (sv->mem->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr,
|
||||
/*timestamp=*/nullptr, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */, nullptr, nullptr, false,
|
||||
memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
sv->imm->GetMergeOperands(
|
||||
lkey, &s, &merge_context, &max_covering_tombstone_seq,
|
||||
read_options, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
}
|
||||
}
|
||||
if (!s.ok() && !s.IsMergeInProgress() && !s.IsNotFound()) {
|
||||
assert(done);
|
||||
CO_RETURN s;
|
||||
}
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::GetImpl:PostMemTableGet:0");
|
||||
TEST_SYNC_POINT("DBImpl::GetImpl:PostMemTableGet:1");
|
||||
PinnedIteratorsManager pinned_iters_mgr;
|
||||
if (!done) {
|
||||
PERF_TIMER_GUARD(get_from_output_files_time);
|
||||
CO_AWAIT(
|
||||
sv->current->Get, read_options, lkey, get_impl_options.value,
|
||||
get_impl_options.columns, timestamp, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, &pinned_iters_mgr,
|
||||
get_impl_options.get_value ? get_impl_options.value_found : nullptr,
|
||||
nullptr, nullptr,
|
||||
get_impl_options.get_value ? get_impl_options.callback : nullptr,
|
||||
get_impl_options.get_value ? is_blob_ptr : nullptr,
|
||||
get_impl_options.get_value);
|
||||
if (get_impl_options.get_value && resolve_direct_write_value) {
|
||||
std::string blob_lookup_key_storage;
|
||||
MaybeResolveDirectWriteValue(
|
||||
read_options,
|
||||
GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage),
|
||||
resolve_direct_write_value, sv->current, cfd, get_impl_options.value,
|
||||
get_impl_options.columns, &s, &is_blob_index,
|
||||
get_impl_options.value_found);
|
||||
}
|
||||
RecordTick(stats_, MEMTABLE_MISS);
|
||||
}
|
||||
|
||||
{
|
||||
PERF_TIMER_GUARD(get_post_process_time);
|
||||
|
||||
RecordTick(stats_, NUMBER_KEYS_READ);
|
||||
size_t size = 0;
|
||||
if (s.ok()) {
|
||||
const auto& merge_threshold = read_options.merge_operand_count_threshold;
|
||||
if (merge_threshold.has_value() &&
|
||||
merge_context.GetNumOperands() > merge_threshold.value()) {
|
||||
s = Status::OkMergeOperandThresholdExceeded();
|
||||
}
|
||||
|
||||
if (get_impl_options.get_value) {
|
||||
if (get_impl_options.value) {
|
||||
size = get_impl_options.value->size();
|
||||
} else if (get_impl_options.columns) {
|
||||
size = get_impl_options.columns->payload_size();
|
||||
}
|
||||
} else {
|
||||
// Return all merge operands for get_impl_options.key
|
||||
*get_impl_options.number_of_operands =
|
||||
static_cast<int>(merge_context.GetNumOperands());
|
||||
// OK status is returned, some merge operand is found.
|
||||
assert(*get_impl_options.number_of_operands > 0);
|
||||
if (*get_impl_options.number_of_operands >
|
||||
get_impl_options.get_merge_operands_options
|
||||
->expected_max_number_of_operands) {
|
||||
s = Status::Incomplete(
|
||||
Status::SubCode::KMergeOperandsInsufficientCapacity);
|
||||
} else {
|
||||
// Each operand depends on one of the following resources: `sv`,
|
||||
// `pinned_iters_mgr`, or `merge_context`. It would be crazy expensive
|
||||
// to reference `sv` for each operand relying on it because `sv` is
|
||||
// (un)ref'd in all threads using the DB. Furthermore, we do not track
|
||||
// on which resource each operand depends.
|
||||
//
|
||||
// To solve this, we bundle the resources in a `GetMergeOperandsState`
|
||||
// and manage them with a `SharedCleanablePtr` shared among the
|
||||
// `PinnableSlice`s we return. This bundle includes one `sv` reference
|
||||
// and ownership of the `merge_context` and `pinned_iters_mgr`
|
||||
// objects.
|
||||
bool ref_sv = ShouldReferenceSuperVersion(merge_context);
|
||||
if (ref_sv) {
|
||||
assert(!merge_context.GetOperands().empty());
|
||||
SharedCleanablePtr shared_cleanable;
|
||||
GetMergeOperandsState* state = new GetMergeOperandsState(
|
||||
std::move(merge_context), std::move(pinned_iters_mgr));
|
||||
|
||||
sv->Ref();
|
||||
|
||||
state->sv_handle = new SuperVersionHandle(
|
||||
this, &mutex_, sv,
|
||||
immutable_db_options_.avoid_unnecessary_blocking_io);
|
||||
|
||||
shared_cleanable.Allocate();
|
||||
shared_cleanable->RegisterCleanup(CleanupGetMergeOperandsState,
|
||||
state /* arg1 */,
|
||||
nullptr /* arg2 */);
|
||||
for (size_t i = 0; i < state->merge_context.GetOperands().size();
|
||||
++i) {
|
||||
const Slice& sl = state->merge_context.GetOperands()[i];
|
||||
size += sl.size();
|
||||
|
||||
get_impl_options.merge_operands->PinSlice(
|
||||
sl, nullptr /* cleanable */);
|
||||
if (i == state->merge_context.GetOperands().size() - 1) {
|
||||
shared_cleanable.MoveAsCleanupTo(
|
||||
get_impl_options.merge_operands);
|
||||
} else {
|
||||
shared_cleanable.RegisterCopyWith(
|
||||
get_impl_options.merge_operands);
|
||||
}
|
||||
get_impl_options.merge_operands++;
|
||||
}
|
||||
} else {
|
||||
for (const Slice& sl : merge_context.GetOperands()) {
|
||||
size += sl.size();
|
||||
get_impl_options.merge_operands->PinSelf(sl);
|
||||
get_impl_options.merge_operands++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RecordTick(stats_, BYTES_READ, size);
|
||||
PERF_COUNTER_ADD(get_read_bytes, size);
|
||||
}
|
||||
|
||||
RecordInHistogram(stats_, BYTES_PER_READ, size);
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
|
||||
// The actual implementation of batched MultiGet. Parameters -
|
||||
// start_key - Index in the sorted_keys vector to start processing from
|
||||
// num_keys - Number of keys to lookup, starting with sorted_keys[start_key]
|
||||
// sorted_keys - The entire batch of sorted keys for this CF
|
||||
//
|
||||
// The per key status is returned in the KeyContext structures pointed to by
|
||||
// sorted_keys. An overall Status is also returned, with the only possible
|
||||
// values being Status::OK() and Status::TimedOut(). The latter indicates
|
||||
// that the call exceeded read_options.deadline
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::MultiGetImpl)
|
||||
(const ReadOptions& read_options, size_t start_key, size_t num_keys,
|
||||
autovector<KeyContext*, MultiGetContext::MAX_BATCH_SIZE>* sorted_keys,
|
||||
SuperVersion* super_version, SequenceNumber snapshot, ReadCallback* callback) {
|
||||
#if defined(WITHOUT_COROUTINES)
|
||||
PERF_CPU_TIMER_GUARD(get_cpu_nanos, immutable_db_options_.clock);
|
||||
#endif // defined(WITHOUT_COROUTINES)
|
||||
StopWatch sw(immutable_db_options_.clock, stats_, DB_MULTIGET);
|
||||
|
||||
assert(sorted_keys);
|
||||
assert(start_key + num_keys <= sorted_keys->size());
|
||||
if (num_keys == 0) {
|
||||
CO_RETURN Status::OK();
|
||||
}
|
||||
auto* cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
(*sorted_keys)[start_key]->column_family);
|
||||
ColumnFamilyData* cfd = cfh->cfd();
|
||||
auto* partition_mgr = cfd->blob_partition_manager();
|
||||
std::optional<VersionBlobFetcher> memtable_blob_fetcher;
|
||||
if (partition_mgr != nullptr) {
|
||||
memtable_blob_fetcher.emplace(super_version->current, read_options,
|
||||
cfd->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/true);
|
||||
}
|
||||
const BlobFetcher* memtable_blob_fetcher_ptr =
|
||||
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
|
||||
|
||||
#ifdef WITH_COROUTINES
|
||||
constexpr bool kUseCoroRead = true;
|
||||
#else
|
||||
constexpr bool kUseCoroRead = false;
|
||||
#endif
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
for (size_t i = start_key; i < start_key + num_keys; ++i) {
|
||||
KeyContext* kctx = (*sorted_keys)[i];
|
||||
if (kctx->timestamp) {
|
||||
kctx->timestamp->clear();
|
||||
}
|
||||
}
|
||||
|
||||
// For each of the given keys, apply the entire "get" process as follows:
|
||||
// First look in the memtable, then in the immutable memtable (if any).
|
||||
// s is both in/out. When in, s could either be OK or MergeInProgress.
|
||||
// merge_operands will contain the sequence of merges in the latter case.
|
||||
size_t keys_left = num_keys;
|
||||
Status s;
|
||||
uint64_t curr_value_size = 0;
|
||||
while (keys_left) {
|
||||
if (read_options.deadline.count() &&
|
||||
immutable_db_options_.clock->NowMicros() >
|
||||
static_cast<uint64_t>(read_options.deadline.count())) {
|
||||
s = Status::TimedOut();
|
||||
break;
|
||||
}
|
||||
|
||||
size_t batch_size = (keys_left > MultiGetContext::MAX_BATCH_SIZE)
|
||||
? MultiGetContext::MAX_BATCH_SIZE
|
||||
: keys_left;
|
||||
MultiGetContext ctx(sorted_keys, start_key + num_keys - keys_left,
|
||||
batch_size, snapshot, read_options, GetFileSystem(),
|
||||
stats_, kUseCoroRead);
|
||||
MultiGetRange range = ctx.GetMultiGetRange();
|
||||
range.AddValueSize(curr_value_size);
|
||||
bool lookup_current = true;
|
||||
|
||||
keys_left -= batch_size;
|
||||
for (auto mget_iter = range.begin(); mget_iter != range.end();
|
||||
++mget_iter) {
|
||||
mget_iter->merge_context.Clear();
|
||||
*mget_iter->s = Status::OK();
|
||||
}
|
||||
|
||||
bool skip_memtable =
|
||||
(read_options.read_tier == kPersistedTier &&
|
||||
has_unpersisted_data_.load(std::memory_order_relaxed));
|
||||
if (!skip_memtable) {
|
||||
super_version->mem->MultiGet(read_options, &range, callback,
|
||||
false /* immutable_memtable */,
|
||||
memtable_blob_fetcher_ptr);
|
||||
if (!range.empty()) {
|
||||
super_version->imm->MultiGet(read_options, &range, callback,
|
||||
memtable_blob_fetcher_ptr);
|
||||
}
|
||||
if (!range.empty()) {
|
||||
uint64_t left = range.KeysLeft();
|
||||
RecordTick(stats_, MEMTABLE_MISS, left);
|
||||
} else {
|
||||
lookup_current = false;
|
||||
}
|
||||
}
|
||||
if (lookup_current) {
|
||||
PERF_TIMER_GUARD(get_from_output_files_time);
|
||||
CO_AWAIT(super_version->current->MultiGet, read_options, &range,
|
||||
callback);
|
||||
}
|
||||
curr_value_size = range.GetValueSize();
|
||||
if (curr_value_size > read_options.value_size_soft_limit) {
|
||||
s = Status::Aborted();
|
||||
break;
|
||||
}
|
||||
|
||||
// This could be a long-running operation
|
||||
bool aborted = ROCKSDB_THREAD_YIELD_CHECK_ABORT();
|
||||
if (aborted) {
|
||||
s = Status::Aborted("Query abort.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Post processing (decrement reference counts and record statistics)
|
||||
PERF_TIMER_GUARD(get_post_process_time);
|
||||
size_t num_found = 0;
|
||||
uint64_t bytes_read = 0;
|
||||
// value_size_soft_limit was enforced above (and inside per-file MultiGet) on
|
||||
// pre-resolution sizes. Direct-write blob references, though, are resolved
|
||||
// below -- after that check counted only their encoded blob-index bytes.
|
||||
// Enforce the limit on them here too, but only abort a key once the bytes
|
||||
// already returned by this MultiGet exceed the limit. That way at least one
|
||||
// key is always read even if it alone exceeds the limit ("always make
|
||||
// progress"), so a caller retrying aborted keys cannot loop forever on a
|
||||
// single oversized value.
|
||||
const bool enforce_direct_write_soft_limit =
|
||||
partition_mgr != nullptr &&
|
||||
read_options.value_size_soft_limit !=
|
||||
std::numeric_limits<uint64_t>::max() &&
|
||||
read_options.read_tier != kBlockCacheTier;
|
||||
for (size_t i = start_key; i < start_key + num_keys - keys_left; ++i) {
|
||||
KeyContext* key = (*sorted_keys)[i];
|
||||
assert(key);
|
||||
assert(key->s);
|
||||
|
||||
if (partition_mgr != nullptr && key->s->ok()) {
|
||||
const bool has_unresolved_direct_write =
|
||||
key->is_blob_index ||
|
||||
(key->columns != nullptr &&
|
||||
!PinnableWideColumnsHelper::GetUnresolvedBlobColumnIndices(
|
||||
*key->columns)
|
||||
.empty());
|
||||
if (enforce_direct_write_soft_limit && has_unresolved_direct_write &&
|
||||
bytes_read > read_options.value_size_soft_limit) {
|
||||
// Prior keys in this MultiGet have already returned more than the soft
|
||||
// limit, so skip fetching this key's (possibly large) blob value(s).
|
||||
if (key->value != nullptr) {
|
||||
key->value->Reset();
|
||||
}
|
||||
if (key->columns != nullptr) {
|
||||
key->columns->Reset();
|
||||
}
|
||||
key->is_blob_index = false;
|
||||
*key->s = Status::Aborted();
|
||||
} else {
|
||||
std::string blob_lookup_key_storage;
|
||||
MaybeResolveDirectWriteValue(
|
||||
read_options,
|
||||
GetBlobLookupUserKey(*key->key, key->timestamp,
|
||||
&blob_lookup_key_storage),
|
||||
/*resolve_direct_write_value=*/true, super_version->current, cfd,
|
||||
key->value, key->columns, key->s, &key->is_blob_index);
|
||||
}
|
||||
}
|
||||
|
||||
if (key->s->ok()) {
|
||||
const auto& merge_threshold = read_options.merge_operand_count_threshold;
|
||||
if (merge_threshold.has_value() &&
|
||||
key->merge_context.GetNumOperands() > merge_threshold) {
|
||||
*(key->s) = Status::OkMergeOperandThresholdExceeded();
|
||||
}
|
||||
|
||||
if (key->value) {
|
||||
bytes_read += key->value->size();
|
||||
} else {
|
||||
assert(key->columns);
|
||||
bytes_read += key->columns->payload_size();
|
||||
}
|
||||
|
||||
num_found++;
|
||||
}
|
||||
}
|
||||
if (keys_left) {
|
||||
assert(s.IsTimedOut() || s.IsAborted());
|
||||
for (size_t i = start_key + num_keys - keys_left; i < start_key + num_keys;
|
||||
++i) {
|
||||
KeyContext* key = (*sorted_keys)[i];
|
||||
*key->s = s;
|
||||
}
|
||||
}
|
||||
|
||||
RecordTick(stats_, NUMBER_MULTIGET_CALLS);
|
||||
RecordTick(stats_, NUMBER_MULTIGET_KEYS_READ, num_keys);
|
||||
RecordTick(stats_, NUMBER_MULTIGET_KEYS_FOUND, num_found);
|
||||
RecordTick(stats_, NUMBER_MULTIGET_BYTES_READ, bytes_read);
|
||||
RecordInHistogram(stats_, BYTES_PER_MULTIGET, bytes_read);
|
||||
PERF_COUNTER_ADD(multiget_read_bytes, bytes_read);
|
||||
PERF_TIMER_STOP(get_post_process_time);
|
||||
|
||||
CO_RETURN s;
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGetCommon)
|
||||
(const ReadOptions& read_options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys, PinnableSlice* values,
|
||||
PinnableWideColumns* columns, std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input) {
|
||||
if (num_keys == 0) {
|
||||
CO_RETURN;
|
||||
}
|
||||
bool should_fail = false;
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
ColumnFamilyHandle* cfh = column_families[i];
|
||||
if (read_options.timestamp) {
|
||||
statuses[i] = FailIfTsMismatchCf(cfh, *(read_options.timestamp));
|
||||
if (!statuses[i].ok()) {
|
||||
should_fail = true;
|
||||
}
|
||||
} else {
|
||||
statuses[i] = FailIfCfHasTs(cfh);
|
||||
if (!statuses[i].ok()) {
|
||||
should_fail = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (should_fail) {
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
if (statuses[i].ok()) {
|
||||
statuses[i] = Status::Incomplete(
|
||||
"DB not queried due to invalid argument(s) in the same MultiGet");
|
||||
}
|
||||
}
|
||||
CO_RETURN;
|
||||
}
|
||||
|
||||
if (tracer_) {
|
||||
// TODO: This mutex should be removed later, to improve performance when
|
||||
// tracing is enabled.
|
||||
InstrumentedMutexLock lock(&trace_mutex_);
|
||||
if (tracer_) {
|
||||
// TODO: maybe handle the tracing status?
|
||||
tracer_->MultiGet(num_keys, column_families, keys).PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
|
||||
autovector<KeyContext, MultiGetContext::MAX_BATCH_SIZE> key_context;
|
||||
key_context.reserve(num_keys);
|
||||
autovector<KeyContext*, MultiGetContext::MAX_BATCH_SIZE> sorted_keys;
|
||||
sorted_keys.resize(num_keys);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
PinnableSlice* val = nullptr;
|
||||
PinnableWideColumns* col = nullptr;
|
||||
|
||||
if (values) {
|
||||
val = &values[i];
|
||||
val->Reset();
|
||||
} else {
|
||||
assert(columns);
|
||||
|
||||
col = &columns[i];
|
||||
col->Reset();
|
||||
}
|
||||
|
||||
key_context.emplace_back(column_families[i], keys[i], val, col,
|
||||
timestamps ? ×tamps[i] : nullptr,
|
||||
&statuses[i]);
|
||||
}
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
sorted_keys[i] = &key_context[i];
|
||||
}
|
||||
PrepareMultiGetKeys(num_keys, sorted_input, &sorted_keys);
|
||||
|
||||
autovector<MultiGetKeyRangePerCf, MultiGetContext::MAX_BATCH_SIZE>
|
||||
key_range_per_cf;
|
||||
autovector<ColumnFamilySuperVersionPair, MultiGetContext::MAX_BATCH_SIZE>
|
||||
cf_sv_pairs;
|
||||
size_t cf_start = 0;
|
||||
ColumnFamilyHandle* cf = sorted_keys[0]->column_family;
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
KeyContext* key_ctx = sorted_keys[i];
|
||||
if (key_ctx->column_family != cf) {
|
||||
key_range_per_cf.emplace_back(cf_start, i - cf_start);
|
||||
cf_sv_pairs.emplace_back(cf, nullptr);
|
||||
cf_start = i;
|
||||
cf = key_ctx->column_family;
|
||||
}
|
||||
}
|
||||
|
||||
key_range_per_cf.emplace_back(cf_start, num_keys - cf_start);
|
||||
cf_sv_pairs.emplace_back(cf, nullptr);
|
||||
|
||||
SequenceNumber consistent_seqnum = kMaxSequenceNumber;
|
||||
bool sv_from_thread_local = false;
|
||||
#ifdef WITH_COROUTINES
|
||||
// The coroutine read path may suspend mid-MultiGet and resume on a shared
|
||||
// executor thread, which is incompatible with the thread-local SuperVersion
|
||||
// cache: it marks the thread's slot kSVInUse for the duration of the read, so
|
||||
// a second coroutine resuming on the same thread would trip the kSVInUse
|
||||
// assertion. Take an independent reference instead, which releases the
|
||||
// thread-local slot before any suspension point.
|
||||
constexpr bool kExtraSvRef = true;
|
||||
#else
|
||||
constexpr bool kExtraSvRef = false;
|
||||
#endif
|
||||
Status s = MultiCFSnapshot<autovector<ColumnFamilySuperVersionPair,
|
||||
MultiGetContext::MAX_BATCH_SIZE>>(
|
||||
read_options, nullptr,
|
||||
[](autovector<ColumnFamilySuperVersionPair,
|
||||
MultiGetContext::MAX_BATCH_SIZE>::iterator& cf_iter) {
|
||||
return &(*cf_iter);
|
||||
},
|
||||
&cf_sv_pairs, kExtraSvRef, &consistent_seqnum, &sv_from_thread_local);
|
||||
|
||||
if (!s.ok()) {
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
if (statuses[i].ok()) {
|
||||
statuses[i] = s;
|
||||
}
|
||||
}
|
||||
CO_RETURN;
|
||||
}
|
||||
|
||||
GetWithTimestampReadCallback timestamp_read_callback(0);
|
||||
ReadCallback* read_callback = nullptr;
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
timestamp_read_callback.Refresh(consistent_seqnum);
|
||||
read_callback = ×tamp_read_callback;
|
||||
}
|
||||
|
||||
assert(key_range_per_cf.size() == cf_sv_pairs.size());
|
||||
auto key_range_per_cf_iter = key_range_per_cf.begin();
|
||||
auto cf_sv_pair_iter = cf_sv_pairs.begin();
|
||||
while (key_range_per_cf_iter != key_range_per_cf.end() &&
|
||||
cf_sv_pair_iter != cf_sv_pairs.end()) {
|
||||
s = CO_AWAIT(MultiGetImpl, read_options, key_range_per_cf_iter->start,
|
||||
key_range_per_cf_iter->num_keys, &sorted_keys,
|
||||
cf_sv_pair_iter->super_version, consistent_seqnum,
|
||||
read_callback);
|
||||
if (!s.ok()) {
|
||||
break;
|
||||
}
|
||||
++key_range_per_cf_iter;
|
||||
++cf_sv_pair_iter;
|
||||
}
|
||||
if (!s.ok()) {
|
||||
assert(s.IsTimedOut() || s.IsAborted());
|
||||
for (++key_range_per_cf_iter;
|
||||
key_range_per_cf_iter != key_range_per_cf.end();
|
||||
++key_range_per_cf_iter) {
|
||||
for (size_t i = key_range_per_cf_iter->start;
|
||||
i < key_range_per_cf_iter->start + key_range_per_cf_iter->num_keys;
|
||||
++i) {
|
||||
*sorted_keys[i]->s = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& cf_sv_pair : cf_sv_pairs) {
|
||||
if (sv_from_thread_local && !kExtraSvRef) {
|
||||
ReturnAndCleanupSuperVersion(cf_sv_pair.cfd, cf_sv_pair.super_version);
|
||||
} else {
|
||||
TEST_SYNC_POINT("DBImpl::MultiCFSnapshot::BeforeLastTryUnRefSV");
|
||||
CleanupSuperVersion(cf_sv_pair.super_version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGet)
|
||||
(const ReadOptions& _read_options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys, PinnableSlice* values,
|
||||
std::string* timestamps, Status* statuses, const bool sorted_input) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kMultiGet) {
|
||||
Status s = Status::InvalidArgument(
|
||||
"Can only call MultiGet with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kMultiGet`");
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
if (statuses[i].ok()) {
|
||||
statuses[i] = s;
|
||||
}
|
||||
}
|
||||
CO_RETURN;
|
||||
}
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kMultiGet;
|
||||
}
|
||||
#ifdef WITH_COROUTINES
|
||||
auto* read_executor = immutable_db_options_.fs->GetReadExecutor();
|
||||
if (read_executor != nullptr) {
|
||||
auto* read_event_base = read_executor->getEventBase();
|
||||
assert(read_event_base != nullptr);
|
||||
co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base),
|
||||
MultiGetCommonCoroutine(
|
||||
read_options, num_keys, column_families, keys, values,
|
||||
/* columns */ nullptr, timestamps, statuses, sorted_input)));
|
||||
co_return;
|
||||
}
|
||||
#endif
|
||||
MultiGetCommon(read_options, num_keys, column_families, keys, values,
|
||||
/* columns */ nullptr, timestamps, statuses, sorted_input);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
#endif
|
||||
+57
-20
@@ -77,10 +77,10 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
|
||||
user_comparator_(cmp),
|
||||
merge_operator_(ioptions.merge_operator.get()),
|
||||
iter_(iter),
|
||||
// Enable the direct-write blob fallback only when this CF has a
|
||||
// write-path partition manager; otherwise the fetcher stays on the plain
|
||||
// Version::GetBlob fast path.
|
||||
blob_state_(version, read_options, cfd ? cfd->blob_file_cache() : nullptr,
|
||||
blob_state_(version, read_options.read_tier,
|
||||
read_options.verify_checksums, read_options.fill_cache,
|
||||
read_options.io_activity,
|
||||
cfd ? cfd->blob_file_cache() : nullptr,
|
||||
cfd != nullptr && cfd->blob_partition_manager() != nullptr),
|
||||
read_callback_(read_callback),
|
||||
sequence_(s),
|
||||
@@ -94,6 +94,8 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
|
||||
trace_db_(db_impl),
|
||||
trace_cf_id_(cfd != nullptr ? cfd->GetID() : 0),
|
||||
has_trace_state_(db_impl != nullptr && cfd != nullptr),
|
||||
allow_blob_write_path_fallback_(cfd != nullptr &&
|
||||
cfd->blob_partition_manager() != nullptr),
|
||||
ingest_sst_lock_(cfd != nullptr ? &cfd->GetIngestSstLock() : nullptr),
|
||||
timestamp_ub_(read_options.timestamp),
|
||||
timestamp_lb_(read_options.iter_start_ts),
|
||||
@@ -257,25 +259,60 @@ void DBIter::Next() {
|
||||
}
|
||||
}
|
||||
|
||||
Status DBIter::BlobReader::RetrieveAndSetBlobValue(const Slice& user_key,
|
||||
const Slice& blob_index) {
|
||||
Status DBIter::BlobReader::RetrieveAndSetBlobValue(
|
||||
const Slice& user_key, const Slice& blob_index,
|
||||
bool allow_write_path_fallback) {
|
||||
assert(blob_value_.empty());
|
||||
|
||||
if (!blob_fetcher_.CanResolve()) {
|
||||
if (!version_ && (!allow_write_path_fallback || !blob_file_cache_)) {
|
||||
return Status::Corruption("Encountered unexpected blob index.");
|
||||
}
|
||||
|
||||
// TODO: consider moving ReadOptions from ArenaWrappedDBIter to DBIter to
|
||||
// avoid having to copy options back and forth.
|
||||
// TODO: plumb Env::IOPriority
|
||||
ReadOptions read_options;
|
||||
read_options.read_tier = read_tier_;
|
||||
read_options.verify_checksums = verify_checksums_;
|
||||
read_options.fill_cache = fill_cache_;
|
||||
read_options.io_activity = io_activity_;
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
constexpr uint64_t* bytes_read = nullptr;
|
||||
return blob_fetcher_.FetchBlob(user_key, blob_index, prefetch_buffer,
|
||||
&blob_value_, bytes_read);
|
||||
|
||||
if (!allow_write_path_fallback) {
|
||||
assert(version_ != nullptr);
|
||||
return version_->GetBlob(read_options, user_key, blob_index,
|
||||
prefetch_buffer, &blob_value_, bytes_read);
|
||||
}
|
||||
|
||||
BlobIndex blob_idx;
|
||||
Status s = blob_idx.DecodeFrom(blob_index);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
return BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
|
||||
read_options, user_key, blob_idx, version_, blob_file_cache_,
|
||||
prefetch_buffer, &blob_value_, bytes_read);
|
||||
}
|
||||
|
||||
BlobFetcher DBIter::BlobReader::CreateBlobFetcher() const {
|
||||
ReadOptions read_options;
|
||||
read_options.read_tier = read_tier_;
|
||||
read_options.verify_checksums = verify_checksums_;
|
||||
read_options.fill_cache = fill_cache_;
|
||||
read_options.io_activity = io_activity_;
|
||||
return BlobFetcher(version_, read_options, blob_file_cache_,
|
||||
allow_write_path_fallback_);
|
||||
}
|
||||
|
||||
bool DBIter::SetValueAndColumnsFromBlobImpl(const Slice& user_key,
|
||||
const Slice& blob_index) {
|
||||
const Status s =
|
||||
blob_state_.mut()->reader.RetrieveAndSetBlobValue(user_key, blob_index);
|
||||
// Keep the non-BDW iterator path on the pre-existing Version::GetBlob()
|
||||
// fast path. Only enable the direct-write fallback when this CF actually
|
||||
// has a write-path partition manager.
|
||||
const Status s = blob_state_.mut()->reader.RetrieveAndSetBlobValue(
|
||||
user_key, blob_index, allow_blob_write_path_fallback_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
valid_ = false;
|
||||
@@ -331,8 +368,7 @@ bool DBIter::SetValueAndColumnsFromEntity(Slice slice) {
|
||||
}
|
||||
if (LIKELY(!has_blob_columns)) {
|
||||
WideColumns& wide_columns = state.wide_columns();
|
||||
const Status s =
|
||||
WideColumnSerialization::DeserializeSimple(slice, wide_columns);
|
||||
const Status s = WideColumnSerialization::Deserialize(slice, wide_columns);
|
||||
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
@@ -354,9 +390,9 @@ bool DBIter::SetValueAndColumnsFromEntity(Slice slice) {
|
||||
state.SaveEntitySliceIfNeeded(slice);
|
||||
|
||||
{
|
||||
const Slice entity = state.PrepareForLazyEntityDeserialize();
|
||||
const Status s = WideColumnSerialization::Deserialize(
|
||||
entity, state.lazy_entity_columns(), &state.lazy_blob_columns());
|
||||
Slice input_copy = state.PrepareForLazyEntityDeserialize();
|
||||
const Status s = WideColumnSerialization::DeserializeV2(
|
||||
input_copy, state.lazy_entity_columns(), state.lazy_blob_columns());
|
||||
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
@@ -1585,8 +1621,8 @@ bool DBIter::MergeWithBlobBaseValue(const Slice& blob_index,
|
||||
return false;
|
||||
}
|
||||
|
||||
const Status s =
|
||||
blob_state_.mut()->reader.RetrieveAndSetBlobValue(user_key, blob_index);
|
||||
const Status s = blob_state_.mut()->reader.RetrieveAndSetBlobValue(
|
||||
user_key, blob_index, allow_blob_write_path_fallback_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
valid_ = false;
|
||||
@@ -1608,11 +1644,12 @@ bool DBIter::MergeWithWideColumnBaseValue(const Slice& entity,
|
||||
const Slice& user_key) {
|
||||
// Resolve V2 entity blob columns if present, since TimedFullMerge only
|
||||
// supports V1 format.
|
||||
BlobFetcher blob_fetcher = blob_state_->reader.CreateBlobFetcher();
|
||||
std::string resolved_entity;
|
||||
Slice effective_entity;
|
||||
Status s_resolve = WideColumnSerialization::ResolveEntityForMerge(
|
||||
entity, user_key, &blob_state_->reader.blob_fetcher(),
|
||||
nullptr /* prefetch_buffers */, resolved_entity, effective_entity);
|
||||
entity, user_key, &blob_fetcher, nullptr /* prefetch_buffers */,
|
||||
resolved_entity, effective_entity);
|
||||
if (!s_resolve.ok()) {
|
||||
status_ = std::move(s_resolve);
|
||||
valid_ = false;
|
||||
|
||||
+29
-19
@@ -276,22 +276,37 @@ class DBIter final : public Iterator {
|
||||
|
||||
class BlobReader {
|
||||
public:
|
||||
BlobReader(const Version* version, const ReadOptions& read_options,
|
||||
BlobFileCache* blob_file_cache, bool allow_write_path_fallback)
|
||||
: blob_fetcher_(version, ReadOptions(read_options), blob_file_cache,
|
||||
allow_write_path_fallback) {}
|
||||
BlobReader(const Version* version, ReadTier read_tier,
|
||||
bool verify_checksums, bool fill_cache,
|
||||
Env::IOActivity io_activity, BlobFileCache* blob_file_cache,
|
||||
bool allow_write_path_fallback)
|
||||
: version_(version),
|
||||
read_tier_(read_tier),
|
||||
verify_checksums_(verify_checksums),
|
||||
fill_cache_(fill_cache),
|
||||
io_activity_(io_activity),
|
||||
blob_file_cache_(blob_file_cache),
|
||||
allow_write_path_fallback_(allow_write_path_fallback) {}
|
||||
|
||||
const Slice& GetBlobValue() const { return blob_value_; }
|
||||
Status RetrieveAndSetBlobValue(const Slice& user_key,
|
||||
const Slice& blob_index);
|
||||
const Slice& blob_index,
|
||||
bool allow_write_path_fallback);
|
||||
void ResetBlobValue() { blob_value_.Reset(); }
|
||||
// The blob fetcher backing this reader, for resolving wide-column entity
|
||||
// blob references (the merge path). Valid for this BlobReader's lifetime.
|
||||
const BlobFetcher& blob_fetcher() const { return blob_fetcher_; }
|
||||
// Create a BlobFetcher with the same read options as this BlobReader.
|
||||
BlobFetcher CreateBlobFetcher() const;
|
||||
|
||||
private:
|
||||
PinnableSlice blob_value_;
|
||||
OwningVersionBlobFetcher blob_fetcher_;
|
||||
const Version* version_;
|
||||
ReadTier read_tier_;
|
||||
bool verify_checksums_;
|
||||
bool fill_cache_;
|
||||
Env::IOActivity io_activity_;
|
||||
// Cache used by the write-path fallback for in-flight direct-write blob
|
||||
// files that are not yet reachable through Version.
|
||||
BlobFileCache* blob_file_cache_;
|
||||
bool allow_write_path_fallback_;
|
||||
};
|
||||
struct BlobState {
|
||||
BlobReader reader;
|
||||
@@ -316,7 +331,9 @@ class DBIter final : public Iterator {
|
||||
ValueColumnsState(const Version* version, const ReadOptions& read_options,
|
||||
ColumnFamilyData* cfd)
|
||||
: entity_blob_resolver_(
|
||||
version, read_options, cfd ? cfd->blob_file_cache() : nullptr,
|
||||
version, read_options.read_tier, read_options.verify_checksums,
|
||||
read_options.fill_cache, read_options.io_activity,
|
||||
cfd ? cfd->blob_file_cache() : nullptr,
|
||||
cfd != nullptr && cfd->blob_partition_manager() != nullptr) {}
|
||||
|
||||
Slice& value() { return value_; }
|
||||
@@ -382,7 +399,7 @@ class DBIter final : public Iterator {
|
||||
}
|
||||
|
||||
// Clears the previous lazy entity metadata and returns the saved entity
|
||||
// buffer as input for Deserialize().
|
||||
// buffer as input for DeserializeV2().
|
||||
Slice PrepareForLazyEntityDeserialize() {
|
||||
ClearLazyEntity();
|
||||
return Slice(saved_value_);
|
||||
@@ -656,14 +673,6 @@ class DBIter final : public Iterator {
|
||||
UserComparatorWrapper user_comparator_;
|
||||
const MergeOperator* const merge_operator_;
|
||||
IteratorWrapper iter_;
|
||||
// TODO: blob_state_'s BlobReader (whole-value blob reads + the merge entity
|
||||
// path) and value_columns_state_'s ReadPathBlobResolver (lazy per-column
|
||||
// entity resolution) each own a Version-backed blob fetcher with the same
|
||||
// {version, read_options, blob_file_cache, allow_write_path_fallback}, so an
|
||||
// iterator holds two ReadOptions copies. Collapse to a single shared fetcher.
|
||||
// Deferred because the resolver must keep owning its fetcher for the planned
|
||||
// lazy entity read path, where the result outlives the read call and owns the
|
||||
// SuperVersion pin; unifying is best done together with that work.
|
||||
DirtyTracked<BlobState> blob_state_;
|
||||
ReadCallback* read_callback_;
|
||||
// Max visible sequence number. It is normally the snapshot seq unless we have
|
||||
@@ -710,6 +719,7 @@ class DBIter final : public Iterator {
|
||||
DBImpl* trace_db_;
|
||||
uint32_t trace_cf_id_;
|
||||
bool has_trace_state_;
|
||||
bool allow_blob_write_path_fallback_;
|
||||
port::RWMutex* ingest_sst_lock_;
|
||||
const Slice* const timestamp_ub_;
|
||||
const Slice* const timestamp_lb_;
|
||||
|
||||
@@ -6268,95 +6268,6 @@ TEST_P(DBMultiScanIteratorTest, WastedBlocksTracking) {
|
||||
ASSERT_GT(wasted, 0);
|
||||
}
|
||||
|
||||
TEST_P(DBMultiScanIteratorTest, PrefetchStatsRecorded) {
|
||||
// Regression test: the MultiScan prefetch stats must still be published after
|
||||
// the prefetch path moved into the IODispatcher. A cold scan populates the
|
||||
// prefetch counters; a warm re-scan registers cache hits.
|
||||
auto options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.statistics = CreateDBStatistics();
|
||||
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.flush_block_policy_factory =
|
||||
std::make_shared<FlushBlockEveryKeyPolicyFactory>();
|
||||
table_options.block_cache = NewLRUCache(10 * 1024 * 1024); // 10MB cache
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const int kNumKeys = 50;
|
||||
std::string value(100, 'v');
|
||||
for (int i = 0; i < kNumKeys; ++i) {
|
||||
std::stringstream ss;
|
||||
ss << "k" << std::setw(3) << std::setfill('0') << i;
|
||||
ASSERT_OK(Put(ss.str(), value));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange({}, nullptr, nullptr));
|
||||
|
||||
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
|
||||
|
||||
auto run_scan = [&]() {
|
||||
MultiScanArgs scan_options(BytewiseComparator());
|
||||
scan_options.use_async_io = false; // deterministic sync path
|
||||
scan_options.insert("k000", "k049");
|
||||
ReadOptions ro;
|
||||
ro.fill_cache = true;
|
||||
std::unique_ptr<MultiScan> iter =
|
||||
dbfull()->NewMultiScan(ro, cfh, scan_options);
|
||||
ASSERT_NE(iter, nullptr);
|
||||
if (iter == nullptr) {
|
||||
return;
|
||||
}
|
||||
int count = 0;
|
||||
try {
|
||||
for (auto range : *iter) {
|
||||
for (auto it : range) {
|
||||
it.first.ToString();
|
||||
count++;
|
||||
}
|
||||
}
|
||||
} catch (MultiScanException& ex) {
|
||||
FAIL() << "Scan failed: " << ex.what();
|
||||
}
|
||||
ASSERT_GT(count, 0);
|
||||
};
|
||||
|
||||
// Cold scan: force blocks to be read from disk so the prefetch path runs.
|
||||
table_options.block_cache->EraseUnRefEntries();
|
||||
SetPerfLevel(kEnableCount);
|
||||
PerfContext* perf = get_perf_context();
|
||||
ASSERT_NE(perf, nullptr);
|
||||
if (perf == nullptr) {
|
||||
return;
|
||||
}
|
||||
perf->Reset();
|
||||
run_scan(); // MultiScanIndexIterator destroyed here -> stats recorded
|
||||
|
||||
ASSERT_GT(TestGetTickerCount(options, MULTISCAN_PREPARE_CALLS), 0);
|
||||
ASSERT_GT(TestGetTickerCount(options, MULTISCAN_BLOCKS_PREFETCHED), 0);
|
||||
ASSERT_GT(TestGetTickerCount(options, MULTISCAN_PREFETCH_BYTES), 0);
|
||||
ASSERT_GT(TestGetTickerCount(options, MULTISCAN_IO_REQUESTS), 0);
|
||||
HistogramData blocks_per_prepare;
|
||||
options.statistics->histogramData(MULTISCAN_BLOCKS_PER_PREPARE,
|
||||
&blocks_per_prepare);
|
||||
ASSERT_GT(blocks_per_prepare.count, 0);
|
||||
|
||||
// PerfContext mirrors the same metrics scoped to this operation/thread.
|
||||
ASSERT_GT(perf->multiscan_prepare_count, 0);
|
||||
ASSERT_GT(perf->multiscan_blocks_prefetched, 0);
|
||||
ASSERT_GT(perf->multiscan_prefetch_bytes, 0);
|
||||
ASSERT_GT(perf->multiscan_io_requests, 0);
|
||||
SetPerfLevel(kDisable);
|
||||
|
||||
// Warm scan: the just-cached blocks now count as cache hits.
|
||||
uint64_t cache_hits_before =
|
||||
TestGetTickerCount(options, MULTISCAN_BLOCKS_FROM_CACHE);
|
||||
run_scan();
|
||||
ASSERT_GT(TestGetTickerCount(options, MULTISCAN_BLOCKS_FROM_CACHE),
|
||||
cache_hits_before);
|
||||
}
|
||||
|
||||
class ReadPathRangeTombstoneTest : public DBIteratorBaseTest,
|
||||
public ::testing::WithParamInterface<bool> {
|
||||
protected:
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
// 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 <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
|
||||
+14
-87
@@ -23,10 +23,6 @@
|
||||
#include "table/format.h"
|
||||
#include "util/random.h"
|
||||
|
||||
#if USE_COROUTINES
|
||||
#include "folly/coro/BlockingWait.h"
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace {
|
||||
@@ -35,7 +31,6 @@ int64_t MaybeCurrentTime(Env* env) {
|
||||
env->GetCurrentTime(&time).PermitUncheckedError();
|
||||
return time;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// Special Env used to delay background operations
|
||||
@@ -862,29 +857,12 @@ Status DBTestBase::SingleDelete(int cf, const std::string& k) {
|
||||
return db_->SingleDelete(WriteOptions(), handles_[cf], k);
|
||||
}
|
||||
|
||||
std::string DBTestBase::Get(const std::string& k, const Snapshot* snapshot,
|
||||
bool use_coroutine) {
|
||||
std::string DBTestBase::Get(const std::string& k, const Snapshot* snapshot) {
|
||||
ReadOptions options;
|
||||
options.verify_checksums = true;
|
||||
options.snapshot = snapshot;
|
||||
std::string result;
|
||||
Status s;
|
||||
#if USE_COROUTINES
|
||||
if (use_coroutine) {
|
||||
PinnableSlice pinnable_value(&result);
|
||||
s = folly::coro::blockingWait(dbfull()->GetCoroutine(
|
||||
options, dbfull()->DefaultColumnFamily(), k, &pinnable_value,
|
||||
/*timestamp=*/nullptr));
|
||||
if (s.ok() && pinnable_value.IsPinned()) {
|
||||
result.assign(pinnable_value.data(), pinnable_value.size());
|
||||
}
|
||||
} else
|
||||
#else
|
||||
(void)use_coroutine;
|
||||
#endif // USE_COROUTINES
|
||||
{
|
||||
s = db_->Get(options, k, &result);
|
||||
}
|
||||
Status s = db_->Get(options, k, &result);
|
||||
if (s.IsNotFound()) {
|
||||
result = "NOT_FOUND";
|
||||
} else if (!s.ok()) {
|
||||
@@ -894,27 +872,12 @@ std::string DBTestBase::Get(const std::string& k, const Snapshot* snapshot,
|
||||
}
|
||||
|
||||
std::string DBTestBase::Get(int cf, const std::string& k,
|
||||
const Snapshot* snapshot, bool use_coroutine) {
|
||||
const Snapshot* snapshot) {
|
||||
ReadOptions options;
|
||||
options.verify_checksums = true;
|
||||
options.snapshot = snapshot;
|
||||
std::string result;
|
||||
Status s;
|
||||
#if USE_COROUTINES
|
||||
if (use_coroutine) {
|
||||
PinnableSlice pinnable_value(&result);
|
||||
s = folly::coro::blockingWait(dbfull()->GetCoroutine(
|
||||
options, handles_[cf], k, &pinnable_value, /*timestamp=*/nullptr));
|
||||
if (s.ok() && pinnable_value.IsPinned()) {
|
||||
result.assign(pinnable_value.data(), pinnable_value.size());
|
||||
}
|
||||
} else
|
||||
#else
|
||||
(void)use_coroutine;
|
||||
#endif // USE_COROUTINES
|
||||
{
|
||||
s = db_->Get(options, handles_[cf], k, &result);
|
||||
}
|
||||
Status s = db_->Get(options, handles_[cf], k, &result);
|
||||
if (s.IsNotFound()) {
|
||||
result = "NOT_FOUND";
|
||||
} else if (!s.ok()) {
|
||||
@@ -926,8 +889,8 @@ std::string DBTestBase::Get(int cf, const std::string& k,
|
||||
std::vector<std::string> DBTestBase::MultiGet(std::vector<int> cfs,
|
||||
const std::vector<std::string>& k,
|
||||
const Snapshot* snapshot,
|
||||
bool batched, bool async,
|
||||
bool use_coroutine) {
|
||||
const bool batched,
|
||||
const bool async) {
|
||||
ReadOptions options;
|
||||
options.verify_checksums = true;
|
||||
options.snapshot = snapshot;
|
||||
@@ -954,19 +917,8 @@ std::vector<std::string> DBTestBase::MultiGet(std::vector<int> cfs,
|
||||
std::vector<PinnableSlice> pin_values(cfs.size());
|
||||
result.resize(cfs.size());
|
||||
s.resize(cfs.size());
|
||||
#if USE_COROUTINES
|
||||
if (use_coroutine) {
|
||||
folly::coro::blockingWait(dbfull()->MultiGetCoroutine(
|
||||
options, cfs.size(), handles.data(), keys.data(), pin_values.data(),
|
||||
/*timestamps=*/nullptr, s.data(), /*sorted_input=*/false));
|
||||
} else
|
||||
#else
|
||||
(void)use_coroutine;
|
||||
#endif // USE_COROUTINES
|
||||
{
|
||||
db_->MultiGet(options, cfs.size(), handles.data(), keys.data(),
|
||||
pin_values.data(), s.data());
|
||||
}
|
||||
db_->MultiGet(options, cfs.size(), handles.data(), keys.data(),
|
||||
pin_values.data(), s.data());
|
||||
for (size_t i = 0; i < s.size(); ++i) {
|
||||
if (s[i].IsNotFound()) {
|
||||
result[i] = "NOT_FOUND";
|
||||
@@ -985,14 +937,11 @@ std::vector<std::string> DBTestBase::MultiGet(std::vector<int> cfs,
|
||||
|
||||
std::vector<std::string> DBTestBase::MultiGet(const std::vector<std::string>& k,
|
||||
const Snapshot* snapshot,
|
||||
bool async,
|
||||
bool optimize_multiget_for_io,
|
||||
bool use_coroutine) {
|
||||
const bool async) {
|
||||
ReadOptions options;
|
||||
options.verify_checksums = true;
|
||||
options.snapshot = snapshot;
|
||||
options.async_io = async;
|
||||
options.optimize_multiget_for_io = optimize_multiget_for_io;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<std::string> result(k.size());
|
||||
std::vector<Status> statuses(k.size());
|
||||
@@ -1001,21 +950,8 @@ std::vector<std::string> DBTestBase::MultiGet(const std::vector<std::string>& k,
|
||||
for (size_t i = 0; i < k.size(); ++i) {
|
||||
keys.emplace_back(k[i]);
|
||||
}
|
||||
#if USE_COROUTINES
|
||||
if (use_coroutine) {
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(),
|
||||
dbfull()->DefaultColumnFamily());
|
||||
folly::coro::blockingWait(dbfull()->MultiGetCoroutine(
|
||||
options, keys.size(), cfs.data(), keys.data(), pin_values.data(),
|
||||
/*timestamps=*/nullptr, statuses.data(), /*sorted_input=*/false));
|
||||
} else
|
||||
#else
|
||||
(void)use_coroutine;
|
||||
#endif // USE_COROUTINES
|
||||
{
|
||||
db_->MultiGet(options, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), pin_values.data(), statuses.data());
|
||||
}
|
||||
db_->MultiGet(options, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), pin_values.data(), statuses.data());
|
||||
for (size_t i = 0; i < statuses.size(); ++i) {
|
||||
if (statuses[i].IsNotFound()) {
|
||||
result[i] = "NOT_FOUND";
|
||||
@@ -1031,20 +967,11 @@ std::vector<std::string> DBTestBase::MultiGet(const std::vector<std::string>& k,
|
||||
return result;
|
||||
}
|
||||
|
||||
Status DBTestBase::Get(const std::string& k, PinnableSlice* v,
|
||||
bool use_coroutine) {
|
||||
Status DBTestBase::Get(const std::string& k, PinnableSlice* v) {
|
||||
ReadOptions options;
|
||||
options.verify_checksums = true;
|
||||
#if USE_COROUTINES
|
||||
if (use_coroutine) {
|
||||
return folly::coro::blockingWait(
|
||||
dbfull()->GetCoroutine(options, dbfull()->DefaultColumnFamily(), k, v,
|
||||
/*timestamp=*/nullptr));
|
||||
}
|
||||
#else
|
||||
(void)use_coroutine;
|
||||
#endif // USE_COROUTINES
|
||||
return dbfull()->Get(options, dbfull()->DefaultColumnFamily(), k, v);
|
||||
Status s = dbfull()->Get(options, dbfull()->DefaultColumnFamily(), k, v);
|
||||
return s;
|
||||
}
|
||||
|
||||
Status DBTestBase::CompactRange(const CompactRangeOptions& options,
|
||||
|
||||
+7
-12
@@ -1231,27 +1231,22 @@ class DBTestBase : public testing::Test {
|
||||
|
||||
Status SingleDelete(int cf, const std::string& k);
|
||||
|
||||
std::string Get(const std::string& k, const Snapshot* snapshot = nullptr,
|
||||
bool use_coroutine = false);
|
||||
std::string Get(const std::string& k, const Snapshot* snapshot = nullptr);
|
||||
|
||||
std::string Get(int cf, const std::string& k,
|
||||
const Snapshot* snapshot = nullptr,
|
||||
bool use_coroutine = false);
|
||||
const Snapshot* snapshot = nullptr);
|
||||
|
||||
Status Get(const std::string& k, PinnableSlice* v,
|
||||
bool use_coroutine = false);
|
||||
Status Get(const std::string& k, PinnableSlice* v);
|
||||
|
||||
std::vector<std::string> MultiGet(std::vector<int> cfs,
|
||||
const std::vector<std::string>& k,
|
||||
const Snapshot* snapshot, bool batched,
|
||||
bool async = false,
|
||||
bool use_coroutine = false);
|
||||
const Snapshot* snapshot,
|
||||
const bool batched,
|
||||
const bool async = false);
|
||||
|
||||
std::vector<std::string> MultiGet(const std::vector<std::string>& k,
|
||||
const Snapshot* snapshot = nullptr,
|
||||
bool async = false,
|
||||
bool optimize_multiget_for_io = true,
|
||||
bool use_coroutine = false);
|
||||
const bool async = false);
|
||||
|
||||
Status CompactRange(const CompactRangeOptions& options,
|
||||
std::optional<Slice> begin, std::optional<Slice> end);
|
||||
|
||||
@@ -418,15 +418,6 @@ TEST_F(DeleteFileTest, BackgroundPurgeCFDropTest) {
|
||||
// Expect 1 sst file.
|
||||
CheckFileTypeCounts(dbname_, 0, 1, 1);
|
||||
|
||||
if (bg_purge) {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"DeleteFileTest::BackgroundPurgeCFDropTest:1",
|
||||
"DBImpl::BGWorkPurge:start"}});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
}
|
||||
|
||||
ASSERT_OK(db_->DropColumnFamily(cfh));
|
||||
// Still 1 file, it won't be deleted while ColumnFamilyHandle is alive.
|
||||
CheckFileTypeCounts(dbname_, 0, 1, 1);
|
||||
@@ -456,6 +447,13 @@ TEST_F(DeleteFileTest, BackgroundPurgeCFDropTest) {
|
||||
Reopen(options);
|
||||
ASSERT_OK(dbfull()->TEST_WaitForPurge());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"DeleteFileTest::BackgroundPurgeCFDropTest:1",
|
||||
"DBImpl::BGWorkPurge:start"}});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
{
|
||||
SCOPED_TRACE("avoid_unnecessary_blocking_io = true");
|
||||
do_test(true);
|
||||
|
||||
+2
-2
@@ -668,7 +668,7 @@ Status ErrorHandler::RecoverFromBGError(bool is_manual) {
|
||||
// can generate background errors should be the flush operations
|
||||
recovery_error_ = IOStatus::OK();
|
||||
recovery_error_.PermitUncheckedError();
|
||||
Status s = db_->ResumeImpl(recover_context_, Env::IOActivity::kFlush);
|
||||
Status s = db_->ResumeImpl(recover_context_);
|
||||
if (s.ok()) {
|
||||
soft_error_no_bg_work_ = false;
|
||||
} else {
|
||||
@@ -764,7 +764,7 @@ void ErrorHandler::RecoverFromRetryableBGIOError() {
|
||||
TEST_SYNC_POINT("RecoverFromRetryableBGIOError:BeforeResume1");
|
||||
recovery_error_ = IOStatus::OK();
|
||||
retry_count++;
|
||||
Status s = db_->ResumeImpl(context, Env::IOActivity::kFlush);
|
||||
Status s = db_->ResumeImpl(context);
|
||||
RecordStats({ERROR_HANDLER_AUTORESUME_RETRY_TOTAL_COUNT},
|
||||
{} /* int_histograms */);
|
||||
if (s.IsShutdownInProgress() ||
|
||||
|
||||
+11
-11
@@ -158,17 +158,17 @@ 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, lf->file_open_metadata);
|
||||
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);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
+15
-12
@@ -520,13 +520,12 @@ TEST_F(EventListenerTest, OnSingleDBFlushTest) {
|
||||
ASSERT_OK(Put(5, "nikitich", std::string(90000, 'n')));
|
||||
ASSERT_OK(Put(6, "alyosha", std::string(90000, 'a')));
|
||||
ASSERT_OK(Put(7, "popovich", std::string(90000, 'p')));
|
||||
FlushOptions flush_opts;
|
||||
flush_opts.listener_wait = true;
|
||||
for (int i = 1; i < 8; ++i) {
|
||||
ASSERT_OK(db_->Flush(flush_opts, handles_[i]));
|
||||
// listener_wait guarantees the OnFlushCompleted callbacks have finished
|
||||
// running by the time Flush() returns, so the listener state below is up to
|
||||
// date without separately waiting for background work.
|
||||
ASSERT_OK(Flush(i));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
// Ensure background work is fully finished including listener callbacks
|
||||
// before accessing listener state.
|
||||
ASSERT_OK(dbfull()->TEST_WaitForBackgroundWork());
|
||||
ASSERT_EQ(listener->flushed_dbs_.size(), i);
|
||||
ASSERT_EQ(listener->flushed_column_family_names_.size(), i);
|
||||
}
|
||||
@@ -636,17 +635,21 @@ TEST_F(EventListenerTest, MultiDBMultiListeners) {
|
||||
}
|
||||
}
|
||||
|
||||
FlushOptions flush_opts;
|
||||
flush_opts.listener_wait = true;
|
||||
for (size_t c = 0; c < cf_names.size(); ++c) {
|
||||
for (int d = 0; d < kNumDBs; ++d) {
|
||||
// listener_wait ensures the OnFlushCompleted callbacks have finished by
|
||||
// the time Flush() returns, so the listener state checked below is up to
|
||||
// date without separately waiting for background work.
|
||||
ASSERT_OK(dbs[d]->Flush(flush_opts, vec_handles[d][c]));
|
||||
ASSERT_OK(dbs[d]->Flush(FlushOptions(), vec_handles[d][c]));
|
||||
ASSERT_OK(static_cast_with_check<DBImpl>(dbs[d].get())
|
||||
->TEST_WaitForFlushMemTable());
|
||||
}
|
||||
}
|
||||
|
||||
for (int d = 0; d < kNumDBs; ++d) {
|
||||
// Ensure background work is fully finished including listener callbacks
|
||||
// before accessing listener state.
|
||||
ASSERT_OK(static_cast_with_check<DBImpl>(dbs[d].get())
|
||||
->TEST_WaitForBackgroundWork());
|
||||
}
|
||||
|
||||
for (auto* listener : listeners) {
|
||||
int pos = 0;
|
||||
for (size_t c = 0; c < cf_names.size(); ++c) {
|
||||
|
||||
+45
-34
@@ -52,6 +52,25 @@ namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace {
|
||||
|
||||
Status GetDefaultColumnBlobIndexSlice(Slice entity, Slice* blob_index_slice) {
|
||||
assert(blob_index_slice != nullptr);
|
||||
|
||||
std::vector<WideColumn> columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
|
||||
Status status =
|
||||
WideColumnSerialization::DeserializeV2(entity, columns, blob_columns);
|
||||
if (status.ok()) {
|
||||
if (columns.empty() || columns.front().name() != kDefaultWideColumnName ||
|
||||
blob_columns.empty() || blob_columns.front().first != 0) {
|
||||
status = Status::Corruption(
|
||||
"Wide column default column blob reference missing");
|
||||
} else {
|
||||
*blob_index_slice = columns.front().value();
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
Status PushWideColumnEntityDefaultOperand(const Slice& user_key,
|
||||
const Slice& entity,
|
||||
MergeContext* merge_context,
|
||||
@@ -59,24 +78,29 @@ Status PushWideColumnEntityDefaultOperand(const Slice& user_key,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
assert(merge_context != nullptr);
|
||||
|
||||
Slice entity_ref = entity;
|
||||
Slice value_of_default;
|
||||
bool is_blob_reference = false;
|
||||
Status status = WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
entity, value_of_default, is_blob_reference);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (!is_blob_reference) {
|
||||
entity_ref, value_of_default);
|
||||
if (status.ok()) {
|
||||
merge_context->PushOperand(value_of_default, operand_pinned);
|
||||
return status;
|
||||
}
|
||||
if (!status.IsNotSupported()) {
|
||||
return status;
|
||||
}
|
||||
if (blob_fetcher == nullptr) {
|
||||
return Status::Corruption(
|
||||
"Cannot resolve blob-backed default column without a blob fetcher");
|
||||
}
|
||||
|
||||
PinnableSlice resolved_default;
|
||||
status = WideColumnSerialization::ResolveDefaultColumnBlobReference(
|
||||
value_of_default, user_key, blob_fetcher, resolved_default);
|
||||
bool resolved = false;
|
||||
status = WideColumnSerialization::GetValueOfDefaultColumnResolvingBlobs(
|
||||
entity, user_key, blob_fetcher, resolved_default, resolved);
|
||||
if (status.ok()) {
|
||||
// Resolved value is backed by this stack-local PinnableSlice, so copy it
|
||||
// into MergeContext instead of pinning its storage.
|
||||
// Resolved blob values are backed by this stack-local PinnableSlice, so
|
||||
// copy them into MergeContext instead of pinning their storage.
|
||||
merge_context->PushOperand(Slice(resolved_default), false);
|
||||
}
|
||||
return status;
|
||||
@@ -1486,34 +1510,21 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
}
|
||||
} else if (s->value) {
|
||||
Slice value_of_default;
|
||||
bool is_blob_reference = false;
|
||||
*(s->status) = WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
v, value_of_default, is_blob_reference);
|
||||
v, value_of_default);
|
||||
if (s->status->ok()) {
|
||||
if (!is_blob_reference) {
|
||||
s->value->assign(value_of_default.data(),
|
||||
value_of_default.size());
|
||||
} else if (s->is_blob_index != nullptr) {
|
||||
// Caller requested the raw blob index (StackableDB BlobDB); hand
|
||||
// back the serialized BlobIndex bytes without resolving.
|
||||
s->value->assign(value_of_default.data(),
|
||||
value_of_default.size());
|
||||
s->value->assign(value_of_default.data(), value_of_default.size());
|
||||
} else if (s->status->IsNotSupported() &&
|
||||
s->is_blob_index != nullptr) {
|
||||
Slice blob_index_slice;
|
||||
*(s->status) = GetDefaultColumnBlobIndexSlice(v, &blob_index_slice);
|
||||
if (s->status->ok()) {
|
||||
s->value->assign(blob_index_slice.data(),
|
||||
blob_index_slice.size());
|
||||
default_blob_index_returned = true;
|
||||
} else {
|
||||
// Base RocksDB read of a blob-backed default column without
|
||||
// is_blob_index requested: not supported here.
|
||||
*(s->status) = Status::NotSupported(
|
||||
"Encountered blob-backed default column without "
|
||||
"is_blob_index");
|
||||
}
|
||||
}
|
||||
} else if (s->columns) {
|
||||
// NOTE/TODO: copying all of the wide columns that are inlined in the
|
||||
// memtable. (Blob-indirect columns avoid the copy in
|
||||
// MaybeResolveMemtableBlobValue()). We could consider avoiding the
|
||||
// copy, perhaps conditional on size, by pinning the SuperVersion with
|
||||
// the result, but there's no precedent for that, even without wide
|
||||
// columns (plain memtable values are copied too).
|
||||
*(s->status) = s->columns->SetWideColumnValue(v);
|
||||
}
|
||||
|
||||
@@ -1847,7 +1858,7 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
range->AddValueSize(iter->value->size());
|
||||
} else {
|
||||
assert(iter->columns);
|
||||
range->AddValueSize(iter->columns->payload_size());
|
||||
range->AddValueSize(iter->columns->serialized_size());
|
||||
}
|
||||
|
||||
range->MarkKeyDone(iter);
|
||||
@@ -1905,7 +1916,7 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
range->AddValueSize(iter->value->size());
|
||||
} else {
|
||||
assert(iter->columns);
|
||||
range->AddValueSize(iter->columns->payload_size());
|
||||
range->AddValueSize(iter->columns->serialized_size());
|
||||
}
|
||||
|
||||
range->MarkKeyDone(iter);
|
||||
|
||||
+1
-29
@@ -287,12 +287,7 @@ class MemTableList {
|
||||
|
||||
// Should not delete MemTableList without making sure MemTableList::current()
|
||||
// is Unref()'d.
|
||||
~MemTableList() {
|
||||
// All flush-completion notifications must have drained before this column
|
||||
// family's memtable list is destroyed; otherwise a reserve leaked (see
|
||||
// num_pending_flush_notifications_).
|
||||
assert(num_pending_flush_notifications_ == 0);
|
||||
}
|
||||
~MemTableList() {}
|
||||
|
||||
MemTableListVersion* current() const { return current_; }
|
||||
|
||||
@@ -310,20 +305,6 @@ class MemTableList {
|
||||
// completely flushed and logged.
|
||||
int NumFlushed() const;
|
||||
|
||||
// Flush-completion listener notification accounting. All require the DB
|
||||
// mutex to be held (this class is not internally synchronized). See
|
||||
// num_pending_flush_notifications_ for details.
|
||||
void AddPendingFlushNotifications(int n) {
|
||||
num_pending_flush_notifications_ += n;
|
||||
}
|
||||
void SubPendingFlushNotifications(int n) {
|
||||
num_pending_flush_notifications_ -= n;
|
||||
assert(num_pending_flush_notifications_ >= 0);
|
||||
}
|
||||
int NumPendingFlushNotifications() const {
|
||||
return num_pending_flush_notifications_;
|
||||
}
|
||||
|
||||
// Returns true if there is at least one memtable on which flush has
|
||||
// not yet started.
|
||||
bool IsFlushPending() const;
|
||||
@@ -571,15 +552,6 @@ class MemTableList {
|
||||
// Last memtabe list version id, increase by 1 each time a new
|
||||
// MemtableListVersion is installed.
|
||||
uint64_t last_memtable_list_version_id_;
|
||||
|
||||
// Number of committed flush results for this column family whose
|
||||
// EventListener::OnFlushCompleted callbacks have not finished running yet.
|
||||
// Incremented (by the background flush thread) at flush-commit time, before
|
||||
// the DB mutex is released to invoke the callbacks, and decremented after the
|
||||
// callbacks return. Consulted by WaitForFlushMemTables() when the caller set
|
||||
// FlushOptions::listener_wait, so that Flush(wait=true) does not return
|
||||
// before the callbacks complete. Guarded by the DB mutex.
|
||||
int num_pending_flush_notifications_ = 0;
|
||||
};
|
||||
|
||||
// Installs memtable atomic flush results.
|
||||
|
||||
+3
-7
@@ -254,13 +254,9 @@ Status MergeHelper::TimedFullMergeImpl(
|
||||
statistics, clock, update_num_ops_stats, op_failure_scope,
|
||||
std::move(visitor));
|
||||
if (s.ok()) {
|
||||
// Check merge result fits in uint32_t (a BlockBuilder assumption). For an
|
||||
// entity result this is the V1-serialized size that will actually be
|
||||
// stored, so use SerializedSizeV1() rather than payload_size().
|
||||
size_t result_size = result_value
|
||||
? result_value->size()
|
||||
: WideColumnSerialization::SerializedSizeV1(
|
||||
result_entity->columns());
|
||||
// Check merge result fits in uint32_t (a BlockBuilder assumption)
|
||||
size_t result_size =
|
||||
result_value ? result_value->size() : result_entity->serialized_size();
|
||||
if (UNLIKELY(result_size > std::numeric_limits<uint32_t>::max())) {
|
||||
return Status::Corruption("Merge result exceeds 4GB");
|
||||
}
|
||||
|
||||
+2
-2
@@ -100,8 +100,8 @@ class MergeHelper {
|
||||
Slice entity_copy(entity);
|
||||
WideColumns existing_columns;
|
||||
|
||||
const Status s = WideColumnSerialization::DeserializeSimple(
|
||||
entity_copy, existing_columns);
|
||||
const Status s =
|
||||
WideColumnSerialization::Deserialize(entity_copy, existing_columns);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -3,17 +3,13 @@
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/db_test_util.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/table.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "util/cast_util.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
class OptionsFileTest : public testing::Test {
|
||||
@@ -24,91 +20,6 @@ class OptionsFileTest : public testing::Test {
|
||||
};
|
||||
|
||||
namespace {
|
||||
std::string Basename(const std::string& fname) {
|
||||
const size_t basename_pos = fname.find_last_of("/\\");
|
||||
return basename_pos == std::string::npos ? fname
|
||||
: fname.substr(basename_pos + 1);
|
||||
}
|
||||
|
||||
bool IsOptionsFile(const std::string& fname) {
|
||||
return Basename(fname).find(kOptionsFileNamePrefix) == 0;
|
||||
}
|
||||
|
||||
class FailOptionsDirFsyncFileSystem : public FileSystemWrapper {
|
||||
public:
|
||||
explicit FailOptionsDirFsyncFileSystem(std::shared_ptr<FileSystem> target)
|
||||
: FileSystemWrapper(std::move(target)) {}
|
||||
|
||||
static const char* kClassName() { return "FailOptionsDirFsyncFileSystem"; }
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
void FailOptionsDirFsyncAfterNextOptionsFile() {
|
||||
observe_options_file_.store(true, std::memory_order_release);
|
||||
fail_options_dir_fsync_.store(false, std::memory_order_release);
|
||||
options_dir_fsync_failures_.store(0, std::memory_order_release);
|
||||
}
|
||||
|
||||
int options_dir_fsync_failures() const {
|
||||
return options_dir_fsync_failures_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
IOStatus NewWritableFile(const std::string& fname,
|
||||
const FileOptions& file_opts,
|
||||
std::unique_ptr<FSWritableFile>* result,
|
||||
IODebugContext* dbg) override {
|
||||
IOStatus s =
|
||||
FileSystemWrapper::NewWritableFile(fname, file_opts, result, dbg);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (observe_options_file_.load(std::memory_order_acquire) &&
|
||||
IsOptionsFile(fname)) {
|
||||
fail_options_dir_fsync_.store(true, std::memory_order_release);
|
||||
observe_options_file_.store(false, std::memory_order_release);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
IOStatus NewDirectory(const std::string& name, const IOOptions& io_opts,
|
||||
std::unique_ptr<FSDirectory>* result,
|
||||
IODebugContext* dbg) override {
|
||||
IOStatus s = FileSystemWrapper::NewDirectory(name, io_opts, result, dbg);
|
||||
if (s.ok()) {
|
||||
*result = std::make_unique<FailOptionsDirFsyncDirectory>(
|
||||
std::move(*result), *this);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private:
|
||||
class FailOptionsDirFsyncDirectory : public FSDirectoryWrapper {
|
||||
public:
|
||||
FailOptionsDirFsyncDirectory(std::unique_ptr<FSDirectory>&& dir,
|
||||
FailOptionsDirFsyncFileSystem& fs)
|
||||
: FSDirectoryWrapper(std::move(dir)), fs_(fs) {}
|
||||
|
||||
IOStatus FsyncWithDirOptions(
|
||||
const IOOptions& options, IODebugContext* dbg,
|
||||
const DirFsyncOptions& dir_fsync_options) override {
|
||||
if (fs_.fail_options_dir_fsync_.load(std::memory_order_acquire) &&
|
||||
IsOptionsFile(dir_fsync_options.renamed_new_name)) {
|
||||
fs_.fail_options_dir_fsync_.store(false, std::memory_order_release);
|
||||
fs_.options_dir_fsync_failures_.fetch_add(1, std::memory_order_acq_rel);
|
||||
return IOStatus::IOError("Injected OPTIONS directory fsync failure");
|
||||
}
|
||||
return FSDirectoryWrapper::FsyncWithDirOptions(options, dbg,
|
||||
dir_fsync_options);
|
||||
}
|
||||
|
||||
private:
|
||||
FailOptionsDirFsyncFileSystem& fs_;
|
||||
};
|
||||
|
||||
std::atomic<bool> observe_options_file_{false};
|
||||
std::atomic<bool> fail_options_dir_fsync_{false};
|
||||
std::atomic<int> options_dir_fsync_failures_{0};
|
||||
};
|
||||
|
||||
void UpdateOptionsFiles(DB* db,
|
||||
std::unordered_set<std::string>* filename_history,
|
||||
int* options_files_count) {
|
||||
@@ -147,34 +58,6 @@ void VerifyOptionsFileName(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int CountOptionsFiles(Env* env, const std::string& dbname) {
|
||||
std::vector<std::string> filenames;
|
||||
EXPECT_OK(env->GetChildren(dbname, &filenames));
|
||||
|
||||
int options_files_count = 0;
|
||||
uint64_t number;
|
||||
FileType type;
|
||||
for (const auto& filename : filenames) {
|
||||
if (ParseFileName(filename, &number, &type) && type == kOptionsFile) {
|
||||
++options_files_count;
|
||||
}
|
||||
}
|
||||
return options_files_count;
|
||||
}
|
||||
|
||||
int CountOptionsFiles(DB* db) {
|
||||
return CountOptionsFiles(db->GetEnv(), db->GetName());
|
||||
}
|
||||
|
||||
void GenerateStaleOptionsFiles(DB* db, int options_file_count) {
|
||||
ASSERT_OK(db->DisableFileDeletions());
|
||||
for (int i = 0; i < options_file_count; ++i) {
|
||||
ASSERT_OK(db->SetOptions(
|
||||
{{"level0_file_num_compaction_trigger", std::to_string(8 + i)}}));
|
||||
}
|
||||
ASSERT_GT(CountOptionsFiles(db), 2);
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
TEST_F(OptionsFileTest, NumberOfOptionsFiles) {
|
||||
@@ -196,80 +79,6 @@ TEST_F(OptionsFileTest, NumberOfOptionsFiles) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(OptionsFileTest, ObsoleteOptionsFilesPurgedSynchronouslyOnOpen) {
|
||||
Options opt;
|
||||
opt.create_if_missing = true;
|
||||
ASSERT_OK(DestroyDB(dbname_, opt));
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(opt, dbname_, &db));
|
||||
GenerateStaleOptionsFiles(db.get(), 5);
|
||||
db.reset();
|
||||
|
||||
opt.create_if_missing = false;
|
||||
opt.avoid_unnecessary_blocking_io = false;
|
||||
ASSERT_OK(DB::Open(opt, dbname_, &db));
|
||||
ASSERT_LE(CountOptionsFiles(db.get()), 2);
|
||||
}
|
||||
|
||||
TEST_F(OptionsFileTest, ObsoleteOptionsFilesPurgedInBackgroundOnOpen) {
|
||||
Options opt;
|
||||
opt.create_if_missing = true;
|
||||
ASSERT_OK(DestroyDB(dbname_, opt));
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(opt, dbname_, &db));
|
||||
GenerateStaleOptionsFiles(db.get(), 5);
|
||||
db.reset();
|
||||
|
||||
opt.create_if_missing = false;
|
||||
opt.avoid_unnecessary_blocking_io = true;
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"OptionsFileTest::ObsoleteOptionsFilesPurgedInBackgroundOnOpen:"
|
||||
"ReleasePurge",
|
||||
"DBImpl::BGWorkPurge:start"}});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(DB::Open(opt, dbname_, &db));
|
||||
ASSERT_GT(CountOptionsFiles(db.get()), 2);
|
||||
|
||||
TEST_SYNC_POINT(
|
||||
"OptionsFileTest::ObsoleteOptionsFilesPurgedInBackgroundOnOpen:"
|
||||
"ReleasePurge");
|
||||
ASSERT_OK(static_cast_with_check<DBImpl>(db.get())->TEST_WaitForPurge());
|
||||
ASSERT_LE(CountOptionsFiles(db.get()), 2);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(OptionsFileTest, ObsoleteOptionsFilesPurgedSynchronouslyOnOpenFailure) {
|
||||
auto fail_fs =
|
||||
std::make_shared<FailOptionsDirFsyncFileSystem>(FileSystem::Default());
|
||||
std::unique_ptr<Env> env(NewCompositeEnv(fail_fs));
|
||||
|
||||
Options opt;
|
||||
opt.env = env.get();
|
||||
opt.create_if_missing = true;
|
||||
ASSERT_OK(DestroyDB(dbname_, opt));
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(opt, dbname_, &db));
|
||||
GenerateStaleOptionsFiles(db.get(), 5);
|
||||
db.reset();
|
||||
|
||||
opt.create_if_missing = false;
|
||||
opt.avoid_unnecessary_blocking_io = true;
|
||||
|
||||
fail_fs->FailOptionsDirFsyncAfterNextOptionsFile();
|
||||
ASSERT_NOK(DB::Open(opt, dbname_, &db));
|
||||
ASSERT_EQ(fail_fs->options_dir_fsync_failures(), 1);
|
||||
ASSERT_LE(CountOptionsFiles(opt.env, dbname_), 2);
|
||||
}
|
||||
|
||||
TEST_F(OptionsFileTest, OptionsFileName) {
|
||||
const uint64_t kOptionsFileNum = 12345;
|
||||
uint64_t number;
|
||||
|
||||
+1
-279
@@ -7,41 +7,23 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "env/composite_env_wrapper.h"
|
||||
#include "monitoring/histogram.h"
|
||||
#include "monitoring/instrumented_mutex.h"
|
||||
#include "monitoring/perf_context_imp.h"
|
||||
#include "monitoring/thread_status_util.h"
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "rocksdb/iostats_context.h"
|
||||
#include "rocksdb/memtablerep.h"
|
||||
#include "rocksdb/slice_transform.h"
|
||||
#include "rocksdb/system_clock.h"
|
||||
#include "test_util/mock_time_env.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "util/stop_watch.h"
|
||||
#include "util/string_util.h"
|
||||
#include "utilities/merge_operators.h"
|
||||
|
||||
#if defined(USE_COROUTINES)
|
||||
#include "folly/Executor.h"
|
||||
#include "folly/coro/BlockingWait.h"
|
||||
#include "folly/coro/Collect.h"
|
||||
#include "folly/coro/CurrentExecutor.h"
|
||||
#include "folly/coro/Task.h"
|
||||
#include "folly/executors/IOThreadPoolExecutor.h"
|
||||
#include "folly/io/async/EventBase.h"
|
||||
#include "folly/io/async/Request.h"
|
||||
#include "util/coro_stats_util.h"
|
||||
#endif
|
||||
|
||||
bool FLAGS_random_key = false;
|
||||
bool FLAGS_use_set_based_memetable = false;
|
||||
int FLAGS_total_keys = 100;
|
||||
@@ -82,267 +64,7 @@ std::unique_ptr<DB> OpenDb(bool read_only = false) {
|
||||
return db;
|
||||
}
|
||||
|
||||
class PerfContextTest : public testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
SetPerfLevel(PerfLevel::kEnableCount);
|
||||
get_perf_context()->ClearPerLevelPerfContext();
|
||||
get_perf_context()->Reset();
|
||||
get_iostats_context()->Reset();
|
||||
get_iostats_context()->disable_iostats = false;
|
||||
}
|
||||
};
|
||||
|
||||
#if defined(USE_COROUTINES)
|
||||
TEST_F(PerfContextTest, CoroutineStatsContextScopeCollectsStats) {
|
||||
SetPerfLevel(PerfLevel::kEnableTimeAndCPUTimeExceptForMutex);
|
||||
get_perf_context()->EnablePerLevelPerfContext();
|
||||
|
||||
folly::IOThreadPoolExecutor executor(1);
|
||||
folly::EventBase* event_base = executor.getEventBase();
|
||||
ASSERT_NE(nullptr, event_base);
|
||||
|
||||
auto clock = std::make_shared<MockSystemClock>(SystemClock::Default());
|
||||
clock->SetCurrentTime(1);
|
||||
CoroutineStatsConfig stats_config = CaptureCoroutineStatsConfig();
|
||||
folly::coro::blockingWait(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(event_base),
|
||||
[event_base, clock = clock.get(),
|
||||
stats_config =
|
||||
std::move(stats_config)]() mutable -> folly::coro::Task<void> {
|
||||
EXPECT_TRUE(event_base->isInEventBaseThread());
|
||||
|
||||
CoroutineStatsContextScope scope(std::move(stats_config),
|
||||
Env::Default());
|
||||
|
||||
EXPECT_EQ(PerfLevel::kEnableTimeAndCPUTimeExceptForMutex,
|
||||
GetPerfLevel());
|
||||
EXPECT_EQ(0, get_perf_context()->block_read_count);
|
||||
EXPECT_TRUE(get_perf_context()->per_level_perf_context_enabled);
|
||||
|
||||
get_perf_context()->block_read_count += 3;
|
||||
(*get_perf_context()->level_to_perf_context)[2].block_cache_hit_count +=
|
||||
5;
|
||||
EXPECT_EQ(0, get_iostats_context()->bytes_read);
|
||||
get_iostats_context()->bytes_read += 7;
|
||||
get_iostats_context()->cpu_read_nanos += 11;
|
||||
get_iostats_context()
|
||||
->file_io_stats_by_temperature.hot_file_read_count += 1;
|
||||
|
||||
{
|
||||
PERF_TIMER_GUARD_WITH_CLOCK(block_read_time, clock);
|
||||
clock->SleepForMicroseconds(7);
|
||||
}
|
||||
|
||||
co_await folly::coro::co_reschedule_on_current_executor;
|
||||
|
||||
EXPECT_TRUE(event_base->isInEventBaseThread());
|
||||
EXPECT_EQ(3, get_perf_context()->block_read_count);
|
||||
EXPECT_TRUE(get_perf_context()->per_level_perf_context_enabled);
|
||||
|
||||
get_perf_context()->block_read_count += 5;
|
||||
(*get_perf_context()->level_to_perf_context)[2].block_cache_hit_count +=
|
||||
2;
|
||||
EXPECT_EQ(7, get_iostats_context()->bytes_read);
|
||||
get_iostats_context()->bytes_read += 11;
|
||||
get_iostats_context()->cpu_read_nanos += 13;
|
||||
get_iostats_context()
|
||||
->file_io_stats_by_temperature.hot_file_read_count += 2;
|
||||
|
||||
scope.Finalize();
|
||||
auto verify_stats = [] {
|
||||
const PerfContext* captured_perf_context = get_perf_context();
|
||||
ASSERT_NE(nullptr, captured_perf_context);
|
||||
EXPECT_EQ(8, captured_perf_context->block_read_count);
|
||||
EXPECT_EQ(7000, captured_perf_context->block_read_time);
|
||||
ASSERT_NE(nullptr, captured_perf_context->level_to_perf_context);
|
||||
EXPECT_EQ(7, (*captured_perf_context->level_to_perf_context)[2]
|
||||
.block_cache_hit_count);
|
||||
|
||||
const IOStatsContext* iostats_context = get_iostats_context();
|
||||
ASSERT_NE(nullptr, iostats_context);
|
||||
EXPECT_EQ(18, iostats_context->bytes_read);
|
||||
EXPECT_EQ(24, iostats_context->cpu_read_nanos);
|
||||
EXPECT_EQ(3, iostats_context->file_io_stats_by_temperature
|
||||
.hot_file_read_count);
|
||||
};
|
||||
verify_stats();
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_F(PerfContextTest, CoroutineStatsContextsRemainIsolatedWhenInterleaved) {
|
||||
CoroutineStatsConfig first_stats_config = CaptureCoroutineStatsConfig();
|
||||
|
||||
SetPerfLevel(PerfLevel::kEnableTimeExceptForMutex);
|
||||
get_perf_context()->EnablePerLevelPerfContext();
|
||||
get_iostats_context()->disable_iostats = true;
|
||||
CoroutineStatsConfig second_stats_config = CaptureCoroutineStatsConfig();
|
||||
|
||||
folly::IOThreadPoolExecutor executor(1);
|
||||
folly::EventBase* event_base = executor.getEventBase();
|
||||
ASSERT_NE(nullptr, event_base);
|
||||
|
||||
folly::coro::blockingWait(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(event_base),
|
||||
[first_stats_config = std::move(first_stats_config),
|
||||
second_stats_config = std::move(
|
||||
second_stats_config)]() mutable -> folly::coro::Task<void> {
|
||||
get_perf_context()->Reset();
|
||||
get_perf_context()->ClearPerLevelPerfContext();
|
||||
get_perf_context()->block_read_count = 100;
|
||||
get_iostats_context()->Reset();
|
||||
get_iostats_context()->bytes_read = 200;
|
||||
|
||||
auto make_task = [](CoroutineStatsConfig stats_config, uint64_t first,
|
||||
uint64_t second) -> folly::coro::Task<uint64_t> {
|
||||
const PerfLevel expected_perf_level = stats_config.perf_level;
|
||||
const bool expected_per_level_perf_context_enabled =
|
||||
stats_config.per_level_perf_context_enabled;
|
||||
const bool expected_iostats_disabled = stats_config.iostats_disabled;
|
||||
CoroutineStatsContextScope scope(std::move(stats_config),
|
||||
Env::Default());
|
||||
|
||||
EXPECT_EQ(expected_perf_level, GetPerfLevel());
|
||||
EXPECT_EQ(expected_per_level_perf_context_enabled,
|
||||
get_perf_context()->per_level_perf_context_enabled);
|
||||
EXPECT_EQ(expected_iostats_disabled,
|
||||
get_iostats_context()->disable_iostats);
|
||||
EXPECT_EQ(0, get_perf_context()->block_read_count);
|
||||
EXPECT_EQ(0, get_iostats_context()->bytes_read);
|
||||
|
||||
get_perf_context()->block_read_count += first;
|
||||
get_iostats_context()->bytes_read += first;
|
||||
co_await folly::coro::co_reschedule_on_current_executor;
|
||||
|
||||
EXPECT_EQ(expected_perf_level, GetPerfLevel());
|
||||
EXPECT_EQ(expected_per_level_perf_context_enabled,
|
||||
get_perf_context()->per_level_perf_context_enabled);
|
||||
EXPECT_EQ(expected_iostats_disabled,
|
||||
get_iostats_context()->disable_iostats);
|
||||
EXPECT_EQ(first, get_perf_context()->block_read_count);
|
||||
EXPECT_EQ(first, get_iostats_context()->bytes_read);
|
||||
get_perf_context()->block_read_count += second;
|
||||
get_iostats_context()->bytes_read += second;
|
||||
|
||||
scope.Finalize();
|
||||
EXPECT_EQ(first + second, get_iostats_context()->bytes_read);
|
||||
co_return get_perf_context()->block_read_count;
|
||||
};
|
||||
|
||||
std::vector<folly::coro::Task<uint64_t>> tasks;
|
||||
tasks.push_back(make_task(std::move(first_stats_config), 1, 10));
|
||||
tasks.push_back(make_task(std::move(second_stats_config), 2, 20));
|
||||
std::vector<uint64_t> results =
|
||||
co_await folly::coro::collectAllRange(std::move(tasks));
|
||||
|
||||
EXPECT_EQ((std::vector<uint64_t>{11, 22}), results);
|
||||
|
||||
get_perf_context()->Reset();
|
||||
get_iostats_context()->Reset();
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_F(PerfContextTest, CoroutineStatsContextScopeCollectsGetCpuNanos) {
|
||||
SetPerfLevel(PerfLevel::kEnableTimeAndCPUTimeExceptForMutex);
|
||||
|
||||
auto clock = std::make_shared<MockSystemClock>(SystemClock::Default());
|
||||
CompositeEnvWrapper env(Env::Default(), clock);
|
||||
clock->SetCurrentCPUTimeNanos(0);
|
||||
{
|
||||
CoroutineStatsContextScope scope(CaptureCoroutineStatsConfig(), &env);
|
||||
|
||||
clock->MockSleepForCPUNanos(17);
|
||||
clock->MockSleepForCPUNanos(5);
|
||||
// Time while the request context is unset is not charged to the request.
|
||||
{
|
||||
folly::RequestContextScopeGuard context_scope;
|
||||
clock->MockSleepForCPUNanos(13);
|
||||
}
|
||||
clock->MockSleepForCPUNanos(7);
|
||||
scope.Finalize();
|
||||
EXPECT_EQ(29, get_perf_context()->get_cpu_nanos);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
TEST_F(PerfContextTest, AsyncCallbackStatsStartEmptyOnSyncFallback) {
|
||||
class NoReadExecutorFileSystem final : public FileSystemWrapper {
|
||||
public:
|
||||
explicit NoReadExecutorFileSystem(const std::shared_ptr<FileSystem>& target)
|
||||
: FileSystemWrapper(target) {}
|
||||
|
||||
const char* Name() const override { return "NoReadExecutorFileSystem"; }
|
||||
folly::IOExecutor* GetReadExecutor() override { return nullptr; }
|
||||
};
|
||||
|
||||
class StatsCallback final : public DB::AsyncCallback {
|
||||
public:
|
||||
bool EnableStats() const override { return true; }
|
||||
|
||||
void OnComplete(const PerfContext* callback_perf_context,
|
||||
const IOStatsContext* callback_iostats_context) override {
|
||||
ASSERT_NE(nullptr, callback_perf_context);
|
||||
ASSERT_NE(nullptr, callback_iostats_context);
|
||||
called = true;
|
||||
get_read_bytes = callback_perf_context->get_read_bytes;
|
||||
multiget_read_bytes = callback_perf_context->multiget_read_bytes;
|
||||
}
|
||||
|
||||
bool called = false;
|
||||
uint64_t get_read_bytes = 0;
|
||||
uint64_t multiget_read_bytes = 0;
|
||||
};
|
||||
|
||||
auto file_system =
|
||||
std::make_shared<NoReadExecutorFileSystem>(FileSystem::Default());
|
||||
std::unique_ptr<Env> env = NewCompositeEnv(file_system);
|
||||
Options options;
|
||||
options.create_if_missing = true;
|
||||
options.env = env.get();
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(options, kDbName, &db));
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key", "value"));
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key2", "value2"));
|
||||
|
||||
get_perf_context()->get_read_bytes = 13;
|
||||
|
||||
StatsCallback callback;
|
||||
Status status;
|
||||
std::string value;
|
||||
db->GetAsync(ReadOptions(), "key", &value, status, callback);
|
||||
|
||||
EXPECT_TRUE(callback.called);
|
||||
ASSERT_OK(status);
|
||||
EXPECT_EQ("value", value);
|
||||
EXPECT_EQ(value.size(), callback.get_read_bytes);
|
||||
|
||||
get_perf_context()->Reset();
|
||||
get_perf_context()->multiget_read_bytes = 29;
|
||||
|
||||
StatsCallback multiget_callback;
|
||||
ColumnFamilyHandle* column_families[] = {db->DefaultColumnFamily(),
|
||||
db->DefaultColumnFamily()};
|
||||
const std::string key_strings[] = {"key", "key2"};
|
||||
const Slice keys[] = {key_strings[0], key_strings[1]};
|
||||
PinnableSlice values[2];
|
||||
Status statuses[2];
|
||||
db->MultiGetAsync(ReadOptions(), 2, column_families, keys, values, statuses,
|
||||
/*sorted_input=*/false, multiget_callback);
|
||||
|
||||
EXPECT_TRUE(multiget_callback.called);
|
||||
EXPECT_OK(statuses[0]);
|
||||
EXPECT_OK(statuses[1]);
|
||||
EXPECT_EQ("value", values[0].ToString());
|
||||
EXPECT_EQ("value2", values[1].ToString());
|
||||
EXPECT_EQ(values[0].size() + values[1].size(),
|
||||
multiget_callback.multiget_read_bytes);
|
||||
|
||||
db.reset();
|
||||
ASSERT_OK(DestroyDB(kDbName, options));
|
||||
}
|
||||
class PerfContextTest : public testing::Test {};
|
||||
|
||||
TEST_F(PerfContextTest, SeekIntoDeletion) {
|
||||
ASSERT_OK(DestroyDB(kDbName, Options()));
|
||||
|
||||
@@ -593,6 +593,93 @@ bool TableCache::GetFromRowCache(const Slice& user_key, IterKey& row_cache_key,
|
||||
return found;
|
||||
}
|
||||
|
||||
Status TableCache::Get(const ReadOptions& options,
|
||||
const InternalKeyComparator& internal_comparator,
|
||||
const FileMetaData& file_meta, const Slice& k,
|
||||
GetContext* get_context,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
HistogramImpl* file_read_hist, bool skip_filters,
|
||||
int level, size_t max_file_size_for_l0_meta_pin) {
|
||||
auto& fd = file_meta.fd;
|
||||
std::string* row_cache_entry = nullptr;
|
||||
bool done = false;
|
||||
IterKey row_cache_key;
|
||||
std::string row_cache_entry_buffer;
|
||||
|
||||
// Check row cache if enabled.
|
||||
// Reuse row_cache_key sequence number when row cache hits.
|
||||
Status s;
|
||||
if (ioptions_.row_cache && !get_context->NeedToReadSequence()) {
|
||||
auto user_key = ExtractUserKey(k);
|
||||
uint64_t cache_entry_seq_no =
|
||||
CreateRowCacheKeyPrefix(options, fd, k, get_context, row_cache_key);
|
||||
done = GetFromRowCache(user_key, row_cache_key, row_cache_key.Size(),
|
||||
get_context, &s, cache_entry_seq_no);
|
||||
if (!done) {
|
||||
row_cache_entry = &row_cache_entry_buffer;
|
||||
}
|
||||
}
|
||||
TEST_SYNC_POINT_CALLBACK("TableCache::Get::BeforeFindTable",
|
||||
const_cast<FileDescriptor*>(&fd));
|
||||
TableReader* t = nullptr;
|
||||
TypedHandle* handle = nullptr;
|
||||
if (s.ok() && !done) {
|
||||
s = FindTable(options, file_options_, internal_comparator, file_meta,
|
||||
&handle, mutable_cf_options, &t,
|
||||
options.read_tier == kBlockCacheTier /* no_io */,
|
||||
file_read_hist, skip_filters, level,
|
||||
true /* prefetch_index_and_filter_in_cache */,
|
||||
max_file_size_for_l0_meta_pin, file_meta.temperature,
|
||||
should_pin_table_handles_);
|
||||
SequenceNumber* max_covering_tombstone_seq =
|
||||
get_context->max_covering_tombstone_seq();
|
||||
if (s.ok() && max_covering_tombstone_seq != nullptr &&
|
||||
!options.ignore_range_deletions) {
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
|
||||
t->NewRangeTombstoneIterator(options));
|
||||
if (range_del_iter != nullptr) {
|
||||
SequenceNumber seq =
|
||||
range_del_iter->MaxCoveringTombstoneSeqnum(ExtractUserKey(k));
|
||||
if (seq > *max_covering_tombstone_seq) {
|
||||
*max_covering_tombstone_seq = seq;
|
||||
if (get_context->NeedTimestamp()) {
|
||||
get_context->SetTimestampFromRangeTombstone(
|
||||
range_del_iter->timestamp());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
get_context->SetReplayLog(row_cache_entry); // nullptr if no cache.
|
||||
s = t->Get(options, k, get_context,
|
||||
mutable_cf_options.prefix_extractor.get(), skip_filters);
|
||||
get_context->SetReplayLog(nullptr);
|
||||
} else if (options.read_tier == kBlockCacheTier && s.IsIncomplete()) {
|
||||
// Couldn't find table in cache and couldn't open it because of no_io.
|
||||
get_context->MarkKeyMayExist();
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Put the replay log in row cache only if something was found.
|
||||
if (!done && s.ok() && row_cache_entry && !row_cache_entry->empty()) {
|
||||
RowCacheInterface row_cache{ioptions_.row_cache.get()};
|
||||
size_t charge = row_cache_entry->capacity() + sizeof(std::string);
|
||||
auto row_ptr = new std::string(std::move(*row_cache_entry));
|
||||
Status rcs = row_cache.Insert(row_cache_key.GetUserKey(), row_ptr, charge);
|
||||
if (!rcs.ok()) {
|
||||
// If row cache is full, it's OK to continue, but we keep ownership of
|
||||
// row_ptr.
|
||||
delete row_ptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (handle != nullptr) {
|
||||
cache_.Release(handle);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void TableCache::UpdateRangeTombstoneSeqnums(
|
||||
const ReadOptions& options, TableReader* t,
|
||||
MultiGetContext::Range& table_range) {
|
||||
|
||||
+7
-8
@@ -144,14 +144,13 @@ class TableCache {
|
||||
// recorded
|
||||
// @param skip_filters Disables loading/accessing the filter block
|
||||
// @param level The level this table is at, -1 for "not set / don't know"
|
||||
DECLARE_SYNC_AND_ASYNC(Status, Get, const ReadOptions& options,
|
||||
const InternalKeyComparator& internal_comparator,
|
||||
const FileMetaData& file_meta, const Slice& k,
|
||||
GetContext* get_context,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
HistogramImpl* file_read_hist = nullptr,
|
||||
bool skip_filters = false, int level = -1,
|
||||
size_t max_file_size_for_l0_meta_pin = 0);
|
||||
Status Get(const ReadOptions& options,
|
||||
const InternalKeyComparator& internal_comparator,
|
||||
const FileMetaData& file_meta, const Slice& k,
|
||||
GetContext* get_context,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
HistogramImpl* file_read_hist = nullptr, bool skip_filters = false,
|
||||
int level = -1, size_t max_file_size_for_l0_meta_pin = 0);
|
||||
|
||||
// Return the range delete tombstone iterator of the file specified by
|
||||
// `file_meta`.
|
||||
|
||||
@@ -8,93 +8,10 @@
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, TableCache::Get)
|
||||
(const ReadOptions& options, const InternalKeyComparator& internal_comparator,
|
||||
const FileMetaData& file_meta, const Slice& k, GetContext* get_context,
|
||||
const MutableCFOptions& mutable_cf_options, HistogramImpl* file_read_hist,
|
||||
bool skip_filters, int level, size_t max_file_size_for_l0_meta_pin) {
|
||||
auto& fd = file_meta.fd;
|
||||
std::string* row_cache_entry = nullptr;
|
||||
bool done = false;
|
||||
IterKey row_cache_key;
|
||||
std::string row_cache_entry_buffer;
|
||||
|
||||
// Check row cache if enabled.
|
||||
// Reuse row_cache_key sequence number when row cache hits.
|
||||
Status s;
|
||||
if (ioptions_.row_cache && !get_context->NeedToReadSequence()) {
|
||||
auto user_key = ExtractUserKey(k);
|
||||
uint64_t cache_entry_seq_no =
|
||||
CreateRowCacheKeyPrefix(options, fd, k, get_context, row_cache_key);
|
||||
done = GetFromRowCache(user_key, row_cache_key, row_cache_key.Size(),
|
||||
get_context, &s, cache_entry_seq_no);
|
||||
if (!done) {
|
||||
row_cache_entry = &row_cache_entry_buffer;
|
||||
}
|
||||
}
|
||||
TEST_SYNC_POINT_CALLBACK("TableCache::Get::BeforeFindTable",
|
||||
const_cast<FileDescriptor*>(&fd));
|
||||
TableReader* t = nullptr;
|
||||
TypedHandle* handle = nullptr;
|
||||
if (s.ok() && !done) {
|
||||
s = FindTable(options, file_options_, internal_comparator, file_meta,
|
||||
&handle, mutable_cf_options, &t,
|
||||
options.read_tier == kBlockCacheTier /* no_io */,
|
||||
file_read_hist, skip_filters, level,
|
||||
true /* prefetch_index_and_filter_in_cache */,
|
||||
max_file_size_for_l0_meta_pin, file_meta.temperature,
|
||||
should_pin_table_handles_);
|
||||
SequenceNumber* max_covering_tombstone_seq =
|
||||
get_context->max_covering_tombstone_seq();
|
||||
if (s.ok() && max_covering_tombstone_seq != nullptr &&
|
||||
!options.ignore_range_deletions) {
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
|
||||
t->NewRangeTombstoneIterator(options));
|
||||
if (range_del_iter != nullptr) {
|
||||
SequenceNumber seq =
|
||||
range_del_iter->MaxCoveringTombstoneSeqnum(ExtractUserKey(k));
|
||||
if (seq > *max_covering_tombstone_seq) {
|
||||
*max_covering_tombstone_seq = seq;
|
||||
if (get_context->NeedTimestamp()) {
|
||||
get_context->SetTimestampFromRangeTombstone(
|
||||
range_del_iter->timestamp());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
get_context->SetReplayLog(row_cache_entry); // nullptr if no cache.
|
||||
s = CO_AWAIT(t->Get, options, k, get_context,
|
||||
mutable_cf_options.prefix_extractor.get(), skip_filters);
|
||||
get_context->SetReplayLog(nullptr);
|
||||
} else if (options.read_tier == kBlockCacheTier && s.IsIncomplete()) {
|
||||
// Couldn't find table in cache and couldn't open it because of no_io.
|
||||
get_context->MarkKeyMayExist();
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Put the replay log in row cache only if something was found.
|
||||
if (!done && s.ok() && row_cache_entry && !row_cache_entry->empty()) {
|
||||
RowCacheInterface row_cache{ioptions_.row_cache.get()};
|
||||
size_t charge = row_cache_entry->capacity() + sizeof(std::string);
|
||||
auto row_ptr = new std::string(std::move(*row_cache_entry));
|
||||
Status rcs = row_cache.Insert(row_cache_key.GetUserKey(), row_ptr, charge);
|
||||
if (!rcs.ok()) {
|
||||
// If row cache is full, it's OK to continue, but we keep ownership of
|
||||
// row_ptr.
|
||||
delete row_ptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (handle != nullptr) {
|
||||
cache_.Release(handle);
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
#if defined(WITHOUT_COROUTINES)
|
||||
#endif
|
||||
|
||||
// Batched version of TableCache::MultiGet.
|
||||
DEFINE_SYNC_AND_ASYNC(Status, TableCache::MultiGet)
|
||||
@@ -168,8 +85,9 @@ DEFINE_SYNC_AND_ASYNC(Status, TableCache::MultiGet)
|
||||
UpdateRangeTombstoneSeqnums(options, t, table_range);
|
||||
}
|
||||
if (s.ok()) {
|
||||
CO_AWAIT(t->MultiGet, options, &table_range,
|
||||
mutable_cf_options.prefix_extractor.get(), skip_filters);
|
||||
CO_AWAIT(t->MultiGet)
|
||||
(options, &table_range, mutable_cf_options.prefix_extractor.get(),
|
||||
skip_filters);
|
||||
} else if (options.read_tier == kBlockCacheTier && s.IsIncomplete()) {
|
||||
for (auto iter = table_range.begin(); iter != table_range.end(); ++iter) {
|
||||
Status* status = iter->s;
|
||||
|
||||
+1
-5
@@ -811,8 +811,7 @@ class VersionEdit {
|
||||
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 = "",
|
||||
const std::string& file_open_metadata = "") {
|
||||
const std::string& max_timestamp = "") {
|
||||
assert(smallest_seqno <= largest_seqno);
|
||||
new_files_.emplace_back(
|
||||
level,
|
||||
@@ -823,9 +822,6 @@ class VersionEdit {
|
||||
epoch_number, file_checksum, file_checksum_func_name, unique_id,
|
||||
compensated_range_deletion_size, tail_size,
|
||||
user_defined_timestamps_persisted, min_timestamp, max_timestamp));
|
||||
if (!file_open_metadata.empty()) {
|
||||
new_files_.back().second.file_open_metadata = file_open_metadata;
|
||||
}
|
||||
files_to_quarantine_.push_back(file);
|
||||
if (!HasLastSequence() || largest_seqno > GetLastSequence()) {
|
||||
SetLastSequence(largest_seqno);
|
||||
|
||||
@@ -35,13 +35,13 @@ void VersionEditHandlerBase::Iterate(log::Reader& reader,
|
||||
VersionEdit edit;
|
||||
s = edit.DecodeFrom(record);
|
||||
if (s.ok()) {
|
||||
last_valid_record_end_ = reader.LastRecordEnd();
|
||||
s = read_buffer_.AddEdit(&edit);
|
||||
}
|
||||
if (s.ok()) {
|
||||
ColumnFamilyData* cfd = nullptr;
|
||||
if (edit.IsInAtomicGroup()) {
|
||||
if (read_buffer_.IsFull()) {
|
||||
last_valid_record_end_ = reader.LastRecordEnd();
|
||||
s = OnAtomicGroupReplayBegin();
|
||||
for (size_t i = 0; s.ok() && i < read_buffer_.replay_buffer().size();
|
||||
i++) {
|
||||
@@ -57,7 +57,6 @@ void VersionEditHandlerBase::Iterate(log::Reader& reader,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
last_valid_record_end_ = reader.LastRecordEnd();
|
||||
s = ApplyVersionEdit(edit, &cfd);
|
||||
if (s.ok()) {
|
||||
recovered_edits++;
|
||||
|
||||
@@ -54,10 +54,8 @@ class VersionEditHandlerBase {
|
||||
|
||||
const ReadOptions& read_options_;
|
||||
|
||||
// File offset at the end of the last fully applied logical unit.
|
||||
// For non-atomic edits, this advances after each edit is decoded.
|
||||
// For atomic groups, this only advances after the entire group is
|
||||
// decoded, so an incomplete atomic group at the tail is excluded.
|
||||
// File offset at the end of the last successfully completed and
|
||||
// decoded logical record.
|
||||
uint64_t last_valid_record_end_ = 0;
|
||||
|
||||
private:
|
||||
|
||||
+485
-28
@@ -45,6 +45,12 @@
|
||||
#include "db/version_edit_handler.h"
|
||||
#include "db/wide/wide_columns_helper.h"
|
||||
#include "file/file_util.h"
|
||||
#include "table/compaction_merging_iterator.h"
|
||||
|
||||
#if USE_COROUTINES
|
||||
#include "folly/coro/BlockingWait.h"
|
||||
#include "folly/coro/Collect.h"
|
||||
#endif
|
||||
#include "file/filename.h"
|
||||
#include "file/random_access_file_reader.h"
|
||||
#include "file/read_write_util.h"
|
||||
@@ -57,7 +63,6 @@
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/merge_operator.h"
|
||||
#include "rocksdb/write_buffer_manager.h"
|
||||
#include "table/compaction_merging_iterator.h"
|
||||
#include "table/format.h"
|
||||
#include "table/get_context.h"
|
||||
#include "table/internal_iterator.h"
|
||||
@@ -75,10 +80,18 @@
|
||||
#include "util/string_util.h"
|
||||
#include "util/user_comparator_wrapper.h"
|
||||
|
||||
#if USE_COROUTINES
|
||||
#include "folly/coro/BlockingWait.h"
|
||||
#include "folly/coro/Collect.h"
|
||||
#endif // USE_COROUTINES
|
||||
// Generate the regular and coroutine versions of some methods by
|
||||
// including version_set_sync_and_async.h twice
|
||||
// Macros in the header will expand differently based on whether
|
||||
// WITH_COROUTINES or WITHOUT_COROUTINES is defined
|
||||
// clang-format off
|
||||
#define WITHOUT_COROUTINES
|
||||
#include "db/version_set_sync_and_async.h"
|
||||
#undef WITHOUT_COROUTINES
|
||||
#define WITH_COROUTINES
|
||||
#include "db/version_set_sync_and_async.h"
|
||||
#undef WITH_COROUTINES
|
||||
// clang-format on
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -302,8 +315,6 @@ Status OverlapWithIterator(const Comparator* ucmp,
|
||||
return iter->status();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Class to help choose the next file to search for the particular key.
|
||||
// Searches and returns files level by level.
|
||||
// We can search level-by-level since entries never hop across
|
||||
@@ -516,6 +527,7 @@ class FilePicker {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
} // anonymous namespace
|
||||
|
||||
class FilePickerMultiGet {
|
||||
private:
|
||||
@@ -2892,7 +2904,7 @@ void Version::MultiGetBlob(
|
||||
} else {
|
||||
assert(key_context->columns);
|
||||
key_context->columns->SetPlainValue(std::move(blob.result));
|
||||
range.AddValueSize(key_context->columns->payload_size());
|
||||
range.AddValueSize(key_context->columns->serialized_size());
|
||||
}
|
||||
// MultiGetBlob() has already materialized the blob reference into the
|
||||
// user-visible value, so clear this flag before later MultiGet
|
||||
@@ -2912,6 +2924,470 @@ void Version::MultiGetBlob(
|
||||
}
|
||||
}
|
||||
|
||||
void Version::Get(const ReadOptions& read_options, const LookupKey& k,
|
||||
PinnableSlice* value, PinnableWideColumns* columns,
|
||||
std::string* timestamp, Status* status,
|
||||
MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
PinnedIteratorsManager* pinned_iters_mgr, bool* value_found,
|
||||
bool* key_exists, SequenceNumber* seq, ReadCallback* callback,
|
||||
bool* is_blob, bool do_merge) {
|
||||
Slice ikey = k.internal_key();
|
||||
Slice user_key = k.user_key();
|
||||
|
||||
assert(status->ok() || status->IsMergeInProgress());
|
||||
|
||||
if (key_exists != nullptr) {
|
||||
// will falsify below if not found
|
||||
*key_exists = true;
|
||||
}
|
||||
|
||||
uint64_t tracing_get_id = BlockCacheTraceHelper::kReservedGetId;
|
||||
if (vset_ && vset_->block_cache_tracer_ &&
|
||||
vset_->block_cache_tracer_->is_tracing_enabled()) {
|
||||
tracing_get_id = vset_->block_cache_tracer_->NextGetId();
|
||||
}
|
||||
|
||||
// Note: the old StackableDB-based BlobDB passes in
|
||||
// GetImplOptions::is_blob_index; for the integrated BlobDB implementation, we
|
||||
// need to provide it here.
|
||||
bool is_blob_index = false;
|
||||
bool* const is_blob_to_use = is_blob ? is_blob : &is_blob_index;
|
||||
BlobFetcher blob_fetcher(this, read_options);
|
||||
|
||||
assert(pinned_iters_mgr);
|
||||
GetContext get_context(
|
||||
user_comparator(), merge_operator_, info_log_, db_statistics_,
|
||||
status->ok() ? GetContext::kNotFound : GetContext::kMerge, user_key,
|
||||
do_merge ? value : nullptr, do_merge ? columns : nullptr,
|
||||
do_merge ? timestamp : nullptr, value_found, merge_context, do_merge,
|
||||
max_covering_tombstone_seq, clock_, seq,
|
||||
merge_operator_ ? pinned_iters_mgr : nullptr, callback, is_blob_to_use,
|
||||
tracing_get_id, &blob_fetcher);
|
||||
|
||||
// Pin blocks that we read to hold merge operands
|
||||
if (merge_operator_) {
|
||||
pinned_iters_mgr->StartPinning();
|
||||
}
|
||||
|
||||
FilePicker fp(user_key, ikey, &storage_info_.level_files_brief_,
|
||||
storage_info_.num_non_empty_levels_,
|
||||
&storage_info_.file_indexer_, user_comparator(),
|
||||
internal_comparator());
|
||||
FdWithKeyRange* f = fp.GetNextFile();
|
||||
|
||||
while (f != nullptr) {
|
||||
if (*max_covering_tombstone_seq > 0) {
|
||||
// The remaining files we look at will only contain covered keys, so we
|
||||
// stop here.
|
||||
break;
|
||||
}
|
||||
if (get_context.sample()) {
|
||||
sample_file_read_inc(f->file_metadata);
|
||||
}
|
||||
|
||||
bool timer_enabled =
|
||||
GetPerfLevel() >= PerfLevel::kEnableTimeExceptForMutex &&
|
||||
get_perf_context()->per_level_perf_context_enabled;
|
||||
StopWatchNano timer(clock_, timer_enabled /* auto_start */);
|
||||
*status = table_cache_->Get(
|
||||
read_options, *internal_comparator(), *f->file_metadata, ikey,
|
||||
&get_context, mutable_cf_options_,
|
||||
cfd_->internal_stats()->GetFileReadHist(fp.GetHitFileLevel()),
|
||||
IsFilterSkipped(static_cast<int>(fp.GetHitFileLevel()),
|
||||
fp.IsHitFileLastInLevel()),
|
||||
fp.GetHitFileLevel(), max_file_size_for_l0_meta_pin_);
|
||||
// TODO: examine the behavior for corrupted key
|
||||
if (timer_enabled) {
|
||||
PERF_COUNTER_BY_LEVEL_ADD(get_from_table_nanos, timer.ElapsedNanos(),
|
||||
fp.GetHitFileLevel());
|
||||
}
|
||||
if (!status->ok()) {
|
||||
if (db_statistics_ != nullptr) {
|
||||
get_context.ReportCounters();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// report the counters before returning
|
||||
if (get_context.State() != GetContext::kNotFound &&
|
||||
get_context.State() != GetContext::kMerge &&
|
||||
db_statistics_ != nullptr) {
|
||||
get_context.ReportCounters();
|
||||
}
|
||||
switch (get_context.State()) {
|
||||
case GetContext::kNotFound:
|
||||
// Keep searching in other files
|
||||
if (get_context.sample()) {
|
||||
sample_collapsible_entry_file_read_inc(f->file_metadata);
|
||||
}
|
||||
break;
|
||||
case GetContext::kMerge:
|
||||
// TODO: update per-level perfcontext user_key_return_count for kMerge
|
||||
if (get_context.sample()) {
|
||||
sample_collapsible_entry_file_read_inc(f->file_metadata);
|
||||
}
|
||||
break;
|
||||
case GetContext::kFound:
|
||||
if (fp.GetHitFileLevel() == 0) {
|
||||
RecordTick(db_statistics_, GET_HIT_L0);
|
||||
} else if (fp.GetHitFileLevel() == 1) {
|
||||
RecordTick(db_statistics_, GET_HIT_L1);
|
||||
} else if (fp.GetHitFileLevel() >= 2) {
|
||||
RecordTick(db_statistics_, GET_HIT_L2_AND_UP);
|
||||
}
|
||||
|
||||
PERF_COUNTER_BY_LEVEL_ADD(user_key_return_count, 1,
|
||||
fp.GetHitFileLevel());
|
||||
|
||||
if (is_blob_index && do_merge && (value || columns)) {
|
||||
Slice blob_index =
|
||||
value ? *value
|
||||
: WideColumnsHelper::GetDefaultColumn(columns->columns());
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("Version::Get::TamperWithBlobIndex",
|
||||
&blob_index);
|
||||
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
|
||||
PinnableSlice result;
|
||||
|
||||
constexpr uint64_t* bytes_read = nullptr;
|
||||
|
||||
*status = GetBlob(read_options, get_context.ukey_to_get_blob_value(),
|
||||
blob_index, prefetch_buffer, &result, bytes_read);
|
||||
if (!status->ok()) {
|
||||
if (status->IsIncomplete()) {
|
||||
get_context.MarkKeyMayExist();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
*value = std::move(result);
|
||||
} else {
|
||||
assert(columns);
|
||||
columns->SetPlainValue(std::move(result));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
case GetContext::kDeleted:
|
||||
// Use empty error message for speed
|
||||
*status = Status::NotFound();
|
||||
if (get_context.sample()) {
|
||||
sample_collapsible_entry_file_read_inc(f->file_metadata);
|
||||
}
|
||||
return;
|
||||
case GetContext::kCorrupt:
|
||||
*status = Status::Corruption("corrupted key for ", user_key);
|
||||
return;
|
||||
case GetContext::kUnexpectedBlobIndex:
|
||||
ROCKS_LOG_ERROR(info_log_, "Encounter unexpected blob index.");
|
||||
*status = Status::NotSupported(
|
||||
"Encounter unexpected blob index. Please open DB with "
|
||||
"ROCKSDB_NAMESPACE::blob_db::BlobDB instead.");
|
||||
return;
|
||||
case GetContext::kMergeOperatorFailed:
|
||||
*status = Status::Corruption(Status::SubCode::kMergeOperatorFailed);
|
||||
return;
|
||||
}
|
||||
f = fp.GetNextFile();
|
||||
}
|
||||
if (db_statistics_ != nullptr) {
|
||||
get_context.ReportCounters();
|
||||
}
|
||||
if (GetContext::kMerge == get_context.State()) {
|
||||
if (!do_merge) {
|
||||
*status = Status::OK();
|
||||
return;
|
||||
}
|
||||
if (!merge_operator_) {
|
||||
*status = Status::InvalidArgument(
|
||||
"merge_operator is not properly initialized.");
|
||||
return;
|
||||
}
|
||||
// merge_operands are in saver and we hit the beginning of the key history
|
||||
// do a final merge of nullptr and operands;
|
||||
if (value || columns) {
|
||||
// `op_failure_scope` (an output parameter) is not provided (set to
|
||||
// nullptr) since a failure must be propagated regardless of its value.
|
||||
*status = MergeHelper::TimedFullMerge(
|
||||
merge_operator_, user_key, MergeHelper::kNoBaseValue,
|
||||
merge_context->GetOperands(), info_log_, db_statistics_, clock_,
|
||||
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
|
||||
value ? value->GetSelf() : nullptr, columns);
|
||||
if (status->ok()) {
|
||||
if (LIKELY(value != nullptr)) {
|
||||
value->PinSelf();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (key_exists != nullptr) {
|
||||
*key_exists = false;
|
||||
}
|
||||
*status = Status::NotFound(); // Use an empty error message for speed
|
||||
}
|
||||
}
|
||||
|
||||
void Version::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
ReadCallback* callback) {
|
||||
PinnedIteratorsManager pinned_iters_mgr;
|
||||
|
||||
// Pin blocks that we read to hold merge operands
|
||||
if (merge_operator_) {
|
||||
pinned_iters_mgr.StartPinning();
|
||||
}
|
||||
uint64_t tracing_mget_id = BlockCacheTraceHelper::kReservedGetId;
|
||||
|
||||
if (vset_ && vset_->block_cache_tracer_ &&
|
||||
vset_->block_cache_tracer_->is_tracing_enabled()) {
|
||||
tracing_mget_id = vset_->block_cache_tracer_->NextGetId();
|
||||
}
|
||||
// Even though we know the batch size won't be > MAX_BATCH_SIZE,
|
||||
// use autovector in order to avoid unnecessary construction of GetContext
|
||||
// objects, which is expensive
|
||||
autovector<GetContext, 16> get_ctx;
|
||||
BlobFetcher blob_fetcher(this, read_options);
|
||||
for (auto iter = range->begin(); iter != range->end(); ++iter) {
|
||||
assert(iter->s->ok() || iter->s->IsMergeInProgress());
|
||||
get_ctx.emplace_back(
|
||||
user_comparator(), merge_operator_, info_log_, db_statistics_,
|
||||
iter->s->ok() ? GetContext::kNotFound : GetContext::kMerge,
|
||||
iter->ukey_with_ts, iter->value, iter->columns, iter->timestamp,
|
||||
nullptr, &(iter->merge_context), true,
|
||||
&iter->max_covering_tombstone_seq, clock_, nullptr,
|
||||
merge_operator_ ? &pinned_iters_mgr : nullptr, callback,
|
||||
&iter->is_blob_index, tracing_mget_id, &blob_fetcher);
|
||||
// MergeInProgress status, if set, has been transferred to the get_context
|
||||
// state, so we set status to ok here. From now on, the iter status will
|
||||
// be used for IO errors, and get_context state will be used for any
|
||||
// key level errors
|
||||
*(iter->s) = Status::OK();
|
||||
}
|
||||
int get_ctx_index = 0;
|
||||
for (auto iter = range->begin(); iter != range->end();
|
||||
++iter, get_ctx_index++) {
|
||||
iter->get_context = &(get_ctx[get_ctx_index]);
|
||||
}
|
||||
|
||||
Status s;
|
||||
// blob_file => [[blob_idx, it], ...]
|
||||
std::unordered_map<uint64_t, BlobReadContexts> blob_ctxs;
|
||||
MultiGetRange keys_with_blobs_range(*range, range->begin(), range->end());
|
||||
#if USE_COROUTINES
|
||||
if (read_options.async_io && read_options.optimize_multiget_for_io &&
|
||||
using_coroutines() && use_async_io_) {
|
||||
s = MultiGetAsync(read_options, range, &blob_ctxs);
|
||||
} else
|
||||
#endif // USE_COROUTINES
|
||||
{
|
||||
MultiGetRange file_picker_range(*range, range->begin(), range->end());
|
||||
FilePickerMultiGet fp(&file_picker_range, &storage_info_.level_files_brief_,
|
||||
storage_info_.num_non_empty_levels_,
|
||||
&storage_info_.file_indexer_, user_comparator(),
|
||||
internal_comparator());
|
||||
FdWithKeyRange* f = fp.GetNextFileInLevel();
|
||||
uint64_t num_index_read = 0;
|
||||
uint64_t num_filter_read = 0;
|
||||
uint64_t num_sst_read = 0;
|
||||
uint64_t num_level_read = 0;
|
||||
|
||||
int prev_level = -1;
|
||||
|
||||
while (!fp.IsSearchEnded()) {
|
||||
// This will be set to true later if we actually look up in a file in L0.
|
||||
// For per level stats purposes, an L0 file is treated as a level
|
||||
bool dump_stats_for_l0_file = false;
|
||||
|
||||
// Avoid using the coroutine version if we're looking in a L0 file, since
|
||||
// L0 files won't be parallelized anyway. The regular synchronous version
|
||||
// is faster.
|
||||
if (!read_options.async_io || !using_coroutines() || !use_async_io_ ||
|
||||
fp.GetHitFileLevel() == 0 || !fp.RemainingOverlapInLevel()) {
|
||||
if (f) {
|
||||
bool skip_filters =
|
||||
IsFilterSkipped(static_cast<int>(fp.GetHitFileLevel()),
|
||||
fp.IsHitFileLastInLevel());
|
||||
// Call MultiGetFromSST for looking up a single file
|
||||
s = MultiGetFromSST(read_options, fp.CurrentFileRange(),
|
||||
fp.GetHitFileLevel(), skip_filters,
|
||||
/*skip_range_deletions=*/false, f, blob_ctxs,
|
||||
/*table_handle=*/nullptr, num_filter_read,
|
||||
num_index_read, num_sst_read);
|
||||
if (fp.GetHitFileLevel() == 0) {
|
||||
dump_stats_for_l0_file = true;
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
f = fp.GetNextFileInLevel();
|
||||
}
|
||||
#if USE_COROUTINES
|
||||
} else {
|
||||
std::vector<folly::coro::Task<Status>> mget_tasks;
|
||||
while (f != nullptr) {
|
||||
MultiGetRange file_range = fp.CurrentFileRange();
|
||||
TableCache::TypedHandle* table_handle = nullptr;
|
||||
bool skip_filters =
|
||||
IsFilterSkipped(static_cast<int>(fp.GetHitFileLevel()),
|
||||
fp.IsHitFileLastInLevel());
|
||||
bool skip_range_deletions = false;
|
||||
if (!skip_filters) {
|
||||
Status status = table_cache_->MultiGetFilter(
|
||||
read_options, *internal_comparator(), *f->file_metadata,
|
||||
mutable_cf_options_,
|
||||
cfd_->internal_stats()->GetFileReadHist(fp.GetHitFileLevel()),
|
||||
fp.GetHitFileLevel(), &file_range, &table_handle);
|
||||
skip_range_deletions = true;
|
||||
if (status.ok()) {
|
||||
skip_filters = true;
|
||||
} else if (!status.IsNotSupported()) {
|
||||
s = status;
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!file_range.empty()) {
|
||||
mget_tasks.emplace_back(MultiGetFromSSTCoroutine(
|
||||
read_options, file_range, fp.GetHitFileLevel(), skip_filters,
|
||||
skip_range_deletions, f, blob_ctxs, table_handle,
|
||||
num_filter_read, num_index_read, num_sst_read));
|
||||
}
|
||||
if (fp.KeyMaySpanNextFile()) {
|
||||
break;
|
||||
}
|
||||
f = fp.GetNextFileInLevel();
|
||||
}
|
||||
if (mget_tasks.size() > 0) {
|
||||
RecordTick(db_statistics_, MULTIGET_COROUTINE_COUNT,
|
||||
mget_tasks.size());
|
||||
// Collect all results so far
|
||||
std::vector<Status> statuses =
|
||||
folly::coro::blockingWait(co_withExecutor(
|
||||
&range->context()->executor(),
|
||||
folly::coro::collectAllRange(std::move(mget_tasks))));
|
||||
if (s.ok()) {
|
||||
for (Status stat : statuses) {
|
||||
if (!stat.ok()) {
|
||||
s = std::move(stat);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s.ok() && fp.KeyMaySpanNextFile()) {
|
||||
f = fp.GetNextFileInLevel();
|
||||
}
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
}
|
||||
// If bad status or we found final result for all the keys
|
||||
if (!s.ok() || file_picker_range.empty()) {
|
||||
break;
|
||||
}
|
||||
if (!f) {
|
||||
// Reached the end of this level. Prepare the next level
|
||||
fp.PrepareNextLevelForSearch();
|
||||
if (!fp.IsSearchEnded()) {
|
||||
// Its possible there is no overlap on this level and f is nullptr
|
||||
f = fp.GetNextFileInLevel();
|
||||
}
|
||||
if (dump_stats_for_l0_file ||
|
||||
(prev_level != 0 && prev_level != (int)fp.GetHitFileLevel())) {
|
||||
// Dump the stats if the search has moved to the next level and
|
||||
// reset for next level.
|
||||
if (num_filter_read + num_index_read) {
|
||||
RecordInHistogram(db_statistics_,
|
||||
NUM_INDEX_AND_FILTER_BLOCKS_READ_PER_LEVEL,
|
||||
num_index_read + num_filter_read);
|
||||
}
|
||||
if (num_sst_read) {
|
||||
RecordInHistogram(db_statistics_, NUM_SST_READ_PER_LEVEL,
|
||||
num_sst_read);
|
||||
num_level_read++;
|
||||
}
|
||||
num_filter_read = 0;
|
||||
num_index_read = 0;
|
||||
num_sst_read = 0;
|
||||
}
|
||||
prev_level = fp.GetHitFileLevel();
|
||||
}
|
||||
}
|
||||
|
||||
// Dump stats for most recent level
|
||||
if (num_filter_read + num_index_read) {
|
||||
RecordInHistogram(db_statistics_,
|
||||
NUM_INDEX_AND_FILTER_BLOCKS_READ_PER_LEVEL,
|
||||
num_index_read + num_filter_read);
|
||||
}
|
||||
if (num_sst_read) {
|
||||
RecordInHistogram(db_statistics_, NUM_SST_READ_PER_LEVEL, num_sst_read);
|
||||
num_level_read++;
|
||||
}
|
||||
if (num_level_read) {
|
||||
RecordInHistogram(db_statistics_, NUM_LEVEL_READ_PER_MULTIGET,
|
||||
num_level_read);
|
||||
}
|
||||
}
|
||||
|
||||
if (!blob_ctxs.empty()) {
|
||||
MultiGetBlob(read_options, keys_with_blobs_range, blob_ctxs);
|
||||
}
|
||||
|
||||
// Process any left over keys
|
||||
for (auto iter = range->begin(); s.ok() && iter != range->end(); ++iter) {
|
||||
GetContext& get_context = *iter->get_context;
|
||||
Status* status = iter->s;
|
||||
Slice user_key = iter->lkey->user_key();
|
||||
|
||||
if (db_statistics_ != nullptr) {
|
||||
get_context.ReportCounters();
|
||||
}
|
||||
if (GetContext::kMerge == get_context.State()) {
|
||||
if (!merge_operator_) {
|
||||
*status = Status::InvalidArgument(
|
||||
"merge_operator is not properly initialized.");
|
||||
range->MarkKeyDone(iter);
|
||||
continue;
|
||||
}
|
||||
// merge_operands are in saver and we hit the beginning of the key history
|
||||
// do a final merge of nullptr and operands;
|
||||
// `op_failure_scope` (an output parameter) is not provided (set to
|
||||
// nullptr) since a failure must be propagated regardless of its value.
|
||||
*status = MergeHelper::TimedFullMerge(
|
||||
merge_operator_, user_key, MergeHelper::kNoBaseValue,
|
||||
iter->merge_context.GetOperands(), info_log_, db_statistics_, clock_,
|
||||
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
|
||||
iter->value ? iter->value->GetSelf() : nullptr, iter->columns);
|
||||
if (LIKELY(iter->value != nullptr)) {
|
||||
iter->value->PinSelf();
|
||||
range->AddValueSize(iter->value->size());
|
||||
} else {
|
||||
assert(iter->columns);
|
||||
range->AddValueSize(iter->columns->serialized_size());
|
||||
}
|
||||
|
||||
range->MarkKeyDone(iter);
|
||||
if (range->GetValueSize() > read_options.value_size_soft_limit) {
|
||||
s = Status::Aborted();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
range->MarkKeyDone(iter);
|
||||
*status = Status::NotFound(); // Use an empty error message for speed
|
||||
}
|
||||
}
|
||||
|
||||
for (auto iter = range->begin(); iter != range->end(); ++iter) {
|
||||
range->MarkKeyDone(iter);
|
||||
*(iter->s) = s;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_COROUTINES
|
||||
Status Version::ProcessBatch(
|
||||
const ReadOptions& read_options, FilePickerMultiGet* batch,
|
||||
@@ -7201,11 +7677,6 @@ Status VersionSet::WriteCurrentStateToManifest(
|
||||
|
||||
for (const auto& f : level_files) {
|
||||
assert(f);
|
||||
// Preserve file_open_metadata so live SSTs still benefit from
|
||||
// fast_sst_open after a MANIFEST rotation. Without this, the
|
||||
// rotated MANIFEST snapshot drops kFileOpenMetadata for unchanged
|
||||
// files and their fast-open advantage is lost until they are
|
||||
// rewritten.
|
||||
edit.AddFile(level, f->fd.GetNumber(), f->fd.GetPathId(),
|
||||
f->fd.GetFileSize(), f->smallest, f->largest,
|
||||
f->fd.smallest_seqno, f->fd.largest_seqno,
|
||||
@@ -7215,7 +7686,7 @@ Status VersionSet::WriteCurrentStateToManifest(
|
||||
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->file_open_metadata);
|
||||
f->max_timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8115,17 +8586,3 @@ std::vector<VersionEdit>& ReactiveVersionSet::replay_buffer() {
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
// Generate the regular and coroutine versions of the functions defined in
|
||||
// version_set_sync_and_async.h by including it twice. Included here, after the
|
||||
// namespace and all definitions in this file, so the generated bodies see the
|
||||
// file-local types and helpers they use. The macros expand differently based on
|
||||
// whether WITH_COROUTINES or WITHOUT_COROUTINES is defined.
|
||||
// clang-format off
|
||||
#define WITHOUT_COROUTINES
|
||||
#include "db/version_set_sync_and_async.h"
|
||||
#undef WITHOUT_COROUTINES
|
||||
#define WITH_COROUTINES
|
||||
#include "db/version_set_sync_and_async.h"
|
||||
#undef WITH_COROUTINES
|
||||
// clang-format on
|
||||
+14
-11
@@ -48,6 +48,10 @@
|
||||
#include "db/version_edit.h"
|
||||
#include "db/write_controller.h"
|
||||
#include "env/file_system_tracer.h"
|
||||
#if USE_COROUTINES
|
||||
#include "folly/coro/BlockingWait.h"
|
||||
#include "folly/coro/Collect.h"
|
||||
#endif
|
||||
#include "monitoring/instrumented_mutex.h"
|
||||
#include "options/db_options.h"
|
||||
#include "options/offpeak_time_info.h"
|
||||
@@ -962,18 +966,17 @@ class Version {
|
||||
// merge_context.operands_list and don't merge the operands
|
||||
// REQUIRES: lock is not held
|
||||
// REQUIRES: pinned_iters_mgr != nullptr
|
||||
DECLARE_SYNC_AND_ASYNC(
|
||||
void, Get, const ReadOptions&, const LookupKey& key, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, std::string* timestamp, Status* status,
|
||||
MergeContext* merge_context, SequenceNumber* max_covering_tombstone_seq,
|
||||
PinnedIteratorsManager* pinned_iters_mgr, bool* value_found = nullptr,
|
||||
bool* key_exists = nullptr, SequenceNumber* seq = nullptr,
|
||||
ReadCallback* callback = nullptr, bool* is_blob = nullptr,
|
||||
bool do_merge = true);
|
||||
void Get(const ReadOptions&, const LookupKey& key, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, std::string* timestamp, Status* status,
|
||||
MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
PinnedIteratorsManager* pinned_iters_mgr,
|
||||
bool* value_found = nullptr, bool* key_exists = nullptr,
|
||||
SequenceNumber* seq = nullptr, ReadCallback* callback = nullptr,
|
||||
bool* is_blob = nullptr, bool do_merge = true);
|
||||
|
||||
DECLARE_SYNC_AND_ASYNC(void, MultiGet, const ReadOptions&,
|
||||
MultiGetRange* range,
|
||||
ReadCallback* callback = nullptr);
|
||||
void MultiGet(const ReadOptions&, MultiGetRange* range,
|
||||
ReadCallback* callback = nullptr);
|
||||
|
||||
// Interprets blob_index_slice as a blob reference, and (assuming the
|
||||
// corresponding blob file is part of this Version) retrieves the blob and
|
||||
|
||||
@@ -11,216 +11,6 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(void, Version::Get)
|
||||
(const ReadOptions& read_options, const LookupKey& k, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, std::string* timestamp, Status* status,
|
||||
MergeContext* merge_context, SequenceNumber* max_covering_tombstone_seq,
|
||||
PinnedIteratorsManager* pinned_iters_mgr, bool* value_found, bool* key_exists,
|
||||
SequenceNumber* seq, ReadCallback* callback, bool* is_blob, bool do_merge) {
|
||||
Slice ikey = k.internal_key();
|
||||
Slice user_key = k.user_key();
|
||||
|
||||
assert(status->ok() || status->IsMergeInProgress());
|
||||
|
||||
if (key_exists != nullptr) {
|
||||
// will falsify below if not found
|
||||
*key_exists = true;
|
||||
}
|
||||
|
||||
uint64_t tracing_get_id = BlockCacheTraceHelper::kReservedGetId;
|
||||
if (vset_ && vset_->block_cache_tracer_ &&
|
||||
vset_->block_cache_tracer_->is_tracing_enabled()) {
|
||||
tracing_get_id = vset_->block_cache_tracer_->NextGetId();
|
||||
}
|
||||
|
||||
// Note: the old StackableDB-based BlobDB passes in
|
||||
// GetImplOptions::is_blob_index; for the integrated BlobDB implementation, we
|
||||
// need to provide it here.
|
||||
bool is_blob_index = false;
|
||||
bool* const is_blob_to_use = is_blob ? is_blob : &is_blob_index;
|
||||
VersionBlobFetcher blob_fetcher(this, read_options);
|
||||
|
||||
assert(pinned_iters_mgr);
|
||||
GetContext get_context(
|
||||
user_comparator(), merge_operator_, info_log_, db_statistics_,
|
||||
status->ok() ? GetContext::kNotFound : GetContext::kMerge, user_key,
|
||||
do_merge ? value : nullptr, do_merge ? columns : nullptr,
|
||||
do_merge ? timestamp : nullptr, value_found, merge_context, do_merge,
|
||||
max_covering_tombstone_seq, clock_, seq,
|
||||
merge_operator_ ? pinned_iters_mgr : nullptr, callback, is_blob_to_use,
|
||||
tracing_get_id, &blob_fetcher);
|
||||
|
||||
// Pin blocks that we read to hold merge operands
|
||||
if (merge_operator_) {
|
||||
pinned_iters_mgr->StartPinning();
|
||||
}
|
||||
|
||||
FilePicker fp(user_key, ikey, &storage_info_.level_files_brief_,
|
||||
storage_info_.num_non_empty_levels_,
|
||||
&storage_info_.file_indexer_, user_comparator(),
|
||||
internal_comparator());
|
||||
FdWithKeyRange* f = fp.GetNextFile();
|
||||
|
||||
while (f != nullptr) {
|
||||
if (*max_covering_tombstone_seq > 0) {
|
||||
// The remaining files we look at will only contain covered keys, so we
|
||||
// stop here.
|
||||
break;
|
||||
}
|
||||
if (get_context.sample()) {
|
||||
sample_file_read_inc(f->file_metadata);
|
||||
}
|
||||
|
||||
#if defined(WITHOUT_COROUTINES)
|
||||
bool timer_enabled =
|
||||
GetPerfLevel() >= PerfLevel::kEnableTimeExceptForMutex &&
|
||||
get_perf_context()->per_level_perf_context_enabled;
|
||||
StopWatchNano timer(clock_, timer_enabled /* auto_start */);
|
||||
#endif
|
||||
*status =
|
||||
CO_AWAIT(table_cache_->Get, read_options, *internal_comparator(),
|
||||
*f->file_metadata, ikey, &get_context, mutable_cf_options_,
|
||||
cfd_->internal_stats()->GetFileReadHist(fp.GetHitFileLevel()),
|
||||
IsFilterSkipped(static_cast<int>(fp.GetHitFileLevel()),
|
||||
fp.IsHitFileLastInLevel()),
|
||||
fp.GetHitFileLevel(), max_file_size_for_l0_meta_pin_);
|
||||
// TODO: examine the behavior for corrupted key
|
||||
#if defined(WITHOUT_COROUTINES)
|
||||
if (timer_enabled) {
|
||||
PERF_COUNTER_BY_LEVEL_ADD(get_from_table_nanos, timer.ElapsedNanos(),
|
||||
fp.GetHitFileLevel());
|
||||
}
|
||||
#endif
|
||||
if (!status->ok()) {
|
||||
if (db_statistics_ != nullptr) {
|
||||
get_context.ReportCounters();
|
||||
}
|
||||
CO_RETURN;
|
||||
}
|
||||
|
||||
// report the counters before returning
|
||||
if (get_context.State() != GetContext::kNotFound &&
|
||||
get_context.State() != GetContext::kMerge &&
|
||||
db_statistics_ != nullptr) {
|
||||
get_context.ReportCounters();
|
||||
}
|
||||
switch (get_context.State()) {
|
||||
case GetContext::kNotFound:
|
||||
// Keep searching in other files
|
||||
if (get_context.sample()) {
|
||||
sample_collapsible_entry_file_read_inc(f->file_metadata);
|
||||
}
|
||||
break;
|
||||
case GetContext::kMerge:
|
||||
// TODO: update per-level perfcontext user_key_return_count for kMerge
|
||||
if (get_context.sample()) {
|
||||
sample_collapsible_entry_file_read_inc(f->file_metadata);
|
||||
}
|
||||
break;
|
||||
case GetContext::kFound:
|
||||
if (fp.GetHitFileLevel() == 0) {
|
||||
RecordTick(db_statistics_, GET_HIT_L0);
|
||||
} else if (fp.GetHitFileLevel() == 1) {
|
||||
RecordTick(db_statistics_, GET_HIT_L1);
|
||||
} else if (fp.GetHitFileLevel() >= 2) {
|
||||
RecordTick(db_statistics_, GET_HIT_L2_AND_UP);
|
||||
}
|
||||
|
||||
PERF_COUNTER_BY_LEVEL_ADD(user_key_return_count, 1,
|
||||
fp.GetHitFileLevel());
|
||||
|
||||
if (is_blob_index && do_merge && (value || columns)) {
|
||||
Slice blob_index =
|
||||
value ? *value
|
||||
: WideColumnsHelper::GetDefaultColumn(columns->columns());
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("Version::Get::TamperWithBlobIndex",
|
||||
&blob_index);
|
||||
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
|
||||
PinnableSlice result;
|
||||
|
||||
constexpr uint64_t* bytes_read = nullptr;
|
||||
|
||||
*status = GetBlob(read_options, get_context.ukey_to_get_blob_value(),
|
||||
blob_index, prefetch_buffer, &result, bytes_read);
|
||||
if (!status->ok()) {
|
||||
if (status->IsIncomplete()) {
|
||||
get_context.MarkKeyMayExist();
|
||||
}
|
||||
CO_RETURN;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
*value = std::move(result);
|
||||
} else {
|
||||
assert(columns);
|
||||
columns->SetPlainValue(std::move(result));
|
||||
}
|
||||
}
|
||||
|
||||
CO_RETURN;
|
||||
case GetContext::kDeleted:
|
||||
// Use empty error message for speed
|
||||
*status = Status::NotFound();
|
||||
if (get_context.sample()) {
|
||||
sample_collapsible_entry_file_read_inc(f->file_metadata);
|
||||
}
|
||||
CO_RETURN;
|
||||
case GetContext::kCorrupt:
|
||||
*status = Status::Corruption("corrupted key for ", user_key);
|
||||
CO_RETURN;
|
||||
case GetContext::kUnexpectedBlobIndex:
|
||||
ROCKS_LOG_ERROR(info_log_, "Encounter unexpected blob index.");
|
||||
*status = Status::NotSupported(
|
||||
"Encounter unexpected blob index. Please open DB with "
|
||||
"ROCKSDB_NAMESPACE::blob_db::BlobDB instead.");
|
||||
CO_RETURN;
|
||||
case GetContext::kMergeOperatorFailed:
|
||||
*status = Status::Corruption(Status::SubCode::kMergeOperatorFailed);
|
||||
CO_RETURN;
|
||||
}
|
||||
f = fp.GetNextFile();
|
||||
}
|
||||
if (db_statistics_ != nullptr) {
|
||||
get_context.ReportCounters();
|
||||
}
|
||||
if (GetContext::kMerge == get_context.State()) {
|
||||
if (!do_merge) {
|
||||
*status = Status::OK();
|
||||
CO_RETURN;
|
||||
}
|
||||
if (!merge_operator_) {
|
||||
*status = Status::InvalidArgument(
|
||||
"merge_operator is not properly initialized.");
|
||||
CO_RETURN;
|
||||
}
|
||||
// merge_operands are in saver and we hit the beginning of the key history
|
||||
// do a final merge of nullptr and operands;
|
||||
if (value || columns) {
|
||||
// `op_failure_scope` (an output parameter) is not provided (set to
|
||||
// nullptr) since a failure must be propagated regardless of its value.
|
||||
*status = MergeHelper::TimedFullMerge(
|
||||
merge_operator_, user_key, MergeHelper::kNoBaseValue,
|
||||
merge_context->GetOperands(), info_log_, db_statistics_, clock_,
|
||||
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
|
||||
value ? value->GetSelf() : nullptr, columns);
|
||||
if (status->ok()) {
|
||||
if (LIKELY(value != nullptr)) {
|
||||
value->PinSelf();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (key_exists != nullptr) {
|
||||
*key_exists = false;
|
||||
}
|
||||
*status = Status::NotFound(); // Use an empty error message for speed
|
||||
}
|
||||
CO_RETURN;
|
||||
}
|
||||
|
||||
// Lookup a batch of keys in a single SST file
|
||||
DEFINE_SYNC_AND_ASYNC(Status, Version::MultiGetFromSST)
|
||||
(const ReadOptions& read_options, MultiGetRange file_range, int hit_file_level,
|
||||
@@ -228,24 +18,21 @@ DEFINE_SYNC_AND_ASYNC(Status, Version::MultiGetFromSST)
|
||||
std::unordered_map<uint64_t, BlobReadContexts>& blob_ctxs,
|
||||
TableCache::TypedHandle* table_handle, uint64_t& num_filter_read,
|
||||
uint64_t& num_index_read, uint64_t& num_sst_read) {
|
||||
Status s;
|
||||
#if defined(WITHOUT_COROUTINES)
|
||||
bool timer_enabled = GetPerfLevel() >= PerfLevel::kEnableTimeExceptForMutex &&
|
||||
get_perf_context()->per_level_perf_context_enabled;
|
||||
|
||||
Status s;
|
||||
StopWatchNano timer(clock_, timer_enabled /* auto_start */);
|
||||
#endif
|
||||
s = CO_AWAIT(table_cache_->MultiGet, read_options, *internal_comparator(),
|
||||
*f->file_metadata, &file_range, mutable_cf_options_,
|
||||
cfd_->internal_stats()->GetFileReadHist(hit_file_level),
|
||||
skip_filters, skip_range_deletions, hit_file_level,
|
||||
table_handle);
|
||||
s = CO_AWAIT(table_cache_->MultiGet)(
|
||||
read_options, *internal_comparator(), *f->file_metadata, &file_range,
|
||||
mutable_cf_options_,
|
||||
cfd_->internal_stats()->GetFileReadHist(hit_file_level), skip_filters,
|
||||
skip_range_deletions, hit_file_level, table_handle);
|
||||
// TODO: examine the behavior for corrupted key
|
||||
#if defined(WITHOUT_COROUTINES)
|
||||
if (timer_enabled) {
|
||||
PERF_COUNTER_BY_LEVEL_ADD(get_from_table_nanos, timer.ElapsedNanos(),
|
||||
hit_file_level);
|
||||
}
|
||||
#endif
|
||||
if (!s.ok()) {
|
||||
// TODO: Set status for individual keys appropriately
|
||||
for (auto iter = file_range.begin(); iter != file_range.end(); ++iter) {
|
||||
@@ -346,7 +133,7 @@ DEFINE_SYNC_AND_ASYNC(Status, Version::MultiGetFromSST)
|
||||
file_range.AddValueSize(iter->value->size());
|
||||
} else {
|
||||
assert(iter->columns);
|
||||
file_range.AddValueSize(iter->columns->payload_size());
|
||||
file_range.AddValueSize(iter->columns->serialized_size());
|
||||
}
|
||||
|
||||
if (file_range.GetValueSize() > read_options.value_size_soft_limit) {
|
||||
@@ -382,281 +169,5 @@ DEFINE_SYNC_AND_ASYNC(Status, Version::MultiGetFromSST)
|
||||
RecordInHistogram(db_statistics_, SST_BATCH_SIZE, batch_size);
|
||||
CO_RETURN s;
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(void, Version::MultiGet)
|
||||
(const ReadOptions& read_options, MultiGetRange* range,
|
||||
ReadCallback* callback) {
|
||||
PinnedIteratorsManager pinned_iters_mgr;
|
||||
|
||||
// Pin blocks that we read to hold merge operands
|
||||
if (merge_operator_) {
|
||||
pinned_iters_mgr.StartPinning();
|
||||
}
|
||||
uint64_t tracing_mget_id = BlockCacheTraceHelper::kReservedGetId;
|
||||
|
||||
if (vset_ && vset_->block_cache_tracer_ &&
|
||||
vset_->block_cache_tracer_->is_tracing_enabled()) {
|
||||
tracing_mget_id = vset_->block_cache_tracer_->NextGetId();
|
||||
}
|
||||
// Even though we know the batch size won't be > MAX_BATCH_SIZE,
|
||||
// use autovector in order to avoid unnecessary construction of GetContext
|
||||
// objects, which is expensive
|
||||
autovector<GetContext, 16> get_ctx;
|
||||
VersionBlobFetcher blob_fetcher(this, read_options);
|
||||
for (auto iter = range->begin(); iter != range->end(); ++iter) {
|
||||
assert(iter->s->ok() || iter->s->IsMergeInProgress());
|
||||
get_ctx.emplace_back(
|
||||
user_comparator(), merge_operator_, info_log_, db_statistics_,
|
||||
iter->s->ok() ? GetContext::kNotFound : GetContext::kMerge,
|
||||
iter->ukey_with_ts, iter->value, iter->columns, iter->timestamp,
|
||||
nullptr, &(iter->merge_context), true,
|
||||
&iter->max_covering_tombstone_seq, clock_, nullptr,
|
||||
merge_operator_ ? &pinned_iters_mgr : nullptr, callback,
|
||||
&iter->is_blob_index, tracing_mget_id, &blob_fetcher);
|
||||
// MergeInProgress status, if set, has been transferred to the get_context
|
||||
// state, so we set status to ok here. From now on, the iter status will
|
||||
// be used for IO errors, and get_context state will be used for any
|
||||
// key level errors
|
||||
*(iter->s) = Status::OK();
|
||||
}
|
||||
int get_ctx_index = 0;
|
||||
for (auto iter = range->begin(); iter != range->end();
|
||||
++iter, get_ctx_index++) {
|
||||
iter->get_context = &(get_ctx[get_ctx_index]);
|
||||
}
|
||||
|
||||
Status s;
|
||||
// blob_file => [[blob_idx, it], ...]
|
||||
std::unordered_map<uint64_t, BlobReadContexts> blob_ctxs;
|
||||
MultiGetRange keys_with_blobs_range(*range, range->begin(), range->end());
|
||||
#if defined(WITHOUT_COROUTINES) && defined(USE_COROUTINES)
|
||||
// optimize_multiget_for_io overlaps reads across levels via blockingWait; it
|
||||
// is only used by the synchronous variant. The coroutine variant overlaps
|
||||
// within a level via co_await below.
|
||||
if (read_options.async_io && read_options.optimize_multiget_for_io &&
|
||||
using_coroutines() && use_async_io_) {
|
||||
s = MultiGetAsync(read_options, range, &blob_ctxs);
|
||||
} else
|
||||
#endif // WITHOUT_COROUTINES && USE_COROUTINES
|
||||
{
|
||||
MultiGetRange file_picker_range(*range, range->begin(), range->end());
|
||||
FilePickerMultiGet fp(&file_picker_range, &storage_info_.level_files_brief_,
|
||||
storage_info_.num_non_empty_levels_,
|
||||
&storage_info_.file_indexer_, user_comparator(),
|
||||
internal_comparator());
|
||||
FdWithKeyRange* f = fp.GetNextFileInLevel();
|
||||
uint64_t num_index_read = 0;
|
||||
uint64_t num_filter_read = 0;
|
||||
uint64_t num_sst_read = 0;
|
||||
uint64_t num_level_read = 0;
|
||||
|
||||
int prev_level = -1;
|
||||
|
||||
while (!fp.IsSearchEnded()) {
|
||||
// This will be set to true later if we actually look up in a file in L0.
|
||||
// For per level stats purposes, an L0 file is treated as a level
|
||||
bool dump_stats_for_l0_file = false;
|
||||
|
||||
// Avoid using the coroutine version if we're looking in a L0 file, since
|
||||
// L0 files won't be parallelized anyway. The regular synchronous version
|
||||
// is faster.
|
||||
#ifdef WITH_COROUTINES
|
||||
if (fp.GetHitFileLevel() == 0 || !fp.RemainingOverlapInLevel()) {
|
||||
#else
|
||||
if (!read_options.async_io || !using_coroutines() || !use_async_io_ ||
|
||||
fp.GetHitFileLevel() == 0 || !fp.RemainingOverlapInLevel()) {
|
||||
#endif
|
||||
if (f) {
|
||||
bool skip_filters =
|
||||
IsFilterSkipped(static_cast<int>(fp.GetHitFileLevel()),
|
||||
fp.IsHitFileLastInLevel());
|
||||
s = CO_AWAIT(MultiGetFromSST, read_options, fp.CurrentFileRange(),
|
||||
fp.GetHitFileLevel(), skip_filters,
|
||||
/*skip_range_deletions=*/false, f, blob_ctxs,
|
||||
/*table_handle=*/nullptr, num_filter_read,
|
||||
num_index_read, num_sst_read);
|
||||
#ifdef WITH_COROUTINES
|
||||
// Count this per-file coroutine read. The overlap branch below
|
||||
// records the same ticker for its batch of parallel reads.
|
||||
RecordTick(db_statistics_, MULTIGET_COROUTINE_COUNT);
|
||||
#endif
|
||||
if (fp.GetHitFileLevel() == 0) {
|
||||
dump_stats_for_l0_file = true;
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
f = fp.GetNextFileInLevel();
|
||||
}
|
||||
#if USE_COROUTINES
|
||||
} else {
|
||||
std::vector<folly::coro::Task<Status>> mget_tasks;
|
||||
while (f != nullptr) {
|
||||
MultiGetRange file_range = fp.CurrentFileRange();
|
||||
TableCache::TypedHandle* table_handle = nullptr;
|
||||
bool skip_filters =
|
||||
IsFilterSkipped(static_cast<int>(fp.GetHitFileLevel()),
|
||||
fp.IsHitFileLastInLevel());
|
||||
bool skip_range_deletions = false;
|
||||
if (!skip_filters) {
|
||||
Status status = table_cache_->MultiGetFilter(
|
||||
read_options, *internal_comparator(), *f->file_metadata,
|
||||
mutable_cf_options_,
|
||||
cfd_->internal_stats()->GetFileReadHist(fp.GetHitFileLevel()),
|
||||
fp.GetHitFileLevel(), &file_range, &table_handle);
|
||||
skip_range_deletions = true;
|
||||
if (status.ok()) {
|
||||
skip_filters = true;
|
||||
} else if (!status.IsNotSupported()) {
|
||||
s = status;
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!file_range.empty()) {
|
||||
mget_tasks.emplace_back(MultiGetFromSSTCoroutine(
|
||||
read_options, file_range, fp.GetHitFileLevel(), skip_filters,
|
||||
skip_range_deletions, f, blob_ctxs, table_handle,
|
||||
num_filter_read, num_index_read, num_sst_read));
|
||||
}
|
||||
if (fp.KeyMaySpanNextFile()) {
|
||||
break;
|
||||
}
|
||||
f = fp.GetNextFileInLevel();
|
||||
}
|
||||
if (mget_tasks.size() > 0) {
|
||||
RecordTick(db_statistics_, MULTIGET_COROUTINE_COUNT,
|
||||
mget_tasks.size());
|
||||
// Collect all results so far.
|
||||
#ifdef WITH_COROUTINES
|
||||
std::vector<Status> statuses = co_await folly::coro::co_nothrow(
|
||||
folly::coro::collectAllRange(std::move(mget_tasks)));
|
||||
#else
|
||||
std::vector<Status> statuses =
|
||||
folly::coro::blockingWait(co_withExecutor(
|
||||
&range->context()->executor(),
|
||||
folly::coro::collectAllRange(std::move(mget_tasks))));
|
||||
#endif
|
||||
if (s.ok()) {
|
||||
for (Status stat : statuses) {
|
||||
if (!stat.ok()) {
|
||||
s = std::move(stat);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s.ok() && fp.KeyMaySpanNextFile()) {
|
||||
f = fp.GetNextFileInLevel();
|
||||
}
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
}
|
||||
// If bad status or we found final result for all the keys
|
||||
if (!s.ok() || file_picker_range.empty()) {
|
||||
break;
|
||||
}
|
||||
if (!f) {
|
||||
// Reached the end of this level. Prepare the next level
|
||||
fp.PrepareNextLevelForSearch();
|
||||
if (!fp.IsSearchEnded()) {
|
||||
// Its possible there is no overlap on this level and f is nullptr
|
||||
f = fp.GetNextFileInLevel();
|
||||
}
|
||||
if (dump_stats_for_l0_file ||
|
||||
(prev_level != 0 && prev_level != (int)fp.GetHitFileLevel())) {
|
||||
// Dump the stats if the search has moved to the next level and
|
||||
// reset for next level.
|
||||
if (num_filter_read + num_index_read) {
|
||||
RecordInHistogram(db_statistics_,
|
||||
NUM_INDEX_AND_FILTER_BLOCKS_READ_PER_LEVEL,
|
||||
num_index_read + num_filter_read);
|
||||
}
|
||||
if (num_sst_read) {
|
||||
RecordInHistogram(db_statistics_, NUM_SST_READ_PER_LEVEL,
|
||||
num_sst_read);
|
||||
num_level_read++;
|
||||
}
|
||||
num_filter_read = 0;
|
||||
num_index_read = 0;
|
||||
num_sst_read = 0;
|
||||
}
|
||||
prev_level = fp.GetHitFileLevel();
|
||||
}
|
||||
}
|
||||
|
||||
// Dump stats for most recent level
|
||||
if (num_filter_read + num_index_read) {
|
||||
RecordInHistogram(db_statistics_,
|
||||
NUM_INDEX_AND_FILTER_BLOCKS_READ_PER_LEVEL,
|
||||
num_index_read + num_filter_read);
|
||||
}
|
||||
if (num_sst_read) {
|
||||
RecordInHistogram(db_statistics_, NUM_SST_READ_PER_LEVEL, num_sst_read);
|
||||
num_level_read++;
|
||||
}
|
||||
if (num_level_read) {
|
||||
RecordInHistogram(db_statistics_, NUM_LEVEL_READ_PER_MULTIGET,
|
||||
num_level_read);
|
||||
}
|
||||
}
|
||||
|
||||
if (!blob_ctxs.empty()) {
|
||||
MultiGetBlob(read_options, keys_with_blobs_range, blob_ctxs);
|
||||
}
|
||||
|
||||
// Process any left over keys
|
||||
for (auto iter = range->begin(); s.ok() && iter != range->end(); ++iter) {
|
||||
GetContext& get_context = *iter->get_context;
|
||||
Status* status = iter->s;
|
||||
Slice user_key = iter->lkey->user_key();
|
||||
|
||||
if (db_statistics_ != nullptr) {
|
||||
get_context.ReportCounters();
|
||||
}
|
||||
if (GetContext::kMerge == get_context.State()) {
|
||||
if (!merge_operator_) {
|
||||
*status = Status::InvalidArgument(
|
||||
"merge_operator is not properly initialized.");
|
||||
range->MarkKeyDone(iter);
|
||||
continue;
|
||||
}
|
||||
// merge_operands are in saver and we hit the beginning of the key history
|
||||
// do a final merge of nullptr and operands;
|
||||
// `op_failure_scope` (an output parameter) is not provided (set to
|
||||
// nullptr) since a failure must be propagated regardless of its value.
|
||||
*status = MergeHelper::TimedFullMerge(
|
||||
merge_operator_, user_key, MergeHelper::kNoBaseValue,
|
||||
iter->merge_context.GetOperands(), info_log_, db_statistics_, clock_,
|
||||
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
|
||||
iter->value ? iter->value->GetSelf() : nullptr, iter->columns);
|
||||
if (LIKELY(iter->value != nullptr)) {
|
||||
iter->value->PinSelf();
|
||||
range->AddValueSize(iter->value->size());
|
||||
} else {
|
||||
assert(iter->columns);
|
||||
range->AddValueSize(iter->columns->payload_size());
|
||||
}
|
||||
|
||||
range->MarkKeyDone(iter);
|
||||
if (range->GetValueSize() > read_options.value_size_soft_limit) {
|
||||
s = Status::Aborted();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
range->MarkKeyDone(iter);
|
||||
*status = Status::NotFound(); // Use an empty error message for speed
|
||||
}
|
||||
}
|
||||
|
||||
for (auto iter = range->begin(); iter != range->end(); ++iter) {
|
||||
range->MarkKeyDone(iter);
|
||||
*(iter->s) = s;
|
||||
}
|
||||
|
||||
CO_RETURN;
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
#endif
|
||||
@@ -1703,231 +1703,6 @@ TEST_F(DBWideBlobDirectWriteTest,
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBWideBlobDirectWriteTest,
|
||||
DirectWriteGetEntityReturnsZeroCopyBlobBuffersAcrossMove) {
|
||||
// Protects the zero-copy blob resolution optimization for direct-write wide
|
||||
// entities read pre-flush: each resolved blob-backed column Slice must point
|
||||
// directly at the buffer fetched during resolution (no copy), and must stay
|
||||
// valid and identical after the PinnableWideColumns result is moved (the
|
||||
// backing store uses address-stable forward_list nodes). A sync point
|
||||
// captures the fetched buffer pointers so we can compare them against
|
||||
// columns() before and after moving, for both GetEntity and MultiGetEntity.
|
||||
Options options = GetDirectWriteOptions();
|
||||
options.min_blob_size = 64;
|
||||
|
||||
Reopen(options);
|
||||
|
||||
const std::string key = "zero_copy_entity";
|
||||
const std::string inline_value = "inline"; // < min_blob_size: stays inline
|
||||
const std::string large1(128, 'a'); // >= min_blob_size: blob column
|
||||
const std::string large2(160, 'b'); // >= min_blob_size: blob column
|
||||
const WideColumns columns{{kDefaultWideColumnName, inline_value},
|
||||
{"big1", large1},
|
||||
{"big2", large2}};
|
||||
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
// Intentionally do not flush: the entity (and its direct-write blob
|
||||
// references) stays in the memtable so reads resolve through
|
||||
// DBImpl::ResolveDirectWriteWideColumns().
|
||||
|
||||
std::vector<const char*> fetched_ptrs;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::ResolveDirectWriteWideColumns:BlobFetched", [&](void* arg) {
|
||||
fetched_ptrs.push_back(static_cast<PinnableSlice*>(arg)->data());
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// GetEntity: two blob columns are fetched, each resolved column Slice points
|
||||
// straight at its fetched buffer, and the pointers survive move-construction
|
||||
// and move-assignment.
|
||||
{
|
||||
fetched_ptrs.clear();
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), key,
|
||||
&result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
ASSERT_EQ(fetched_ptrs.size(), 2U);
|
||||
|
||||
// columns() is sorted by name -> [default(""), big1, big2]; blob fetches
|
||||
// happen in that order, so fetched_ptrs[0]=big1, fetched_ptrs[1]=big2.
|
||||
const char* big1_ptr = result.columns()[1].value().data();
|
||||
const char* big2_ptr = result.columns()[2].value().data();
|
||||
ASSERT_EQ(big1_ptr, fetched_ptrs[0]);
|
||||
ASSERT_EQ(big2_ptr, fetched_ptrs[1]);
|
||||
|
||||
PinnableWideColumns moved(std::move(result));
|
||||
ASSERT_EQ(moved.columns(), columns);
|
||||
ASSERT_EQ(moved.columns()[1].value().data(), big1_ptr);
|
||||
ASSERT_EQ(moved.columns()[2].value().data(), big2_ptr);
|
||||
|
||||
PinnableWideColumns move_assigned;
|
||||
move_assigned = std::move(moved);
|
||||
ASSERT_EQ(move_assigned.columns(), columns);
|
||||
ASSERT_EQ(move_assigned.columns()[1].value().data(), big1_ptr);
|
||||
ASSERT_EQ(move_assigned.columns()[2].value().data(), big2_ptr);
|
||||
}
|
||||
|
||||
// MultiGetEntity: same zero-copy guarantee, including across a move.
|
||||
{
|
||||
fetched_ptrs.clear();
|
||||
const std::array<Slice, 1> keys{Slice(key)};
|
||||
std::array<PinnableWideColumns, 1> results;
|
||||
std::array<Status, 1> statuses;
|
||||
db_->MultiGetEntity(ReadOptions(), db_->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), results.data(), statuses.data());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(results[0].columns(), columns);
|
||||
ASSERT_EQ(fetched_ptrs.size(), 2U);
|
||||
|
||||
const char* big1_ptr = results[0].columns()[1].value().data();
|
||||
const char* big2_ptr = results[0].columns()[2].value().data();
|
||||
ASSERT_EQ(big1_ptr, fetched_ptrs[0]);
|
||||
ASSERT_EQ(big2_ptr, fetched_ptrs[1]);
|
||||
|
||||
PinnableWideColumns moved(std::move(results[0]));
|
||||
ASSERT_EQ(moved.columns(), columns);
|
||||
ASSERT_EQ(moved.columns()[1].value().data(), big1_ptr);
|
||||
ASSERT_EQ(moved.columns()[2].value().data(), big2_ptr);
|
||||
}
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(DBWideBlobDirectWriteTest,
|
||||
MultiGetEntitySoftLimitBoundsDirectWriteBlobResolution) {
|
||||
// value_size_soft_limit bounds direct-write blob resolution, which happens in
|
||||
// MultiGetImpl's post-processing loop (after the batch-loop check counted
|
||||
// only the small encoded blob-index bytes). With two over-limit entities, the
|
||||
// first one is read -- making progress even though it exceeds the limit --
|
||||
// and once the returned size is over the limit the second one is aborted
|
||||
// before its blob is fetched.
|
||||
Options options = GetDirectWriteOptions();
|
||||
|
||||
Reopen(options);
|
||||
|
||||
// Sorted order visits "a_large" before "b_large".
|
||||
const std::string first_key = "a_large";
|
||||
const std::string second_key = "b_large";
|
||||
const std::string first_blob(10000, 'a'); // >= min_blob_size: blob column
|
||||
const std::string second_blob(10000, 'b'); // >= min_blob_size: blob column
|
||||
const WideColumns first_columns{{kDefaultWideColumnName, "1"},
|
||||
{"c", first_blob}};
|
||||
const WideColumns second_columns{{kDefaultWideColumnName, "2"},
|
||||
{"c", second_blob}};
|
||||
|
||||
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
|
||||
first_key, first_columns));
|
||||
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
|
||||
second_key, second_columns));
|
||||
// Intentionally do not flush: the entities (and their direct-write blob
|
||||
// references) stay in the memtable so reads resolve through
|
||||
// DBImpl::ResolveDirectWriteWideColumns() in the post-processing loop.
|
||||
|
||||
int blob_fetches = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::ResolveDirectWriteWideColumns:BlobFetched",
|
||||
[&](void* /*arg*/) { ++blob_fetches; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Below either entity's resolved size (~10000 bytes), so reading the first
|
||||
// already pushes the returned total over the limit.
|
||||
ReadOptions read_options;
|
||||
read_options.value_size_soft_limit = 2000;
|
||||
|
||||
const std::array<Slice, 2> keys{Slice(first_key), Slice(second_key)};
|
||||
std::array<PinnableWideColumns, 2> results;
|
||||
std::array<Status, 2> statuses;
|
||||
db_->MultiGetEntity(read_options, db_->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), results.data(), statuses.data());
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(results[0].columns(), first_columns);
|
||||
ASSERT_TRUE(statuses[1].IsAborted());
|
||||
// Only the first key's blob was fetched; the second key's fetch was skipped
|
||||
// because the returned size was already over the limit.
|
||||
ASSERT_EQ(blob_fetches, 1);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(DBWideBlobDirectWriteTest,
|
||||
MultiGetEntitySoftLimitAlwaysReadsOversizedKey) {
|
||||
// "Always make progress": a single entity whose blob value alone exceeds the
|
||||
// soft limit is still read (not aborted), so a caller cannot loop forever
|
||||
// retrying a key that by itself is over the limit.
|
||||
Options options = GetDirectWriteOptions();
|
||||
|
||||
Reopen(options);
|
||||
|
||||
const std::string key = "oversized";
|
||||
const std::string large_blob(10000, 'b'); // >= min_blob_size: blob column
|
||||
const WideColumns columns{{kDefaultWideColumnName, "v"}, {"c", large_blob}};
|
||||
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
// Intentionally do not flush: resolution happens in the post-processing loop.
|
||||
|
||||
int blob_fetches = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::ResolveDirectWriteWideColumns:BlobFetched",
|
||||
[&](void* /*arg*/) { ++blob_fetches; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Far below the entity's resolved size (~10000 bytes).
|
||||
ReadOptions read_options;
|
||||
read_options.value_size_soft_limit = 2000;
|
||||
|
||||
const std::array<Slice, 1> keys{Slice(key)};
|
||||
std::array<PinnableWideColumns, 1> results;
|
||||
std::array<Status, 1> statuses;
|
||||
db_->MultiGetEntity(read_options, db_->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), results.data(), statuses.data());
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(results[0].columns(), columns);
|
||||
ASSERT_EQ(blob_fetches, 1);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(DBWideBlobDirectWriteTest,
|
||||
MultiGetSoftLimitBoundsDirectWritePlainValue) {
|
||||
// Same behavior for plain values stored as direct-write blob references (the
|
||||
// is_blob_index path): the first over-limit value is read (progress), and the
|
||||
// second is aborted once the returned size is over the limit.
|
||||
Options options = GetDirectWriteOptions();
|
||||
|
||||
Reopen(options);
|
||||
|
||||
// Sorted order visits "a_large" before "b_large".
|
||||
const std::string first_key = "a_large";
|
||||
const std::string second_key = "b_large";
|
||||
const std::string first_value(10000, 'a'); // >= min_blob_size: direct write
|
||||
const std::string second_value(10000, 'b'); // >= min_blob_size: direct write
|
||||
|
||||
ASSERT_OK(db_->Put(WriteOptions(), first_key, first_value));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), second_key, second_value));
|
||||
// Intentionally do not flush: the values stay in the memtable as direct-write
|
||||
// blob references resolved in the post-processing loop.
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.value_size_soft_limit = 2000;
|
||||
|
||||
const std::array<Slice, 2> keys{Slice(first_key), Slice(second_key)};
|
||||
std::array<PinnableSlice, 2> values;
|
||||
std::array<Status, 2> statuses;
|
||||
db_->MultiGet(read_options, db_->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0], first_value);
|
||||
ASSERT_TRUE(statuses[1].IsAborted());
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -12,12 +12,30 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
ReadPathBlobResolver::ReadPathBlobResolver(const Version* version,
|
||||
const ReadOptions& read_options,
|
||||
BlobFileCache* blob_file_cache,
|
||||
bool allow_write_path_fallback)
|
||||
: blob_fetcher_(version, ReadOptions(read_options), blob_file_cache,
|
||||
allow_write_path_fallback) {}
|
||||
namespace {
|
||||
|
||||
ReadOptions BuildReadPathBlobResolverReadOptions(ReadTier read_tier,
|
||||
bool verify_checksums,
|
||||
bool fill_cache,
|
||||
Env::IOActivity io_activity) {
|
||||
ReadOptions read_options;
|
||||
read_options.read_tier = read_tier;
|
||||
read_options.verify_checksums = verify_checksums;
|
||||
read_options.fill_cache = fill_cache;
|
||||
read_options.io_activity = io_activity;
|
||||
return read_options;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ReadPathBlobResolver::ReadPathBlobResolver(
|
||||
const Version* version, ReadTier read_tier, bool verify_checksums,
|
||||
bool fill_cache, Env::IOActivity io_activity,
|
||||
BlobFileCache* blob_file_cache, bool allow_write_path_fallback)
|
||||
: blob_fetcher_(version,
|
||||
BuildReadPathBlobResolverReadOptions(
|
||||
read_tier, verify_checksums, fill_cache, io_activity),
|
||||
blob_file_cache, allow_write_path_fallback) {}
|
||||
|
||||
void ReadPathBlobResolver::Reset(
|
||||
const Slice& user_key, const std::vector<WideColumn>* columns,
|
||||
|
||||
@@ -43,7 +43,9 @@ class Version;
|
||||
// resolver (ensured by SuperVersion pinning in the caller).
|
||||
class ReadPathBlobResolver {
|
||||
public:
|
||||
ReadPathBlobResolver(const Version* version, const ReadOptions& read_options,
|
||||
ReadPathBlobResolver(const Version* version, ReadTier read_tier,
|
||||
bool verify_checksums, bool fill_cache,
|
||||
Env::IOActivity io_activity,
|
||||
BlobFileCache* blob_file_cache = nullptr,
|
||||
bool allow_write_path_fallback = false);
|
||||
|
||||
@@ -91,10 +93,7 @@ class ReadPathBlobResolver {
|
||||
}
|
||||
|
||||
private:
|
||||
// Owns its ReadOptions: a resolver's lifetime is independent of the caller
|
||||
// that created it (e.g. it may outlive the originating ReadOptions once
|
||||
// returned as part of a lazy result).
|
||||
OwningVersionBlobFetcher blob_fetcher_;
|
||||
BlobFetcher blob_fetcher_;
|
||||
|
||||
Slice user_key_;
|
||||
const std::vector<WideColumn>* columns_ = nullptr;
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include "db/wide/wide_columns_helper.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "util/autovector.h"
|
||||
#include "util/cast_util.h"
|
||||
#include "util/coding.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -102,20 +101,6 @@ Status WideColumnSerialization::Serialize(const WideColumns& columns,
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
size_t WideColumnSerialization::SerializedSizeV1(const WideColumns& columns) {
|
||||
size_t size = VarintLength(kVersion1) +
|
||||
VarintLength(static_cast<uint32_t>(columns.size()));
|
||||
|
||||
for (const auto& column : columns) {
|
||||
const size_t name_size = column.name().size();
|
||||
const size_t value_size = column.value().size();
|
||||
size += VarintLength(name_size) + name_size + VarintLength(value_size) +
|
||||
value_size;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
template <typename GetName, typename GetValue>
|
||||
Status WideColumnSerialization::SerializeV2Impl(
|
||||
size_t num_columns,
|
||||
@@ -334,14 +319,6 @@ Status WideColumnSerialization::DeserializeV1(
|
||||
pos += value_size;
|
||||
}
|
||||
|
||||
// Reject any trailing bytes after the last value: a serialized entity must be
|
||||
// exactly the whole input (V2 already enforces this via its section-size
|
||||
// check; this keeps V1 consistent).
|
||||
if (pos != data.size()) {
|
||||
return Status::Corruption(
|
||||
"Unexpected trailing data after wide column entity");
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -436,9 +413,7 @@ Status WideColumnSerialization::DeserializeV2Impl(
|
||||
value_pos += vs;
|
||||
}
|
||||
|
||||
// Validate that the consumed bytes match the declared section sizes. Because
|
||||
// a V2 entity's values section is defined to extend to the end of `input`,
|
||||
// this also rejects any trailing bytes after the entity.
|
||||
// Validate that the consumed bytes match the declared section sizes
|
||||
const size_t total_values_bytes = static_cast<size_t>(input_end - s7);
|
||||
if (s4 != s4_limit || s5 != s5_limit || name_pos != names_bytes ||
|
||||
value_pos != total_values_bytes) {
|
||||
@@ -448,23 +423,17 @@ Status WideColumnSerialization::DeserializeV2Impl(
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status WideColumnSerialization::Deserialize(
|
||||
const Slice& entity, WideColumns& columns,
|
||||
std::vector<std::pair<size_t, BlobIndex>>* blob_columns) {
|
||||
Status WideColumnSerialization::Deserialize(Slice& input,
|
||||
WideColumns& columns) {
|
||||
assert(columns.empty());
|
||||
assert(!blob_columns || blob_columns->empty());
|
||||
|
||||
Slice input = entity;
|
||||
uint32_t version = 0;
|
||||
if (!GetVarint32(&input, &version)) {
|
||||
return Status::Corruption("Error decoding wide column version");
|
||||
}
|
||||
|
||||
// A version newer than kVersion2 is reported as Corruption, consistent with
|
||||
// how the rest of the codebase classifies an unrecognized serialized
|
||||
// version/type (BlobIndex, blob log header, table footer).
|
||||
if (version > kVersion2) {
|
||||
return Status::Corruption("Corrupt or unsupported wide column version");
|
||||
return Status::NotSupported("Unsupported wide column version");
|
||||
}
|
||||
|
||||
uint32_t num_columns = 0;
|
||||
@@ -472,44 +441,82 @@ Status WideColumnSerialization::Deserialize(
|
||||
return Status::Corruption("Error decoding number of wide columns");
|
||||
}
|
||||
|
||||
// A zero-column entity is still dispatched to the version-specific parser
|
||||
// below (rather than returning OK here) so that trailing bytes after an empty
|
||||
// entity are rejected as Corruption too -- both parsers handle num_columns==0
|
||||
// and validate that the input is exactly one serialized entity.
|
||||
if (!num_columns) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
if (version < kVersion2) {
|
||||
return DeserializeV1(input, num_columns, columns);
|
||||
}
|
||||
|
||||
// V2 layout: parse columns and extract any blob column info.
|
||||
// V2: need to check for blob columns
|
||||
std::vector<ValueType> column_types;
|
||||
if (Status s = DeserializeV2Impl(input, num_columns, columns, column_types);
|
||||
!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
assert(column_types.size() == num_columns);
|
||||
assert(columns.size() == num_columns);
|
||||
|
||||
for (uint32_t i = 0; i < num_columns; ++i) {
|
||||
if (column_types[i] != kTypeBlobIndex) {
|
||||
continue;
|
||||
if (column_types[i] == kTypeBlobIndex) {
|
||||
return Status::NotSupported(
|
||||
"Wide column contains blob references. Use DeserializeV2.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!blob_columns) {
|
||||
// The caller promised a fully resolved entity; a blob reference here is
|
||||
// unexpected and may indicate corruption (or a logic error)
|
||||
return Status::Corruption(
|
||||
"Wide column blob reference in a context where it is not expected.");
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
BlobIndex blob_idx;
|
||||
Slice blob_slice = columns[i].value();
|
||||
if (blob_slice.empty()) {
|
||||
return Status::Corruption("Empty blob index in wide column");
|
||||
Status WideColumnSerialization::DeserializeV2(
|
||||
Slice& input, std::vector<WideColumn>& columns,
|
||||
std::vector<std::pair<size_t, BlobIndex>>& blob_columns) {
|
||||
assert(columns.empty());
|
||||
assert(blob_columns.empty());
|
||||
|
||||
uint32_t version = 0;
|
||||
if (!GetVarint32(&input, &version)) {
|
||||
return Status::Corruption("Error decoding wide column version");
|
||||
}
|
||||
|
||||
if (version > kVersion2) {
|
||||
return Status::NotSupported("Unsupported wide column version");
|
||||
}
|
||||
|
||||
uint32_t num_columns = 0;
|
||||
if (!GetVarint32(&input, &num_columns)) {
|
||||
return Status::Corruption("Error decoding number of wide columns");
|
||||
}
|
||||
|
||||
if (!num_columns) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
if (version >= kVersion2) {
|
||||
// V2 layout: parse columns and extract blob column info
|
||||
std::vector<ValueType> column_types;
|
||||
|
||||
if (Status s = DeserializeV2Impl(input, num_columns, columns, column_types);
|
||||
!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (Status bs = blob_idx.DecodeFrom(blob_slice); !bs.ok()) {
|
||||
return Status::Corruption("Error decoding blob index in wide column");
|
||||
assert(column_types.size() == num_columns);
|
||||
assert(columns.size() == num_columns);
|
||||
|
||||
// Decode blob indices from value data
|
||||
for (uint32_t i = 0; i < num_columns; ++i) {
|
||||
if (column_types[i] == kTypeBlobIndex) {
|
||||
BlobIndex blob_idx;
|
||||
Slice blob_slice = columns[i].value();
|
||||
if (blob_slice.empty()) {
|
||||
return Status::Corruption("Empty blob index in wide column");
|
||||
}
|
||||
if (Status bs = blob_idx.DecodeFrom(blob_slice); !bs.ok()) {
|
||||
return Status::Corruption("Error decoding blob index in wide column");
|
||||
}
|
||||
blob_columns.emplace_back(i, blob_idx);
|
||||
}
|
||||
}
|
||||
blob_columns->emplace_back(i, blob_idx);
|
||||
} else {
|
||||
return DeserializeV1(input, num_columns, columns);
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
@@ -530,9 +537,6 @@ Status WideColumnSerialization::HasBlobColumns(const Slice& input,
|
||||
if (version < kVersion2) {
|
||||
return Status::OK();
|
||||
}
|
||||
if (version > kVersion2) {
|
||||
return Status::Corruption("Corrupt or unsupported wide column version");
|
||||
}
|
||||
|
||||
uint32_t num_columns = 0;
|
||||
if (!GetVarint32(&input_ref, &num_columns)) {
|
||||
@@ -578,9 +582,6 @@ Status WideColumnSerialization::ForEachBlobFileNumber(
|
||||
if (version < kVersion2) {
|
||||
return Status::OK();
|
||||
}
|
||||
if (version > kVersion2) {
|
||||
return Status::Corruption("Corrupt or unsupported wide column version");
|
||||
}
|
||||
|
||||
uint32_t num_columns = 0;
|
||||
if (!GetVarint32(&input_ref, &num_columns)) {
|
||||
@@ -606,18 +607,12 @@ Status WideColumnSerialization::ForEachBlobFileNumber(
|
||||
return Status::Corruption("Error decoding wide column types");
|
||||
}
|
||||
|
||||
// Validate every column type, and note whether any blob columns are present.
|
||||
// An unrecognized/unsupported type byte is Corruption (consistent with
|
||||
// Deserialize() and HasBlobColumns()) rather than silently treated as a
|
||||
// non-blob column.
|
||||
// Collect blob column indices
|
||||
bool has_any_blob = false;
|
||||
for (uint32_t i = 0; i < num_columns; ++i) {
|
||||
const auto type = lossless_cast<ValueType>(input_ref[i]);
|
||||
if (!IsValidColumnValueType(type)) {
|
||||
return Status::Corruption("Unsupported wide column ValueType");
|
||||
}
|
||||
if (type == kTypeBlobIndex) {
|
||||
if (static_cast<uint8_t>(input_ref[i]) == kTypeBlobIndex) {
|
||||
has_any_blob = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -697,10 +692,8 @@ Status WideColumnSerialization::GetVersion(const Slice& input,
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
const Slice& input, Slice& value, bool& is_blob_reference) {
|
||||
is_blob_reference = false;
|
||||
|
||||
Status WideColumnSerialization::GetValueOfDefaultColumn(Slice& input,
|
||||
Slice& value) {
|
||||
Slice input_ref = input;
|
||||
|
||||
uint32_t version = 0;
|
||||
@@ -708,9 +701,8 @@ Status WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
return Status::Corruption("Error decoding wide column version");
|
||||
}
|
||||
|
||||
// See Deserialize(): a too-new version is Corruption, not NotSupported.
|
||||
if (version > kVersion2) {
|
||||
return Status::Corruption("Corrupt or unsupported wide column version");
|
||||
return Status::NotSupported("Unsupported wide column version");
|
||||
}
|
||||
|
||||
uint32_t num_columns = 0;
|
||||
@@ -741,19 +733,15 @@ Status WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
return Status::Corruption("Error decoding wide column names bytes");
|
||||
}
|
||||
|
||||
// Read COLUMN TYPES (N bytes). We only need column 0's type here (it is the
|
||||
// default column's type only if column 0 is actually the default column,
|
||||
// checked via its name size below), but validate it is a recognized type so
|
||||
// a corrupt/unsupported type byte is reported as Corruption rather than
|
||||
// silently treated as an inline value.
|
||||
// Read COLUMN TYPES (N bytes)
|
||||
if (input_ref.size() < num_columns) {
|
||||
return Status::Corruption("Error decoding wide column types");
|
||||
}
|
||||
const auto column0_type = lossless_cast<ValueType>(input_ref[0]);
|
||||
if (!IsValidColumnValueType(column0_type)) {
|
||||
return Status::Corruption("Unsupported wide column ValueType");
|
||||
// Check if default column (index 0) is a blob reference
|
||||
if (static_cast<uint8_t>(input_ref[0]) == kTypeBlobIndex) {
|
||||
return Status::NotSupported(
|
||||
"Wide column contains blob references. Use DeserializeV2.");
|
||||
}
|
||||
const bool column0_is_blob = column0_type == kTypeBlobIndex;
|
||||
input_ref.remove_prefix(num_columns);
|
||||
|
||||
// Peek first name size from NAME SIZES section
|
||||
@@ -791,20 +779,18 @@ Status WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
}
|
||||
input_ref.remove_prefix(names_bytes);
|
||||
|
||||
// Read the first value from VALUES section. For a blob-referenced default
|
||||
// column these are the raw serialized BlobIndex bytes.
|
||||
// Read the first value from VALUES section
|
||||
if (input_ref.size() < first_value_size) {
|
||||
return Status::Corruption("Error decoding wide column value payload");
|
||||
}
|
||||
value = Slice(input_ref.data(), first_value_size);
|
||||
is_blob_reference = column0_is_blob;
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
// V1 fallback: full deserialization. V1 has no blob references.
|
||||
// V1 fallback: full deserialization
|
||||
WideColumns columns;
|
||||
|
||||
if (Status s = DeserializeSimple(input, columns); !s.ok()) {
|
||||
if (Status s = Deserialize(input, columns); !s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -823,31 +809,15 @@ Status WideColumnSerialization::ResolveEntityBlobColumns(
|
||||
const BlobFetcher* blob_fetcher, PrefetchBufferCollection* prefetch_buffers,
|
||||
std::string& resolved_entity, bool& resolved, uint64_t* total_bytes_read,
|
||||
uint64_t* num_blobs_resolved) {
|
||||
// Resolve into zero-copy columns backed by fetched blob buffers, then
|
||||
// serialize that result as a V1 (all-inline) entity.
|
||||
WideColumns resolved_columns;
|
||||
std::forward_list<PinnableSlice> extra_buffers;
|
||||
Status s = ResolveEntityBlobColumnsMultiBuffer(
|
||||
entity_value, user_key, blob_fetcher, prefetch_buffers, resolved_columns,
|
||||
extra_buffers, resolved, total_bytes_read, num_blobs_resolved);
|
||||
if (s.ok() && resolved) {
|
||||
s = Serialize(resolved_columns, resolved_entity);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
assert(blob_fetcher);
|
||||
|
||||
Status WideColumnSerialization::ResolveEntityBlobColumnsMultiBuffer(
|
||||
const Slice& entity_value, const Slice& user_key,
|
||||
const BlobFetcher* blob_fetcher, PrefetchBufferCollection* prefetch_buffers,
|
||||
WideColumns& resolved_columns,
|
||||
std::forward_list<PinnableSlice>& extra_buffers, bool& resolved,
|
||||
uint64_t* total_bytes_read, uint64_t* num_blobs_resolved) {
|
||||
resolved = false;
|
||||
|
||||
std::vector<WideColumn> columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
|
||||
|
||||
if (Status s = Deserialize(entity_value, columns, &blob_columns); !s.ok()) {
|
||||
Slice input_copy = entity_value;
|
||||
if (Status s = DeserializeV2(input_copy, columns, blob_columns); !s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -857,26 +827,19 @@ Status WideColumnSerialization::ResolveEntityBlobColumnsMultiBuffer(
|
||||
|
||||
resolved = true;
|
||||
|
||||
// Fetch each blob value
|
||||
std::vector<std::string> resolved_blob_values;
|
||||
resolved_blob_values.reserve(blob_columns.size());
|
||||
|
||||
for (const auto& blob_col : blob_columns) {
|
||||
const size_t column_idx = blob_col.first;
|
||||
const BlobIndex& blob_idx = blob_col.second;
|
||||
|
||||
if (blob_idx.IsInlined()) {
|
||||
// The inlined value bytes live in `entity_value`; point the column value
|
||||
// at them directly (zero copy). The caller keeps `entity_value` alive.
|
||||
columns[column_idx].value() = blob_idx.value();
|
||||
resolved_blob_values.emplace_back(blob_idx.value().data(),
|
||||
blob_idx.value().size());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (blob_fetcher == nullptr) {
|
||||
// A blob-backed entity reached a read context with no blob fetcher (a
|
||||
// reader without blob support, e.g. SstFileReader, or corrupt/legacy
|
||||
// data). This is the single place entity blob resolution reports that
|
||||
// condition; callers no longer pre-check the fetcher.
|
||||
return Status::Corruption(
|
||||
"Cannot resolve blob columns in entity without a blob fetcher");
|
||||
}
|
||||
|
||||
FilePrefetchBuffer* prefetch_buffer =
|
||||
prefetch_buffers ? prefetch_buffers->GetOrCreatePrefetchBuffer(
|
||||
blob_idx.file_number())
|
||||
@@ -884,17 +847,14 @@ Status WideColumnSerialization::ResolveEntityBlobColumnsMultiBuffer(
|
||||
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
// Fetch the blob value into a fresh, address-stable backing node.
|
||||
extra_buffers.emplace_front();
|
||||
PinnableSlice& blob_value = extra_buffers.front();
|
||||
PinnableSlice blob_value;
|
||||
const Status fetch_s = blob_fetcher->FetchBlob(
|
||||
user_key, blob_idx, prefetch_buffer, &blob_value, &bytes_read);
|
||||
if (!fetch_s.ok()) {
|
||||
return fetch_s;
|
||||
}
|
||||
|
||||
// Zero-copy Slice into the node we just added.
|
||||
columns[column_idx].value() = blob_value;
|
||||
resolved_blob_values.emplace_back(blob_value.data(), blob_value.size());
|
||||
|
||||
if (total_bytes_read) {
|
||||
*total_bytes_read += bytes_read;
|
||||
@@ -905,41 +865,53 @@ Status WideColumnSerialization::ResolveEntityBlobColumnsMultiBuffer(
|
||||
*num_blobs_resolved += blob_columns.size();
|
||||
}
|
||||
|
||||
resolved_columns = std::move(columns);
|
||||
|
||||
return Status::OK();
|
||||
return SerializeResolvedEntity(columns, blob_columns, resolved_blob_values,
|
||||
resolved_entity);
|
||||
}
|
||||
|
||||
Status WideColumnSerialization::ResolveDefaultColumnBlobReference(
|
||||
const Slice& blob_index, const Slice& user_key,
|
||||
const BlobFetcher* blob_fetcher, PinnableSlice& value) {
|
||||
if (blob_index.empty()) {
|
||||
return Status::Corruption("Empty blob index in wide column default value");
|
||||
Status WideColumnSerialization::GetValueOfDefaultColumnResolvingBlobs(
|
||||
const Slice& entity_value, const Slice& user_key,
|
||||
const BlobFetcher* blob_fetcher, PinnableSlice& result, bool& resolved) {
|
||||
assert(blob_fetcher);
|
||||
|
||||
resolved = false;
|
||||
|
||||
std::vector<WideColumn> columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
|
||||
|
||||
Slice input_copy = entity_value;
|
||||
if (Status s = DeserializeV2(input_copy, columns, blob_columns); !s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
BlobIndex blob_idx;
|
||||
if (Status s = blob_idx.DecodeFrom(blob_index); !s.ok()) {
|
||||
return Status::Corruption(
|
||||
"Error decoding blob index in wide column default value");
|
||||
}
|
||||
|
||||
if (blob_idx.IsInlined()) {
|
||||
value.PinSelf(blob_idx.value());
|
||||
// The default column (empty name) is always at index 0 when present
|
||||
// (columns are sorted by name).
|
||||
if (columns.empty() || columns[0].name() != kDefaultWideColumnName) {
|
||||
result.PinSelf(Slice());
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
if (blob_fetcher == nullptr) {
|
||||
// A blob-backed default column reached a read context with no blob fetcher
|
||||
// (a reader without blob support, e.g. SstFileReader, or corrupt/legacy
|
||||
// data). This is the single place default-column blob resolution reports
|
||||
// that condition; callers no longer pre-check the fetcher.
|
||||
return Status::Corruption(
|
||||
"Cannot resolve blob-backed default column without a blob fetcher");
|
||||
// Check if the default column (index 0) is a blob reference
|
||||
for (const auto& blob_col : blob_columns) {
|
||||
if (blob_col.first == 0) {
|
||||
const BlobIndex& blob_idx = blob_col.second;
|
||||
|
||||
resolved = true;
|
||||
|
||||
if (blob_idx.IsInlined()) {
|
||||
result.PinSelf(blob_idx.value());
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
return blob_fetcher->FetchBlob(user_key, blob_idx,
|
||||
nullptr /* prefetch_buffer */, &result,
|
||||
nullptr /* bytes_read */);
|
||||
}
|
||||
}
|
||||
|
||||
return blob_fetcher->FetchBlob(user_key, blob_idx,
|
||||
nullptr /* prefetch_buffer */, &value,
|
||||
nullptr /* bytes_read */);
|
||||
// Default column is inline
|
||||
result.PinSelf(columns[0].value());
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status WideColumnSerialization::ResolveEntityForMerge(
|
||||
@@ -951,9 +923,10 @@ Status WideColumnSerialization::ResolveEntityForMerge(
|
||||
if (status.ok()) {
|
||||
if (!has_blob_columns) {
|
||||
effective_entity = entity_value;
|
||||
} else if (!blob_fetcher) {
|
||||
status = Status::Corruption(
|
||||
"Cannot resolve blob columns in entity without a blob fetcher");
|
||||
} else {
|
||||
// A null blob_fetcher with blob columns present is reported as Corruption
|
||||
// inside ResolveEntityBlobColumns -> ResolveEntityBlobColumnsMultiBuffer.
|
||||
bool resolved = false;
|
||||
status = ResolveEntityBlobColumns(
|
||||
entity_value, user_key, blob_fetcher, prefetch_buffers,
|
||||
@@ -967,4 +940,40 @@ Status WideColumnSerialization::ResolveEntityForMerge(
|
||||
return status;
|
||||
}
|
||||
|
||||
Status WideColumnSerialization::SerializeResolvedEntity(
|
||||
const std::vector<WideColumn>& columns,
|
||||
const std::vector<std::pair<size_t, BlobIndex>>& blob_columns,
|
||||
const std::vector<std::string>& resolved_blob_values, std::string& output) {
|
||||
assert(blob_columns.size() == resolved_blob_values.size());
|
||||
#ifndef NDEBUG
|
||||
for (size_t i = 1; i < blob_columns.size(); ++i) {
|
||||
assert(blob_columns[i].first > blob_columns[i - 1].first);
|
||||
}
|
||||
#endif
|
||||
|
||||
// blob_columns is sorted by column index and typically small, so use a
|
||||
// linear scan with a cursor instead of an unordered_map.
|
||||
size_t blob_cursor = 0;
|
||||
|
||||
// Build result columns with resolved blob values
|
||||
WideColumns result_columns;
|
||||
result_columns.reserve(columns.size());
|
||||
|
||||
for (size_t i = 0; i < columns.size(); ++i) {
|
||||
if (blob_cursor < blob_columns.size() &&
|
||||
blob_columns[blob_cursor].first == i) {
|
||||
// This is a blob column - use the resolved value
|
||||
result_columns.emplace_back(columns[i].name(),
|
||||
Slice(resolved_blob_values[blob_cursor]));
|
||||
++blob_cursor;
|
||||
} else {
|
||||
// This is an inline column - use the original value
|
||||
result_columns.emplace_back(columns[i].name(), columns[i].value());
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize using V1 format (all values inline)
|
||||
return Serialize(result_columns, output);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -6,7 +6,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <forward_list>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
@@ -88,9 +87,8 @@ class Slice;
|
||||
// ct values are ValueType entries from db/dbformat.h, e.g.:
|
||||
// kTypeValue (0x01) = inline value
|
||||
// kTypeBlobIndex (0x11) = blob index reference
|
||||
// Future per-column types (kTypeMerge, kTypeDeletion, etc.) can be added
|
||||
// without changing the byte layout, though a reader predating a given type
|
||||
// rejects it as Corruption (see IsValidColumnValueType).
|
||||
// Future per-column types (kTypeMerge, kTypeDeletion, etc.) can be
|
||||
// added without format changes.
|
||||
//
|
||||
// Section 4: NAME SIZES (N varints)
|
||||
// +----------+----------+---...---+------------+
|
||||
@@ -131,10 +129,6 @@ class WideColumnSerialization {
|
||||
// Serialize columns using version 1 format (no blob support)
|
||||
static Status Serialize(const WideColumns& columns, std::string& output);
|
||||
|
||||
// Returns the exact number of bytes Serialize() would produce for `columns`
|
||||
// (the version 1 serialized size), computed without allocating.
|
||||
static size_t SerializedSizeV1(const WideColumns& columns);
|
||||
|
||||
// Serialize columns with some columns replaced by blob indices (version 2)
|
||||
// columns: vector of (column_name, column_value) pairs
|
||||
// blob_columns: vector of (column_index, blob_index) pairs indicating which
|
||||
@@ -151,35 +145,24 @@ class WideColumnSerialization {
|
||||
const std::vector<std::pair<size_t, BlobIndex>>& blob_columns,
|
||||
std::string& output);
|
||||
|
||||
// Deserialize a wide-column entity (version 1, or version 2 with or without
|
||||
// blob references). `input` must be exactly one serialized entity; trailing
|
||||
// bytes are reported as Corruption (the deserializers are not prefix-parsers
|
||||
// -- an entity value is always the whole stored value).
|
||||
//
|
||||
// Deserialize columns (version 1 format only)
|
||||
static Status Deserialize(Slice& input, WideColumns& columns);
|
||||
|
||||
// Deserialize a V2 entity.
|
||||
// columns: receives the full sorted column set. For blob-backed columns,
|
||||
// value() is initially the serialized BlobIndex bytes from the entity.
|
||||
//
|
||||
// blob_columns: if non-null, receives (column_index, blob_index) pairs
|
||||
// identifying which entries in `columns` are blob references, and their
|
||||
// decoded BlobIndex data. If null, the caller is promising the entity is
|
||||
// fully resolved (no blob references); encountering one returns Corruption
|
||||
// (as it could theoretically arise from data corruption).
|
||||
// When non-null, this agrees with HasBlobColumns() about whether blob column
|
||||
// references are present.
|
||||
static Status Deserialize(
|
||||
const Slice& input, WideColumns& columns,
|
||||
std::vector<std::pair<size_t, BlobIndex>>* blob_columns);
|
||||
|
||||
// Convenience wrapper of Deserialize() for callers that own a fully resolved
|
||||
// entity with no blob references (returns Corruption if one is present).
|
||||
static Status DeserializeSimple(const Slice& input, WideColumns& columns) {
|
||||
return Deserialize(input, columns, nullptr /* blob_columns */);
|
||||
}
|
||||
// blob_columns: receives (column_index, blob_index) pairs identifying which
|
||||
// entries in `columns` are blob references and their decoded BlobIndex data.
|
||||
// For valid input, this must agree with HasBlobColumns() about whether blob
|
||||
// column references are present.
|
||||
static Status DeserializeV2(
|
||||
Slice& input, std::vector<WideColumn>& columns,
|
||||
std::vector<std::pair<size_t, BlobIndex>>& blob_columns);
|
||||
|
||||
// Check if the serialized entity has any blob column references.
|
||||
// Sets *has_blob_columns to true if version >= 2 and at least one column
|
||||
// has blob type; false otherwise.
|
||||
// For valid input, this must agree with Deserialize() about whether blob
|
||||
// For valid input, this must agree with DeserializeV2() about whether blob
|
||||
// column references are present.
|
||||
// Returns Status::Corruption on decode errors.
|
||||
static Status HasBlobColumns(const Slice& input, bool& has_blob_columns);
|
||||
@@ -192,17 +175,7 @@ class WideColumnSerialization {
|
||||
const Slice& input,
|
||||
const std::function<Status(const BlobIndex&)>& callback);
|
||||
|
||||
// Extracts the default column (empty name) from a serialized entity, without
|
||||
// fetching any blob. Uses an O(1) fast path (no full deserialization).
|
||||
// is_blob_reference == false: `value` is the inline default column value.
|
||||
// is_blob_reference == true : `value` is the raw serialized BlobIndex of
|
||||
// the default column; resolve it with
|
||||
// ResolveDefaultColumnBlobReference().
|
||||
// Either way `value` is a zero-copy Slice into `input`. If there is no
|
||||
// default column, `value` is empty and is_blob_reference is false. Returns
|
||||
// Corruption on decode errors (never NotSupported).
|
||||
static Status GetValueOfDefaultColumn(const Slice& input, Slice& value,
|
||||
bool& is_blob_reference);
|
||||
static Status GetValueOfDefaultColumn(Slice& input, Slice& value);
|
||||
|
||||
// Resolves all blob column references in a V2 wide-column entity,
|
||||
// fetches the blob values, and re-serializes as a V1 entity (all inline).
|
||||
@@ -227,40 +200,17 @@ class WideColumnSerialization {
|
||||
PrefetchBufferCollection* prefetch_buffers, std::string& resolved_entity,
|
||||
bool& resolved, uint64_t* total_bytes_read, uint64_t* num_blobs_resolved);
|
||||
|
||||
// Multi-buffer counterpart to ResolveEntityBlobColumns that avoids
|
||||
// re-serialization. Whereas ResolveEntityBlobColumns produces a single
|
||||
// re-serialized V1 entity string, this deserializes the V2 entity and, for
|
||||
// each blob column, fetches the blob value into a freshly appended node of
|
||||
// `extra_buffers` (whose address is stable). On success, `resolved_columns`
|
||||
// holds the full column set: inline columns keep zero-copy Slices into
|
||||
// `entity_value`, and blob columns get zero-copy Slices into the
|
||||
// corresponding `extra_buffers` node. The caller must keep both
|
||||
// `entity_value` and `extra_buffers` alive while using `resolved_columns`
|
||||
// (e.g. via PinnableWideColumnsHelper::ResolveColumns, which splices
|
||||
// `extra_buffers` in alongside the original entity buffer).
|
||||
// Extracts the default column value from a V2 entity, resolving its
|
||||
// blob reference if needed. The default column (empty name) is always
|
||||
// at index 0 when present (columns are sorted).
|
||||
//
|
||||
// Sets `resolved` to false and leaves the outputs empty when no blob columns
|
||||
// are present. A non-inlined blob column with a null blob_fetcher (nothing to
|
||||
// read the blob with) is reported as Corruption because this could
|
||||
// hypothetically be caused by data corruption (more likely a programming
|
||||
// mistake); inlined blob columns need no fetcher.
|
||||
static Status ResolveEntityBlobColumnsMultiBuffer(
|
||||
// Sets result to the resolved default column value (fetching from blob
|
||||
// file if it's a blob reference). If there is no default column, result
|
||||
// is set to empty. Sets *resolved to true if a blob was found for the
|
||||
// default column, false otherwise.
|
||||
static Status GetValueOfDefaultColumnResolvingBlobs(
|
||||
const Slice& entity_value, const Slice& user_key,
|
||||
const BlobFetcher* blob_fetcher,
|
||||
PrefetchBufferCollection* prefetch_buffers, WideColumns& resolved_columns,
|
||||
std::forward_list<PinnableSlice>& extra_buffers, bool& resolved,
|
||||
uint64_t* total_bytes_read, uint64_t* num_blobs_resolved);
|
||||
|
||||
// Resolves the default column's blob reference -- the raw serialized
|
||||
// BlobIndex returned by GetValueOfDefaultColumn() when is_blob_reference is
|
||||
// true -- into its value. An inlined BlobIndex needs no fetch. A non-inlined
|
||||
// reference with a null blob_fetcher returns Corruption: a blob-backed value
|
||||
// with no way to read the blob (a reader without blob support, e.g.
|
||||
// SstFileReader, or corrupt/legacy data). This is the single place that
|
||||
// condition is handled for default-column reads.
|
||||
static Status ResolveDefaultColumnBlobReference(
|
||||
const Slice& blob_index, const Slice& user_key,
|
||||
const BlobFetcher* blob_fetcher, PinnableSlice& value);
|
||||
const BlobFetcher* blob_fetcher, PinnableSlice& result, bool& resolved);
|
||||
|
||||
// Resolves V2 entity blob columns if present, returning an effective
|
||||
// Slice for use with TimedFullMerge. If the entity has no blob columns
|
||||
@@ -281,6 +231,14 @@ class WideColumnSerialization {
|
||||
// Returns Status::Corruption on decode errors.
|
||||
static Status GetVersion(const Slice& input, uint32_t& version);
|
||||
|
||||
// Merges deserialized columns with resolved blob values and serializes
|
||||
// the result using version 1 format (all values inline).
|
||||
static Status SerializeResolvedEntity(
|
||||
const std::vector<WideColumn>& columns,
|
||||
const std::vector<std::pair<size_t, BlobIndex>>& blob_columns,
|
||||
const std::vector<std::string>& resolved_blob_values,
|
||||
std::string& output);
|
||||
|
||||
// Returns InvalidArgument with the given message if size exceeds uint32_t.
|
||||
static Status ValidateWideColumnLimit(size_t size, const char* msg) {
|
||||
if (size > static_cast<size_t>(std::numeric_limits<uint32_t>::max())) {
|
||||
@@ -314,12 +272,14 @@ class WideColumnSerialization {
|
||||
std::vector<const BlobIndex*>& blob_index_map);
|
||||
|
||||
// Parses V1 layout (interleaved name/value_size pairs followed by values)
|
||||
// into columns. Used by Deserialize() for version 1 entities.
|
||||
// into columns. Used by both Deserialize and DeserializeV2 to avoid
|
||||
// code duplication.
|
||||
static Status DeserializeV1(Slice& input, uint32_t num_columns,
|
||||
std::vector<WideColumn>& columns);
|
||||
|
||||
// Parses V2 layout sections 2-7 (skip info through values) into columns and
|
||||
// column types. Used by Deserialize() for version 2 entities.
|
||||
// column types. Used by both Deserialize and DeserializeV2 to avoid
|
||||
// code duplication.
|
||||
static Status DeserializeV2Impl(Slice& input, uint32_t num_columns,
|
||||
std::vector<WideColumn>& columns,
|
||||
std::vector<ValueType>& column_types);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "db/wide/wide_column_serialization.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <forward_list>
|
||||
#include <limits>
|
||||
|
||||
#include "db/blob/blob_index.h"
|
||||
@@ -24,6 +23,15 @@ class WideColumnSerializationTest : public testing::Test {
|
||||
static Status GetVersion(const Slice& input, uint32_t& version) {
|
||||
return WideColumnSerialization::GetVersion(input, version);
|
||||
}
|
||||
|
||||
static Status SerializeResolvedEntity(
|
||||
const std::vector<WideColumn>& columns,
|
||||
const std::vector<std::pair<size_t, BlobIndex>>& blob_columns,
|
||||
const std::vector<std::string>& resolved_blob_values,
|
||||
std::string& output) {
|
||||
return WideColumnSerialization::SerializeResolvedEntity(
|
||||
columns, blob_columns, resolved_blob_values, output);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(WideColumnSerializationTest, Construct) {
|
||||
@@ -111,8 +119,7 @@ TEST_F(WideColumnSerializationTest, SerializeDeserialize) {
|
||||
Slice input(output);
|
||||
WideColumns deserialized_columns;
|
||||
|
||||
ASSERT_OK(
|
||||
WideColumnSerialization::DeserializeSimple(input, deserialized_columns));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(input, deserialized_columns));
|
||||
ASSERT_EQ(columns, deserialized_columns);
|
||||
|
||||
{
|
||||
@@ -166,15 +173,13 @@ TEST_F(WideColumnSerializationTest, DeserializeVersionError) {
|
||||
Slice input(buf);
|
||||
WideColumns columns;
|
||||
|
||||
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
|
||||
const Status s = WideColumnSerialization::Deserialize(input, columns);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_TRUE(std::strstr(s.getState(), "version"));
|
||||
}
|
||||
|
||||
TEST_F(WideColumnSerializationTest, DeserializeUnsupportedVersion) {
|
||||
// A version newer than kVersion2 is reported as Corruption (an unrecognized
|
||||
// serialized version is treated as bad data, consistent with the rest of the
|
||||
// codebase's format parsers).
|
||||
// Unsupported version
|
||||
constexpr uint32_t future_version = 1000;
|
||||
|
||||
std::string buf;
|
||||
@@ -183,40 +188,11 @@ TEST_F(WideColumnSerializationTest, DeserializeUnsupportedVersion) {
|
||||
Slice input(buf);
|
||||
WideColumns columns;
|
||||
|
||||
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
const Status s = WideColumnSerialization::Deserialize(input, columns);
|
||||
ASSERT_TRUE(s.IsNotSupported());
|
||||
ASSERT_TRUE(std::strstr(s.getState(), "version"));
|
||||
}
|
||||
|
||||
TEST_F(WideColumnSerializationTest, FutureVersionRejectedConsistently) {
|
||||
// Every entry point must reject a future/unknown version as Corruption, so a
|
||||
// corrupt version byte cannot be misclassified (e.g. as a blob reference) by
|
||||
// any read path.
|
||||
constexpr uint32_t future_version = 1000;
|
||||
|
||||
std::string buf;
|
||||
PutVarint32(&buf, future_version);
|
||||
Slice input(buf);
|
||||
|
||||
WideColumns columns;
|
||||
ASSERT_TRUE(WideColumnSerialization::DeserializeSimple(input, columns)
|
||||
.IsCorruption());
|
||||
|
||||
Slice value;
|
||||
bool is_blob_reference = false;
|
||||
ASSERT_TRUE(WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
input, value, is_blob_reference)
|
||||
.IsCorruption());
|
||||
|
||||
bool has_blob_columns = false;
|
||||
ASSERT_TRUE(WideColumnSerialization::HasBlobColumns(input, has_blob_columns)
|
||||
.IsCorruption());
|
||||
|
||||
ASSERT_TRUE(WideColumnSerialization::ForEachBlobFileNumber(
|
||||
input, [](const BlobIndex&) { return Status::OK(); })
|
||||
.IsCorruption());
|
||||
}
|
||||
|
||||
TEST_F(WideColumnSerializationTest, DeserializeNumberOfColumnsError) {
|
||||
// Can't decode number of columns
|
||||
|
||||
@@ -226,7 +202,7 @@ TEST_F(WideColumnSerializationTest, DeserializeNumberOfColumnsError) {
|
||||
Slice input(buf);
|
||||
WideColumns columns;
|
||||
|
||||
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
|
||||
const Status s = WideColumnSerialization::Deserialize(input, columns);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_TRUE(std::strstr(s.getState(), "number"));
|
||||
}
|
||||
@@ -244,7 +220,7 @@ TEST_F(WideColumnSerializationTest, DeserializeV2Error) {
|
||||
Slice input(buf);
|
||||
WideColumns columns;
|
||||
|
||||
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
|
||||
const Status s = WideColumnSerialization::Deserialize(input, columns);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_TRUE(std::strstr(s.getState(), "name"));
|
||||
}
|
||||
@@ -257,7 +233,7 @@ TEST_F(WideColumnSerializationTest, DeserializeV2Error) {
|
||||
Slice input(buf);
|
||||
WideColumns columns;
|
||||
|
||||
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
|
||||
const Status s = WideColumnSerialization::Deserialize(input, columns);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_TRUE(std::strstr(s.getState(), "value size"));
|
||||
}
|
||||
@@ -270,7 +246,7 @@ TEST_F(WideColumnSerializationTest, DeserializeV2Error) {
|
||||
Slice input(buf);
|
||||
WideColumns columns;
|
||||
|
||||
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
|
||||
const Status s = WideColumnSerialization::Deserialize(input, columns);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_TRUE(std::strstr(s.getState(), "name"));
|
||||
}
|
||||
@@ -283,7 +259,7 @@ TEST_F(WideColumnSerializationTest, DeserializeV2Error) {
|
||||
Slice input(buf);
|
||||
WideColumns columns;
|
||||
|
||||
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
|
||||
const Status s = WideColumnSerialization::Deserialize(input, columns);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_TRUE(std::strstr(s.getState(), "value size"));
|
||||
}
|
||||
@@ -296,7 +272,7 @@ TEST_F(WideColumnSerializationTest, DeserializeV2Error) {
|
||||
Slice input(buf);
|
||||
WideColumns columns;
|
||||
|
||||
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
|
||||
const Status s = WideColumnSerialization::Deserialize(input, columns);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_TRUE(std::strstr(s.getState(), "payload"));
|
||||
}
|
||||
@@ -308,7 +284,7 @@ TEST_F(WideColumnSerializationTest, DeserializeV2Error) {
|
||||
Slice input(buf);
|
||||
WideColumns columns;
|
||||
|
||||
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
|
||||
const Status s = WideColumnSerialization::Deserialize(input, columns);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_TRUE(std::strstr(s.getState(), "payload"));
|
||||
}
|
||||
@@ -320,7 +296,7 @@ TEST_F(WideColumnSerializationTest, DeserializeV2Error) {
|
||||
Slice input(buf);
|
||||
WideColumns columns;
|
||||
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeSimple(input, columns));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(input, columns));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,7 +320,7 @@ TEST_F(WideColumnSerializationTest, DeserializeV2OutOfOrder) {
|
||||
Slice input(buf);
|
||||
WideColumns columns;
|
||||
|
||||
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
|
||||
const Status s = WideColumnSerialization::Deserialize(input, columns);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_TRUE(std::strstr(s.getState(), "order"));
|
||||
}
|
||||
@@ -383,13 +359,13 @@ TEST_F(WideColumnSerializationTest, DeserializeV2RejectsRecursiveType) {
|
||||
// Section 7: VALUES (8 bytes of placeholder data)
|
||||
buf.append(8, 'x');
|
||||
|
||||
// Deserialize should reject with Corruption
|
||||
// DeserializeV2 should reject with Corruption
|
||||
{
|
||||
Slice input(buf);
|
||||
std::vector<WideColumn> columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
|
||||
const Status s =
|
||||
WideColumnSerialization::Deserialize(input, columns, &blob_columns);
|
||||
WideColumnSerialization::DeserializeV2(input, columns, blob_columns);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_TRUE(std::strstr(s.getState(), "Unsupported wide column ValueType"));
|
||||
}
|
||||
@@ -398,54 +374,11 @@ TEST_F(WideColumnSerializationTest, DeserializeV2RejectsRecursiveType) {
|
||||
{
|
||||
Slice input(buf);
|
||||
WideColumns columns;
|
||||
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
|
||||
const Status s = WideColumnSerialization::Deserialize(input, columns);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(WideColumnSerializationTest, FastPathsRejectUnsupportedColumnType) {
|
||||
// GetValueOfDefaultColumn() and ForEachBlobFileNumber() read the COLUMN TYPES
|
||||
// section on a fast path. An unrecognized/unsupported type byte (here
|
||||
// kTypeWideColumnEntity) must be rejected as Corruption -- consistent with
|
||||
// Deserialize()/HasBlobColumns() -- rather than silently treated as an inline
|
||||
// value or a non-blob column.
|
||||
std::string buf;
|
||||
PutVarint32(&buf, WideColumnSerialization::kVersion2);
|
||||
PutVarint32(&buf, 1); // num_columns
|
||||
|
||||
// Section 2: SKIP INFO
|
||||
PutVarint32(&buf, 1); // name_sizes_bytes (one varint: the name size 0)
|
||||
PutVarint32(&buf, 1); // value_sizes_bytes (one varint: the value size 3)
|
||||
PutVarint32(&buf, 0); // names_bytes (default column name is empty)
|
||||
|
||||
// Section 3: COLUMN TYPES -- unsupported type for the (default) column
|
||||
buf.push_back(static_cast<char>(kTypeWideColumnEntity));
|
||||
|
||||
// Section 4: NAME SIZES
|
||||
PutVarint32(&buf, 0); // empty default column name
|
||||
// Section 5: VALUE SIZES
|
||||
PutVarint32(&buf, 3);
|
||||
// Section 6: NAMES (empty)
|
||||
// Section 7: VALUES
|
||||
buf.append("xyz");
|
||||
|
||||
{
|
||||
Slice value;
|
||||
bool is_blob_reference = false;
|
||||
const Status s = WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
Slice(buf), value, is_blob_reference);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_TRUE(std::strstr(s.getState(), "Unsupported wide column ValueType"));
|
||||
}
|
||||
|
||||
{
|
||||
const Status s = WideColumnSerialization::ForEachBlobFileNumber(
|
||||
Slice(buf), [](const BlobIndex&) { return Status::OK(); });
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_TRUE(std::strstr(s.getState(), "Unsupported wide column ValueType"));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: create a BlobIndex from EncodeBlob parameters.
|
||||
static BlobIndex MakeBlobIndex(uint64_t file_number, uint64_t offset,
|
||||
uint64_t size,
|
||||
@@ -458,7 +391,7 @@ static BlobIndex MakeBlobIndex(uint64_t file_number, uint64_t offset,
|
||||
return bi;
|
||||
}
|
||||
|
||||
// Helper: V2 serialize -> Deserialize round-trip, returning
|
||||
// Helper: V2 serialize -> DeserializeV2 round-trip, returning
|
||||
// deserialized columns and blob column info.
|
||||
static void V2SerializeAndDeserialize(
|
||||
const std::vector<std::pair<std::string, std::string>>& columns,
|
||||
@@ -470,8 +403,8 @@ static void V2SerializeAndDeserialize(
|
||||
*serialized_out));
|
||||
|
||||
Slice input(*serialized_out);
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(input, *deserialized,
|
||||
blob_columns_out));
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(input, *deserialized,
|
||||
*blob_columns_out));
|
||||
ASSERT_EQ(deserialized->size(), columns.size());
|
||||
for (size_t i = 0; i < columns.size(); ++i) {
|
||||
ASSERT_EQ((*deserialized)[i].name(), columns[i].first);
|
||||
@@ -497,7 +430,7 @@ static void VerifyDeserialize(
|
||||
const std::vector<std::string>& expected_values) {
|
||||
Slice input(serialized);
|
||||
WideColumns deserialized;
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeSimple(input, deserialized));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(input, deserialized));
|
||||
ASSERT_EQ(deserialized.size(), expected.size());
|
||||
for (size_t i = 0; i < expected.size(); ++i) {
|
||||
ASSERT_EQ(deserialized[i].name(), expected[i].first);
|
||||
@@ -548,13 +481,58 @@ static void VerifyGetDefaultColumn(
|
||||
|
||||
Slice input(serialized);
|
||||
Slice value;
|
||||
bool is_blob_reference = false;
|
||||
ASSERT_OK(WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
input, value, is_blob_reference));
|
||||
ASSERT_FALSE(is_blob_reference);
|
||||
ASSERT_OK(WideColumnSerialization::GetValueOfDefaultColumn(input, value));
|
||||
ASSERT_EQ(value, expected_value);
|
||||
}
|
||||
|
||||
TEST_F(WideColumnSerializationTest, SerializeResolvedEntity) {
|
||||
// Test resolve with mixed, all-blob, and no-blob configurations
|
||||
struct TestCase {
|
||||
std::vector<std::pair<std::string, std::string>> columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_cols;
|
||||
std::vector<std::string> resolved_values;
|
||||
std::vector<std::string> expected_values;
|
||||
};
|
||||
|
||||
std::vector<TestCase> cases = {
|
||||
// Mixed inline and blob
|
||||
{.columns = {{"a", "inline_a"}, {"b", "ph"}, {"c", "inline_c"}},
|
||||
.blob_cols = {{1, MakeBlobIndex(50, 500, 100)}},
|
||||
.resolved_values = {"resolved_b"},
|
||||
.expected_values = {"inline_a", "resolved_b", "inline_c"}},
|
||||
// All blob columns
|
||||
{.columns = {{"x", "ph1"}, {"y", "ph2"}, {"z", "ph3"}},
|
||||
.blob_cols = {{0, MakeBlobIndex(10, 100, 50)},
|
||||
{1, MakeBlobIndex(20, 200, 60)},
|
||||
{2, MakeBlobIndex(30, 300, 70)}},
|
||||
.resolved_values = {"val_x", "val_y", "val_z"},
|
||||
.expected_values = {"val_x", "val_y", "val_z"}},
|
||||
// No blob columns
|
||||
{.columns = {{"alpha", "val_alpha"}, {"beta", "val_beta"}},
|
||||
.blob_cols = {},
|
||||
.resolved_values = {},
|
||||
.expected_values = {"val_alpha", "val_beta"}},
|
||||
};
|
||||
|
||||
for (const auto& tc : cases) {
|
||||
std::string serialized;
|
||||
std::vector<WideColumn> deserialized;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_out;
|
||||
V2SerializeAndDeserialize(tc.columns, tc.blob_cols, &deserialized,
|
||||
&blob_out, &serialized);
|
||||
|
||||
std::string resolved_output;
|
||||
ASSERT_OK(WideColumnSerializationTest::SerializeResolvedEntity(
|
||||
deserialized, blob_out, tc.resolved_values, resolved_output));
|
||||
|
||||
uint32_t v = 0;
|
||||
ASSERT_OK(GetVersion(Slice(resolved_output), v));
|
||||
ASSERT_EQ(v, WideColumnSerialization::kVersion1);
|
||||
|
||||
VerifyDeserialize(resolved_output, tc.columns, tc.expected_values);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(WideColumnSerializationTest, V2GetValueOfDefaultColumn) {
|
||||
// V2 with default column present
|
||||
VerifyGetDefaultColumn({{"", "default_value"}, {"col1", "value1"}},
|
||||
@@ -572,10 +550,7 @@ TEST_F(WideColumnSerializationTest, V2GetValueOfDefaultColumn) {
|
||||
|
||||
Slice input(serialized);
|
||||
Slice value;
|
||||
bool is_blob_reference = false;
|
||||
ASSERT_OK(WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
input, value, is_blob_reference));
|
||||
ASSERT_FALSE(is_blob_reference);
|
||||
ASSERT_OK(WideColumnSerialization::GetValueOfDefaultColumn(input, value));
|
||||
ASSERT_EQ(value, "v1_default");
|
||||
}
|
||||
}
|
||||
@@ -592,8 +567,8 @@ TEST_F(WideColumnSerializationTest, V2BlobColumnRejectsDeserialize) {
|
||||
|
||||
Slice input(serialized);
|
||||
WideColumns deserialized;
|
||||
ASSERT_TRUE(WideColumnSerialization::DeserializeSimple(input, deserialized)
|
||||
.IsCorruption());
|
||||
ASSERT_TRUE(WideColumnSerialization::Deserialize(input, deserialized)
|
||||
.IsNotSupported());
|
||||
}
|
||||
|
||||
TEST_F(WideColumnSerializationTest, PinnableWideColumnsFallbacksToV2) {
|
||||
@@ -609,8 +584,8 @@ TEST_F(WideColumnSerializationTest, PinnableWideColumnsFallbacksToV2) {
|
||||
std::vector<WideColumn> expected_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> expected_blob_columns;
|
||||
Slice input(serialized);
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(input, expected_columns,
|
||||
&expected_blob_columns));
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(input, expected_columns,
|
||||
expected_blob_columns));
|
||||
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(result.SetWideColumnValue(serialized));
|
||||
@@ -623,9 +598,8 @@ TEST_F(WideColumnSerializationTest, PinnableWideColumnsFallbacksToV2) {
|
||||
}
|
||||
|
||||
TEST_F(WideColumnSerializationTest, V2GetValueOfDefaultColumnBlobRef) {
|
||||
// When the default column (index 0) is a blob reference,
|
||||
// GetValueOfDefaultColumn succeeds and flags it via is_blob_reference,
|
||||
// returning the raw serialized BlobIndex bytes (no fetch).
|
||||
// When default column (index 0) is a blob reference,
|
||||
// GetValueOfDefaultColumn should return NotSupported.
|
||||
std::vector<std::pair<std::string, std::string>> columns = {
|
||||
{"", "placeholder"}, {"col1", "value1"}};
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns = {
|
||||
@@ -637,17 +611,8 @@ TEST_F(WideColumnSerializationTest, V2GetValueOfDefaultColumnBlobRef) {
|
||||
|
||||
Slice input(serialized);
|
||||
Slice value;
|
||||
bool is_blob_reference = false;
|
||||
ASSERT_OK(WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
input, value, is_blob_reference));
|
||||
ASSERT_TRUE(is_blob_reference);
|
||||
ASSERT_FALSE(value.empty());
|
||||
|
||||
// Resolving a non-inlined reference without a blob fetcher is Corruption.
|
||||
PinnableSlice resolved;
|
||||
ASSERT_TRUE(WideColumnSerialization::ResolveDefaultColumnBlobReference(
|
||||
value, "user_key", /*blob_fetcher=*/nullptr, resolved)
|
||||
.IsCorruption());
|
||||
ASSERT_TRUE(WideColumnSerialization::GetValueOfDefaultColumn(input, value)
|
||||
.IsNotSupported());
|
||||
}
|
||||
|
||||
TEST_F(WideColumnSerializationTest, SerializeV2Errors) {
|
||||
@@ -833,7 +798,7 @@ TEST_F(WideColumnSerializationTest, RandomizedSerializeDeserializeRoundTrip) {
|
||||
}
|
||||
}
|
||||
|
||||
// V2 serialize -> Deserialize round-trip
|
||||
// V2 serialize -> DeserializeV2 round-trip
|
||||
std::string serialized;
|
||||
std::vector<WideColumn> deserialized;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_out;
|
||||
@@ -930,245 +895,6 @@ TEST_F(WideColumnSerializationTest, ResolveEntityForMergeNullBlobFetcher) {
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
}
|
||||
|
||||
TEST_F(WideColumnSerializationTest, DeserializeRejectsTrailingData) {
|
||||
const WideColumns columns{{kDefaultWideColumnName, "d"}, {"attr", "val"}};
|
||||
|
||||
auto check = [](const std::string& serialized) {
|
||||
std::vector<WideColumn> out;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(Slice(serialized), out,
|
||||
&blob_columns));
|
||||
|
||||
// A trailing byte must be rejected as Corruption.
|
||||
const std::string with_trailing = serialized + "x";
|
||||
out.clear();
|
||||
blob_columns.clear();
|
||||
ASSERT_TRUE(WideColumnSerialization::Deserialize(Slice(with_trailing), out,
|
||||
&blob_columns)
|
||||
.IsCorruption());
|
||||
};
|
||||
|
||||
// V1 layout (the underlying DeserializeV1 must reject trailing data, matching
|
||||
// the V2 layout below).
|
||||
{
|
||||
std::string serialized;
|
||||
ASSERT_OK(WideColumnSerialization::Serialize(columns, serialized));
|
||||
check(serialized);
|
||||
}
|
||||
|
||||
// V2 layout with no blob columns.
|
||||
{
|
||||
std::string serialized;
|
||||
ASSERT_OK(WideColumnSerialization::SerializeV2(
|
||||
columns, {} /* blob_columns */, serialized));
|
||||
check(serialized);
|
||||
}
|
||||
|
||||
// Empty (zero-column) entities must reject trailing data too.
|
||||
const WideColumns empty_columns;
|
||||
{
|
||||
std::string serialized;
|
||||
ASSERT_OK(WideColumnSerialization::Serialize(empty_columns, serialized));
|
||||
check(serialized);
|
||||
}
|
||||
{
|
||||
std::string serialized;
|
||||
ASSERT_OK(WideColumnSerialization::SerializeV2(
|
||||
empty_columns, {} /* blob_columns */, serialized));
|
||||
check(serialized);
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
size_t ExpectedPayloadSize(const WideColumns& columns) {
|
||||
size_t total = 0;
|
||||
for (const auto& column : columns) {
|
||||
total += column.name().size() + column.value().size();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(PinnableWideColumnsTest, SetPlainValue) {
|
||||
PinnableWideColumns columns;
|
||||
columns.SetPlainValue(Slice("hello"));
|
||||
|
||||
ASSERT_EQ(columns.columns().size(), 1);
|
||||
EXPECT_EQ(columns.columns()[0].name(), kDefaultWideColumnName);
|
||||
EXPECT_EQ(columns.columns()[0].value(), Slice("hello"));
|
||||
|
||||
// For a plain value, payload_size() equals the raw value size.
|
||||
EXPECT_EQ(columns.payload_size(), 5);
|
||||
}
|
||||
|
||||
TEST(PinnableWideColumnsTest, SetWideColumnValueAndPayloadSize) {
|
||||
// Columns must be sorted (default/empty name first).
|
||||
const WideColumns entity_columns{
|
||||
{kDefaultWideColumnName, "default"}, {"a", "bb"}, {"ccc", "dddd"}};
|
||||
|
||||
std::string serialized;
|
||||
ASSERT_OK(WideColumnSerialization::Serialize(entity_columns, serialized));
|
||||
|
||||
PinnableWideColumns columns;
|
||||
ASSERT_OK(columns.SetWideColumnValue(std::move(serialized)));
|
||||
|
||||
ASSERT_EQ(columns.columns(), entity_columns);
|
||||
EXPECT_EQ(columns.payload_size(), ExpectedPayloadSize(entity_columns));
|
||||
}
|
||||
|
||||
TEST(PinnableWideColumnsTest, SerializedSizeMatchesSerializedSizeV1) {
|
||||
{
|
||||
// Plain value: default column with the raw value.
|
||||
PinnableWideColumns columns;
|
||||
columns.SetPlainValue(Slice("plain-value"));
|
||||
|
||||
EXPECT_EQ(columns.serialized_size(),
|
||||
WideColumnSerialization::SerializedSizeV1(columns.columns()));
|
||||
}
|
||||
|
||||
{
|
||||
// V1 entity: serialized_size() equals the length of the exact bytes that
|
||||
// Serialize() produced.
|
||||
const WideColumns entity_columns{
|
||||
{kDefaultWideColumnName, "default"}, {"attr1", "val1"}, {"attr2", ""}};
|
||||
|
||||
std::string serialized;
|
||||
ASSERT_OK(WideColumnSerialization::Serialize(entity_columns, serialized));
|
||||
const size_t serialized_len = serialized.size();
|
||||
|
||||
PinnableWideColumns columns;
|
||||
ASSERT_OK(columns.SetWideColumnValue(std::move(serialized)));
|
||||
|
||||
EXPECT_EQ(columns.serialized_size(),
|
||||
WideColumnSerialization::SerializedSizeV1(columns.columns()));
|
||||
EXPECT_EQ(columns.serialized_size(), serialized_len);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PinnableWideColumnsTest, Reset) {
|
||||
PinnableWideColumns columns;
|
||||
columns.SetPlainValue(Slice("something"));
|
||||
ASSERT_FALSE(columns.columns().empty());
|
||||
|
||||
columns.Reset();
|
||||
EXPECT_TRUE(columns.columns().empty());
|
||||
EXPECT_EQ(columns.payload_size(), 0);
|
||||
|
||||
// Reusable after Reset.
|
||||
columns.SetPlainValue(Slice("again"));
|
||||
ASSERT_EQ(columns.columns().size(), 1);
|
||||
EXPECT_EQ(columns.columns()[0].value(), Slice("again"));
|
||||
}
|
||||
|
||||
TEST(PinnableWideColumnsTest, MovePlainValueSelfPinnedStable) {
|
||||
// A short (SSO) plain value is self-pinned inside a backing node. The node
|
||||
// must not relocate on move, so the column Slice stays valid and its data
|
||||
// pointer is preserved -- the guarantee that lets us drop the old
|
||||
// re-derivation-on-move logic.
|
||||
PinnableWideColumns columns;
|
||||
columns.SetPlainValue(std::string("tiny"));
|
||||
|
||||
const char* const value_data = columns.columns()[0].value().data();
|
||||
|
||||
PinnableWideColumns moved(std::move(columns));
|
||||
ASSERT_EQ(moved.columns().size(), 1);
|
||||
EXPECT_EQ(moved.columns()[0].name(), kDefaultWideColumnName);
|
||||
EXPECT_EQ(moved.columns()[0].value(), Slice("tiny"));
|
||||
EXPECT_EQ(moved.columns()[0].value().data(), value_data);
|
||||
}
|
||||
|
||||
TEST(PinnableWideColumnsTest, ResolveColumnsZeroCopy) {
|
||||
// Original entity with two inline columns.
|
||||
const WideColumns entity_columns{{kDefaultWideColumnName, "default-value"},
|
||||
{"blob_col", "encoded-blob-index"}};
|
||||
|
||||
std::string serialized;
|
||||
ASSERT_OK(WideColumnSerialization::Serialize(entity_columns, serialized));
|
||||
|
||||
PinnableWideColumns columns;
|
||||
ASSERT_OK(columns.SetWideColumnValue(std::move(serialized)));
|
||||
|
||||
// Inline (default) column keeps its Slice into the original entity buffer.
|
||||
const Slice inline_name = columns.columns()[0].name();
|
||||
const Slice inline_value = columns.columns()[0].value();
|
||||
const char* const inline_value_data = inline_value.data();
|
||||
|
||||
// Resolve "blob_col" into its own backing buffer.
|
||||
std::forward_list<PinnableSlice> extra_buffers;
|
||||
extra_buffers.emplace_front();
|
||||
extra_buffers.front().PinSelf(Slice("resolved-blob-payload"));
|
||||
const char* const resolved_data = extra_buffers.front().data();
|
||||
|
||||
WideColumns resolved_columns;
|
||||
resolved_columns.emplace_back(inline_name, inline_value);
|
||||
resolved_columns.emplace_back(columns.columns()[1].name(),
|
||||
Slice(extra_buffers.front()));
|
||||
|
||||
PinnableWideColumnsHelper::ResolveColumns(
|
||||
columns, std::move(resolved_columns), std::move(extra_buffers));
|
||||
|
||||
ASSERT_EQ(columns.columns().size(), 2);
|
||||
|
||||
// Inline column still points into the original entity buffer (no copy, no
|
||||
// re-serialization).
|
||||
EXPECT_EQ(columns.columns()[0].value(), Slice("default-value"));
|
||||
EXPECT_EQ(columns.columns()[0].value().data(), inline_value_data);
|
||||
|
||||
// Resolved blob column points directly into the spliced-in backing buffer,
|
||||
// not into a re-serialized entity.
|
||||
EXPECT_EQ(columns.columns()[1].name(), Slice("blob_col"));
|
||||
EXPECT_EQ(columns.columns()[1].value(), Slice("resolved-blob-payload"));
|
||||
EXPECT_EQ(columns.columns()[1].value().data(), resolved_data);
|
||||
}
|
||||
|
||||
TEST(PinnableWideColumnsTest, MultiBufferMovePreservesPointers) {
|
||||
const WideColumns entity_columns{{kDefaultWideColumnName, "default-value"},
|
||||
{"blob_col", "encoded-blob-index"}};
|
||||
|
||||
std::string serialized;
|
||||
ASSERT_OK(WideColumnSerialization::Serialize(entity_columns, serialized));
|
||||
|
||||
PinnableWideColumns columns;
|
||||
ASSERT_OK(columns.SetWideColumnValue(std::move(serialized)));
|
||||
|
||||
std::forward_list<PinnableSlice> extra_buffers;
|
||||
extra_buffers.emplace_front();
|
||||
extra_buffers.front().PinSelf(Slice("a-fairly-long-resolved-blob-payload"));
|
||||
|
||||
WideColumns resolved_columns;
|
||||
resolved_columns.emplace_back(columns.columns()[0].name(),
|
||||
columns.columns()[0].value());
|
||||
resolved_columns.emplace_back(columns.columns()[1].name(),
|
||||
Slice(extra_buffers.front()));
|
||||
|
||||
PinnableWideColumnsHelper::ResolveColumns(
|
||||
columns, std::move(resolved_columns), std::move(extra_buffers));
|
||||
|
||||
const char* const inline_data = columns.columns()[0].value().data();
|
||||
const char* const resolved_data = columns.columns()[1].value().data();
|
||||
|
||||
// Move construction: nodes are stolen, not relocated -- data pointers stable.
|
||||
PinnableWideColumns moved(std::move(columns));
|
||||
ASSERT_EQ(moved.columns().size(), 2);
|
||||
EXPECT_EQ(moved.columns()[0].value(), Slice("default-value"));
|
||||
EXPECT_EQ(moved.columns()[1].value(),
|
||||
Slice("a-fairly-long-resolved-blob-payload"));
|
||||
EXPECT_EQ(moved.columns()[0].value().data(), inline_data);
|
||||
EXPECT_EQ(moved.columns()[1].value().data(), resolved_data);
|
||||
|
||||
// Move assignment: same guarantee.
|
||||
PinnableWideColumns move_assigned;
|
||||
move_assigned = std::move(moved);
|
||||
ASSERT_EQ(move_assigned.columns().size(), 2);
|
||||
EXPECT_EQ(move_assigned.columns()[0].value(), Slice("default-value"));
|
||||
EXPECT_EQ(move_assigned.columns()[1].value(),
|
||||
Slice("a-fairly-long-resolved-blob-payload"));
|
||||
EXPECT_EQ(move_assigned.columns()[0].value().data(), inline_data);
|
||||
EXPECT_EQ(move_assigned.columns()[1].value().data(), resolved_data);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+17
-20
@@ -5,8 +5,6 @@
|
||||
|
||||
#include "rocksdb/wide_columns.h"
|
||||
|
||||
#include <iterator>
|
||||
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/wide/wide_column_serialization.h"
|
||||
|
||||
@@ -16,31 +14,30 @@ const Slice kDefaultWideColumnName;
|
||||
|
||||
const WideColumns kNoWideColumns;
|
||||
|
||||
Status PinnableWideColumns::BuildColumnsForEntity() {
|
||||
Status PinnableWideColumns::CreateIndexForWideColumns() {
|
||||
columns_.clear();
|
||||
unresolved_blob_column_indices_.clear();
|
||||
|
||||
// Called right after a Set*() has placed the serialized entity in a single
|
||||
// backing buffer; the whole entity must live in backing_.front().
|
||||
assert(!backing_.empty());
|
||||
assert(std::next(backing_.begin()) == backing_.end());
|
||||
|
||||
// Collect any blob column references; a resolved entity simply yields none.
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
|
||||
Status status = WideColumnSerialization::Deserialize(backing_.front(),
|
||||
columns_, &blob_columns);
|
||||
if (status.ok()) {
|
||||
unresolved_blob_column_indices_.reserve(blob_columns.size());
|
||||
for (const auto& blob_column : blob_columns) {
|
||||
unresolved_blob_column_indices_.push_back(blob_column.first);
|
||||
Slice value_copy = value_;
|
||||
Status status = WideColumnSerialization::Deserialize(value_copy, columns_);
|
||||
if (status.IsNotSupported()) {
|
||||
// Deserialize() may already populate columns_ before discovering V2 blob
|
||||
// references and returning NotSupported. Clear the partial result before
|
||||
// falling back to DeserializeV2(), which requires an empty output vector.
|
||||
columns_.clear();
|
||||
value_copy = value_;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
|
||||
status = WideColumnSerialization::DeserializeV2(value_copy, columns_,
|
||||
blob_columns);
|
||||
if (status.ok()) {
|
||||
unresolved_blob_column_indices_.reserve(blob_columns.size());
|
||||
for (const auto& blob_column : blob_columns) {
|
||||
unresolved_blob_column_indices_.push_back(blob_column.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
size_t PinnableWideColumns::serialized_size() const {
|
||||
return WideColumnSerialization::SerializedSizeV1(columns_);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -34,8 +34,7 @@ Status WideColumnsHelper::DumpSliceAsWideColumns(const Slice& value,
|
||||
std::ostream& os, bool hex) {
|
||||
WideColumns columns;
|
||||
Slice value_copy = value;
|
||||
const Status s =
|
||||
WideColumnSerialization::DeserializeSimple(value_copy, columns);
|
||||
const Status s = WideColumnSerialization::Deserialize(value_copy, columns);
|
||||
if (s.ok()) {
|
||||
DumpWideColumns(columns, os, hex);
|
||||
}
|
||||
|
||||
@@ -7,15 +7,9 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <forward_list>
|
||||
#include <iterator>
|
||||
#include <ostream>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/wide_columns.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -69,44 +63,4 @@ class WideColumnsHelper {
|
||||
}
|
||||
};
|
||||
|
||||
// Internal helper for mutating the private backing storage of a
|
||||
// PinnableWideColumns without exposing these operations on its public API.
|
||||
class PinnableWideColumnsHelper {
|
||||
public:
|
||||
// Replaces `columns`'s current columns with an already-resolved set. Every
|
||||
// name/value Slice in `resolved_columns` must point into stable memory:
|
||||
// either a buffer already held by `columns` (e.g. the original entity, for
|
||||
// inline columns) or one of the buffers in `extra_buffers` (e.g. fetched blob
|
||||
// values). Ownership of `extra_buffers` is transferred via splice; its nodes
|
||||
// are individually heap-allocated and never relocate, so Slices into them
|
||||
// stay valid. Clears any unresolved-blob metadata. No serialization occurs.
|
||||
static void ResolveColumns(PinnableWideColumns& columns,
|
||||
WideColumns&& resolved_columns,
|
||||
std::forward_list<PinnableSlice>&& extra_buffers) {
|
||||
columns.backing_.splice_after(columns.backing_.before_begin(),
|
||||
std::move(extra_buffers));
|
||||
columns.columns_ = std::move(resolved_columns);
|
||||
columns.unresolved_blob_column_indices_.clear();
|
||||
}
|
||||
|
||||
// Sorted positions of the columns whose values still hold encoded BlobIndex
|
||||
// payloads (V2 entities that have not been blob-resolved yet). Empty once
|
||||
// resolved or for entities without blob references.
|
||||
static const std::vector<size_t>& GetUnresolvedBlobColumnIndices(
|
||||
const PinnableWideColumns& columns) {
|
||||
return columns.unresolved_blob_column_indices_;
|
||||
}
|
||||
|
||||
// The backing buffer holding the serialized entity. Valid only before
|
||||
// resolution, when the entity occupies a single backing buffer (the state
|
||||
// right after SetWideColumnValue()); used as the input for blob resolution.
|
||||
static Slice GetSerializedEntity(const PinnableWideColumns& columns) {
|
||||
assert(!columns.backing_.empty());
|
||||
// Exactly one backing buffer: there is a single serialized entity and it
|
||||
// has not been split across resolved blob buffers yet.
|
||||
assert(std::next(columns.backing_.begin()) == columns.backing_.end());
|
||||
return columns.backing_.front();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -163,7 +163,7 @@ class BatchedOpsStressTest : public StressTest {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
keys[i] += key.ToString();
|
||||
key_slices[i] = keys[i];
|
||||
s = DbStressGet(db_, readoptionscopy, cfh, key_slices[i], &from_db);
|
||||
s = db_->Get(readoptionscopy, cfh, key_slices[i], &from_db);
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
fprintf(stderr, "get error: %s\n", s.ToString().c_str());
|
||||
values[i] = "";
|
||||
@@ -235,8 +235,8 @@ class BatchedOpsStressTest : public StressTest {
|
||||
key_str.emplace_back(keys[key] + Key(rand_keys[rand_key]));
|
||||
key_slices.emplace_back(key_str.back());
|
||||
}
|
||||
DbStressMultiGet(db_, readoptionscopy, cfh, num_prefixes,
|
||||
key_slices.data(), values.data(), statuses.data());
|
||||
db_->MultiGet(readoptionscopy, cfh, num_prefixes, key_slices.data(),
|
||||
values.data(), statuses.data());
|
||||
for (size_t i = 0; i < num_prefixes; i++) {
|
||||
Status s = statuses[i];
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
|
||||
@@ -218,7 +218,7 @@ class CfConsistencyStressTest : public StressTest {
|
||||
column_families_[rand_column_families[thread->rand.Next() %
|
||||
rand_column_families.size()]];
|
||||
std::string from_db;
|
||||
s = DbStressGet(db_, readoptions, cfh, key, &from_db);
|
||||
s = db_->Get(readoptions, cfh, key, &from_db);
|
||||
} else {
|
||||
// 1/2 chance, comparing one key is the same across all CFs
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
@@ -226,8 +226,8 @@ class CfConsistencyStressTest : public StressTest {
|
||||
readoptionscopy.snapshot = snapshot;
|
||||
|
||||
std::string value0;
|
||||
s = DbStressGet(db_, readoptionscopy,
|
||||
column_families_[rand_column_families[0]], key, &value0);
|
||||
s = db_->Get(readoptionscopy, column_families_[rand_column_families[0]],
|
||||
key, &value0);
|
||||
|
||||
// Temporarily disable error injection for verification
|
||||
if (db_fault_injection_fs_) {
|
||||
@@ -241,9 +241,8 @@ class CfConsistencyStressTest : public StressTest {
|
||||
bool found = s.ok();
|
||||
for (size_t i = 1; i < rand_column_families.size(); i++) {
|
||||
std::string value1;
|
||||
s = DbStressGet(db_, readoptionscopy,
|
||||
column_families_[rand_column_families[i]], key,
|
||||
&value1);
|
||||
s = db_->Get(readoptionscopy,
|
||||
column_families_[rand_column_families[i]], key, &value1);
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
break;
|
||||
}
|
||||
@@ -327,8 +326,8 @@ class CfConsistencyStressTest : public StressTest {
|
||||
key_str.emplace_back(Key(rand_keys[i]));
|
||||
keys.emplace_back(key_str.back());
|
||||
}
|
||||
DbStressMultiGet(db_, readoptionscopy, cfh, num_keys, keys.data(),
|
||||
values.data(), statuses.data());
|
||||
db_->MultiGet(readoptionscopy, cfh, num_keys, keys.data(), values.data(),
|
||||
statuses.data());
|
||||
for (const auto& s : statuses) {
|
||||
if (s.ok()) {
|
||||
// found case
|
||||
@@ -1478,14 +1477,14 @@ class CfConsistencyStressTest : public StressTest {
|
||||
db_->GetEntity(snapshot_read_opts, cfh, key, &snapshot_entity);
|
||||
std::string snapshot_value;
|
||||
const Status snapshot_value_status =
|
||||
DbStressGet(db_, snapshot_read_opts, cfh, key, &snapshot_value);
|
||||
db_->Get(snapshot_read_opts, cfh, key, &snapshot_value);
|
||||
|
||||
PinnableWideColumns latest_entity;
|
||||
const Status latest_entity_status =
|
||||
db_->GetEntity(latest_read_opts, cfh, key, &latest_entity);
|
||||
std::string latest_value;
|
||||
const Status latest_value_status =
|
||||
DbStressGet(db_, latest_read_opts, cfh, key, &latest_value);
|
||||
db_->Get(latest_read_opts, cfh, key, &latest_value);
|
||||
|
||||
std::string snapshot_verify = "n/a";
|
||||
if (snapshot_entity_status.ok()) {
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
#include "db_stress_tool/db_stress_common.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
|
||||
#include "db_stress_tool/db_stress_test_base.h"
|
||||
#include "file/file_util.h"
|
||||
@@ -38,84 +36,6 @@ constexpr int64_t zipf_sum_size = 100000;
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace {
|
||||
|
||||
class BlockingAsyncCallback : public DB::AsyncCallback {
|
||||
public:
|
||||
void OnComplete(const PerfContext* /*perf_context*/,
|
||||
const IOStatsContext* /*iostats_context*/) override {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
done_ = true;
|
||||
cv_.notify_one();
|
||||
}
|
||||
|
||||
void Wait() {
|
||||
std::unique_lock<std::mutex> lock(mu_);
|
||||
cv_.wait(lock, [this] { return done_; });
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mu_;
|
||||
std::condition_variable cv_;
|
||||
bool done_ = false;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Status DbStressGet(DB* db, const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp) {
|
||||
#if USE_COROUTINES
|
||||
if (FLAGS_use_async_db_api) {
|
||||
BlockingAsyncCallback callback;
|
||||
Status status;
|
||||
db->GetAsync(options, column_family, key, value, timestamp, status,
|
||||
callback);
|
||||
callback.Wait();
|
||||
return status;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
return db->Get(options, column_family, key, value, timestamp);
|
||||
}
|
||||
|
||||
Status DbStressGet(DB* db, const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
std::string* value, std::string* timestamp) {
|
||||
#if USE_COROUTINES
|
||||
if (FLAGS_use_async_db_api) {
|
||||
BlockingAsyncCallback callback;
|
||||
Status status;
|
||||
db->GetAsync(options, column_family, key, value, timestamp, status,
|
||||
callback);
|
||||
callback.Wait();
|
||||
return status;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
return db->Get(options, column_family, key, value, timestamp);
|
||||
}
|
||||
|
||||
Status DbStressGet(DB* db, const ReadOptions& options, const Slice& key,
|
||||
std::string* value) {
|
||||
return DbStressGet(db, options, db->DefaultColumnFamily(), key, value);
|
||||
}
|
||||
|
||||
void DbStressMultiGet(DB* db, const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, size_t num_keys,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
Status* statuses) {
|
||||
#if USE_COROUTINES
|
||||
if (FLAGS_use_async_db_api) {
|
||||
std::vector<ColumnFamilyHandle*> column_families(num_keys, column_family);
|
||||
BlockingAsyncCallback callback;
|
||||
db->MultiGetAsync(options, num_keys, column_families.data(), keys, values,
|
||||
statuses, callback);
|
||||
callback.Wait();
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
db->MultiGet(options, column_family, num_keys, keys, values, statuses);
|
||||
}
|
||||
|
||||
// Zipfian distribution is generated based on a pre-calculated array.
|
||||
// It should be used before start the stress test.
|
||||
// First, the probability distribution function (PDF) of this Zipfian follows
|
||||
|
||||
Loaded 100 of 209 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user