mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-07 11:32:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a19ac636e7 | ||
|
|
4cc5e15c15 | ||
|
|
b3063c1db0 | ||
|
|
c62e9a0d19 | ||
|
|
abeebd9630 | ||
|
|
3730e34363 | ||
|
|
d4a9fdc01b | ||
|
|
9657aad4ec | ||
|
|
b4fd1daa68 | ||
|
|
3a75b13f6a | ||
|
|
7247edbf0a | ||
|
|
f5c0c3e274 | ||
|
|
2fc58a678b | ||
|
|
a4645e864d | ||
|
|
039c45075e | ||
|
|
fbd446cebb | ||
|
|
6f5587ae2d | ||
|
|
c73841572a | ||
|
|
14a7646fb5 | ||
|
|
03c3c090fb | ||
|
|
71d3bd1773 | ||
|
|
bb77d17072 | ||
|
|
1eb4a6e3e1 | ||
|
|
19482988e3 | ||
|
|
0c0bfbe8be | ||
|
|
fac47c3072 | ||
|
|
f56112abb7 | ||
|
|
744f120b12 | ||
|
|
0347b4df61 | ||
|
|
755c104bca | ||
|
|
0a2ba2a164 | ||
|
|
dcb5c2bf53 | ||
|
|
7c385f4441 | ||
|
|
2c77c32ac9 | ||
|
|
2b628ccfe5 | ||
|
|
0745290ca6 | ||
|
|
ffa95117b9 | ||
|
|
30ba4908ba | ||
|
|
144deb248a | ||
|
|
990827e916 | ||
|
|
7c4bf11044 | ||
|
|
81ea677072 | ||
|
|
04cad97e7e | ||
|
|
865568398a | ||
|
|
2809ce9d0f | ||
|
|
223b5ee13d | ||
|
|
6d8370ec56 | ||
|
|
d461606f0f | ||
|
|
c9536aaef7 | ||
|
|
cf22ee8f5b | ||
|
|
c7f7b1b2ae | ||
|
|
dc72a64a5b | ||
|
|
74aba599ab | ||
|
|
329d9e7adf | ||
|
|
5b1c3b37ac | ||
|
|
eca1a114e0 | ||
|
|
7c80a5ac85 | ||
|
|
053a9de468 | ||
|
|
f32f747125 | ||
|
|
c3f6e4d625 | ||
|
|
e1ea04d9ef | ||
|
|
27b48646cd | ||
|
|
66bccb467d | ||
|
|
0f15da1c2d | ||
|
|
13d55eba73 | ||
|
|
1192897aad |
No files matched your search
@@ -714,8 +714,6 @@ jobs:
|
||||
CODEX_HOME: /tmp/codex-home
|
||||
CODEX_MODEL: ${{ inputs.default_model }}
|
||||
CODEX_REASONING_EFFORT: ${{ steps.budget_codex.outputs.effort }}
|
||||
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
|
||||
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
|
||||
run: |
|
||||
if [ -z "${OPENAI_API_KEY}" ]; then
|
||||
echo "OPENAI_API_KEY not configured, skipping Codex review"
|
||||
@@ -723,9 +721,7 @@ jobs:
|
||||
fi
|
||||
mkdir -p "${CODEX_HOME}"
|
||||
set +e
|
||||
codex exec review \
|
||||
--base "${BASE_SHA}" \
|
||||
--title "${PR_TITLE}" \
|
||||
codex exec \
|
||||
-m "${CODEX_MODEL}" \
|
||||
-c model_reasoning_effort="${CODEX_REASONING_EFFORT}" \
|
||||
--dangerously-bypass-approvals-and-sandbox \
|
||||
@@ -1340,8 +1336,6 @@ jobs:
|
||||
CODEX_HOME: /tmp/codex-home
|
||||
CODEX_MODEL: ${{ inputs.selected_model != '' && inputs.selected_model || inputs.default_model }}
|
||||
CODEX_REASONING_EFFORT: ${{ steps.manual_budget_codex.outputs.effort }}
|
||||
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
|
||||
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
|
||||
COMMAND_TYPE: ${{ steps.request.outputs.type }}
|
||||
run: |
|
||||
mkdir -p "${CODEX_HOME}"
|
||||
@@ -1354,9 +1348,7 @@ jobs:
|
||||
--output-last-message codex-review-output.md \
|
||||
- < /tmp/prompt.txt > codex-review-execution.log 2>&1
|
||||
else
|
||||
codex exec review \
|
||||
--base "${BASE_SHA}" \
|
||||
--title "${PR_TITLE}" \
|
||||
codex exec \
|
||||
-m "${CODEX_MODEL}" \
|
||||
-c model_reasoning_effort="${CODEX_REASONING_EFFORT}" \
|
||||
--dangerously-bypass-approvals-and-sandbox \
|
||||
|
||||
@@ -40,6 +40,7 @@ jobs:
|
||||
name: ${{ inputs.result_artifact_name }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path: _artifact
|
||||
continue-on-error: true
|
||||
|
||||
- name: Post or update PR comment
|
||||
@@ -52,8 +53,18 @@ jobs:
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync(process.env.COMMENT_FILE) ||
|
||||
!fs.existsSync('pr_number.txt')) {
|
||||
const path = require('path');
|
||||
const artifactDir = '_artifact';
|
||||
const commentFile = path.join(
|
||||
artifactDir,
|
||||
process.env.COMMENT_FILE
|
||||
);
|
||||
const prNumberFile = path.join(artifactDir, 'pr_number.txt');
|
||||
const triggerTypeFile = path.join(artifactDir, 'trigger_type.txt');
|
||||
const headShaFile = path.join(artifactDir, 'head_sha.txt');
|
||||
|
||||
if (!fs.existsSync(commentFile) ||
|
||||
!fs.existsSync(prNumberFile)) {
|
||||
core.info('No review results found; skipping.');
|
||||
return;
|
||||
}
|
||||
@@ -61,16 +72,16 @@ jobs:
|
||||
const post = require('./.github/scripts/post-pr-comment.js');
|
||||
const provider = process.env.PROVIDER;
|
||||
const providerTitle = provider === 'codex' ? 'Codex' : 'Claude';
|
||||
const body = fs.readFileSync(process.env.COMMENT_FILE, 'utf8');
|
||||
const body = fs.readFileSync(commentFile, 'utf8');
|
||||
const prNumber = parseInt(
|
||||
fs.readFileSync('pr_number.txt', 'utf8').trim(),
|
||||
fs.readFileSync(prNumberFile, 'utf8').trim(),
|
||||
10
|
||||
);
|
||||
const trigger = fs.existsSync('trigger_type.txt') ?
|
||||
fs.readFileSync('trigger_type.txt', 'utf8').trim() :
|
||||
const trigger = fs.existsSync(triggerTypeFile) ?
|
||||
fs.readFileSync(triggerTypeFile, 'utf8').trim() :
|
||||
'auto';
|
||||
const headSha = fs.existsSync('head_sha.txt') ?
|
||||
fs.readFileSync('head_sha.txt', 'utf8').trim() :
|
||||
const headSha = fs.existsSync(headShaFile) ?
|
||||
fs.readFileSync(headShaFile, 'utf8').trim() :
|
||||
'';
|
||||
const shortSha = headSha ? headSha.substring(0, 7) : '';
|
||||
const runId = process.env.RUN_ID;
|
||||
@@ -119,12 +130,16 @@ jobs:
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync('trigger_type.txt')) return;
|
||||
const trigger = fs.readFileSync('trigger_type.txt', 'utf8').trim();
|
||||
const path = require('path');
|
||||
const artifactDir = '_artifact';
|
||||
const triggerTypeFile = path.join(artifactDir, 'trigger_type.txt');
|
||||
const commentIdFile = path.join(artifactDir, 'comment_id.txt');
|
||||
if (!fs.existsSync(triggerTypeFile)) return;
|
||||
const trigger = fs.readFileSync(triggerTypeFile, 'utf8').trim();
|
||||
if (trigger !== 'manual') return;
|
||||
if (!fs.existsSync('comment_id.txt')) return;
|
||||
if (!fs.existsSync(commentIdFile)) return;
|
||||
const commentId = parseInt(
|
||||
fs.readFileSync('comment_id.txt', 'utf8').trim(),
|
||||
fs.readFileSync(commentIdFile, 'utf8').trim(),
|
||||
10
|
||||
);
|
||||
if (!commentId) return;
|
||||
@@ -147,6 +162,7 @@ jobs:
|
||||
name: ${{ inputs.result_artifact_name }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path: _artifact
|
||||
continue-on-error: true
|
||||
|
||||
- name: React with failure
|
||||
@@ -155,9 +171,11 @@ jobs:
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync('comment_id.txt')) return;
|
||||
const path = require('path');
|
||||
const commentIdFile = path.join('_artifact', 'comment_id.txt');
|
||||
if (!fs.existsSync(commentIdFile)) return;
|
||||
const commentId = parseInt(
|
||||
fs.readFileSync('comment_id.txt', 'utf8').trim(),
|
||||
fs.readFileSync(commentIdFile, 'utf8').trim(),
|
||||
10
|
||||
);
|
||||
if (!commentId) return;
|
||||
|
||||
@@ -25,6 +25,7 @@ jobs:
|
||||
name: clang-tidy-result
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path: _artifact
|
||||
continue-on-error: true
|
||||
|
||||
- name: Post or update PR comment
|
||||
@@ -33,12 +34,19 @@ jobs:
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync('clang-tidy-comment.md') || !fs.existsSync('pr_number.txt')) {
|
||||
if (!fs.existsSync('_artifact/clang-tidy-comment.md') ||
|
||||
!fs.existsSync('_artifact/pr_number.txt')) {
|
||||
core.info('No clang-tidy results found; skipping.');
|
||||
return;
|
||||
}
|
||||
const body = fs.readFileSync('clang-tidy-comment.md', 'utf8');
|
||||
const prNumber = parseInt(fs.readFileSync('pr_number.txt', 'utf8').trim());
|
||||
const body = fs.readFileSync(
|
||||
'_artifact/clang-tidy-comment.md',
|
||||
'utf8'
|
||||
);
|
||||
const prNumber = parseInt(
|
||||
fs.readFileSync('_artifact/pr_number.txt', 'utf8').trim(),
|
||||
10
|
||||
);
|
||||
const post = require('./.github/scripts/post-pr-comment.js');
|
||||
|
||||
await post({
|
||||
|
||||
@@ -22,4 +22,3 @@ jobs:
|
||||
provider: claude
|
||||
result_artifact_name: claude-review-result
|
||||
comment_file: claude-review-comment.md
|
||||
secrets: inherit
|
||||
@@ -22,4 +22,3 @@ jobs:
|
||||
provider: codex
|
||||
result_artifact_name: codex-review-result
|
||||
comment_file: codex-review-comment.md
|
||||
secrets: inherit
|
||||
@@ -7,14 +7,9 @@
|
||||
name: Codex Code Review
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["facebook/rocksdb/pr-jobs"]
|
||||
types: [completed]
|
||||
|
||||
# The early pull_request_target path is limited to same-repo PRs by the job
|
||||
# condition below. Fork PRs skip this path and rely on workflow_run instead.
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
# Automatic PR reviews are temporarily disabled while Codex authentication is
|
||||
# repaired. Manual review requests remain available through issue comments
|
||||
# and workflow_dispatch.
|
||||
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
@@ -268,6 +268,7 @@ 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",
|
||||
@@ -306,6 +307,7 @@ 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",
|
||||
@@ -379,11 +381,21 @@ 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)
|
||||
@@ -4743,6 +4755,12 @@ 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.
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -243,6 +243,11 @@ from an implementation detail instead of an explicit option.
|
||||
* Don't manually edit BUCK file, after updating src.mk, run
|
||||
/usr/local/bin/python3 buckifier/buckify_rocksdb.py to update it
|
||||
* For -j in make command, use the number of CPU cores to decide it.
|
||||
* When searching for references to something (a symbol, library, etc.), do not
|
||||
restrict or truncate your search based on presumed relevance or scope. It is
|
||||
important and time-saving to keep the repo reasonably consistent across
|
||||
different build systems, programming languages, and even between
|
||||
documentation and implementation.
|
||||
|
||||
### Avoiding mixed build modes with Make (use `AUTO_CLEAN=1`)
|
||||
|
||||
@@ -293,6 +298,7 @@ build (and where noted, run tests) across:
|
||||
| Axis | Must support |
|
||||
|------|--------------|
|
||||
| OS | Linux (x86_64 + ARM), macOS, Windows |
|
||||
| Architecture | Little-endian and big-endian targets, including Linux on s390x |
|
||||
| Compiler | GCC, Clang (libstdc++ **and** libc++), AppleClang, **MSVC (VS2022)**, MinGW (Linux cross-compile, build-only, no gflags) |
|
||||
| Build system | Make, CMake, and BUCK (internal) -- keep all in sync (see "Build system" above) |
|
||||
| Config | release (`-fno-rtti`), `ASSERT_STATUS_CHECKED`, ASAN/UBSAN/TSAN, folly, unity build, JNI/Java |
|
||||
@@ -307,6 +313,12 @@ abstractions; otherwise guard with `#ifdef OS_WIN` (POSIX `<unistd.h>` ->
|
||||
Windows `<process.h>`). Because libc++ is also tested, include what you use
|
||||
rather than relying on libstdc++ transitive includes.
|
||||
|
||||
Do not use `static_assert` to reject a supported architecture for an optional
|
||||
feature. Check an existing `port::` capability such as `port::kLittleEndian`
|
||||
and disable only the unsupported feature so the rest of RocksDB still builds.
|
||||
Use a portable representation when the feature itself must work across
|
||||
architectures.
|
||||
|
||||
### Unit Test
|
||||
* After all of the unit tests are added, review them and try to extract common
|
||||
reusable utility functions to reduce code duplication due to copy past between
|
||||
@@ -334,6 +346,9 @@ rather than relying on libstdc++ transitive includes.
|
||||
* 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
|
||||
@@ -381,6 +396,8 @@ rather than relying on libstdc++ transitive includes.
|
||||
|
||||
### 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
|
||||
|
||||
+29
-3
@@ -698,10 +698,19 @@ if(USE_FOLLY)
|
||||
|
||||
include(${FOLLY_INST_PATH}/lib/cmake/folly/folly-config.cmake)
|
||||
|
||||
# Fix gflags library name for debug builds
|
||||
# Fix gflags library name for debug builds. The getdeps gflags shared
|
||||
# library is versioned (e.g. libgflags_debug.so.2.3), so resolve the
|
||||
# actual file by glob rather than hard-coding a version.
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
file(GLOB GFLAGS_DEBUG_SHARED_LIBS
|
||||
"${GFLAGS_INST_PATH}/lib/libgflags_debug.so.*")
|
||||
if(NOT GFLAGS_DEBUG_SHARED_LIBS)
|
||||
message(FATAL_ERROR
|
||||
"Could not find getdeps libgflags_debug.so under ${GFLAGS_INST_PATH}/lib")
|
||||
endif()
|
||||
list(GET GFLAGS_DEBUG_SHARED_LIBS 0 GFLAGS_DEBUG_SHARED_LIB)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath=${GFLAGS_INST_PATH}/lib")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${GFLAGS_INST_PATH}/lib/libgflags_debug.so.2.2")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${GFLAGS_DEBUG_SHARED_LIB}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -993,6 +1002,7 @@ 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
|
||||
@@ -1014,6 +1024,7 @@ 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
|
||||
@@ -1631,6 +1642,7 @@ 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
|
||||
@@ -1698,7 +1710,21 @@ 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")
|
||||
gtest_discover_tests(${exename} DISCOVERY_TIMEOUT 120)
|
||||
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()
|
||||
add_dependencies(rocksdb_check ${exename}${ARTIFACT_SUFFIX})
|
||||
endif()
|
||||
endforeach(sourcefile ${TESTS})
|
||||
|
||||
+30
@@ -1,6 +1,36 @@
|
||||
# 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.
|
||||
* Added `MultiScanArgs::reverse` to support reverse MultiScan reads over bounded scan ranges.
|
||||
|
||||
### Behavior Changes
|
||||
* When using `LockWAL()`, live-file capture APIs such as checkpoint and backup now flush WAL-disabled unpersisted data; they can block until `UnlockWAL()` or return `Aborted` when called from the same thread that holds `LockWAL()` to avoid deadlock. `UnlockWAL()` must now also be called from the same thread that called `LockWAL()`.
|
||||
|
||||
### Performance Improvements
|
||||
* Fixed slow DB open with `best_efforts_recovery`, and slow catch-up of secondary and follower (read-only) instances, in which time and memory grew quadratically with the number of files in the DB (so the slowdown was worst for large DBs with long history).
|
||||
|
||||
## 11.6.0 (07/02/2026)
|
||||
### New Features
|
||||
* Added EXPERIMENTAL embedded blob SST support through `SstFileWriter::OpenWithEmbeddedBlobs()`, storing eligible large values as same-file blob records in block-based SST files and resolving them transparently for reads. This niche feature currently supports uncompressed embedded blobs only; compression options are placeholders and compression support is deferred to follow-up work.
|
||||
|
||||
@@ -1174,30 +1174,43 @@ 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); 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.
|
||||
# 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.
|
||||
@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; \
|
||||
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; \
|
||||
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; \
|
||||
elif [ -n "$$strict" ]; then \
|
||||
echo "ERROR: no clang++ found and CHECK_C_API_GEN_STRICT is set; cannot run the C API staleness check" >&2; exit 1; \
|
||||
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; \
|
||||
else \
|
||||
echo "Skipping C API codegen staleness check (no clang++ found; install clang++ or set CXX to a clang to enable)"; \
|
||||
echo "Skipping C API codegen staleness check (no clang++ found; CLANG_CXX unset, install 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
|
||||
$(PYTHON) tools/c_api_gen/verify_generated_up_to_date.py --clang "$(CLANG_CXX)"
|
||||
$(MAKE) c_test
|
||||
@tmpdir=$$(mktemp -d); \
|
||||
trap 'rm -rf "$$tmpdir"' EXIT; \
|
||||
@@ -1816,6 +1829,9 @@ 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,11 +145,21 @@ 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,13 +89,36 @@ 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" -a -x "$(command -v clang++)" ]; then
|
||||
CXX=clang++
|
||||
if [ "$USE_CLANG" ] && [ -n "$CLANG_CXX" ]; then
|
||||
CXX="$CLANG_CXX"
|
||||
elif [ -x "$(command -v g++)" ]; then
|
||||
CXX=g++
|
||||
elif [ -x "$(command -v clang++)" ]; then
|
||||
CXX=clang++
|
||||
elif [ -n "$CLANG_CXX" ]; then
|
||||
CXX="$CLANG_CXX"
|
||||
else
|
||||
CXX=g++
|
||||
fi
|
||||
@@ -113,6 +136,17 @@ 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`
|
||||
@@ -127,7 +161,7 @@ if test -z "$CLANG_SCAN_BUILD"; then
|
||||
fi
|
||||
|
||||
if test -z "$CLANG_ANALYZER"; then
|
||||
CLANG_ANALYZER=$(command -v clang++ 2> /dev/null)
|
||||
CLANG_ANALYZER="$CLANG_CXX"
|
||||
fi
|
||||
|
||||
if test -z "$FIND"; then
|
||||
@@ -818,6 +852,10 @@ 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,6 +104,10 @@ 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"
|
||||
|
||||
@@ -167,4 +171,4 @@ EXEC_LDFLAGS_SHARED="$SNAPPY_LIBS $ZLIB_LIBS $BZIP_LIBS $LZ4_LIBS $ZSTD_LIBS $GF
|
||||
|
||||
VALGRIND_VER="$VALGRIND_BASE/bin/"
|
||||
|
||||
export CC CXX AR AS CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD
|
||||
export CC CXX AR AS CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_CXX CLANG_ANALYZER CLANG_SCAN_BUILD
|
||||
@@ -1,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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.
|
||||
@@ -478,6 +482,17 @@ 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;
|
||||
@@ -490,6 +505,17 @@ 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,3 +1,7 @@
|
||||
// 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.
|
||||
@@ -342,6 +346,14 @@ 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);
|
||||
@@ -352,6 +364,14 @@ 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,3 +1,7 @@
|
||||
// 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.
|
||||
@@ -137,6 +141,15 @@ 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;
|
||||
@@ -1674,6 +1687,16 @@ 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,
|
||||
@@ -2703,6 +2726,36 @@ 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;
|
||||
@@ -2754,26 +2807,6 @@ 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;
|
||||
@@ -2794,16 +2827,6 @@ 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;
|
||||
@@ -2858,16 +2881,6 @@ 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;
|
||||
@@ -2890,6 +2903,16 @@ 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,3 +1,7 @@
|
||||
// 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.
|
||||
@@ -51,6 +55,12 @@ 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);
|
||||
|
||||
@@ -541,6 +551,12 @@ 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(
|
||||
@@ -1313,6 +1329,30 @@ 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);
|
||||
|
||||
@@ -1350,22 +1390,6 @@ 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);
|
||||
@@ -1382,14 +1406,6 @@ 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);
|
||||
@@ -1431,14 +1447,6 @@ 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);
|
||||
@@ -1455,6 +1463,14 @@ 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,9 +1,13 @@
|
||||
// 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: python3 tools/c_api_gen/regen_all.py
|
||||
// Rerun `make regen-c-api`
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
static void rocksdb_roundtrip_test_rocksdb_block_based_options(void) {
|
||||
@@ -459,6 +463,10 @@ 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);
|
||||
@@ -841,6 +849,10 @@ 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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,3 +1,7 @@
|
||||
// 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.
|
||||
|
||||
+34
-7
@@ -7,6 +7,7 @@
|
||||
|
||||
#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 {
|
||||
@@ -25,21 +26,47 @@ Status BlobFetcher::FetchBlob(const Slice& user_key,
|
||||
return status;
|
||||
}
|
||||
|
||||
Status BlobFetcher::FetchBlob(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
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();
|
||||
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
|
||||
+157
-13
@@ -5,6 +5,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
@@ -16,32 +18,174 @@ class Slice;
|
||||
class FilePrefetchBuffer;
|
||||
class PinnableSlice;
|
||||
class BlobIndex;
|
||||
class SameFileBlobReader;
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
class BlobFetcher {
|
||||
public:
|
||||
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) {}
|
||||
virtual ~BlobFetcher() = default;
|
||||
|
||||
// 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;
|
||||
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) {}
|
||||
|
||||
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,11 +215,10 @@ 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::DeserializeV2(entity_ref, columns,
|
||||
existing_blob_columns);
|
||||
Status status = WideColumnSerialization::Deserialize(entity, 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
|
||||
|
||||
+306
-13
@@ -143,14 +143,24 @@ class DBBlobIndexTest : public DBTestBase {
|
||||
columns, s, 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);
|
||||
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);
|
||||
}
|
||||
|
||||
Options GetTestOptions() {
|
||||
@@ -309,17 +319,65 @@ TEST_F(DBBlobIndexTest,
|
||||
value.GetSelf()->assign(blob_index.data(), blob_index.size());
|
||||
value.PinSelf();
|
||||
|
||||
Status s = Status::OK();
|
||||
bool did_resolve = false;
|
||||
bool is_blob_index = true;
|
||||
ASSERT_TRUE(MaybeResolveMemtableBlobValueForTest(
|
||||
Slice("key"), /*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr, &s,
|
||||
&is_blob_index));
|
||||
Status s = MaybeResolveMemtableBlobValueForTest(
|
||||
Slice("key"), /*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr,
|
||||
&did_resolve, &is_blob_index);
|
||||
|
||||
ASSERT_TRUE(s.IsNotSupported()) << s.ToString();
|
||||
ASSERT_TRUE(did_resolve);
|
||||
ASSERT_TRUE(s.IsCorruption()) << 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
|
||||
@@ -775,6 +833,241 @@ 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,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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: python3 tools/c_api_gen/regen_all.py
|
||||
// then rerun `make regen-c-api`
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
#include "rocksdb/c.h"
|
||||
@@ -5289,6 +5289,17 @@ 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;
|
||||
@@ -5301,6 +5312,17 @@ 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;
|
||||
@@ -9085,6 +9107,15 @@ 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;
|
||||
@@ -10622,6 +10653,16 @@ 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,
|
||||
@@ -11651,6 +11692,36 @@ 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;
|
||||
@@ -11702,26 +11773,6 @@ 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;
|
||||
@@ -11742,16 +11793,6 @@ 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;
|
||||
@@ -11806,16 +11847,6 @@ 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;
|
||||
@@ -11838,6 +11869,16 @@ 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,6 +4531,11 @@ 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,8 +251,9 @@ void CompactionIterator::SetBlobFetcher(const Version* version,
|
||||
read_options.io_activity = io_activity;
|
||||
read_options.fill_cache = false;
|
||||
|
||||
blob_fetcher_ = std::make_unique<BlobFetcher>(
|
||||
version, read_options, blob_file_cache, allow_write_path_fallback);
|
||||
blob_fetcher_ = std::make_unique<OwningVersionBlobFetcher>(
|
||||
version, std::move(read_options), blob_file_cache,
|
||||
allow_write_path_fallback);
|
||||
blob_resolver_.Init(blob_fetcher_.get(), prefetch_buffers_.get(),
|
||||
&iter_stats_);
|
||||
}
|
||||
@@ -467,14 +468,14 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
}
|
||||
}
|
||||
if (UNLIKELY(has_blob_columns)) {
|
||||
// Entity has blob columns. DeserializeV2() populates
|
||||
// Entity has blob columns. Deserialize() 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::DeserializeV2(
|
||||
input_copy, entity_columns_, entity_blob_columns_);
|
||||
Status s = WideColumnSerialization::Deserialize(
|
||||
input_copy, entity_columns_, &entity_blob_columns_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
@@ -541,7 +542,7 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
} else {
|
||||
// No blob columns, use fast path
|
||||
Slice value_copy = value_;
|
||||
const Status s = WideColumnSerialization::Deserialize(
|
||||
const Status s = WideColumnSerialization::DeserializeSimple(
|
||||
value_copy, filter_existing_columns_);
|
||||
|
||||
if (!s.ok()) {
|
||||
@@ -1550,8 +1551,8 @@ void CompactionIterator::ExtractLargeColumnValuesIfNeeded() {
|
||||
Slice entity_slice = value_;
|
||||
entity_columns_.clear();
|
||||
entity_blob_columns_.clear();
|
||||
Status s = WideColumnSerialization::DeserializeV2(
|
||||
entity_slice, entity_columns_, entity_blob_columns_);
|
||||
Status s = WideColumnSerialization::Deserialize(
|
||||
entity_slice, entity_columns_, &entity_blob_columns_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
@@ -1669,7 +1670,7 @@ bool CompactionIterator::FetchBlobsNeedingGC(
|
||||
Status s = blob_fetcher_->FetchBlob(user_key(), blob_index, prefetch_buffer,
|
||||
&blob_value, &bytes_read);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
status_ = std::move(s);
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
@@ -1817,8 +1818,8 @@ void CompactionIterator::GarbageCollectEntityBlobsIfNeeded() {
|
||||
Slice entity_slice = value_;
|
||||
entity_columns_.clear();
|
||||
entity_blob_columns_.clear();
|
||||
Status s = WideColumnSerialization::DeserializeV2(
|
||||
entity_slice, entity_columns_, entity_blob_columns_);
|
||||
Status s = WideColumnSerialization::Deserialize(
|
||||
entity_slice, entity_columns_, &entity_blob_columns_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
@@ -1869,13 +1870,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() + DeserializeV2().
|
||||
// This avoids the double parse of HasBlobColumns() + Deserialize().
|
||||
entity_columns_.clear();
|
||||
entity_blob_columns_.clear();
|
||||
Slice entity_slice = value_;
|
||||
{
|
||||
Status s_deser = WideColumnSerialization::DeserializeV2(
|
||||
entity_slice, entity_columns_, entity_blob_columns_);
|
||||
Status s_deser = WideColumnSerialization::Deserialize(
|
||||
entity_slice, entity_columns_, &entity_blob_columns_);
|
||||
if (!s_deser.ok()) {
|
||||
status_ = s_deser;
|
||||
validity_info_.Invalidate();
|
||||
@@ -2064,8 +2065,9 @@ std::unique_ptr<BlobFetcher> CompactionIterator::CreateBlobFetcherIfNeeded(
|
||||
}
|
||||
}
|
||||
|
||||
return std::unique_ptr<BlobFetcher>(new BlobFetcher(
|
||||
version, read_options, blob_file_cache, allow_write_path_fallback));
|
||||
return std::unique_ptr<BlobFetcher>(
|
||||
new OwningVersionBlobFetcher(version, std::move(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::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(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::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(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::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(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::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns));
|
||||
|
||||
// The default column should be a blob reference
|
||||
ASSERT_EQ(1, blob_columns.size());
|
||||
@@ -2078,11 +2078,14 @@ TEST_F(WideColumnEntityBlobExtractionTest,
|
||||
// Mock BlobFetcher for testing blob GC without a real Version
|
||||
class MockBlobFetcher : public BlobFetcher {
|
||||
public:
|
||||
MockBlobFetcher() : BlobFetcher(nullptr /* version */, ReadOptions()) {}
|
||||
MockBlobFetcher() = default;
|
||||
|
||||
using BlobFetcher::FetchBlob;
|
||||
|
||||
Status FetchBlob(const Slice& /* user_key */, const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* /* prefetch_buffer */,
|
||||
PinnableSlice* blob_value, uint64_t* bytes_read) const {
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const override {
|
||||
auto it = blob_data_.find(
|
||||
std::make_pair(blob_index.file_number(), blob_index.offset()));
|
||||
if (it == blob_data_.end()) {
|
||||
@@ -2096,6 +2099,8 @@ 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;
|
||||
@@ -2103,6 +2108,7 @@ 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
|
||||
@@ -2372,8 +2378,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::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns_out));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns_out));
|
||||
|
||||
// Entity structure should be preserved
|
||||
ASSERT_EQ(2, output_columns.size());
|
||||
@@ -2431,8 +2437,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::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns_out));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns_out));
|
||||
|
||||
// Blob column should still exist and reference the same file
|
||||
ASSERT_EQ(1, blob_columns_out.size());
|
||||
@@ -2491,8 +2497,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::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns_out));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns_out));
|
||||
|
||||
// All three columns should exist
|
||||
ASSERT_EQ(3, output_columns.size());
|
||||
@@ -2563,8 +2569,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::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns_out));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns_out));
|
||||
|
||||
// Both columns should exist
|
||||
ASSERT_EQ(2, output_columns.size());
|
||||
@@ -2788,8 +2794,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::DeserializeV2(value_slice, output_columns,
|
||||
output_blob_columns));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&output_blob_columns));
|
||||
|
||||
ASSERT_EQ(2, output_columns.size());
|
||||
ASSERT_EQ("alpha", output_columns[0].name().ToString());
|
||||
|
||||
@@ -777,6 +777,40 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
||||
OptionTypeFlags::kNone}},
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
// Like OptionTypeInfo::Array<T, kSize> but deserialization tolerates
|
||||
// size mismatches: extra elements beyond kSize are silently ignored,
|
||||
// missing elements are zero-filled. Serialization and comparison are
|
||||
// inherited from Array<T, kSize> unchanged; only the parse
|
||||
// (deserialize) callback is overridden.
|
||||
template <typename T, size_t kSize>
|
||||
OptionTypeInfo SizeTolerantDeserializeArray(int offset,
|
||||
OptionVerificationType verification,
|
||||
OptionTypeFlags flags,
|
||||
const OptionTypeInfo& elem_info,
|
||||
char separator = ':') {
|
||||
return OptionTypeInfo::Array<T, kSize>(offset, verification, flags, elem_info,
|
||||
separator)
|
||||
.SetParseFunc([elem_info, separator](
|
||||
const ConfigOptions& opts, const std::string& name,
|
||||
const std::string& value, void* addr) {
|
||||
std::vector<T> parsed;
|
||||
Status s =
|
||||
ParseVector<T>(opts, elem_info, separator, name, value, &parsed);
|
||||
if (!s.ok()) {
|
||||
return Status(std::move(s));
|
||||
}
|
||||
auto* arr = static_cast<std::array<T, kSize>*>(addr);
|
||||
for (size_t i = 0; i < kSize; ++i) {
|
||||
(*arr)[i] = (i < parsed.size()) ? parsed[i] : T{};
|
||||
}
|
||||
return Status::OK();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo>
|
||||
compaction_stats_type_info = {
|
||||
{"micros",
|
||||
@@ -868,12 +902,20 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
||||
{offsetof(struct InternalStats::CompactionStats, count),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
// The remote worker may serialize a different number of
|
||||
// CompactionReason counts than this primary expects (RocksDB versions
|
||||
// can disagree on kNumOfReasons). Deserialization tolerates the
|
||||
// mismatch: extra trailing elements are dropped, missing ones are
|
||||
// zero-filled.
|
||||
//
|
||||
// The serialized width is intentionally kept at kNumOfReasons - 1 (the
|
||||
// reduced width) so this change does NOT alter what is written -- only
|
||||
// the read path becomes tolerant. Widening the serialized width to the
|
||||
// full kNumOfReasons is a separate, later change that must not land
|
||||
// until this tolerant read has been deployed to every primary,
|
||||
// including rollback targets.
|
||||
{"counts",
|
||||
OptionTypeInfo::Array<
|
||||
/* In release 11.2, a new compaction reason was added. This broken
|
||||
* the reader and writer. To unblock release 11.1, we temporarily
|
||||
* reduce the count array size to the old one. TODO add a proper
|
||||
* serialization and deserialization method. */
|
||||
SizeTolerantDeserializeArray<
|
||||
int, static_cast<int>(CompactionReason::kNumOfReasons) - 1>(
|
||||
offsetof(struct InternalStats::CompactionStats, counts),
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "file/file_util.h"
|
||||
#include "port/stack_trace.h"
|
||||
@@ -12,6 +14,71 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace {
|
||||
|
||||
// Append ":extra_value" to every "counts=...;" field in a serialized
|
||||
// CompactionServiceResult string to simulate a worker with more
|
||||
// CompactionReason enums than the reader. Loops because the serialized
|
||||
// result contains multiple CompactionStats structs (output_level_stats,
|
||||
// proximal_level_stats), each with its own "counts=" field.
|
||||
void AppendToCountsFields(std::string* s, const std::string& extra_value) {
|
||||
const std::string prefix = "counts=";
|
||||
size_t pos = s->find(prefix);
|
||||
while (pos != std::string::npos) {
|
||||
// Find the semicolon that terminates this field's value.
|
||||
size_t semi = s->find(';', pos + prefix.size());
|
||||
assert(semi != std::string::npos);
|
||||
// Insert ":extra_value" just before the semicolon.
|
||||
s->insert(semi, ":" + extra_value);
|
||||
// Advance past the inserted text to find the next occurrence.
|
||||
pos = s->find(prefix, semi + extra_value.size() + 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the last colon-separated element from every "counts=...;" field
|
||||
// to simulate a worker with fewer CompactionReason enums than the reader.
|
||||
// Loops for the same reason as AppendToCountsFields (multiple counts fields).
|
||||
void RemoveLastFromCountsFields(std::string* s) {
|
||||
const std::string prefix = "counts=";
|
||||
size_t pos = s->find(prefix);
|
||||
while (pos != std::string::npos) {
|
||||
size_t val_begin = pos + prefix.size();
|
||||
size_t semi = s->find(';', val_begin);
|
||||
assert(semi != std::string::npos);
|
||||
// Find the last ':' separator within the value.
|
||||
size_t last_sep = s->rfind(':', semi - 1);
|
||||
assert(last_sep != std::string::npos && last_sep >= val_begin);
|
||||
// Erase from the last ':' up to (not including) the ';'.
|
||||
// e.g. "counts=0:3:5;" -> erase ":5" -> "counts=0:3;"
|
||||
s->erase(last_sep, semi - last_sep);
|
||||
pos = s->find(prefix, last_sep);
|
||||
}
|
||||
}
|
||||
|
||||
// Count the colon-separated elements in the first "counts=...;" field of a
|
||||
// serialized CompactionServiceResult. Used to assert the serialized width
|
||||
// (how many CompactionReason counts this binary writes).
|
||||
size_t CountFirstCountsFieldElements(const std::string& s) {
|
||||
const std::string prefix = "counts=";
|
||||
size_t pos = s.find(prefix);
|
||||
assert(pos != std::string::npos);
|
||||
size_t val_begin = pos + prefix.size();
|
||||
size_t semi = s.find(';', val_begin);
|
||||
assert(semi != std::string::npos);
|
||||
if (semi == val_begin) {
|
||||
return 0;
|
||||
}
|
||||
size_t count = 1;
|
||||
for (size_t i = val_begin; i < semi; ++i) {
|
||||
if (s[i] == ':') {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class MyTestCompactionService : public CompactionService {
|
||||
public:
|
||||
MyTestCompactionService(
|
||||
@@ -1609,6 +1676,68 @@ TEST_F(CompactionServiceTest, InvalidResultFallsBackToLocal) {
|
||||
my_cs->GetFinalCompactionServiceJobStatus());
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, CompatCheckCountsWidthTolerance) {
|
||||
// Number of per-CompactionReason counts this binary serializes and expects.
|
||||
constexpr size_t kExpected =
|
||||
static_cast<size_t>(CompactionReason::kNumOfReasons) - 1;
|
||||
constexpr size_t kLastIndex = kExpected - 1;
|
||||
constexpr int kFirstCount = 3;
|
||||
constexpr int kLastCount = 5;
|
||||
|
||||
CompactionServiceResult result;
|
||||
result.status = Status::OK();
|
||||
result.output_level = 2;
|
||||
result.output_path = "/tmp/output";
|
||||
result.internal_stats.output_level_stats.counts[0] = kFirstCount;
|
||||
result.internal_stats.output_level_stats.counts[kLastIndex] = kLastCount;
|
||||
std::string serialized;
|
||||
ASSERT_OK(result.Write(&serialized));
|
||||
|
||||
// Guard the produced format: this binary writes exactly kExpected counts.
|
||||
// That element count is what a remote worker hands to the primary to read,
|
||||
// so pinning it catches an accidental change to what is written.
|
||||
ASSERT_EQ(CountFirstCountsFieldElements(serialized), kExpected);
|
||||
|
||||
// Peer agrees on kNumOfReasons: every written count is read back unchanged.
|
||||
{
|
||||
CompactionServiceResult parsed;
|
||||
ASSERT_OK(CompactionServiceResult::Read(serialized, &parsed));
|
||||
ASSERT_OK(parsed.status);
|
||||
const auto& counts = parsed.internal_stats.output_level_stats.counts;
|
||||
ASSERT_EQ(counts[0], kFirstCount);
|
||||
ASSERT_EQ(counts[kLastIndex], kLastCount);
|
||||
}
|
||||
|
||||
// Peer serialized one more count than this binary knows (a worker with an
|
||||
// extra CompactionReason): the surplus trailing count is ignored, and the
|
||||
// counts this binary understands are read unchanged.
|
||||
{
|
||||
std::string from_wider_peer = serialized;
|
||||
AppendToCountsFields(&from_wider_peer, "7");
|
||||
ASSERT_EQ(CountFirstCountsFieldElements(from_wider_peer), kExpected + 1);
|
||||
CompactionServiceResult parsed;
|
||||
ASSERT_OK(CompactionServiceResult::Read(from_wider_peer, &parsed));
|
||||
ASSERT_OK(parsed.status);
|
||||
const auto& counts = parsed.internal_stats.output_level_stats.counts;
|
||||
ASSERT_EQ(counts[0], kFirstCount);
|
||||
ASSERT_EQ(counts[kLastIndex], kLastCount);
|
||||
}
|
||||
|
||||
// Peer serialized one fewer count than this binary knows (a worker missing
|
||||
// the newest CompactionReason): the absent trailing count reads as zero.
|
||||
{
|
||||
std::string from_narrower_peer = serialized;
|
||||
RemoveLastFromCountsFields(&from_narrower_peer);
|
||||
ASSERT_EQ(CountFirstCountsFieldElements(from_narrower_peer), kExpected - 1);
|
||||
CompactionServiceResult parsed;
|
||||
ASSERT_OK(CompactionServiceResult::Read(from_narrower_peer, &parsed));
|
||||
ASSERT_OK(parsed.status);
|
||||
const auto& counts = parsed.internal_stats.output_level_stats.counts;
|
||||
ASSERT_EQ(counts[0], kFirstCount);
|
||||
ASSERT_EQ(counts[kLastIndex], 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, SubCompaction) {
|
||||
Options options = CurrentOptions();
|
||||
options.max_subcompactions = 10;
|
||||
|
||||
+357
-269
@@ -10,6 +10,9 @@
|
||||
#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"
|
||||
@@ -78,6 +81,7 @@ class MyFlushBlockPolicyFactory : public FlushBlockPolicyFactory {
|
||||
private:
|
||||
const int num_keys_in_block_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
static bool enable_io_uring = true;
|
||||
@@ -88,6 +92,15 @@ 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_;
|
||||
@@ -207,8 +220,9 @@ TEST_F(DBBasicTest, OptimizeManifestForRecoveryEmitsPerCFWhenLogAdvances) {
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// With track_and_verify_wals_in_manifest=true, the wal_deletion edit
|
||||
// must STILL be emitted (the DeleteWalsBefore record is required).
|
||||
// With track_and_verify_wals_in_manifest=true and a clean close, the
|
||||
// close-time code already advances WalSet markers. Recovery's
|
||||
// DeleteWalsBefore is redundant and should be skipped.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryPreservesWalTracking) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
@@ -224,7 +238,7 @@ TEST_F(DBBasicTest, OptimizeManifestForRecoveryPreservesWalTracking) {
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_EQ(0, counters.wal_deletion.load());
|
||||
ASSERT_EQ(1, counters.wal_deletion.load());
|
||||
}
|
||||
|
||||
// best_efforts_recovery requires a fresh MANIFEST + CURRENT to be
|
||||
@@ -497,11 +511,60 @@ TEST_F(DBBasicTest, OptimizeManifestForRecoveryEmitsWalDeletionWhenTracking) {
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_GT(counters.per_cf.load(), 0);
|
||||
ASSERT_EQ(1, records.load());
|
||||
ASSERT_EQ(0, records.load());
|
||||
ASSERT_GT(dbfull()->GetVersionSet()->GetWalSet().GetMinWalNumberToKeep(), 0u);
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// Zero MANIFEST growth on clean reopen with optimize_manifest_for_recovery=1,
|
||||
// reuse_manifest_on_open=1, and track_and_verify_wals_in_manifest=1.
|
||||
// Regression test for the stress test failure where a redundant
|
||||
// DeleteWalsBefore edit was written during recovery.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryZeroGrowthOnCleanReopen) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
options.track_and_verify_wals_in_manifest = true;
|
||||
options.avoid_flush_during_recovery = true;
|
||||
options.write_dbid_to_manifest = false;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
// Get MANIFEST size after close
|
||||
std::vector<std::string> files;
|
||||
ASSERT_OK(env_->GetChildren(dbname_, &files));
|
||||
uint64_t manifest_size_before = 0;
|
||||
for (const auto& f : files) {
|
||||
if (f.find("MANIFEST-") == 0) {
|
||||
ASSERT_OK(env_->GetFileSize(dbname_ + "/" + f, &manifest_size_before));
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_GT(manifest_size_before, 0u);
|
||||
|
||||
// Reopen - should not grow MANIFEST at all
|
||||
Reopen(options);
|
||||
|
||||
// Get MANIFEST size after reopen
|
||||
files.clear();
|
||||
ASSERT_OK(env_->GetChildren(dbname_, &files));
|
||||
uint64_t manifest_size_after = 0;
|
||||
for (const auto& f : files) {
|
||||
if (f.find("MANIFEST-") == 0) {
|
||||
ASSERT_OK(env_->GetFileSize(dbname_ + "/" + f, &manifest_size_after));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_EQ(manifest_size_before, manifest_size_after)
|
||||
<< "MANIFEST grew by " << (manifest_size_after - manifest_size_before)
|
||||
<< " bytes on clean reopen; expected zero growth";
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// Regression for the close-time marker path: if a new WAL is created while an
|
||||
// otherwise-empty user CF stays empty, Close() must reserve the next file
|
||||
// number before persisting SetLogNumber(cur_wal + 1) for that CF.
|
||||
@@ -958,6 +1021,92 @@ 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");
|
||||
@@ -1320,27 +1469,36 @@ TEST_F(DBBasicTest, LevelLimitReopen) {
|
||||
ASSERT_OK(TryReopenWithColumnFamilies({"default", "pikachu"}, options));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, PutDeleteGet) {
|
||||
TEST_P(DBBasicGetWithParam, PutDeleteGet) {
|
||||
do {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
ASSERT_OK(Put(1, "foo", "v1"));
|
||||
ASSERT_EQ("v1", Get(1, "foo"));
|
||||
ASSERT_EQ("v1", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_OK(Put(1, "foo", "v2"));
|
||||
ASSERT_EQ("v2", Get(1, "foo"));
|
||||
ASSERT_EQ("v2", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_OK(Delete(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(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());
|
||||
} while (ChangeOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, PutSingleDeleteGet) {
|
||||
TEST_P(DBBasicGetWithParam, PutSingleDeleteGet) {
|
||||
do {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
ASSERT_OK(Put(1, "foo", "v1"));
|
||||
ASSERT_EQ("v1", Get(1, "foo"));
|
||||
ASSERT_EQ("v1", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_OK(Put(1, "foo2", "v2"));
|
||||
ASSERT_EQ("v2", Get(1, "foo2"));
|
||||
ASSERT_EQ("v2", Get(1, "foo2", nullptr, use_coroutine_));
|
||||
ASSERT_OK(SingleDelete(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "foo", nullptr, use_coroutine_));
|
||||
// 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.
|
||||
@@ -1348,40 +1506,40 @@ TEST_F(DBBasicTest, PutSingleDeleteGet) {
|
||||
kSkipMergePut));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, TimedPutBasic) {
|
||||
TEST_P(DBBasicGetWithParam, 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"));
|
||||
ASSERT_EQ("v1", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_OK(TimedPut(1, "foo", "v2.1", /*write_unix_time=*/3));
|
||||
ASSERT_EQ("v2.1", Get(1, "foo"));
|
||||
ASSERT_EQ("v2.1", Get(1, "foo", nullptr, use_coroutine_));
|
||||
|
||||
// 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"));
|
||||
ASSERT_EQ("v2.1,v2.2", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_OK(Delete(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "foo", nullptr, use_coroutine_));
|
||||
|
||||
ASSERT_OK(TimedPut(1, "bar", "bv1", /*write_unix_time=*/0));
|
||||
ASSERT_EQ("bv1", Get(1, "bar"));
|
||||
ASSERT_EQ("bv1", Get(1, "bar", nullptr, use_coroutine_));
|
||||
ASSERT_OK(TimedPut(1, "baz", "bzv1", /*write_unix_time=*/0));
|
||||
ASSERT_EQ("bzv1", Get(1, "baz"));
|
||||
ASSERT_EQ("bzv1", Get(1, "baz", nullptr, use_coroutine_));
|
||||
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"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "bar", nullptr, use_coroutine_));
|
||||
}
|
||||
|
||||
ASSERT_EQ("bzv1", Get(1, "baz"));
|
||||
ASSERT_EQ("bzv1", Get(1, "baz", nullptr, use_coroutine_));
|
||||
ASSERT_OK(SingleDelete(1, "baz"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "baz"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "baz", nullptr, use_coroutine_));
|
||||
} while (ChangeOptions(kSkipPlainTable));
|
||||
}
|
||||
|
||||
@@ -1407,17 +1565,17 @@ TEST_F(DBBasicTest, EmptyFlush) {
|
||||
kSkipMergePut));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, GetFromVersions) {
|
||||
TEST_P(DBBasicGetWithParam, GetFromVersions) {
|
||||
do {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
ASSERT_OK(Put(1, "foo", "v1"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_EQ("v1", Get(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(0, "foo"));
|
||||
ASSERT_EQ("v1", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("NOT_FOUND", Get(0, "foo", nullptr, use_coroutine_));
|
||||
} while (ChangeOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, GetSnapshot) {
|
||||
TEST_P(DBBasicGetWithParam, GetSnapshot) {
|
||||
anon::OptionsOverride options_override;
|
||||
options_override.skip_policy = kSkipNoSnapshot;
|
||||
do {
|
||||
@@ -1428,11 +1586,11 @@ TEST_F(DBBasicTest, GetSnapshot) {
|
||||
ASSERT_OK(Put(1, key, "v1"));
|
||||
const Snapshot* s1 = db_->GetSnapshot();
|
||||
ASSERT_OK(Put(1, key, "v2"));
|
||||
ASSERT_EQ("v2", Get(1, key));
|
||||
ASSERT_EQ("v1", Get(1, key, s1));
|
||||
ASSERT_EQ("v2", Get(1, key, nullptr, use_coroutine_));
|
||||
ASSERT_EQ("v1", Get(1, key, s1, use_coroutine_));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_EQ("v2", Get(1, key));
|
||||
ASSERT_EQ("v1", Get(1, key, s1));
|
||||
ASSERT_EQ("v2", Get(1, key, nullptr, use_coroutine_));
|
||||
ASSERT_EQ("v1", Get(1, key, s1, use_coroutine_));
|
||||
db_->ReleaseSnapshot(s1);
|
||||
}
|
||||
} while (ChangeOptions());
|
||||
@@ -1689,7 +1847,7 @@ TEST_F(DBBasicTest, LockFileRecovery) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, Snapshot) {
|
||||
TEST_P(DBBasicGetWithParam, Snapshot) {
|
||||
env_->SetMockSleep();
|
||||
anon::OptionsOverride options_override;
|
||||
options_override.skip_policy = kSkipNoSnapshot;
|
||||
@@ -1723,31 +1881,31 @@ TEST_F(DBBasicTest, Snapshot) {
|
||||
|
||||
ASSERT_OK(Put(0, "foo", "0v4"));
|
||||
ASSERT_OK(Put(1, "foo", "1v4"));
|
||||
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("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(2U, GetNumSnapshots());
|
||||
ASSERT_EQ(time_snap1, GetTimeOldestSnapshots());
|
||||
ASSERT_EQ(GetSequenceOldestSnapshots(), s1->GetSequenceNumber());
|
||||
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"));
|
||||
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_));
|
||||
|
||||
db_->ReleaseSnapshot(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"));
|
||||
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(1U, GetNumSnapshots());
|
||||
ASSERT_LT(time_snap1, GetTimeOldestSnapshots());
|
||||
ASSERT_EQ(GetSequenceOldestSnapshots(), s2->GetSequenceNumber());
|
||||
@@ -1755,11 +1913,19 @@ TEST_F(DBBasicTest, Snapshot) {
|
||||
db_->ReleaseSnapshot(s2);
|
||||
ASSERT_EQ(0U, GetNumSnapshots());
|
||||
ASSERT_EQ(GetSequenceOldestSnapshots(), 0);
|
||||
ASSERT_EQ("0v4", Get(0, "foo"));
|
||||
ASSERT_EQ("1v4", Get(1, "foo"));
|
||||
ASSERT_EQ("0v4", Get(0, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("1v4", Get(1, "foo", nullptr, use_coroutine_));
|
||||
} 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:
|
||||
@@ -3708,11 +3874,15 @@ INSTANTIATE_TEST_CASE_P(DBMultiGetTestWithParam, DBMultiGetTestWithParam,
|
||||
testing::Combine(testing::Bool(), testing::Bool()));
|
||||
|
||||
#if USE_COROUTINES
|
||||
class DBMultiGetAsyncIOTest : public DBBasicTest,
|
||||
public ::testing::WithParamInterface<bool> {
|
||||
class DBMultiGetAsyncIOTest
|
||||
: public DBBasicTest,
|
||||
public ::testing::WithParamInterface<std::tuple<bool, bool>> {
|
||||
public:
|
||||
DBMultiGetAsyncIOTest()
|
||||
: DBBasicTest(), statistics_(ROCKSDB_NAMESPACE::CreateDBStatistics()) {
|
||||
: DBBasicTest(),
|
||||
optimize_multiget_for_io_(std::get<0>(GetParam())),
|
||||
use_coroutine_(std::get<1>(GetParam())),
|
||||
statistics_(ROCKSDB_NAMESPACE::CreateDBStatistics()) {
|
||||
BlockBasedTableOptions bbto;
|
||||
bbto.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
options_ = CurrentOptions();
|
||||
@@ -3786,6 +3956,20 @@ class DBMultiGetAsyncIOTest : public DBBasicTest,
|
||||
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_);
|
||||
@@ -3801,6 +3985,9 @@ class DBMultiGetAsyncIOTest : public DBBasicTest,
|
||||
|
||||
void ReopenDB() { Reopen(options_); }
|
||||
|
||||
const bool optimize_multiget_for_io_;
|
||||
const bool use_coroutine_;
|
||||
|
||||
private:
|
||||
std::shared_ptr<Statistics> statistics_;
|
||||
Options options_;
|
||||
@@ -3809,39 +3996,28 @@ class DBMultiGetAsyncIOTest : public DBBasicTest,
|
||||
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();
|
||||
|
||||
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());
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
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 (GetParam()) {
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 1);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 3);
|
||||
if (UseCoroutineRead()) {
|
||||
AssertMultiGetIOBatchSize(3, 1);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
|
||||
} else if (optimize_multiget_for_io_) {
|
||||
AssertMultiGetIOBatchSize(1, 3);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
|
||||
} else {
|
||||
// Without Async IO, MultiGet will call MultiRead 3 times, once for each
|
||||
// L0 file
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 3);
|
||||
AssertMultiGetIOBatchSize(3, 1);
|
||||
}
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
@@ -3849,64 +4025,58 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL0) {
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL1) {
|
||||
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());
|
||||
std::vector<std::string> key_strs{Key(33), Key(54), Key(102)};
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
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());
|
||||
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(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
|
||||
// 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);
|
||||
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);
|
||||
#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;
|
||||
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());
|
||||
std::vector<std::string> key_strs{Key(33), Key(54), Key(102)};
|
||||
|
||||
int count = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
@@ -3935,147 +4105,100 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1Error) {
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
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());
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
ASSERT_EQ(values.size(), 3);
|
||||
ASSERT_EQ(statuses[0], Status::OK());
|
||||
ASSERT_EQ(statuses[1], Status::OK());
|
||||
ASSERT_EQ(statuses[2], Status::IOError());
|
||||
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());
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
#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
|
||||
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());
|
||||
std::vector<std::string> key_strs{Key(21), Key(54), Key(102)};
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
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());
|
||||
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(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
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 2);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 2);
|
||||
if (UseCoroutineRead()) {
|
||||
AssertMultiGetIOBatchSize(3, 1);
|
||||
} else {
|
||||
AssertMultiGetIOBatchSize(2, 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
|
||||
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());
|
||||
std::vector<std::string> key_strs{Key(33), Key(56), Key(102)};
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
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());
|
||||
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(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.
|
||||
ASSERT_EQ(multiget_io_batch_size.count, GetParam() ? 1 : 2);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, GetParam() ? 3 : 2);
|
||||
if (UseCoroutineRead()) {
|
||||
AssertMultiGetIOBatchSize(3, 1);
|
||||
} else {
|
||||
AssertMultiGetIOBatchSize(optimize_multiget_for_io_ ? 1 : 2,
|
||||
optimize_multiget_for_io_ ? 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
|
||||
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());
|
||||
std::vector<std::string> key_strs{Key(19), Key(26)};
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
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());
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
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 will avoid the coroutine calls in those levels
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 2);
|
||||
// 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);
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
@@ -4083,62 +4206,35 @@ 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
|
||||
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());
|
||||
std::vector<std::string> key_strs{Key(139), Key(163)};
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
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());
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ASSERT_EQ(values.size(), 2);
|
||||
ASSERT_EQ(statuses[0], Status::NotFound());
|
||||
ASSERT_EQ(statuses[1], Status::NotFound());
|
||||
ASSERT_EQ(values[0], "NOT_FOUND");
|
||||
ASSERT_EQ(values[1], "NOT_FOUND");
|
||||
|
||||
// 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
|
||||
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());
|
||||
std::vector<std::string> key_strs{Key(139), Key(144), Key(163)};
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
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());
|
||||
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");
|
||||
ASSERT_EQ(values[1], "val_l1_" + std::to_string(144));
|
||||
ASSERT_EQ(statuses[2], Status::NotFound());
|
||||
ASSERT_EQ(values[2], "NOT_FOUND");
|
||||
|
||||
// Bloom filters in L0/L1 will avoid the coroutine calls in those levels
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
|
||||
@@ -4146,32 +4242,18 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2WithRangeDelInL1) {
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetNoIOUring) {
|
||||
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());
|
||||
std::vector<std::string> key_strs{Key(33), Key(54), Key(102)};
|
||||
|
||||
enable_io_uring = false;
|
||||
ReopenDB();
|
||||
|
||||
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());
|
||||
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(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 async_read_bytes;
|
||||
|
||||
@@ -4179,11 +4261,17 @@ 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);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_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);
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(DBMultiGetAsyncIOTest, DBMultiGetAsyncIOTest,
|
||||
testing::Bool());
|
||||
testing::Combine(testing::Bool(), testing::Bool()));
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetStats) {
|
||||
|
||||
+124
-1
@@ -28,6 +28,7 @@
|
||||
#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"
|
||||
|
||||
@@ -7902,7 +7903,10 @@ class FastOpenTestFS : public FileSystemWrapper {
|
||||
IODebugContext* dbg) override {
|
||||
if (file_opts.file_metadata != nullptr) {
|
||||
metadata_passed_on_open_.fetch_add(1, std::memory_order_relaxed);
|
||||
last_metadata_received_ = *file_opts.file_metadata;
|
||||
{
|
||||
MutexLock l(&last_metadata_mu_);
|
||||
last_metadata_received_ = *file_opts.file_metadata;
|
||||
}
|
||||
}
|
||||
IOStatus s = target()->NewRandomAccessFile(fname, file_opts, result, dbg);
|
||||
if (s.ok()) {
|
||||
@@ -7921,18 +7925,21 @@ 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_;
|
||||
};
|
||||
|
||||
@@ -8274,6 +8281,122 @@ 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) {
|
||||
|
||||
@@ -252,7 +252,7 @@ Status DBImpl::GetLiveFilesStorageInfo(
|
||||
"capturing live files. Retry with flush enabled.");
|
||||
}
|
||||
if (flush_memtable) {
|
||||
wal_locked = lock_wal_count_ > 0;
|
||||
wal_locked = !lock_wal_owner_thread_id_counts_.empty();
|
||||
if (wal_locked) {
|
||||
if (needs_blob_direct_write_flush) {
|
||||
mutex_.Unlock();
|
||||
@@ -260,8 +260,12 @@ Status DBImpl::GetLiveFilesStorageInfo(
|
||||
"Blob direct write requires flushing active blob files before "
|
||||
"capturing live files. Retry with WAL unlocked.");
|
||||
}
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"Can't FlushForGetLiveFiles while WAL is locked");
|
||||
}
|
||||
if (wal_locked && !has_unpersisted_data_.load(std::memory_order_relaxed)) {
|
||||
ROCKS_LOG_INFO(
|
||||
immutable_db_options_.info_log,
|
||||
"Skipping FlushForGetLiveFiles while WAL is locked and all data is "
|
||||
"already persisted");
|
||||
} else {
|
||||
Status status = FlushForGetLiveFiles(opts.atomic_flush);
|
||||
if (!status.ok()) {
|
||||
|
||||
+138
-5
@@ -8,7 +8,9 @@
|
||||
// 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"
|
||||
@@ -2219,8 +2221,12 @@ 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
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
// 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));
|
||||
});
|
||||
// Wait for first flush started.
|
||||
TEST_SYNC_POINT(
|
||||
@@ -2233,15 +2239,142 @@ TEST_F(DBFlushTest, FireOnFlushCompletedAfterCommittedResult) {
|
||||
flush_opts.wait = false;
|
||||
ASSERT_OK(db_->Flush(flush_opts));
|
||||
t1.join();
|
||||
// Ensure background work is fully finished including listener callbacks
|
||||
// before accessing listener state.
|
||||
ASSERT_OK(dbfull()->TEST_WaitForBackgroundWork());
|
||||
// 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.
|
||||
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;
|
||||
|
||||
|
||||
+13
-182
@@ -34,188 +34,6 @@ 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();
|
||||
@@ -301,3 +119,16 @@ 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,18 +26,21 @@ class CompactedDBImpl : public DBImpl {
|
||||
|
||||
// Implementations of the DB interface
|
||||
using DB::Get;
|
||||
Status Get(const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp) override;
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, Get, const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp);
|
||||
|
||||
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.
|
||||
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;
|
||||
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);
|
||||
|
||||
using DBImpl::Put;
|
||||
Status Put(const WriteOptions& /*options*/,
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
// 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
|
||||
+403
-857
File diff suppressed because it is too large.
Load diff
+93
-49
@@ -67,6 +67,7 @@
|
||||
#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"
|
||||
@@ -268,9 +269,14 @@ class DBImpl : public DB {
|
||||
bool HasAnyBlobDirectWriteColumnFamily();
|
||||
|
||||
using DB::Get;
|
||||
Status Get(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp) override;
|
||||
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);
|
||||
|
||||
using DB::GetEntity;
|
||||
Status GetEntity(const ReadOptions& options,
|
||||
@@ -295,6 +301,7 @@ 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
|
||||
@@ -302,10 +309,16 @@ 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
|
||||
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;
|
||||
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 MultiGetWithCallback(
|
||||
const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
@@ -718,13 +731,13 @@ class DBImpl : public DB {
|
||||
int* number_of_operands = nullptr;
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
Status GetImpl(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp);
|
||||
DECLARE_SYNC_AND_ASYNC(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
|
||||
@@ -733,8 +746,9 @@ 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
|
||||
virtual Status GetImpl(const ReadOptions& options, const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
DECLARE_SYNC_AND_ASYNC_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,
|
||||
@@ -1189,6 +1203,13 @@ 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
|
||||
@@ -1541,7 +1562,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();
|
||||
Status DeleteObsoleteOptionsFiles(bool schedule_only);
|
||||
|
||||
void NotifyOnManualFlushScheduled(autovector<ColumnFamilyData*> cfds,
|
||||
FlushReason flush_reason);
|
||||
@@ -2219,7 +2240,10 @@ class DBImpl : public DB {
|
||||
// Required: DB mutex held
|
||||
Status PersistentStatsProcessFormatVersion();
|
||||
|
||||
Status ResumeImpl(DBRecoverContext context);
|
||||
// `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);
|
||||
|
||||
void MaybeIgnoreError(Status* s) const;
|
||||
|
||||
@@ -2501,18 +2525,24 @@ 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) {
|
||||
std::optional<FlushReason> flush_reason = std::nullopt,
|
||||
bool wait_for_listener_notifications = false) {
|
||||
return WaitForFlushMemTables({cfd}, {flush_memtable_id},
|
||||
resuming_from_bg_err, flush_reason);
|
||||
resuming_from_bg_err, flush_reason,
|
||||
wait_for_listener_notifications);
|
||||
}
|
||||
// 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 resuming_from_bg_err, std::optional<FlushReason> flush_reason,
|
||||
bool wait_for_listener_notifications = false);
|
||||
|
||||
inline void WaitForPendingWrites() {
|
||||
mutex_.AssertHeld();
|
||||
@@ -2541,7 +2571,7 @@ class DBImpl : public DB {
|
||||
}
|
||||
|
||||
// Wait for any LockWAL to clear
|
||||
while (lock_wal_count_ > 0) {
|
||||
while (!lock_wal_owner_thread_id_counts_.empty()) {
|
||||
bg_cv_.Wait();
|
||||
}
|
||||
}
|
||||
@@ -2954,11 +2984,12 @@ class DBImpl : public DB {
|
||||
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);
|
||||
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);
|
||||
|
||||
// A structure to hold the information required to process MultiGet of keys
|
||||
// belonging to one column family. For a multi column family MultiGet, there
|
||||
@@ -3033,8 +3064,9 @@ 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.
|
||||
Status MultiGetImpl(
|
||||
const ReadOptions& read_options, size_t start_key, size_t num_keys,
|
||||
DECLARE_SYNC_AND_ASYNC(
|
||||
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);
|
||||
|
||||
@@ -3073,29 +3105,40 @@ 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.
|
||||
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(
|
||||
// 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(
|
||||
const Slice& key, const std::string* timestamp,
|
||||
bool resolve_blob_backed_memtable_value,
|
||||
const BlobFetcher* memtable_blob_fetcher, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
|
||||
bool* value_found = nullptr);
|
||||
PinnableWideColumns* columns, Status memtable_read_status,
|
||||
bool* is_blob_index, bool* value_found = nullptr);
|
||||
|
||||
template <typename IterType, typename ImplType,
|
||||
typename ErrorIteratorFuncType>
|
||||
@@ -3600,12 +3643,13 @@ class DBImpl : public DB {
|
||||
|
||||
// Stop write token that is acquired when first LockWAL() is called.
|
||||
// Destroyed when last UnlockWAL() is called. Controlled by DB mutex.
|
||||
// See lock_wal_count_
|
||||
// See lock_wal_owner_thread_id_counts_
|
||||
std::unique_ptr<WriteControllerToken> lock_wal_write_token_;
|
||||
|
||||
// The number of LockWAL called without matching UnlockWAL call.
|
||||
// See also lock_wal_write_token_
|
||||
uint32_t lock_wal_count_ = 0;
|
||||
// Thread IDs of valid LockWAL() owners and their recursive lock counts. Each
|
||||
// owning thread must call UnlockWAL() the same number of times before writes
|
||||
// can resume.
|
||||
std::unordered_map<uint64_t, uint32_t> lock_wal_owner_thread_id_counts_;
|
||||
};
|
||||
|
||||
class GetWithTimestampReadCallback : public ReadCallback {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#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 {
|
||||
@@ -358,6 +359,34 @@ 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();
|
||||
@@ -900,6 +929,42 @@ 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)
|
||||
@@ -1623,16 +1688,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);
|
||||
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);
|
||||
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",
|
||||
@@ -2209,7 +2274,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->max_timestamp, f->file_open_metadata);
|
||||
}
|
||||
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
|
||||
"[%s] Apply version edit:\n%s", cfd->GetName().c_str(),
|
||||
@@ -2649,6 +2714,13 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
|
||||
"Please try again later after writes are resumed";
|
||||
return Status::TryAgain(oss.str());
|
||||
}
|
||||
if (flush_options.wait) {
|
||||
InstrumentedMutexLock lock(&mutex_);
|
||||
if (lock_wal_owner_thread_id_counts_.count(env_->GetThreadID()) > 0) {
|
||||
return Status::Aborted(
|
||||
"Likely deadlock as the same thread called LockWAL()");
|
||||
}
|
||||
}
|
||||
Status s;
|
||||
if (!flush_options.allow_write_stall) {
|
||||
bool flush_needed = true;
|
||||
@@ -2787,7 +2859,7 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
|
||||
s = WaitForFlushMemTables(
|
||||
cfds, flush_memtable_ids,
|
||||
flush_reason == FlushReason::kErrorRecovery /* resuming_from_bg_err */,
|
||||
flush_reason);
|
||||
flush_reason, flush_options.listener_wait);
|
||||
InstrumentedMutexLock lock_guard(&mutex_);
|
||||
for (auto* tmp_cfd : cfds) {
|
||||
tmp_cfd->UnrefAndTryDelete();
|
||||
@@ -2807,6 +2879,13 @@ Status DBImpl::AtomicFlushMemTables(
|
||||
"Please try again later after writes are resumed";
|
||||
return Status::TryAgain(oss.str());
|
||||
}
|
||||
if (flush_options.wait) {
|
||||
InstrumentedMutexLock lock(&mutex_);
|
||||
if (lock_wal_owner_thread_id_counts_.count(env_->GetThreadID()) > 0) {
|
||||
return Status::Aborted(
|
||||
"Likely deadlock as the same thread called LockWAL()");
|
||||
}
|
||||
}
|
||||
Status s;
|
||||
autovector<ColumnFamilyData*> candidate_cfds;
|
||||
if (provided_candidate_cfds.empty()) {
|
||||
@@ -2935,7 +3014,7 @@ Status DBImpl::AtomicFlushMemTables(
|
||||
s = WaitForFlushMemTables(
|
||||
cfds, flush_memtable_ids,
|
||||
flush_reason == FlushReason::kErrorRecovery /* resuming_from_bg_err */,
|
||||
flush_reason);
|
||||
flush_reason, flush_options.listener_wait);
|
||||
InstrumentedMutexLock lock_guard(&mutex_);
|
||||
for (auto* cfd : cfds) {
|
||||
cfd->UnrefAndTryDelete();
|
||||
@@ -3104,7 +3183,8 @@ 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 resuming_from_bg_err, std::optional<FlushReason> flush_reason,
|
||||
bool wait_for_listener_notifications) {
|
||||
int num = static_cast<int>(cfds.size());
|
||||
// Wait until the compaction completes
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
@@ -3149,7 +3229,13 @@ Status DBImpl::WaitForFlushMemTables(
|
||||
flush_reason.value() != FlushReason::kExternalFileIngestion ||
|
||||
cfds[i]->GetSuperVersion()->imm->GetID() ==
|
||||
cfds[i]->imm()->current()->GetID()) {
|
||||
++num_finished;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ Status DBImpl::TEST_SwitchWAL() {
|
||||
|
||||
Status DBImpl::TEST_ResumeImpl(DBRecoverContext context) {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
return ResumeImpl(context);
|
||||
return ResumeImpl(context, Env::IOActivity::kFlush);
|
||||
}
|
||||
|
||||
uint64_t DBImpl::TEST_MaxNextLevelOverlappingBytes(
|
||||
@@ -164,6 +164,22 @@ 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->max_timestamp, f->file_open_metadata);
|
||||
}
|
||||
|
||||
status = versions_->LogAndApply(cfd, read_options, write_options, &edit,
|
||||
|
||||
@@ -243,6 +243,11 @@ 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. ");
|
||||
@@ -683,7 +688,8 @@ 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->min_timestamp, f->max_timestamp,
|
||||
f->file_open_metadata);
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"[%s] Moving #%" PRIu64
|
||||
" from from_level-%d to from_level-%d %" PRIu64
|
||||
@@ -1941,10 +1947,8 @@ Status DBImpl::MaybeFlushFinalMemtableOrRestoreActiveLogFiles(
|
||||
const uint64_t new_min_log = max_wal_number + 1;
|
||||
VersionEdit wal_deletion;
|
||||
bool emit_wal_deletion = false;
|
||||
if (immutable_db_options_.track_and_verify_wals_in_manifest) {
|
||||
// Determining whether DeleteWalsBefore actually shrinks WalSet
|
||||
// membership requires WalSet state outside this site, so emit
|
||||
// unconditionally (pre-existing behavior).
|
||||
if (immutable_db_options_.track_and_verify_wals_in_manifest &&
|
||||
new_min_log > versions_->GetWalSet().GetMinWalNumberToKeep()) {
|
||||
wal_deletion.DeleteWalsBefore(new_min_log);
|
||||
emit_wal_deletion = true;
|
||||
}
|
||||
@@ -2873,11 +2877,16 @@ 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();
|
||||
@@ -2967,6 +2976,17 @@ 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 {
|
||||
|
||||
+13
-141
@@ -30,147 +30,6 @@ 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 &&
|
||||
@@ -411,3 +270,16 @@ 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,8 +24,9 @@ class DBImplReadOnly : public DBImpl {
|
||||
|
||||
// Implementations of the DB interface
|
||||
using DBImpl::GetImpl;
|
||||
Status GetImpl(const ReadOptions& options, const Slice& key,
|
||||
GetImplOptions& get_impl_options) override;
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, GetImpl, const ReadOptions& options,
|
||||
const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
|
||||
// TODO: Implement ReadOnly MultiGet?
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
// 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))
|
||||
+17
-187
@@ -336,193 +336,6 @@ 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 &&
|
||||
@@ -1596,9 +1409,13 @@ 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;
|
||||
}
|
||||
@@ -1799,3 +1616,16 @@ 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,8 +97,9 @@ 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;
|
||||
Status GetImpl(const ReadOptions& options, const Slice& key,
|
||||
GetImplOptions& get_impl_options) override;
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, GetImpl, const ReadOptions& options,
|
||||
const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
|
||||
using DBImpl::NewIterator;
|
||||
// Operations on the created iterators can return IOError due to files being
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
// 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))
|
||||
@@ -0,0 +1,828 @@
|
||||
// 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
|
||||
@@ -3119,7 +3119,7 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
|
||||
ReadOnlyMemTable* new_imm,
|
||||
SequenceNumber last_seqno) {
|
||||
mutex_.AssertHeld();
|
||||
assert(lock_wal_count_ == 0);
|
||||
assert(lock_wal_owner_thread_id_counts_.empty());
|
||||
|
||||
// TODO: plumb Env::IOActivity, Env::IOPriority
|
||||
const WriteOptions write_options;
|
||||
|
||||
+84
-57
@@ -77,10 +77,10 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
|
||||
user_comparator_(cmp),
|
||||
merge_operator_(ioptions.merge_operator.get()),
|
||||
iter_(iter),
|
||||
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,
|
||||
// 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,
|
||||
cfd != nullptr && cfd->blob_partition_manager() != nullptr),
|
||||
read_callback_(read_callback),
|
||||
sequence_(s),
|
||||
@@ -94,8 +94,6 @@ 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),
|
||||
@@ -259,60 +257,25 @@ void DBIter::Next() {
|
||||
}
|
||||
}
|
||||
|
||||
Status DBIter::BlobReader::RetrieveAndSetBlobValue(
|
||||
const Slice& user_key, const Slice& blob_index,
|
||||
bool allow_write_path_fallback) {
|
||||
Status DBIter::BlobReader::RetrieveAndSetBlobValue(const Slice& user_key,
|
||||
const Slice& blob_index) {
|
||||
assert(blob_value_.empty());
|
||||
|
||||
if (!version_ && (!allow_write_path_fallback || !blob_file_cache_)) {
|
||||
if (!blob_fetcher_.CanResolve()) {
|
||||
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;
|
||||
|
||||
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_);
|
||||
return blob_fetcher_.FetchBlob(user_key, blob_index, prefetch_buffer,
|
||||
&blob_value_, bytes_read);
|
||||
}
|
||||
|
||||
bool DBIter::SetValueAndColumnsFromBlobImpl(const Slice& user_key,
|
||||
const Slice& 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_);
|
||||
const Status s =
|
||||
blob_state_.mut()->reader.RetrieveAndSetBlobValue(user_key, blob_index);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
valid_ = false;
|
||||
@@ -368,7 +331,8 @@ bool DBIter::SetValueAndColumnsFromEntity(Slice slice) {
|
||||
}
|
||||
if (LIKELY(!has_blob_columns)) {
|
||||
WideColumns& wide_columns = state.wide_columns();
|
||||
const Status s = WideColumnSerialization::Deserialize(slice, wide_columns);
|
||||
const Status s =
|
||||
WideColumnSerialization::DeserializeSimple(slice, wide_columns);
|
||||
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
@@ -390,9 +354,9 @@ bool DBIter::SetValueAndColumnsFromEntity(Slice slice) {
|
||||
state.SaveEntitySliceIfNeeded(slice);
|
||||
|
||||
{
|
||||
Slice input_copy = state.PrepareForLazyEntityDeserialize();
|
||||
const Status s = WideColumnSerialization::DeserializeV2(
|
||||
input_copy, state.lazy_entity_columns(), state.lazy_blob_columns());
|
||||
const Slice entity = state.PrepareForLazyEntityDeserialize();
|
||||
const Status s = WideColumnSerialization::Deserialize(
|
||||
entity, state.lazy_entity_columns(), &state.lazy_blob_columns());
|
||||
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
@@ -1621,8 +1585,8 @@ bool DBIter::MergeWithBlobBaseValue(const Slice& blob_index,
|
||||
return false;
|
||||
}
|
||||
|
||||
const Status s = blob_state_.mut()->reader.RetrieveAndSetBlobValue(
|
||||
user_key, blob_index, allow_blob_write_path_fallback_);
|
||||
const Status s =
|
||||
blob_state_.mut()->reader.RetrieveAndSetBlobValue(user_key, blob_index);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
valid_ = false;
|
||||
@@ -1644,12 +1608,11 @@ 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_fetcher, nullptr /* prefetch_buffers */,
|
||||
resolved_entity, effective_entity);
|
||||
entity, user_key, &blob_state_->reader.blob_fetcher(),
|
||||
nullptr /* prefetch_buffers */, resolved_entity, effective_entity);
|
||||
if (!s_resolve.ok()) {
|
||||
status_ = std::move(s_resolve);
|
||||
valid_ = false;
|
||||
@@ -1921,6 +1884,9 @@ Status DBIter::ValidateScanOptions(const MultiScanArgs& multiscan_opts) const {
|
||||
|
||||
const std::vector<ScanOptions>& scan_opts = multiscan_opts.GetScanRanges();
|
||||
const bool has_limit = scan_opts.front().range.limit.has_value();
|
||||
if (multiscan_opts.reverse && !has_limit) {
|
||||
return Status::InvalidArgument("Reverse MultiScan requires limit");
|
||||
}
|
||||
if (!has_limit && scan_opts.size() > 1) {
|
||||
return Status::InvalidArgument("Scan has no upper bound");
|
||||
}
|
||||
@@ -2004,6 +1970,12 @@ void DBIter::Seek(const Slice& target) {
|
||||
StopWatch sw(clock_, statistics_, DB_SEEK);
|
||||
|
||||
if (scan_opts_.has_value()) {
|
||||
if (scan_opts_->reverse) {
|
||||
status_ = Status::InvalidArgument("Seek called on reverse MultiScan");
|
||||
valid_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the seek target is as expected in the previously prepared range
|
||||
auto const& scan_ranges = scan_opts_.value().GetScanRanges();
|
||||
if (scan_index_ >= scan_ranges.size()) {
|
||||
@@ -2110,6 +2082,61 @@ void DBIter::SeekForPrev(const Slice& target) {
|
||||
PERF_CPU_TIMER_GUARD(iter_seek_cpu_nanos, clock_);
|
||||
StopWatch sw(clock_, statistics_, DB_SEEK);
|
||||
|
||||
if (scan_opts_.has_value()) {
|
||||
if (!scan_opts_->reverse) {
|
||||
status_ =
|
||||
Status::InvalidArgument("SeekForPrev called on forward MultiScan");
|
||||
valid_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
auto const& scan_ranges = scan_opts_.value().GetScanRanges();
|
||||
if (scan_index_ >= scan_ranges.size()) {
|
||||
status_ = Status::InvalidArgument(
|
||||
"SeekForPrev called after exhausting all of the scan ranges");
|
||||
valid_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
auto const& range = scan_ranges[scan_index_];
|
||||
auto const& limit = range.range.limit;
|
||||
assert(limit.has_value());
|
||||
if (!limit.has_value() ||
|
||||
user_comparator_.CompareWithoutTimestamp(target, *limit) != 0) {
|
||||
status_ = Status::InvalidArgument(
|
||||
"SeekForPrev target does not match the limit of the next prepared "
|
||||
"range at index " +
|
||||
std::to_string(scan_index_));
|
||||
valid_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
auto const& start = range.range.start;
|
||||
assert(start.has_value());
|
||||
if (iterate_lower_bound_ == nullptr ||
|
||||
user_comparator_.CompareWithoutTimestamp(start.value(),
|
||||
*iterate_lower_bound_) != 0) {
|
||||
status_ = Status::InvalidArgument(
|
||||
"Lower bound is not set to the same start value of the next "
|
||||
"prepared range at index " +
|
||||
std::to_string(scan_index_));
|
||||
valid_ = false;
|
||||
return;
|
||||
}
|
||||
if (iterate_upper_bound_ == nullptr ||
|
||||
user_comparator_.CompareWithoutTimestamp(limit.value(),
|
||||
*iterate_upper_bound_) != 0) {
|
||||
status_ = Status::InvalidArgument(
|
||||
"Upper bound is not set to the same limit value of the next "
|
||||
"prepared range at index " +
|
||||
std::to_string(scan_index_));
|
||||
valid_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
scan_index_++;
|
||||
}
|
||||
|
||||
if (has_trace_state_) {
|
||||
// TODO: What do we do if this returns an error?
|
||||
Slice lower_bound, upper_bound;
|
||||
|
||||
+19
-29
@@ -276,37 +276,22 @@ class DBIter final : public Iterator {
|
||||
|
||||
class BlobReader {
|
||||
public:
|
||||
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) {}
|
||||
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) {}
|
||||
|
||||
const Slice& GetBlobValue() const { return blob_value_; }
|
||||
Status RetrieveAndSetBlobValue(const Slice& user_key,
|
||||
const Slice& blob_index,
|
||||
bool allow_write_path_fallback);
|
||||
const Slice& blob_index);
|
||||
void ResetBlobValue() { blob_value_.Reset(); }
|
||||
// Create a BlobFetcher with the same read options as this BlobReader.
|
||||
BlobFetcher CreateBlobFetcher() const;
|
||||
// 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_; }
|
||||
|
||||
private:
|
||||
PinnableSlice blob_value_;
|
||||
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_;
|
||||
OwningVersionBlobFetcher blob_fetcher_;
|
||||
};
|
||||
struct BlobState {
|
||||
BlobReader reader;
|
||||
@@ -331,9 +316,7 @@ class DBIter final : public Iterator {
|
||||
ValueColumnsState(const Version* version, const ReadOptions& read_options,
|
||||
ColumnFamilyData* cfd)
|
||||
: entity_blob_resolver_(
|
||||
version, read_options.read_tier, read_options.verify_checksums,
|
||||
read_options.fill_cache, read_options.io_activity,
|
||||
cfd ? cfd->blob_file_cache() : nullptr,
|
||||
version, read_options, cfd ? cfd->blob_file_cache() : nullptr,
|
||||
cfd != nullptr && cfd->blob_partition_manager() != nullptr) {}
|
||||
|
||||
Slice& value() { return value_; }
|
||||
@@ -399,7 +382,7 @@ class DBIter final : public Iterator {
|
||||
}
|
||||
|
||||
// Clears the previous lazy entity metadata and returns the saved entity
|
||||
// buffer as input for DeserializeV2().
|
||||
// buffer as input for Deserialize().
|
||||
Slice PrepareForLazyEntityDeserialize() {
|
||||
ClearLazyEntity();
|
||||
return Slice(saved_value_);
|
||||
@@ -673,6 +656,14 @@ 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
|
||||
@@ -719,7 +710,6 @@ 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_;
|
||||
|
||||
+240
-6
@@ -5418,6 +5418,157 @@ TEST_P(DBMultiScanIteratorTest, AsyncPrefetchAcrossMultipleFiles) {
|
||||
iter.reset();
|
||||
}
|
||||
|
||||
TEST_P(DBMultiScanIteratorTest, ReversePrefetchAcrossMultipleRanges) {
|
||||
auto options = CurrentOptions();
|
||||
options.target_file_size_base = 1 << 15; // 32KiB
|
||||
options.compaction_style = kCompactionStyleUniversal;
|
||||
options.num_levels = 50;
|
||||
options.compression = kNoCompression;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
Random rnd(303);
|
||||
for (int i = 0; i < 1000; ++i) {
|
||||
ASSERT_OK(Put(Key(i), rnd.RandomString(1 << 10)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange({}, nullptr, nullptr));
|
||||
ASSERT_GT(NumTableFilesAtLevel(49), 3);
|
||||
|
||||
ReadOptions ro;
|
||||
ro.fill_cache = GetParam();
|
||||
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
|
||||
auto tracking_dispatcher = std::make_shared<TrackingIODispatcher>();
|
||||
|
||||
MultiScanArgs scan_options(BytewiseComparator());
|
||||
scan_options.use_async_io = false;
|
||||
scan_options.reverse = true;
|
||||
scan_options.io_dispatcher = tracking_dispatcher;
|
||||
std::vector<std::string> key_ranges(
|
||||
{Key(100), Key(200), Key(500), Key(600), Key(800), Key(900)});
|
||||
scan_options.insert(key_ranges[0], key_ranges[1]);
|
||||
scan_options.insert(key_ranges[2], key_ranges[3]);
|
||||
scan_options.insert(key_ranges[4], key_ranges[5]);
|
||||
|
||||
std::unique_ptr<MultiScan> iter =
|
||||
dbfull()->NewMultiScan(ro, cfh, scan_options);
|
||||
ASSERT_NE(iter, nullptr);
|
||||
|
||||
std::vector<std::string> actual_keys;
|
||||
try {
|
||||
for (auto range : *iter) {
|
||||
for (auto it : range) {
|
||||
actual_keys.push_back(it.first.ToString());
|
||||
}
|
||||
}
|
||||
} catch (MultiScanException& ex) {
|
||||
FAIL() << "Iterator returned status " << ex.what();
|
||||
} catch (std::logic_error& ex) {
|
||||
FAIL() << "Iterator returned logic error " << ex.what();
|
||||
}
|
||||
|
||||
std::vector<std::string> expected_keys;
|
||||
for (int i = 199; i >= 100; --i) {
|
||||
expected_keys.push_back(Key(i));
|
||||
}
|
||||
for (int i = 599; i >= 500; --i) {
|
||||
expected_keys.push_back(Key(i));
|
||||
}
|
||||
for (int i = 899; i >= 800; --i) {
|
||||
expected_keys.push_back(Key(i));
|
||||
}
|
||||
ASSERT_EQ(actual_keys, expected_keys);
|
||||
ASSERT_GT(tracking_dispatcher->GetReadSets().size(), 0);
|
||||
}
|
||||
|
||||
TEST_P(DBMultiScanIteratorTest, ReverseUnreleasedPrefetchBlocksCountAsWasted) {
|
||||
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);
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
DestroyAndReopen(options);
|
||||
|
||||
constexpr int kNumKeys = 40;
|
||||
const std::string value(100, 'v');
|
||||
for (int i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(Put(Key(i), value));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange({}, nullptr, nullptr));
|
||||
|
||||
ReadOptions ro;
|
||||
ro.fill_cache = GetParam();
|
||||
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
|
||||
auto tracking_dispatcher = std::make_shared<TrackingIODispatcher>();
|
||||
|
||||
MultiScanArgs scan_options(BytewiseComparator());
|
||||
scan_options.use_async_io = false;
|
||||
scan_options.reverse = true;
|
||||
scan_options.io_dispatcher = tracking_dispatcher;
|
||||
const std::string start_key = Key(0);
|
||||
const std::string limit_key = Key(kNumKeys);
|
||||
scan_options.insert(start_key, limit_key);
|
||||
|
||||
{
|
||||
std::unique_ptr<MultiScan> iter =
|
||||
dbfull()->NewMultiScan(ro, cfh, scan_options);
|
||||
ASSERT_NE(iter, nullptr);
|
||||
|
||||
try {
|
||||
auto range_it = iter->begin();
|
||||
ASSERT_NE(range_it, iter->end());
|
||||
auto range = *range_it;
|
||||
auto row_it = range.begin();
|
||||
ASSERT_NE(row_it, range.end());
|
||||
ASSERT_EQ((*row_it).first.ToString(), Key(kNumKeys - 1));
|
||||
} catch (MultiScanException& ex) {
|
||||
FAIL() << "Iterator returned status " << ex.what();
|
||||
} catch (std::logic_error& ex) {
|
||||
FAIL() << "Iterator returned logic error " << ex.what();
|
||||
}
|
||||
|
||||
ASSERT_GT(tracking_dispatcher->GetReadSets().size(), 0);
|
||||
}
|
||||
|
||||
ASSERT_GT(
|
||||
options.statistics->getTickerCount(MULTISCAN_PREFETCH_BLOCKS_WASTED), 0);
|
||||
}
|
||||
|
||||
TEST_P(DBMultiScanIteratorTest, ReverseRequiresLimits) {
|
||||
auto options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("a", "va"));
|
||||
ASSERT_OK(Put("b", "vb"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.fill_cache = GetParam();
|
||||
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
|
||||
|
||||
MultiScanArgs scan_options(BytewiseComparator());
|
||||
scan_options.reverse = true;
|
||||
scan_options.insert("a");
|
||||
scan_options.insert("b", "c");
|
||||
|
||||
std::unique_ptr<MultiScan> iter =
|
||||
dbfull()->NewMultiScan(ro, cfh, scan_options);
|
||||
ASSERT_NE(iter, nullptr);
|
||||
try {
|
||||
(void)iter->begin();
|
||||
FAIL() << "Expected reverse MultiScan without an upper bound to fail";
|
||||
} catch (MultiScanException& ex) {
|
||||
ASSERT_NOK(ex.status());
|
||||
ASSERT_TRUE(ex.status().IsInvalidArgument());
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper filesystem that does not support async IO.
|
||||
// Used to verify that MultiScan gracefully falls back to sync IO.
|
||||
class NoAsyncIOFS : public FileSystemWrapper {
|
||||
@@ -6114,13 +6265,96 @@ TEST_P(DBMultiScanIteratorTest, WastedBlocksTracking) {
|
||||
uint64_t wasted =
|
||||
options.statistics->getTickerCount(MULTISCAN_PREFETCH_BLOCKS_WASTED);
|
||||
|
||||
// We expect some wasted blocks due to the gap between ranges
|
||||
// The exact number depends on prefetch behavior, but should be > 0
|
||||
// if blocks between k020-k050 were prefetched
|
||||
std::cout << "Wasted blocks: " << wasted << std::endl;
|
||||
ASSERT_GT(wasted, 0);
|
||||
}
|
||||
|
||||
// Note: The test verifies the tracking mechanism works.
|
||||
// The actual count depends on prefetch heuristics which may vary.
|
||||
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,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
|
||||
+87
-14
@@ -23,6 +23,10 @@
|
||||
#include "table/format.h"
|
||||
#include "util/random.h"
|
||||
|
||||
#if USE_COROUTINES
|
||||
#include "folly/coro/BlockingWait.h"
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace {
|
||||
@@ -31,6 +35,7 @@ int64_t MaybeCurrentTime(Env* env) {
|
||||
env->GetCurrentTime(&time).PermitUncheckedError();
|
||||
return time;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// Special Env used to delay background operations
|
||||
@@ -857,12 +862,29 @@ 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) {
|
||||
std::string DBTestBase::Get(const std::string& k, const Snapshot* snapshot,
|
||||
bool use_coroutine) {
|
||||
ReadOptions options;
|
||||
options.verify_checksums = true;
|
||||
options.snapshot = snapshot;
|
||||
std::string result;
|
||||
Status s = db_->Get(options, k, &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);
|
||||
}
|
||||
if (s.IsNotFound()) {
|
||||
result = "NOT_FOUND";
|
||||
} else if (!s.ok()) {
|
||||
@@ -872,12 +894,27 @@ std::string DBTestBase::Get(const std::string& k, const Snapshot* snapshot) {
|
||||
}
|
||||
|
||||
std::string DBTestBase::Get(int cf, const std::string& k,
|
||||
const Snapshot* snapshot) {
|
||||
const Snapshot* snapshot, bool use_coroutine) {
|
||||
ReadOptions options;
|
||||
options.verify_checksums = true;
|
||||
options.snapshot = snapshot;
|
||||
std::string result;
|
||||
Status s = db_->Get(options, handles_[cf], k, &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);
|
||||
}
|
||||
if (s.IsNotFound()) {
|
||||
result = "NOT_FOUND";
|
||||
} else if (!s.ok()) {
|
||||
@@ -889,8 +926,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,
|
||||
const bool batched,
|
||||
const bool async) {
|
||||
bool batched, bool async,
|
||||
bool use_coroutine) {
|
||||
ReadOptions options;
|
||||
options.verify_checksums = true;
|
||||
options.snapshot = snapshot;
|
||||
@@ -917,8 +954,19 @@ 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());
|
||||
db_->MultiGet(options, cfs.size(), handles.data(), keys.data(),
|
||||
pin_values.data(), s.data());
|
||||
#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());
|
||||
}
|
||||
for (size_t i = 0; i < s.size(); ++i) {
|
||||
if (s[i].IsNotFound()) {
|
||||
result[i] = "NOT_FOUND";
|
||||
@@ -937,11 +985,14 @@ 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,
|
||||
const bool async) {
|
||||
bool async,
|
||||
bool optimize_multiget_for_io,
|
||||
bool use_coroutine) {
|
||||
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());
|
||||
@@ -950,8 +1001,21 @@ 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]);
|
||||
}
|
||||
db_->MultiGet(options, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), pin_values.data(), statuses.data());
|
||||
#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());
|
||||
}
|
||||
for (size_t i = 0; i < statuses.size(); ++i) {
|
||||
if (statuses[i].IsNotFound()) {
|
||||
result[i] = "NOT_FOUND";
|
||||
@@ -967,11 +1031,20 @@ std::vector<std::string> DBTestBase::MultiGet(const std::vector<std::string>& k,
|
||||
return result;
|
||||
}
|
||||
|
||||
Status DBTestBase::Get(const std::string& k, PinnableSlice* v) {
|
||||
Status DBTestBase::Get(const std::string& k, PinnableSlice* v,
|
||||
bool use_coroutine) {
|
||||
ReadOptions options;
|
||||
options.verify_checksums = true;
|
||||
Status s = dbfull()->Get(options, dbfull()->DefaultColumnFamily(), k, v);
|
||||
return s;
|
||||
#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 DBTestBase::CompactRange(const CompactRangeOptions& options,
|
||||
|
||||
+12
-7
@@ -1231,22 +1231,27 @@ class DBTestBase : public testing::Test {
|
||||
|
||||
Status SingleDelete(int cf, const std::string& k);
|
||||
|
||||
std::string Get(const std::string& k, const Snapshot* snapshot = nullptr);
|
||||
std::string Get(const std::string& k, const Snapshot* snapshot = nullptr,
|
||||
bool use_coroutine = false);
|
||||
|
||||
std::string Get(int cf, const std::string& k,
|
||||
const Snapshot* snapshot = nullptr);
|
||||
const Snapshot* snapshot = nullptr,
|
||||
bool use_coroutine = false);
|
||||
|
||||
Status Get(const std::string& k, PinnableSlice* v);
|
||||
Status Get(const std::string& k, PinnableSlice* v,
|
||||
bool use_coroutine = false);
|
||||
|
||||
std::vector<std::string> MultiGet(std::vector<int> cfs,
|
||||
const std::vector<std::string>& k,
|
||||
const Snapshot* snapshot,
|
||||
const bool batched,
|
||||
const bool async = false);
|
||||
const Snapshot* snapshot, bool batched,
|
||||
bool async = false,
|
||||
bool use_coroutine = false);
|
||||
|
||||
std::vector<std::string> MultiGet(const std::vector<std::string>& k,
|
||||
const Snapshot* snapshot = nullptr,
|
||||
const bool async = false);
|
||||
bool async = false,
|
||||
bool optimize_multiget_for_io = true,
|
||||
bool use_coroutine = false);
|
||||
|
||||
Status CompactRange(const CompactRangeOptions& options,
|
||||
std::optional<Slice> begin, std::optional<Slice> end);
|
||||
|
||||
+22
-26
@@ -782,19 +782,30 @@ TEST_P(DBWriteTest, LockWALConcurrentRecursive) {
|
||||
ASSERT_OK(db_->GetLiveFilesStorageInfo({lf_opts}, &files));
|
||||
}
|
||||
|
||||
port::Thread parallel_lock_wal{[&]() {
|
||||
ASSERT_OK(db_->LockWAL()); // 2 -> 3 or 1 -> 2
|
||||
}};
|
||||
SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"DBWriteTest::LockWALConcurrentRecursive:ParallelLockAcquired",
|
||||
"DBWriteTest::LockWALConcurrentRecursive:MainUnlocks"},
|
||||
{"DBWriteTest::LockWALConcurrentRecursive:MainAllowsParallelUnlock",
|
||||
"DBWriteTest::LockWALConcurrentRecursive:ParallelUnlocks"}});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(db_->UnlockWAL()); // 2 -> 1 or 3 -> 2
|
||||
port::Thread parallel_lock_wal{[&]() {
|
||||
ASSERT_OK(db_->LockWAL()); // 2 -> 3
|
||||
TEST_SYNC_POINT(
|
||||
"DBWriteTest::LockWALConcurrentRecursive:ParallelLockAcquired");
|
||||
TEST_SYNC_POINT("DBWriteTest::LockWALConcurrentRecursive:ParallelUnlocks");
|
||||
ASSERT_OK(db_->UnlockWAL());
|
||||
}};
|
||||
TEST_SYNC_POINT("DBWriteTest::LockWALConcurrentRecursive:MainUnlocks");
|
||||
|
||||
ASSERT_OK(db_->UnlockWAL()); // 3 -> 2
|
||||
// Give parallel_put an extra chance to jump in case of bug
|
||||
std::this_thread::yield();
|
||||
parallel_lock_wal.join();
|
||||
ASSERT_FALSE(parallel_put_completed.Load());
|
||||
ASSERT_FALSE(parallel_ingest_completed.Load());
|
||||
ASSERT_FALSE(flush_completed.Load());
|
||||
|
||||
// Should now have 2 outstanding LockWAL
|
||||
// Should still have 2 outstanding LockWAL().
|
||||
ASSERT_EQ(Get("k1"), "k1_orig");
|
||||
|
||||
ASSERT_OK(db_->UnlockWAL()); // 2 -> 1
|
||||
@@ -807,26 +818,11 @@ TEST_P(DBWriteTest, LockWALConcurrentRecursive) {
|
||||
ASSERT_EQ(Get("k2"), "NOT_FOUND");
|
||||
ASSERT_EQ(frozen_seqno, db_->GetLatestSequenceNumber());
|
||||
|
||||
// Ensure final Unlock is concurrency safe and extra Unlock is safe but
|
||||
// non-OK
|
||||
std::atomic<int> unlock_ok{0};
|
||||
port::Thread parallel_stuff{[&]() {
|
||||
if (db_->UnlockWAL().ok()) {
|
||||
unlock_ok++;
|
||||
}
|
||||
ASSERT_OK(db_->LockWAL());
|
||||
if (db_->UnlockWAL().ok()) {
|
||||
unlock_ok++;
|
||||
}
|
||||
}};
|
||||
|
||||
if (db_->UnlockWAL().ok()) {
|
||||
unlock_ok++;
|
||||
}
|
||||
parallel_stuff.join();
|
||||
|
||||
// There was one extra unlock, so just one non-ok
|
||||
ASSERT_EQ(unlock_ok.load(), 2);
|
||||
TEST_SYNC_POINT(
|
||||
"DBWriteTest::LockWALConcurrentRecursive:MainAllowsParallelUnlock");
|
||||
parallel_lock_wal.join();
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->LoadDependency({});
|
||||
|
||||
// Write can proceed
|
||||
parallel_put.join();
|
||||
|
||||
@@ -418,6 +418,15 @@ 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);
|
||||
@@ -447,13 +456,6 @@ 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_);
|
||||
Status s = db_->ResumeImpl(recover_context_, Env::IOActivity::kFlush);
|
||||
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);
|
||||
Status s = db_->ResumeImpl(context, Env::IOActivity::kFlush);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
+12
-15
@@ -520,12 +520,13 @@ 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(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_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_EQ(listener->flushed_dbs_.size(), i);
|
||||
ASSERT_EQ(listener->flushed_column_family_names_.size(), i);
|
||||
}
|
||||
@@ -635,21 +636,17 @@ 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) {
|
||||
ASSERT_OK(dbs[d]->Flush(FlushOptions(), vec_handles[d][c]));
|
||||
ASSERT_OK(static_cast_with_check<DBImpl>(dbs[d].get())
|
||||
->TEST_WaitForFlushMemTable());
|
||||
// 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]));
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
+34
-45
@@ -52,25 +52,6 @@ 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,
|
||||
@@ -78,29 +59,24 @@ 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_ref, value_of_default);
|
||||
if (status.ok()) {
|
||||
entity, value_of_default, is_blob_reference);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (!is_blob_reference) {
|
||||
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;
|
||||
bool resolved = false;
|
||||
status = WideColumnSerialization::GetValueOfDefaultColumnResolvingBlobs(
|
||||
entity, user_key, blob_fetcher, resolved_default, resolved);
|
||||
status = WideColumnSerialization::ResolveDefaultColumnBlobReference(
|
||||
value_of_default, user_key, blob_fetcher, resolved_default);
|
||||
if (status.ok()) {
|
||||
// Resolved blob values are backed by this stack-local PinnableSlice, so
|
||||
// copy them into MergeContext instead of pinning their storage.
|
||||
// Resolved value is backed by this stack-local PinnableSlice, so copy it
|
||||
// into MergeContext instead of pinning its storage.
|
||||
merge_context->PushOperand(Slice(resolved_default), false);
|
||||
}
|
||||
return status;
|
||||
@@ -1510,21 +1486,34 @@ 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);
|
||||
v, value_of_default, is_blob_reference);
|
||||
if (s->status->ok()) {
|
||||
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());
|
||||
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());
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1858,7 +1847,7 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
range->AddValueSize(iter->value->size());
|
||||
} else {
|
||||
assert(iter->columns);
|
||||
range->AddValueSize(iter->columns->serialized_size());
|
||||
range->AddValueSize(iter->columns->payload_size());
|
||||
}
|
||||
|
||||
range->MarkKeyDone(iter);
|
||||
@@ -1916,7 +1905,7 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
range->AddValueSize(iter->value->size());
|
||||
} else {
|
||||
assert(iter->columns);
|
||||
range->AddValueSize(iter->columns->serialized_size());
|
||||
range->AddValueSize(iter->columns->payload_size());
|
||||
}
|
||||
|
||||
range->MarkKeyDone(iter);
|
||||
|
||||
+29
-1
@@ -287,7 +287,12 @@ class MemTableList {
|
||||
|
||||
// Should not delete MemTableList without making sure MemTableList::current()
|
||||
// is Unref()'d.
|
||||
~MemTableList() {}
|
||||
~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);
|
||||
}
|
||||
|
||||
MemTableListVersion* current() const { return current_; }
|
||||
|
||||
@@ -305,6 +310,20 @@ 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;
|
||||
@@ -552,6 +571,15 @@ 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.
|
||||
|
||||
+7
-3
@@ -254,9 +254,13 @@ 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)
|
||||
size_t result_size =
|
||||
result_value ? result_value->size() : result_entity->serialized_size();
|
||||
// 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());
|
||||
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::Deserialize(entity_copy, existing_columns);
|
||||
const Status s = WideColumnSerialization::DeserializeSimple(
|
||||
entity_copy, existing_columns);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
+104
-29
@@ -3,14 +3,56 @@
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "file/file_util.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "rocksdb/iterator.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
using MultiScanIterator = MultiScan::MultiScanIterator;
|
||||
|
||||
namespace {
|
||||
|
||||
Status ValidateScanOptionsForMultiScan(const MultiScanArgs& multiscan_opts) {
|
||||
if (multiscan_opts.empty()) {
|
||||
return Status::InvalidArgument("Empty MultiScanArgs");
|
||||
}
|
||||
|
||||
const Comparator* comparator = multiscan_opts.GetComparator();
|
||||
if (comparator == nullptr) {
|
||||
return Status::InvalidArgument("MultiScanArgs requires comparator");
|
||||
}
|
||||
|
||||
const std::vector<ScanOptions>& scan_opts = multiscan_opts.GetScanRanges();
|
||||
for (size_t i = 0; i < scan_opts.size(); ++i) {
|
||||
const auto& scan_range = scan_opts[i].range;
|
||||
if (!scan_range.start.has_value()) {
|
||||
return Status::InvalidArgument("Scan has no start key at index " +
|
||||
std::to_string(i));
|
||||
}
|
||||
if (multiscan_opts.reverse && !scan_range.limit.has_value()) {
|
||||
return Status::InvalidArgument(
|
||||
"Reverse MultiScan requires limit at index " + std::to_string(i));
|
||||
}
|
||||
if (scan_range.limit.has_value() &&
|
||||
comparator->CompareWithoutTimestamp(
|
||||
scan_range.start.value(), /*a_has_ts=*/false,
|
||||
scan_range.limit.value(), /*b_has_ts=*/false) >= 0) {
|
||||
return Status::InvalidArgument(
|
||||
"Scan start key is large or equal than limit at index " +
|
||||
std::to_string(i));
|
||||
}
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MultiScan::MultiScan(const ReadOptions& read_options,
|
||||
const MultiScanArgs& scan_opts, DB* db,
|
||||
ColumnFamilyHandle* cfh)
|
||||
@@ -28,14 +70,28 @@ MultiScan::MultiScan(const ReadOptions& read_options,
|
||||
}()),
|
||||
db_(db),
|
||||
cfh_(cfh) {
|
||||
Status validate_status = ValidateScanOptionsForMultiScan(scan_opts_);
|
||||
if (!validate_status.ok()) {
|
||||
db_iter_.reset(NewErrorIterator(validate_status));
|
||||
return;
|
||||
}
|
||||
|
||||
bool slow_path = false;
|
||||
// Setup read_options with iterate_uuper_bound based on the first scan.
|
||||
// Subsequent scans will update and allocate a new DB iterator as necessary
|
||||
if (scan_opts_.GetScanRanges()[0].range.limit) {
|
||||
upper_bound_ = *scan_opts_.GetScanRanges()[0].range.limit;
|
||||
read_options_.iterate_upper_bound = &upper_bound_;
|
||||
} else {
|
||||
read_options_.iterate_upper_bound = nullptr;
|
||||
// Setup read_options bounds based on the first scan. Subsequent scans will
|
||||
// update the bounds and allocate a new DB iterator as necessary.
|
||||
{
|
||||
const ScanOptions& first_scan = scan_opts_.GetScanRanges()[0];
|
||||
if (scan_opts_.reverse) {
|
||||
lower_bound_ = *first_scan.range.start;
|
||||
read_options_.iterate_lower_bound = &lower_bound_;
|
||||
upper_bound_ = *first_scan.range.limit;
|
||||
read_options_.iterate_upper_bound = &upper_bound_;
|
||||
} else if (first_scan.range.limit) {
|
||||
upper_bound_ = *first_scan.range.limit;
|
||||
read_options_.iterate_upper_bound = &upper_bound_;
|
||||
} else {
|
||||
read_options_.iterate_upper_bound = nullptr;
|
||||
}
|
||||
}
|
||||
for (const auto& opts : scan_opts_.GetScanRanges()) {
|
||||
// Check that all the ScanOptions either specify an upper bound or not. If
|
||||
@@ -54,36 +110,55 @@ MultiScan::MultiScan(const ReadOptions& read_options,
|
||||
}
|
||||
}
|
||||
|
||||
void MultiScanIterator::SeekCurrentRange() {
|
||||
const ScanOptions& scan_opt = scan_opts_[idx_];
|
||||
if (scan_opt.range.limit) {
|
||||
*upper_bound_ = *scan_opt.range.limit;
|
||||
read_options_.iterate_upper_bound = upper_bound_;
|
||||
} else {
|
||||
read_options_.iterate_upper_bound = nullptr;
|
||||
}
|
||||
|
||||
if (reverse_) {
|
||||
*lower_bound_ = *scan_opt.range.start;
|
||||
read_options_.iterate_lower_bound = lower_bound_;
|
||||
assert(scan_opt.range.limit.has_value());
|
||||
db_iter_->SeekForPrev(*scan_opt.range.limit);
|
||||
} else {
|
||||
db_iter_->Seek(*scan_opt.range.start);
|
||||
}
|
||||
}
|
||||
|
||||
MultiScanIterator& MultiScanIterator::operator++() {
|
||||
status_ = db_iter_->status();
|
||||
if (!status_.ok()) {
|
||||
throw MultiScanException(status_);
|
||||
}
|
||||
|
||||
if (idx_ >= scan_opts_.size()) {
|
||||
throw std::logic_error("Index out of range");
|
||||
if (!valid_) {
|
||||
throw std::logic_error("Incrementing exhausted MultiScan iterator");
|
||||
}
|
||||
idx_++;
|
||||
if (idx_ < scan_opts_.size()) {
|
||||
// Check if we need to update read_options_
|
||||
if (scan_opts_[idx_].range.limit.has_value() !=
|
||||
(read_options_.iterate_upper_bound != nullptr)) {
|
||||
if (scan_opts_[idx_].range.limit) {
|
||||
*upper_bound_ = *scan_opts_[idx_].range.limit;
|
||||
read_options_.iterate_upper_bound = upper_bound_;
|
||||
} else {
|
||||
read_options_.iterate_upper_bound = nullptr;
|
||||
}
|
||||
db_iter_.reset(db_->NewIterator(read_options_, cfh_));
|
||||
scan_.Reset(db_iter_.get());
|
||||
} else if (scan_opts_[idx_].range.limit) {
|
||||
*upper_bound_ = *scan_opts_[idx_].range.limit;
|
||||
}
|
||||
db_iter_->Seek(*scan_opts_[idx_].range.start);
|
||||
status_ = db_iter_->status();
|
||||
if (!status_.ok()) {
|
||||
throw MultiScanException(status_);
|
||||
++idx_;
|
||||
if (idx_ >= scan_opts_.size()) {
|
||||
valid_ = false;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Check if we need to update read_options_
|
||||
if (scan_opts_[idx_].range.limit.has_value() !=
|
||||
(read_options_.iterate_upper_bound != nullptr)) {
|
||||
if (scan_opts_[idx_].range.limit) {
|
||||
read_options_.iterate_upper_bound = upper_bound_;
|
||||
} else {
|
||||
read_options_.iterate_upper_bound = nullptr;
|
||||
}
|
||||
db_iter_.reset(db_->NewIterator(read_options_, cfh_));
|
||||
scan_.Reset(db_iter_.get(), reverse_);
|
||||
}
|
||||
SeekCurrentRange();
|
||||
status_ = db_iter_->status();
|
||||
if (!status_.ok()) {
|
||||
throw MultiScanException(status_);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
// 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 {
|
||||
@@ -20,6 +24,91 @@ 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) {
|
||||
@@ -58,6 +147,34 @@ 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) {
|
||||
@@ -79,6 +196,80 @@ 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;
|
||||
|
||||
+279
-1
@@ -7,23 +7,41 @@
|
||||
|
||||
#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;
|
||||
@@ -64,7 +82,267 @@ std::unique_ptr<DB> OpenDb(bool read_only = false) {
|
||||
return db;
|
||||
}
|
||||
|
||||
class PerfContextTest : public testing::Test {};
|
||||
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));
|
||||
}
|
||||
|
||||
TEST_F(PerfContextTest, SeekIntoDeletion) {
|
||||
ASSERT_OK(DestroyDB(kDbName, Options()));
|
||||
|
||||
@@ -593,93 +593,6 @@ 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) {
|
||||
|
||||
+8
-7
@@ -144,13 +144,14 @@ 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"
|
||||
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);
|
||||
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);
|
||||
|
||||
// Return the range delete tombstone iterator of the file specified by
|
||||
// `file_meta`.
|
||||
|
||||
@@ -8,10 +8,93 @@
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
#if defined(WITHOUT_COROUTINES)
|
||||
#endif
|
||||
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;
|
||||
}
|
||||
|
||||
// Batched version of TableCache::MultiGet.
|
||||
DEFINE_SYNC_AND_ASYNC(Status, TableCache::MultiGet)
|
||||
@@ -85,9 +168,8 @@ 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;
|
||||
|
||||
+409
-118
@@ -15,6 +15,7 @@
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
@@ -36,11 +37,46 @@
|
||||
#include "port/port.h"
|
||||
#include "table/table_reader.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/cast_util.h"
|
||||
#include "util/hash_containers.h"
|
||||
#include "util/string_util.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class VersionBuilder::Rep {
|
||||
// Scalar/flag state that a VersionEdit can mutate while being applied. Factored
|
||||
// into a small struct so the single-edit undo (VersionBuilder::Rep::EditUndo)
|
||||
// can snapshot and restore all of it with a single copy/swap, and so the field
|
||||
// list is defined once rather than duplicated between the live state and its
|
||||
// undo record. VersionBuilder::Rep inherits this (as does EditUndo) so the
|
||||
// scalar state can be snapshotted/restored via up_cast<MutableScalars> with a
|
||||
// single copy/swap; Rep is a file-local implementation class, so exposing the
|
||||
// base is inconsequential.
|
||||
struct MutableScalars {
|
||||
// Whether there are invalid new files or invalid deletions on levels larger
|
||||
// than num_levels_.
|
||||
bool has_invalid_levels_ = false;
|
||||
|
||||
// The fields below are only meaningful when track_found_and_missing_files_ is
|
||||
// enabled.
|
||||
|
||||
// The highest file number among all missing blob files, or
|
||||
// kInvalidBlobFileNumber if none are missing. Useful to check whether a
|
||||
// complete Version is available.
|
||||
uint64_t missing_blob_files_high_ = kInvalidBlobFileNumber;
|
||||
// Cached result of the last validity check: true if all the files making up
|
||||
// the Version can be found. Or, when allow_incomplete_valid_version_ is true
|
||||
// and the version was never edited in an atomic group, true if only a suffix
|
||||
// of L0 SST files and their associated blob files are missing.
|
||||
bool valid_version_available_ = false;
|
||||
// True if the version was ever edited in an atomic group.
|
||||
bool edited_in_atomic_group_ = false;
|
||||
// True if the Version was updated since the last validity check, so the
|
||||
// cached `valid_version_available_` must be recomputed on the next check
|
||||
// rather than reused.
|
||||
bool version_updated_since_last_check_ = false;
|
||||
};
|
||||
|
||||
class VersionBuilder::Rep : public MutableScalars {
|
||||
class NewestFirstBySeqNo {
|
||||
public:
|
||||
bool operator()(const FileMetaData* lhs, const FileMetaData* rhs) const {
|
||||
@@ -265,17 +301,13 @@ class VersionBuilder::Rep {
|
||||
// on invalid levels. The version is not consistent if in the end the files
|
||||
// on invalid levels don't cancel out.
|
||||
std::unordered_map<int, size_t> invalid_level_sizes_;
|
||||
// Whether there are invalid new files or invalid deletion on levels larger
|
||||
// than num_levels_.
|
||||
bool has_invalid_levels_;
|
||||
// Current levels of table files affected by additions/deletions.
|
||||
std::unordered_map<uint64_t, int> table_file_levels_;
|
||||
// Current compact cursors that should be changed after the last compaction
|
||||
std::unordered_map<int, InternalKey> updated_compact_cursors_;
|
||||
const std::shared_ptr<const NewestFirstByEpochNumber>
|
||||
level_zero_cmp_by_epochno_;
|
||||
const std::shared_ptr<const NewestFirstBySeqNo> level_zero_cmp_by_seqno_;
|
||||
const std::shared_ptr<const BySmallestKey> level_nonzero_cmp_;
|
||||
const NewestFirstByEpochNumber level_zero_cmp_by_epochno_;
|
||||
const NewestFirstBySeqNo level_zero_cmp_by_seqno_;
|
||||
const BySmallestKey level_nonzero_cmp_;
|
||||
|
||||
// Mutable metadata objects for all blob files affected by the series of
|
||||
// version edits.
|
||||
@@ -302,30 +334,88 @@ class VersionBuilder::Rep {
|
||||
std::unordered_set<uint64_t> non_l0_missing_files_;
|
||||
// Intermediate SST files (blob files not included yet)
|
||||
std::vector<std::string> intermediate_files_;
|
||||
// The highest file number for all the missing blob files, useful to check
|
||||
// if a complete Version is available.
|
||||
uint64_t missing_blob_files_high_ = kInvalidBlobFileNumber;
|
||||
// Missing blob files, useful to check if only the missing L0 files'
|
||||
// associated blob files are missing.
|
||||
std::unordered_set<uint64_t> missing_blob_files_;
|
||||
// True if all files consisting the Version can be found. Or if
|
||||
// `allow_incomplete_valid_version_` is true and the version history is not
|
||||
// ever edited in an atomic group, this will be true if only a
|
||||
// suffix of L0 SST files and their associated blob files are missing.
|
||||
bool valid_version_available_;
|
||||
// True if version is ever edited in an atomic group.
|
||||
bool edited_in_atomic_group_;
|
||||
|
||||
// Flag to indicate if the Version is updated since last validity check. If no
|
||||
// `Apply` call is made between a `Rep`'s construction and a
|
||||
// `ValidVersionAvailable` check or between two `ValidVersionAvailable` calls.
|
||||
// This flag will be true to indicate the cached validity value can be
|
||||
// directly used without a recheck.
|
||||
bool version_updated_since_last_check_;
|
||||
// Note: additional scalar/flag state mutated while applying edits
|
||||
// (has_invalid_levels_, missing_blob_files_high_, valid_version_available_,
|
||||
// edited_in_atomic_group_, version_updated_since_last_check_) lives in the
|
||||
// MutableScalars base so the single-edit undo can snapshot/restore it in one
|
||||
// copy/swap.
|
||||
|
||||
// End of fields that are only tracked when `track_found_and_missing_files_`
|
||||
// is enabled.
|
||||
|
||||
// Records how to undo (and redo) the most recent `Apply()` so that
|
||||
// VersionEditHandlerPointInTime can build a `Version` reflecting the state
|
||||
// *before* an edit (on a valid->invalid negative edge) without copying the
|
||||
// whole `Rep` on every edit. Only populated when
|
||||
// `track_found_and_missing_files_` is true.
|
||||
//
|
||||
// Each recorded map/set entry stores the "other side" value (the value not
|
||||
// currently live). Applying `ToggleUndo()` swaps every recorded slot with the
|
||||
// live state, so it is self-inverse: one call rolls back to the pre-edit
|
||||
// state, a second call redoes to the post-edit state. Objects momentarily
|
||||
// removed from the live state (e.g. blob metadata) are held inside these
|
||||
// records via value/shared_ptr, so nothing referenced by the pre-edit state
|
||||
// is destroyed during the tentative window. `FileMetaData` removed from
|
||||
// `added_files` by a deletion is instead kept alive via `deferred_unref` and
|
||||
// only released by `CommitLastApply()`.
|
||||
struct EditUndo : MutableScalars {
|
||||
// True while the live state has been rolled back to the pre-edit state
|
||||
// (i.e. `ToggleUndo()` has run an odd number of times).
|
||||
bool rolled_back = false;
|
||||
|
||||
// Each map below records, for the keys the edit touches, the value on the
|
||||
// "other side" (the value not currently live): before the edit while in the
|
||||
// forward/post-edit state, and after the edit once rolled back. `nullopt`
|
||||
// means the key is absent on that side. `ToggleUndo()` swaps each recorded
|
||||
// entry with the corresponding live container, so it is self-inverse.
|
||||
|
||||
// levels_[level].added_files entries, keyed by (level, file number). Holds
|
||||
// the FileMetaData* on the non-live side (the map only owns the pointer,
|
||||
// not the object; see `deferred_unref` for lifetime of deleted files).
|
||||
std::map<std::pair<int, uint64_t>, std::optional<FileMetaData*>>
|
||||
added_files;
|
||||
// levels_[level].deleted_files membership, keyed by (level, file number);
|
||||
// value is whether the file number is present on the non-live side.
|
||||
std::map<std::pair<int, uint64_t>, bool> deleted_files;
|
||||
// table_file_levels_ entries, keyed by file number (level, or absent).
|
||||
UnorderedMap<uint64_t, std::optional<int>> table_file_levels;
|
||||
// invalid_level_sizes_ entries, keyed by level (count, or absent).
|
||||
UnorderedMap<int, std::optional<size_t>> invalid_level_sizes;
|
||||
// updated_compact_cursors_ entries, keyed by level (cursor key, or absent).
|
||||
UnorderedMap<int, std::optional<InternalKey>> compact_cursors;
|
||||
// mutable_blob_file_metas_ entries, keyed by blob file number. Holds the
|
||||
// MutableBlobFileMetaData by value on the non-live side; because it owns a
|
||||
// shared_ptr to the SharedBlobFileMetaData, a rolled-back blob addition is
|
||||
// kept alive here rather than destroyed (which would spuriously mark the
|
||||
// blob obsolete).
|
||||
UnorderedMap<uint64_t, std::optional<MutableBlobFileMetaData>> blob_metas;
|
||||
// Membership on the non-live side for each tracking set, keyed by file
|
||||
// number (blob file number for `missing_blob_files`).
|
||||
UnorderedMap<uint64_t, bool> found_files;
|
||||
UnorderedMap<uint64_t, bool> l0_missing_files;
|
||||
UnorderedMap<uint64_t, bool> non_l0_missing_files;
|
||||
UnorderedMap<uint64_t, bool> missing_blob_files;
|
||||
|
||||
// `intermediate_files_` is append-only within an `Apply()`. Restore by
|
||||
// truncating to `intermediate_files_size` (rollback) / re-appending
|
||||
// `intermediate_tail` (redo).
|
||||
size_t intermediate_files_size = 0;
|
||||
std::vector<std::string> intermediate_tail;
|
||||
|
||||
// (Scalar/flag state on the non-live side is held in the MutableScalars
|
||||
// base subobject.)
|
||||
|
||||
// `FileMetaData` removed from `added_files` by a deletion in this edit. The
|
||||
// ref is held here (not dropped) until `CommitLastApply()`, so a rollback
|
||||
// can reinstall the file.
|
||||
std::vector<FileMetaData*> deferred_unref;
|
||||
};
|
||||
std::unique_ptr<EditUndo> undo_;
|
||||
|
||||
public:
|
||||
Rep(const FileOptions& file_options, const ImmutableCFOptions* ioptions,
|
||||
TableCache* table_cache, VersionStorageInfo* base_vstorage,
|
||||
@@ -339,12 +429,9 @@ class VersionBuilder::Rep {
|
||||
base_vstorage_(base_vstorage),
|
||||
version_set_(version_set),
|
||||
num_levels_(base_vstorage->num_levels()),
|
||||
has_invalid_levels_(false),
|
||||
level_zero_cmp_by_epochno_(
|
||||
std::make_shared<NewestFirstByEpochNumber>()),
|
||||
level_zero_cmp_by_seqno_(std::make_shared<NewestFirstBySeqNo>()),
|
||||
level_nonzero_cmp_(std::make_shared<BySmallestKey>(
|
||||
base_vstorage_->InternalComparator())),
|
||||
level_zero_cmp_by_epochno_(),
|
||||
level_zero_cmp_by_seqno_(),
|
||||
level_nonzero_cmp_(base_vstorage_->InternalComparator()),
|
||||
file_metadata_cache_res_mgr_(file_metadata_cache_res_mgr),
|
||||
cfd_(cfd),
|
||||
version_edit_handler_(version_edit_handler),
|
||||
@@ -367,52 +454,17 @@ class VersionBuilder::Rep {
|
||||
}
|
||||
}
|
||||
|
||||
Rep(const Rep& other)
|
||||
: file_options_(other.file_options_),
|
||||
ioptions_(other.ioptions_),
|
||||
table_cache_(other.table_cache_),
|
||||
base_vstorage_(other.base_vstorage_),
|
||||
version_set_(other.version_set_),
|
||||
num_levels_(other.num_levels_),
|
||||
invalid_level_sizes_(other.invalid_level_sizes_),
|
||||
has_invalid_levels_(other.has_invalid_levels_),
|
||||
table_file_levels_(other.table_file_levels_),
|
||||
updated_compact_cursors_(other.updated_compact_cursors_),
|
||||
level_zero_cmp_by_epochno_(other.level_zero_cmp_by_epochno_),
|
||||
level_zero_cmp_by_seqno_(other.level_zero_cmp_by_seqno_),
|
||||
level_nonzero_cmp_(other.level_nonzero_cmp_),
|
||||
mutable_blob_file_metas_(other.mutable_blob_file_metas_),
|
||||
file_metadata_cache_res_mgr_(other.file_metadata_cache_res_mgr_),
|
||||
cfd_(other.cfd_),
|
||||
version_edit_handler_(other.version_edit_handler_),
|
||||
track_found_and_missing_files_(other.track_found_and_missing_files_),
|
||||
allow_incomplete_valid_version_(other.allow_incomplete_valid_version_),
|
||||
found_files_(other.found_files_),
|
||||
l0_missing_files_(other.l0_missing_files_),
|
||||
non_l0_missing_files_(other.non_l0_missing_files_),
|
||||
intermediate_files_(other.intermediate_files_),
|
||||
missing_blob_files_high_(other.missing_blob_files_high_),
|
||||
missing_blob_files_(other.missing_blob_files_),
|
||||
valid_version_available_(other.valid_version_available_),
|
||||
edited_in_atomic_group_(other.edited_in_atomic_group_),
|
||||
version_updated_since_last_check_(
|
||||
other.version_updated_since_last_check_) {
|
||||
assert(ioptions_);
|
||||
levels_ = new LevelState[num_levels_];
|
||||
for (int level = 0; level < num_levels_; level++) {
|
||||
levels_[level] = other.levels_[level];
|
||||
const auto& added = levels_[level].added_files;
|
||||
for (auto& pair : added) {
|
||||
RefFile(pair.second);
|
||||
}
|
||||
}
|
||||
if (track_found_and_missing_files_) {
|
||||
assert(cfd_);
|
||||
assert(version_edit_handler_);
|
||||
}
|
||||
}
|
||||
// Rep is only ever held via std::unique_ptr and is not copied. (The previous
|
||||
// save-point mechanism deep-copied Rep on every edit, which was quadratic;
|
||||
// it has been replaced by single-edit undo, see EditUndo above.)
|
||||
Rep(const Rep&) = delete;
|
||||
Rep& operator=(const Rep&) = delete;
|
||||
|
||||
~Rep() {
|
||||
// Release any FileMetaData whose deletion was deferred by an uncommitted
|
||||
// Apply(), and ensure the live state is in the forward (post-edit) position
|
||||
// so the loop below unrefs exactly the files still owned by added_files.
|
||||
CommitLastApply();
|
||||
for (int level = 0; level < num_levels_; level++) {
|
||||
const auto& added = levels_[level].added_files;
|
||||
for (auto& pair : added) {
|
||||
@@ -423,12 +475,6 @@ class VersionBuilder::Rep {
|
||||
delete[] levels_;
|
||||
}
|
||||
|
||||
void RefFile(FileMetaData* f) {
|
||||
assert(f);
|
||||
assert(f->refs > 0);
|
||||
f->refs++;
|
||||
}
|
||||
|
||||
void UnrefFile(FileMetaData* f) {
|
||||
f->refs--;
|
||||
if (f->refs <= 0) {
|
||||
@@ -439,6 +485,15 @@ class VersionBuilder::Rep {
|
||||
f->fd.pinned_reader.Release(table_cache_->get_cache().get());
|
||||
}
|
||||
|
||||
// Evict from table cache to prevent leaked entries for files whose
|
||||
// last reference is held by the VersionBuilder (e.g., files from a
|
||||
// failed LogAndApply that were never installed in any Version).
|
||||
// Release() above only releases the pinned handle's ref but does not
|
||||
// remove the entry from the cache hash table.
|
||||
if (table_cache_ != nullptr) {
|
||||
TableCache::Evict(table_cache_->get_cache().get(), f->fd.GetNumber());
|
||||
}
|
||||
|
||||
if (file_metadata_cache_res_mgr_) {
|
||||
Status s = file_metadata_cache_res_mgr_->UpdateCacheReservation(
|
||||
f->ApproximateMemoryUsage(), false /* increase */);
|
||||
@@ -448,6 +503,252 @@ class VersionBuilder::Rep {
|
||||
}
|
||||
}
|
||||
|
||||
//======== Single-edit undo support (track_found_and_missing_files_) =======//
|
||||
// See the EditUndo declaration above for the overall model.
|
||||
|
||||
// Records the pre-edit value of `map[key]` into `saved` (first-touch only, so
|
||||
// repeated touches of the same key within one edit keep the true pre-edit
|
||||
// value). The stored value is the "other side" for ToggleMap().
|
||||
template <typename Map, typename Saved, typename K>
|
||||
static void RecordMapEntry(const Map& map, Saved& saved, const K& key) {
|
||||
if (saved.find(key) != saved.end()) {
|
||||
return;
|
||||
}
|
||||
auto it = map.find(key);
|
||||
saved.emplace(key,
|
||||
it != map.end()
|
||||
? std::optional<typename Map::mapped_type>(it->second)
|
||||
: std::nullopt);
|
||||
}
|
||||
|
||||
// Swaps every recorded entry between `map` and `saved` (self-inverse).
|
||||
template <typename Map, typename Saved>
|
||||
static void ToggleMapEntries(Map& map, Saved& saved) {
|
||||
for (auto& elem : saved) {
|
||||
const auto& key = elem.first;
|
||||
auto& other = elem.second;
|
||||
auto it = map.find(key);
|
||||
std::optional<typename Map::mapped_type> live =
|
||||
it != map.end() ? std::optional<typename Map::mapped_type>(it->second)
|
||||
: std::nullopt;
|
||||
if (other.has_value()) {
|
||||
// insert_or_assign avoids requiring a default-constructible mapped_type
|
||||
// (e.g. MutableBlobFileMetaData has no default constructor).
|
||||
map.insert_or_assign(key, std::move(*other));
|
||||
} else if (it != map.end()) {
|
||||
map.erase(it);
|
||||
}
|
||||
other = std::move(live);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Set, typename Saved, typename K>
|
||||
static void RecordSetMembership(const Set& set, Saved& saved, const K& key) {
|
||||
if (saved.find(key) != saved.end()) {
|
||||
return;
|
||||
}
|
||||
saved.emplace(key, set.find(key) != set.end());
|
||||
}
|
||||
|
||||
template <typename Set, typename Saved>
|
||||
static void ToggleSetMembership(Set& set, Saved& saved) {
|
||||
for (auto& elem : saved) {
|
||||
const auto& key = elem.first;
|
||||
bool& other_present = elem.second;
|
||||
bool live_present = set.find(key) != set.end();
|
||||
if (other_present) {
|
||||
set.insert(key);
|
||||
} else {
|
||||
set.erase(key);
|
||||
}
|
||||
other_present = live_present;
|
||||
}
|
||||
}
|
||||
|
||||
void RecordAddedFileEntry(int level, uint64_t fn) {
|
||||
assert(undo_);
|
||||
assert(level < num_levels_);
|
||||
auto key = std::make_pair(level, fn);
|
||||
if (undo_->added_files.find(key) != undo_->added_files.end()) {
|
||||
return;
|
||||
}
|
||||
const auto& map = levels_[level].added_files;
|
||||
auto it = map.find(fn);
|
||||
undo_->added_files.emplace(
|
||||
key, it != map.end() ? std::optional<FileMetaData*>(it->second)
|
||||
: std::nullopt);
|
||||
}
|
||||
|
||||
void RecordDeletedFileEntry(int level, uint64_t fn) {
|
||||
assert(undo_);
|
||||
assert(level < num_levels_);
|
||||
auto key = std::make_pair(level, fn);
|
||||
if (undo_->deleted_files.find(key) != undo_->deleted_files.end()) {
|
||||
return;
|
||||
}
|
||||
const auto& set = levels_[level].deleted_files;
|
||||
undo_->deleted_files.emplace(key, set.find(fn) != set.end());
|
||||
}
|
||||
|
||||
// Snapshots the pre-edit value of every piece of state that `edit` will
|
||||
// mutate, so the edit can later be rolled back / redone. Must be called
|
||||
// before the edit is applied and only in tracking mode.
|
||||
void CaptureUndo(const VersionEdit* edit) {
|
||||
assert(track_found_and_missing_files_);
|
||||
assert(!undo_);
|
||||
undo_ = std::make_unique<EditUndo>();
|
||||
undo_->intermediate_files_size = intermediate_files_.size();
|
||||
// Snapshot the pre-edit scalars (the MutableScalars base subobject).
|
||||
up_cast<MutableScalars>(*undo_) = up_cast<MutableScalars>(*this);
|
||||
|
||||
auto touch_table_file = [&](int level, uint64_t fn) {
|
||||
RecordMapEntry(table_file_levels_, undo_->table_file_levels, fn);
|
||||
RecordSetMembership(found_files_, undo_->found_files, fn);
|
||||
RecordSetMembership(l0_missing_files_, undo_->l0_missing_files, fn);
|
||||
RecordSetMembership(non_l0_missing_files_, undo_->non_l0_missing_files,
|
||||
fn);
|
||||
if (level >= 0 && level < num_levels_) {
|
||||
RecordAddedFileEntry(level, fn);
|
||||
RecordDeletedFileEntry(level, fn);
|
||||
} else if (level >= num_levels_) {
|
||||
RecordMapEntry(invalid_level_sizes_, undo_->invalid_level_sizes, level);
|
||||
}
|
||||
};
|
||||
auto touch_blob_file = [&](uint64_t blob_file_number) {
|
||||
if (blob_file_number == kInvalidBlobFileNumber) {
|
||||
return;
|
||||
}
|
||||
RecordMapEntry(mutable_blob_file_metas_, undo_->blob_metas,
|
||||
blob_file_number);
|
||||
RecordSetMembership(missing_blob_files_, undo_->missing_blob_files,
|
||||
blob_file_number);
|
||||
};
|
||||
|
||||
for (const auto& ba : edit->GetBlobFileAdditions()) {
|
||||
touch_blob_file(ba.GetBlobFileNumber());
|
||||
}
|
||||
for (const auto& bg : edit->GetBlobFileGarbages()) {
|
||||
touch_blob_file(bg.GetBlobFileNumber());
|
||||
}
|
||||
for (const auto& deleted_file : edit->GetDeletedFiles()) {
|
||||
const int level = deleted_file.first;
|
||||
const uint64_t fn = deleted_file.second;
|
||||
touch_table_file(level, fn);
|
||||
// Only reachable blob metadata is touched (mirrors ApplyFileDeletion).
|
||||
if (level >= 0 && level < num_levels_ &&
|
||||
GetCurrentLevelForTableFile(fn) == level) {
|
||||
touch_blob_file(GetOldestBlobFileNumberForTableFile(level, fn));
|
||||
}
|
||||
}
|
||||
for (const auto& new_file : edit->GetNewFiles()) {
|
||||
const int level = new_file.first;
|
||||
const FileMetaData& meta = new_file.second;
|
||||
touch_table_file(level, meta.fd.GetNumber());
|
||||
touch_blob_file(meta.oldest_blob_file_number);
|
||||
}
|
||||
for (const auto& cursor : edit->GetCompactCursors()) {
|
||||
const int level = cursor.first;
|
||||
if (level >= 0 && level < num_levels_) {
|
||||
RecordMapEntry(updated_compact_cursors_, undo_->compact_cursors, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Swaps the live state with the recorded "other side", moving between the
|
||||
// post-edit and pre-edit states. Self-inverse.
|
||||
void ToggleUndo() {
|
||||
assert(undo_);
|
||||
for (auto& elem : undo_->added_files) {
|
||||
const int level = elem.first.first;
|
||||
const uint64_t fn = elem.first.second;
|
||||
auto& other = elem.second;
|
||||
auto& map = levels_[level].added_files;
|
||||
auto it = map.find(fn);
|
||||
std::optional<FileMetaData*> live =
|
||||
it != map.end() ? std::optional<FileMetaData*>(it->second)
|
||||
: std::nullopt;
|
||||
if (other.has_value()) {
|
||||
map[fn] = *other;
|
||||
} else if (it != map.end()) {
|
||||
// Note: erasing the map entry does not delete the FileMetaData; its
|
||||
// lifetime is managed via `deferred_unref` / the built Version's ref.
|
||||
map.erase(it);
|
||||
}
|
||||
other = live;
|
||||
}
|
||||
for (auto& elem : undo_->deleted_files) {
|
||||
const int level = elem.first.first;
|
||||
const uint64_t fn = elem.first.second;
|
||||
bool& other_present = elem.second;
|
||||
auto& set = levels_[level].deleted_files;
|
||||
bool live_present = set.find(fn) != set.end();
|
||||
if (other_present) {
|
||||
set.insert(fn);
|
||||
} else {
|
||||
set.erase(fn);
|
||||
}
|
||||
other_present = live_present;
|
||||
}
|
||||
ToggleMapEntries(table_file_levels_, undo_->table_file_levels);
|
||||
ToggleMapEntries(invalid_level_sizes_, undo_->invalid_level_sizes);
|
||||
ToggleMapEntries(updated_compact_cursors_, undo_->compact_cursors);
|
||||
ToggleMapEntries(mutable_blob_file_metas_, undo_->blob_metas);
|
||||
ToggleSetMembership(found_files_, undo_->found_files);
|
||||
ToggleSetMembership(l0_missing_files_, undo_->l0_missing_files);
|
||||
ToggleSetMembership(non_l0_missing_files_, undo_->non_l0_missing_files);
|
||||
ToggleSetMembership(missing_blob_files_, undo_->missing_blob_files);
|
||||
|
||||
if (!undo_->rolled_back) {
|
||||
// Forward -> pre-edit: peel off the entries appended by this edit.
|
||||
for (size_t i = undo_->intermediate_files_size;
|
||||
i < intermediate_files_.size(); ++i) {
|
||||
undo_->intermediate_tail.push_back(std::move(intermediate_files_[i]));
|
||||
}
|
||||
intermediate_files_.resize(undo_->intermediate_files_size);
|
||||
} else {
|
||||
// Pre-edit -> forward: re-append.
|
||||
for (auto& s : undo_->intermediate_tail) {
|
||||
intermediate_files_.push_back(std::move(s));
|
||||
}
|
||||
undo_->intermediate_tail.clear();
|
||||
}
|
||||
|
||||
// Swap the scalar/flag state (the MutableScalars base subobject).
|
||||
std::swap(up_cast<MutableScalars>(*this), up_cast<MutableScalars>(*undo_));
|
||||
|
||||
undo_->rolled_back = !undo_->rolled_back;
|
||||
}
|
||||
|
||||
// Reverts the most recent Apply() to the pre-edit state.
|
||||
void RollbackLastApply() {
|
||||
assert(undo_);
|
||||
assert(!undo_->rolled_back);
|
||||
ToggleUndo();
|
||||
}
|
||||
|
||||
// Re-applies the most recent Apply() after a RollbackLastApply().
|
||||
void RedoLastApply() {
|
||||
assert(undo_);
|
||||
assert(undo_->rolled_back);
|
||||
ToggleUndo();
|
||||
}
|
||||
|
||||
// Finalizes the most recent Apply(): frees FileMetaData whose deletion was
|
||||
// deferred and drops the undo record. Safe to call with no pending edit.
|
||||
void CommitLastApply() {
|
||||
if (!undo_) {
|
||||
return;
|
||||
}
|
||||
if (undo_->rolled_back) {
|
||||
// Always finalize in the forward (post-edit) state.
|
||||
ToggleUndo();
|
||||
}
|
||||
for (FileMetaData* f : undo_->deferred_unref) {
|
||||
UnrefFile(f);
|
||||
}
|
||||
undo_.reset();
|
||||
}
|
||||
|
||||
// Mapping used for checking the consistency of links between SST files and
|
||||
// blob files. It is built using the forward links (table file -> blob file),
|
||||
// and is subsequently compared with the inverse mapping stored in the
|
||||
@@ -536,7 +837,7 @@ class VersionBuilder::Rep {
|
||||
|
||||
if (epoch_number_requirement ==
|
||||
EpochNumberRequirement::kMightMissing) {
|
||||
if (!level_zero_cmp_by_seqno_->operator()(lhs, rhs)) {
|
||||
if (!level_zero_cmp_by_seqno_(lhs, rhs)) {
|
||||
std::ostringstream oss;
|
||||
oss << "L0 files are not sorted properly: files #"
|
||||
<< lhs->fd.GetNumber() << " with seqnos (largest, smallest) "
|
||||
@@ -568,7 +869,7 @@ class VersionBuilder::Rep {
|
||||
}
|
||||
}
|
||||
|
||||
if (!level_zero_cmp_by_epochno_->operator()(lhs, rhs)) {
|
||||
if (!level_zero_cmp_by_epochno_(lhs, rhs)) {
|
||||
std::ostringstream oss;
|
||||
oss << "L0 files are not sorted properly: files #"
|
||||
<< lhs->fd.GetNumber() << " with epoch number "
|
||||
@@ -597,7 +898,7 @@ class VersionBuilder::Rep {
|
||||
assert(lhs);
|
||||
assert(rhs);
|
||||
|
||||
if (!level_nonzero_cmp_->operator()(lhs, rhs)) {
|
||||
if (!level_nonzero_cmp_(lhs, rhs)) {
|
||||
std::ostringstream oss;
|
||||
oss << 'L' << level << " files are not sorted properly: files #"
|
||||
<< lhs->fd.GetNumber() << ", #" << rhs->fd.GetNumber();
|
||||
@@ -895,7 +1196,12 @@ class VersionBuilder::Rep {
|
||||
auto& add_files = level_state.added_files;
|
||||
auto add_it = add_files.find(file_number);
|
||||
if (add_it != add_files.end()) {
|
||||
UnrefFile(add_it->second);
|
||||
if (undo_) {
|
||||
// Defer freeing so a rollback of this edit can reinstall the file.
|
||||
undo_->deferred_unref.push_back(add_it->second);
|
||||
} else {
|
||||
UnrefFile(add_it->second);
|
||||
}
|
||||
add_files.erase(add_it);
|
||||
}
|
||||
|
||||
@@ -976,7 +1282,7 @@ class VersionBuilder::Rep {
|
||||
delete f;
|
||||
s = Status::MemoryLimit(
|
||||
"Can't allocate " +
|
||||
kCacheEntryRoleToCamelString[static_cast<std::uint32_t>(
|
||||
kCacheEntryRoleToCamelString[lossless_cast<std::uint32_t>(
|
||||
CacheEntryRole::kFileMetadata)] +
|
||||
" due to exceeding the memory limit "
|
||||
"based on "
|
||||
@@ -1054,6 +1360,14 @@ class VersionBuilder::Rep {
|
||||
}
|
||||
}
|
||||
|
||||
// In tracking mode, snapshot how to undo this edit before mutating, so
|
||||
// VersionEditHandlerPointInTime can build a Version from the pre-edit state
|
||||
// (on a valid->invalid negative edge) via RollbackLastApply() instead of
|
||||
// copying the whole Rep on every edit.
|
||||
if (track_found_and_missing_files_) {
|
||||
CaptureUndo(edit);
|
||||
}
|
||||
|
||||
// Note: we process the blob file related changes first because the
|
||||
// table file addition/deletion logic depends on the blob files
|
||||
// already being there.
|
||||
@@ -1491,13 +1805,13 @@ class VersionBuilder::Rep {
|
||||
}
|
||||
|
||||
if (epoch_number_requirement == EpochNumberRequirement::kMightMissing) {
|
||||
SaveSSTFilesTo(vstorage, /* level */ 0, *level_zero_cmp_by_seqno_);
|
||||
SaveSSTFilesTo(vstorage, /* level */ 0, level_zero_cmp_by_seqno_);
|
||||
} else {
|
||||
SaveSSTFilesTo(vstorage, /* level */ 0, *level_zero_cmp_by_epochno_);
|
||||
SaveSSTFilesTo(vstorage, /* level */ 0, level_zero_cmp_by_epochno_);
|
||||
}
|
||||
|
||||
for (int level = 1; level < num_levels_; ++level) {
|
||||
SaveSSTFilesTo(vstorage, level, *level_nonzero_cmp_);
|
||||
SaveSSTFilesTo(vstorage, level, level_nonzero_cmp_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1551,13 +1865,13 @@ class VersionBuilder::Rep {
|
||||
|
||||
if (epoch_number_requirement == EpochNumberRequirement::kMightMissing) {
|
||||
MergeUnorderdAddedFilesWithBase(
|
||||
base_files, unordered_added_files, *level_zero_cmp_by_seqno_,
|
||||
base_files, unordered_added_files, level_zero_cmp_by_seqno_,
|
||||
[&](FileMetaData* file) {
|
||||
expected_sorted_l0_files.push_back(file);
|
||||
});
|
||||
} else {
|
||||
MergeUnorderdAddedFilesWithBase(
|
||||
base_files, unordered_added_files, *level_zero_cmp_by_epochno_,
|
||||
base_files, unordered_added_files, level_zero_cmp_by_epochno_,
|
||||
[&](FileMetaData* file) {
|
||||
expected_sorted_l0_files.push_back(file);
|
||||
});
|
||||
@@ -1747,11 +2061,11 @@ Status VersionBuilder::LoadTableHandlers(
|
||||
max_file_size_for_l0_meta_pin, read_options);
|
||||
}
|
||||
|
||||
void VersionBuilder::CreateOrReplaceSavePoint() {
|
||||
assert(rep_);
|
||||
savepoint_ = std::move(rep_);
|
||||
rep_ = std::make_unique<Rep>(*savepoint_);
|
||||
}
|
||||
void VersionBuilder::RollbackLastApply() { rep_->RollbackLastApply(); }
|
||||
|
||||
void VersionBuilder::RedoLastApply() { rep_->RedoLastApply(); }
|
||||
|
||||
void VersionBuilder::CommitLastApply() { rep_->CommitLastApply(); }
|
||||
|
||||
bool VersionBuilder::ValidVersionAvailable() {
|
||||
return rep_->ValidVersionAvailable();
|
||||
@@ -1765,29 +2079,6 @@ std::vector<std::string>& VersionBuilder::GetAndClearIntermediateFiles() {
|
||||
|
||||
void VersionBuilder::ClearFoundFiles() { return rep_->ClearFoundFiles(); }
|
||||
|
||||
Status VersionBuilder::SaveSavePointTo(VersionStorageInfo* vstorage) const {
|
||||
if (!savepoint_ || !savepoint_->ValidVersionAvailable()) {
|
||||
return Status::InvalidArgument();
|
||||
}
|
||||
return savepoint_->SaveTo(vstorage);
|
||||
}
|
||||
|
||||
Status VersionBuilder::LoadSavePointTableHandlers(
|
||||
InternalStats* internal_stats, int max_threads,
|
||||
bool prefetch_index_and_filter_in_cache, bool is_initial_load,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
size_t max_file_size_for_l0_meta_pin, const ReadOptions& read_options) {
|
||||
if (!savepoint_ || !savepoint_->ValidVersionAvailable()) {
|
||||
return Status::InvalidArgument();
|
||||
}
|
||||
return savepoint_->LoadTableHandlers(
|
||||
internal_stats, max_threads, prefetch_index_and_filter_in_cache,
|
||||
is_initial_load, mutable_cf_options, max_file_size_for_l0_meta_pin,
|
||||
read_options);
|
||||
}
|
||||
|
||||
void VersionBuilder::ClearSavePoint() { savepoint_.reset(nullptr); }
|
||||
|
||||
BaseReferencedVersionBuilder::BaseReferencedVersionBuilder(
|
||||
ColumnFamilyData* cfd, VersionEditHandler* version_edit_handler,
|
||||
bool track_found_and_missing_files, bool allow_incomplete_valid_version)
|
||||
|
||||
+12
-24
@@ -64,14 +64,6 @@ class VersionBuilder {
|
||||
|
||||
//============APIs only used by VersionEditHandlerPointInTime ============//
|
||||
|
||||
// Creates a save point for the Version that has been built so far. Subsequent
|
||||
// VersionEdits applied to the builder will not affect the Version in this
|
||||
// save point. VersionBuilder currently only supports creating one save point,
|
||||
// so when `CreateOrReplaceSavePoint` is called again, the previous save point
|
||||
// is cleared. `ClearSavePoint` can be called explicitly to clear
|
||||
// the save point too.
|
||||
void CreateOrReplaceSavePoint();
|
||||
|
||||
// The builder can find all the files to build a `Version`. Or if
|
||||
// `allow_incomplete_valid_version_` is true and the version history is never
|
||||
// edited in an atomic group, and only a suffix of L0 SST files and their
|
||||
@@ -94,27 +86,23 @@ class VersionBuilder {
|
||||
// Clearing all the found files in this Version.
|
||||
void ClearFoundFiles();
|
||||
|
||||
// Save the Version in the save point to the provided `vstorage`.
|
||||
// Non-OK status will be returned if there is not a valid save point.
|
||||
Status SaveSavePointTo(VersionStorageInfo* vstorage) const;
|
||||
|
||||
// Load all the table handlers for the Version in the save point.
|
||||
// Non-OK status will be returned if there is not a valid save point.
|
||||
Status LoadSavePointTableHandlers(InternalStats* internal_stats,
|
||||
int max_threads,
|
||||
bool prefetch_index_and_filter_in_cache,
|
||||
bool is_initial_load,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
size_t max_file_size_for_l0_meta_pin,
|
||||
const ReadOptions& read_options);
|
||||
|
||||
void ClearSavePoint();
|
||||
// Single-edit undo for building a Version reflecting the state *before* the
|
||||
// most recent `Apply()`. On a valid->invalid negative edge,
|
||||
// `RollbackLastApply()` reverts the builder to the pre-edit state (so
|
||||
// `SaveTo`/`LoadTableHandlers` build the pre-edit Version), then
|
||||
// `RedoLastApply()` restores the post-edit state to continue replay.
|
||||
// `CommitLastApply()` finalizes the most recent `Apply()` (freeing files
|
||||
// whose deletion was deferred) and must be called once per `Apply()`; it is a
|
||||
// no-op if there is nothing pending. This replaces the previous per-edit
|
||||
// whole-Rep save-point copy, which was quadratic over a MANIFEST.
|
||||
void RollbackLastApply();
|
||||
void RedoLastApply();
|
||||
void CommitLastApply();
|
||||
|
||||
//======= End of APIs only used by VersionEditPointInTime==========//
|
||||
|
||||
private:
|
||||
class Rep;
|
||||
std::unique_ptr<Rep> savepoint_;
|
||||
std::unique_ptr<Rep> rep_;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,19 +3,25 @@
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "db/version_builder.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/version_edit.h"
|
||||
#include "db/version_set.h"
|
||||
#include "rocksdb/advanced_options.h"
|
||||
#include "rocksdb/sst_file_writer.h"
|
||||
#include "table/unique_id_impl.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "test_util/testutil.h"
|
||||
#include "util/defer.h"
|
||||
#include "util/string_util.h"
|
||||
#include "utilities/fault_injection_fs.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -153,6 +159,123 @@ class VersionBuilderTest : public testing::Test {
|
||||
void UpdateVersionStorageInfo() { UpdateVersionStorageInfo(&vstorage_); }
|
||||
};
|
||||
|
||||
class VersionBuilderDBTest : public DBTestBase {
|
||||
public:
|
||||
VersionBuilderDBTest()
|
||||
: DBTestBase("version_builder_db_test", /*env_do_fsync=*/false) {}
|
||||
};
|
||||
|
||||
// Regression test: when LogAndApply fails after VersionBuilder pinned a
|
||||
// table reader for a newly-added file (via LoadTableHandlers), the table
|
||||
// cache entry must be evicted by VersionBuilder::UnrefFile when the
|
||||
// FileMetaData is destroyed.
|
||||
//
|
||||
// This test faithfully reproduces the crash-test failure mode
|
||||
// ("File N is not live nor quarantined" from TEST_VerifyNoObsoleteFilesCached
|
||||
// inside Close on ASAN):
|
||||
//
|
||||
// 1. IngestExternalFile submits an AddFile edit through LogAndApply.
|
||||
// 2. LoadTableHandlers inside ProcessManifestWrites pins a table reader
|
||||
// for the new file, creating a table cache entry.
|
||||
// 3. SyncManifest fails (fault injected). The error handler adds the
|
||||
// ingested file to its quarantine list.
|
||||
// 4. A subsequent successful LogAndApply (via manual Resume() and then
|
||||
// Flush) clears the quarantine (version_set.cc:
|
||||
// `ClearFilesToQuarantine`).
|
||||
// 5. Metadata read fault injection prevents FindObsoleteFiles from
|
||||
// scanning the directory, so the on-disk orphan is not discovered.
|
||||
// 6. On DB Close, TEST_VerifyNoObsoleteFilesCached iterates the table
|
||||
// cache and finds an entry for a file that is neither live nor
|
||||
// quarantined -> assertion fires (without the fix).
|
||||
//
|
||||
// The IngestExternalFile path issues a few no-op manifest writes before
|
||||
// the actual AddFile edit; we arm the injection only after
|
||||
// LoadTableHandlers has actually pinned a reader for the ingested file.
|
||||
TEST_F(VersionBuilderDBTest,
|
||||
IngestExternalFileEvictsTableCacheOnLogAndApplyFailure) {
|
||||
auto fault_fs = std::make_shared<FaultInjectionTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.env = fault_env.get();
|
||||
options.disable_auto_compactions = true;
|
||||
// Disable auto-recovery so recovery runs synchronously via our explicit
|
||||
// Resume() call -- no background thread to race with test teardown.
|
||||
options.max_bgerror_resume_count = 0;
|
||||
options.paranoid_file_checks = true;
|
||||
DestroyAndReopen(options);
|
||||
// RAII: ensure the DB is closed before fault_env goes out of scope,
|
||||
// even if an ASSERT_* below early-returns.
|
||||
Defer closer([this]() { Close(); });
|
||||
|
||||
// Seed the memtable with data so a subsequent Flush actually produces a
|
||||
// successful LogAndApply (and thus clears the quarantine).
|
||||
ASSERT_OK(Put("k1", "v1"));
|
||||
|
||||
// Build an external SST through the public SstFileWriter API.
|
||||
const std::string ext_file = dbname_ + "/ingest_me.sst";
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.Open(ext_file));
|
||||
ASSERT_OK(writer.Put("k2", "v2"));
|
||||
ExternalSstFileInfo file_info;
|
||||
ASSERT_OK(writer.Finish(&file_info));
|
||||
|
||||
// Arm the injection only once we have observed VersionBuilder actually
|
||||
// pin a table reader for the ingested file (i.e. after FindTable runs).
|
||||
std::atomic<bool> injection_armed{false};
|
||||
std::atomic<bool> injected_once{false};
|
||||
SyncPoint::GetInstance()->SetCallBack("TableCache::FindTable:0",
|
||||
[&](void*) { injection_armed = true; });
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ProcessManifestWrites:AfterSyncManifest", [&](void* arg) {
|
||||
if (!injection_armed.load()) {
|
||||
return;
|
||||
}
|
||||
if (injected_once.exchange(true)) {
|
||||
return;
|
||||
}
|
||||
auto* ptr = static_cast<IOStatus*>(arg);
|
||||
assert(ptr);
|
||||
*ptr = IOStatus::IOError("injected");
|
||||
// Retryable IOError -> soft error, recoverable via Resume().
|
||||
ptr->SetRetryable(true);
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
IngestExternalFileOptions ifo;
|
||||
Status s = db_->IngestExternalFile({ext_file}, ifo);
|
||||
ASSERT_NOK(s);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// Manual synchronous recovery: clear the soft bg error, then do a
|
||||
// successful LogAndApply (via Flush) which triggers
|
||||
// ClearFilesToQuarantine (version_set.cc line ~6591). This puts the
|
||||
// ingested file into the "not live nor quarantined" state.
|
||||
ASSERT_OK(dbfull()->Resume());
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Prevent the FindObsoleteFiles full-directory scan from finding the
|
||||
// orphan on disk. Mirrors the crash test's open_metadata_read_fault_one_in.
|
||||
fault_fs->SetThreadLocalErrorContext(
|
||||
FaultInjectionIOType::kMetadataRead, /*seed=*/0, /*one_in=*/1,
|
||||
/*retryable=*/false, /*has_data_loss=*/false);
|
||||
fault_fs->EnableThreadLocalErrorInjection(
|
||||
FaultInjectionIOType::kMetadataRead);
|
||||
Defer disable_fault([&] {
|
||||
fault_fs->DisableThreadLocalErrorInjection(
|
||||
FaultInjectionIOType::kMetadataRead);
|
||||
});
|
||||
|
||||
// The RAII Defer above will run Close() on scope exit. Close's
|
||||
// TEST_VerifyNoObsoleteFilesCached (active on ASAN) iterates the table
|
||||
// cache. Without the D99759696 fix, an entry for the ingested file
|
||||
// remains -- a file that is neither live nor quarantined -- and the
|
||||
// assertion fires. With the fix, VersionBuilder::UnrefFile evicts the
|
||||
// entry when the discarded FileMetaData is destroyed.
|
||||
}
|
||||
|
||||
void UnrefFilesInVersion(VersionStorageInfo* new_vstorage) {
|
||||
for (int i = 0; i < new_vstorage->num_levels(); i++) {
|
||||
for (auto* f : new_vstorage->LevelFiles(i)) {
|
||||
|
||||
+5
-1
@@ -811,7 +811,8 @@ 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& max_timestamp = "",
|
||||
const std::string& file_open_metadata = "") {
|
||||
assert(smallest_seqno <= largest_seqno);
|
||||
new_files_.emplace_back(
|
||||
level,
|
||||
@@ -822,6 +823,9 @@ 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,6 +57,7 @@ void VersionEditHandlerBase::Iterate(log::Reader& reader,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
last_valid_record_end_ = reader.LastRecordEnd();
|
||||
s = ApplyVersionEdit(edit, &cfd);
|
||||
if (s.ok()) {
|
||||
recovered_edits++;
|
||||
@@ -872,7 +873,6 @@ Status VersionEditHandlerPointInTime::MaybeCreateVersionBeforeApplyEdit(
|
||||
assert(builder_iter != builders_.end());
|
||||
VersionBuilder* builder = builder_iter->second->version_builder();
|
||||
const bool valid_pit_before_edit = builder->ValidVersionAvailable();
|
||||
builder->CreateOrReplaceSavePoint();
|
||||
s = builder->Apply(&edit);
|
||||
const bool valid_pit_after_edit = builder->ValidVersionAvailable();
|
||||
|
||||
@@ -888,22 +888,40 @@ Status VersionEditHandlerPointInTime::MaybeCreateVersionBeforeApplyEdit(
|
||||
if (s.ok() && !missing_info && !in_atomic_group_ &&
|
||||
((!valid_pit_after_edit && valid_pit_before_edit) ||
|
||||
(valid_pit_after_edit && force_create_version))) {
|
||||
// The Version to be created reflects the state *before* this edit. On a
|
||||
// negative edge (valid before, invalid after), roll the just-applied edit
|
||||
// back so the builder reflects the pre-edit state, build the Version from
|
||||
// it, then redo to restore the post-edit state and continue replay. In the
|
||||
// force case the edit is empty, so pre-edit == post-edit and no rollback is
|
||||
// needed.
|
||||
const bool negative_edge = valid_pit_before_edit && !valid_pit_after_edit;
|
||||
if (negative_edge) {
|
||||
builder->RollbackLastApply();
|
||||
}
|
||||
const auto& mopts = cfd->GetLatestMutableCFOptions();
|
||||
auto* version = new Version(
|
||||
cfd, version_set_, version_set_->file_options_, mopts, io_tracer_,
|
||||
version_set_->current_version_number_++, epoch_number_requirement_);
|
||||
s = builder->LoadSavePointTableHandlers(
|
||||
s = builder->LoadTableHandlers(
|
||||
cfd->internal_stats(),
|
||||
version_set_->db_options_->max_file_opening_threads, false, true, mopts,
|
||||
MaxFileSizeForL0MetaPin(mopts), read_options_);
|
||||
if (!s.ok()) {
|
||||
delete version;
|
||||
if (negative_edge) {
|
||||
builder->RedoLastApply();
|
||||
}
|
||||
if (s.IsCorruption()) {
|
||||
// This point in time cannot be recovered; skip it and continue.
|
||||
s = Status::OK();
|
||||
}
|
||||
builder->CommitLastApply();
|
||||
return s;
|
||||
}
|
||||
s = builder->SaveSavePointTo(version->storage_info());
|
||||
s = builder->SaveTo(version->storage_info());
|
||||
if (negative_edge) {
|
||||
builder->RedoLastApply();
|
||||
}
|
||||
if (s.ok()) {
|
||||
if (AtomicUpdateVersionsContains(cfd->GetID())) {
|
||||
AtomicUpdateVersionsPut(version);
|
||||
@@ -927,7 +945,7 @@ Status VersionEditHandlerPointInTime::MaybeCreateVersionBeforeApplyEdit(
|
||||
}
|
||||
}
|
||||
|
||||
builder->ClearSavePoint();
|
||||
builder->CommitLastApply();
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,8 +54,10 @@ class VersionEditHandlerBase {
|
||||
|
||||
const ReadOptions& read_options_;
|
||||
|
||||
// File offset at the end of the last successfully completed and
|
||||
// decoded logical record.
|
||||
// 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.
|
||||
uint64_t last_valid_record_end_ = 0;
|
||||
|
||||
private:
|
||||
|
||||
Loaded 100 of 247 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user