mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-07 03:22:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a3d362471 | ||
|
|
987e62e576 | ||
|
|
77bd1f3815 | ||
|
|
b7b8326c89 | ||
|
|
ed73bb989e | ||
|
|
951647e596 | ||
|
|
8941ff459d | ||
|
|
1aaec657a5 | ||
|
|
323d915dbc | ||
|
|
2dc6bc51b4 | ||
|
|
7217bf34a7 | ||
|
|
58d835d951 | ||
|
|
276495843d | ||
|
|
ec5106b3b8 | ||
|
|
f9e4c5c51f | ||
|
|
fcf382c95c | ||
|
|
66d7035708 | ||
|
|
9381544cd9 | ||
|
|
f8c601b91c | ||
|
|
5dfde1c262 | ||
|
|
44e0294c1d | ||
|
|
3f4d840e1a | ||
|
|
3949208acf | ||
|
|
88752298ff | ||
|
|
1dfa729793 | ||
|
|
8e1569b351 | ||
|
|
5fe225357a | ||
|
|
e8bbe08760 | ||
|
|
fd7399004f | ||
|
|
d1fbdffdae | ||
|
|
dc4006480b | ||
|
|
af0eae4de3 | ||
|
|
dbf9328c71 | ||
|
|
751bde0f7f | ||
|
|
14ecf7f13c | ||
|
|
b1b50fe102 | ||
|
|
cda4a73a19 | ||
|
|
00e73b99a2 | ||
|
|
460aea1523 | ||
|
|
4b35e9966c | ||
|
|
b7b6d7c8c0 | ||
|
|
afc922bf5f | ||
|
|
6e73763b8a | ||
|
|
f9df618e4d | ||
|
|
026f63de6c | ||
|
|
e2ccb78cf2 | ||
|
|
c64f682946 | ||
|
|
359d57be96 | ||
|
|
ba81144d8d | ||
|
|
4b9070bccb |
No files matched your search
@@ -20,9 +20,15 @@ runs:
|
||||
echo "GTEST_COLOR=1" >> "$GITHUB_ENV"
|
||||
echo "CTEST_OUTPUT_ON_FAILURE=1" >> "$GITHUB_ENV"
|
||||
echo "CTEST_TEST_TIMEOUT=300" >> "$GITHUB_ENV"
|
||||
echo "ZLIB_DOWNLOAD_BASE=https://rocksdb-deps.s3.us-west-2.amazonaws.com/pkgs/zlib" >> "$GITHUB_ENV"
|
||||
echo "BZIP2_DOWNLOAD_BASE=https://rocksdb-deps.s3.us-west-2.amazonaws.com/pkgs/bzip2" >> "$GITHUB_ENV"
|
||||
echo "SNAPPY_DOWNLOAD_BASE=https://rocksdb-deps.s3.us-west-2.amazonaws.com/pkgs/snappy" >> "$GITHUB_ENV"
|
||||
echo "LZ4_DOWNLOAD_BASE=https://rocksdb-deps.s3.us-west-2.amazonaws.com/pkgs/lz4" >> "$GITHUB_ENV"
|
||||
echo "ZSTD_DOWNLOAD_BASE=https://rocksdb-deps.s3.us-west-2.amazonaws.com/pkgs/zstd" >> "$GITHUB_ENV"
|
||||
# These third-party sources are mirrored on the "third-party-deps" GitHub
|
||||
# release. To add one, upload the asset here (intentionally hidden):
|
||||
# https://github.com/facebook/rocksdb/releases/edit/third-party-deps
|
||||
# Note how file names are generated below (vs. from the original source)
|
||||
# before uploading. Removing or modifying files will break builds for
|
||||
# older releases/revisions (not found or checksum mismatch).
|
||||
echo "ZLIB_DOWNLOAD_PREFIX=https://github.com/facebook/rocksdb/releases/download/third-party-deps/zlib-" >> "$GITHUB_ENV"
|
||||
echo "BZIP2_DOWNLOAD_PREFIX=https://github.com/facebook/rocksdb/releases/download/third-party-deps/bzip2-" >> "$GITHUB_ENV"
|
||||
echo "SNAPPY_DOWNLOAD_PREFIX=https://github.com/facebook/rocksdb/releases/download/third-party-deps/snappy-" >> "$GITHUB_ENV"
|
||||
echo "LZ4_DOWNLOAD_PREFIX=https://github.com/facebook/rocksdb/releases/download/third-party-deps/lz4-" >> "$GITHUB_ENV"
|
||||
echo "ZSTD_DOWNLOAD_PREFIX=https://github.com/facebook/rocksdb/releases/download/third-party-deps/zstd-" >> "$GITHUB_ENV"
|
||||
shell: bash
|
||||
@@ -5,23 +5,40 @@
|
||||
// return build({ icon, headerTitle, triggerLine, responseBody, footerLines
|
||||
// });
|
||||
|
||||
// GitHub rejects issue comments longer than this many characters. The model
|
||||
// response (responseBody) is the only unbounded input, so we trim it to fit
|
||||
// while keeping the fixed scaffolding (header, trigger, footer) intact.
|
||||
const MAX_COMMENT_LENGTH = 65536;
|
||||
const TRUNCATION_NOTE =
|
||||
'\n\n*[Truncated — full output in execution log artifact]*';
|
||||
|
||||
module.exports = function buildAiReviewComment(
|
||||
{icon, headerTitle, triggerLine, responseBody, footerLines}) {
|
||||
return [
|
||||
`## ${icon} ${headerTitle}`,
|
||||
'',
|
||||
triggerLine,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
responseBody,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>ℹ️ About this response</summary>',
|
||||
'',
|
||||
...footerLines,
|
||||
'</details>',
|
||||
const assemble = (body) => [`## ${icon} ${headerTitle}`,
|
||||
'',
|
||||
triggerLine,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
body,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>ℹ️ About this response</summary>',
|
||||
'',
|
||||
...footerLines,
|
||||
'</details>',
|
||||
].join('\n');
|
||||
|
||||
const body = responseBody || '';
|
||||
const comment = assemble(body);
|
||||
if (comment.length <= MAX_COMMENT_LENGTH) {
|
||||
return comment;
|
||||
}
|
||||
// Drop only as much of the body as needed to fit, then re-assemble so the
|
||||
// footer/<details> block is preserved rather than cut off mid-comment.
|
||||
const overflow = comment.length - MAX_COMMENT_LENGTH;
|
||||
const keep = Math.max(0, body.length - overflow - TRUNCATION_NOTE.length);
|
||||
return assemble(body.substring(0, keep) + TRUNCATION_NOTE);
|
||||
};
|
||||
@@ -103,7 +103,7 @@ module.exports = function parseClaude({executionFile, conclusion, meta}) {
|
||||
responseBody,
|
||||
footerLines: [
|
||||
'Generated by [Claude Code](https://github.com/anthropics/claude-code-action).',
|
||||
'Review methodology: `claude_md/code_review.md`',
|
||||
'Review methodology: `claude_md/ci_review_prompt.md`',
|
||||
'',
|
||||
'**Limitations:**',
|
||||
'- Claude may miss context from files not in the diff',
|
||||
|
||||
@@ -83,7 +83,7 @@ module.exports = function parseCodex(
|
||||
responseBody,
|
||||
footerLines: [
|
||||
'Generated by Codex CLI.',
|
||||
'Review methodology: `claude_md/code_review.md`',
|
||||
'Review methodology: `claude_md/ci_review_prompt.md`',
|
||||
'',
|
||||
'**Limitations:**',
|
||||
'- Codex may miss context from files not in the diff',
|
||||
|
||||
@@ -140,7 +140,17 @@ jobs:
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: sudo apt-get update && sudo apt-get install -y build-essential libgflags-dev libsnappy-dev zlib1g-dev libbz2-dev liblz4-dev libzstd-dev
|
||||
- run: sudo mount -o remount,size=16G /dev/shm
|
||||
- run: sudo dd bs=1048576 count=4096 if=/dev/zero of=/swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
|
||||
- name: Ensure swap space
|
||||
run: |-
|
||||
if sudo swapon --show=NAME --noheadings --raw | grep -Fxq /swapfile; then
|
||||
echo "/swapfile is already active"
|
||||
else
|
||||
sudo dd bs=1048576 count=4096 if=/dev/zero of=/swapfile
|
||||
sudo chmod 600 /swapfile
|
||||
sudo mkswap /swapfile
|
||||
sudo swapon /swapfile
|
||||
fi
|
||||
sudo swapon --show
|
||||
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=1800 --max_key=2500000' blackbox_crash_test_with_atomic_flush
|
||||
- run: rm -rf /dev/shm/rocksdb.*
|
||||
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=1800 --max_key=2500000' blackbox_crash_test_with_multiops_wc_txn
|
||||
|
||||
@@ -65,7 +65,9 @@ jobs:
|
||||
pip install https://files.pythonhosted.org/packages/fb/ac/3c04772acc0257f5730e83adb542b2603c1a62d1315010ab593a980af404/clang_format-21.1.2-py2.py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
|
||||
clang-format --version
|
||||
- name: Download clang-format-diff.py
|
||||
run: wget https://rocksdb-deps.s3.us-west-2.amazonaws.com/llvm/llvm-project/release/12.x/clang/tools/clang-format/clang-format-diff.py
|
||||
# Mirrored on the "third-party-deps" GitHub release; add/update via
|
||||
# https://github.com/facebook/rocksdb/releases/edit/third-party-deps
|
||||
run: wget --tries=3 --retry-on-host-error https://github.com/facebook/rocksdb/releases/download/third-party-deps/clang-format-diff.py
|
||||
- name: Check format
|
||||
run: VERBOSE_CHECK=1 make check-format
|
||||
- name: Compare buckify output
|
||||
@@ -320,16 +322,46 @@ jobs:
|
||||
# Run the make target in STRICT mode so this core CI job HARD-FAILS rather
|
||||
# than silently skipping if a prerequisite (clang++, clang-format, or the
|
||||
# compat baseline ref) is ever missing. See `make check-c-api-gen`.
|
||||
env:
|
||||
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
PUSH_BEFORE_SHA: ${{ github.event.before }}
|
||||
run: |
|
||||
# The container checkout is owned by a different user, so git refuses to
|
||||
# operate on it ("dubious ownership") -- mark it safe for the `git fetch`
|
||||
# and the `git show` the compat checker runs.
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
git config --global --add safe.directory '*'
|
||||
git fetch --no-tags --depth=1 origin main
|
||||
|
||||
# A PR must preserve the API of its merge target, while a push must
|
||||
# preserve the API of the branch before the push. Comparing every event
|
||||
# with main produces false removals when a release branch predates APIs
|
||||
# added to main.
|
||||
case "$GITHUB_EVENT_NAME" in
|
||||
pull_request) api_compat_ref="$PR_BASE_SHA" ;;
|
||||
push) api_compat_ref="$PUSH_BEFORE_SHA" ;;
|
||||
*)
|
||||
echo "ERROR: unsupported event for C API compatibility: $GITHUB_EVENT_NAME" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$api_compat_ref" ]; then
|
||||
echo "ERROR: C API compatibility baseline is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
zero_sha=0000000000000000000000000000000000000000
|
||||
if [ "$api_compat_ref" = "$zero_sha" ]; then
|
||||
# A newly created branch or tag has no prior ref state to preserve.
|
||||
api_compat_ref=HEAD
|
||||
else
|
||||
git fetch --no-tags --depth=1 origin "$api_compat_ref"
|
||||
api_compat_ref=FETCH_HEAD
|
||||
fi
|
||||
|
||||
CXX=clang++-21 make check-c-api-gen \
|
||||
CHECK_C_API_GEN_STRICT=1 \
|
||||
API_COMPAT_REF=FETCH_HEAD \
|
||||
API_COMPAT_REF="$api_compat_ref" \
|
||||
CLANG_FORMAT_BINARY=clang-format-21
|
||||
- uses: "./.github/actions/teardown-ccache"
|
||||
if: always()
|
||||
@@ -394,13 +426,20 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: mini-crashtest
|
||||
- uses: "./.github/actions/cache-folly"
|
||||
id: cache-folly
|
||||
- uses: "./.github/actions/build-folly"
|
||||
with:
|
||||
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
|
||||
- run: |
|
||||
ulimit -S -n $(ulimit -Hn) # Raise open files soft limit to hard limit
|
||||
echo "Updated ulimit -Hn: $(ulimit -Hn) -Sn: $(ulimit -Sn)"
|
||||
make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=${{ matrix.crash_duration }} --max_key=2500000' ${{ matrix.crash_test_target }}
|
||||
USE_COROUTINES=1 USE_FOLLY=1 LIB_MODE=static make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=${{ matrix.crash_duration }} --max_key=2500000' ${{ matrix.crash_test_target }}
|
||||
- uses: "./.github/actions/teardown-ccache"
|
||||
if: always()
|
||||
- uses: "./.github/actions/post-steps"
|
||||
|
||||
@@ -59,6 +59,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/compaction/sst_partitioner.cc",
|
||||
"db/compaction/subcompaction_state.cc",
|
||||
"db/convenience.cc",
|
||||
"db/coro_db.cc",
|
||||
"db/db_filesnapshot.cc",
|
||||
"db/db_impl/compacted_db_impl.cc",
|
||||
"db/db_impl/db_impl.cc",
|
||||
@@ -103,7 +104,6 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/snapshot_impl.cc",
|
||||
"db/table_cache.cc",
|
||||
"db/table_properties_collector.cc",
|
||||
"db/transaction_log_impl.cc",
|
||||
"db/trim_history_scheduler.cc",
|
||||
"db/version_builder.cc",
|
||||
"db/version_edit.cc",
|
||||
@@ -111,7 +111,9 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/version_set.cc",
|
||||
"db/version_util.cc",
|
||||
"db/wal_edit.cc",
|
||||
"db/wal_iterator_impl.cc",
|
||||
"db/wal_manager.cc",
|
||||
"db/wide/lazy_wide_columns.cc",
|
||||
"db/wide/read_path_blob_resolver.cc",
|
||||
"db/wide/wide_column_serialization.cc",
|
||||
"db/wide/wide_columns.cc",
|
||||
@@ -4947,6 +4949,12 @@ cpp_unittest_wrapper(name="db_kv_checksum_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_lazy_entity_test",
|
||||
srcs=["db/wide/db_lazy_entity_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_log_iter_test",
|
||||
srcs=["db/db_log_iter_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
|
||||
@@ -55,7 +55,7 @@ This document provides guidance for generating and reviewing code in the RocksDB
|
||||
|
||||
**API Consistency:** New APIs should be consistent with existing patterns. Use similar naming conventions, parameter ordering, and return types. Reviewers will suggest changes to improve consistency with the broader codebase.
|
||||
|
||||
**Documentation:** Public APIs must be thoroughly documented. Include usage examples, parameter descriptions, and notes on thread safety, performance characteristics, and compatibility considerations.
|
||||
**Documentation:** Public APIs must be thoroughly documented, without unnecessary embelishment nor dwelling on implementation details nor project planning. When non-obvious, include usage examples, parameter descriptions, known bugs or limitations, notes on thread safety, performance characteristics, and compatibility considerations. Re-read comments for ambiguous terminology and phrasing, such as ambiguously re-purposed programming jargon.
|
||||
|
||||
---
|
||||
|
||||
@@ -259,6 +259,10 @@ will automatically clean object files if the build parameters/flavor have
|
||||
changed. The `build_tools/rockstest.sh` / `rocksptest.sh` helpers described
|
||||
below set `AUTO_CLEAN=1` for you.
|
||||
|
||||
`AUTO_CLEAN=1` does not fix Make failures associated with stale .d files
|
||||
referring to removed files. Resolve that manually or with `make clean`
|
||||
without complaining to the user.
|
||||
|
||||
### Source checks
|
||||
* Run `make check-sources` before committing. This catches non-ASCII
|
||||
characters in source files and other source-level issues that CI will
|
||||
@@ -298,6 +302,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 |
|
||||
@@ -312,6 +317,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
|
||||
@@ -389,6 +400,11 @@ rather than relying on libstdc++ transitive includes.
|
||||
|
||||
### Adding release note
|
||||
* Release note should be kept short at high level for external user consumption.
|
||||
Release notes identify what users might care about most in a release. They
|
||||
are not exhaustive and are not a guide. PLEASE learn from past agents who
|
||||
ried to build elaborate release notes with implementation details and
|
||||
elsewhere-documented nuance. That wastes time. Fight the bias that
|
||||
"my change" is important so must be worthy of release note mention.
|
||||
* If more than single markdown line, consider how their formatting will be
|
||||
integrated into HISTORY.md.
|
||||
|
||||
|
||||
+4
-1
@@ -801,6 +801,7 @@ set(SOURCES
|
||||
db/compaction/sst_partitioner.cc
|
||||
db/compaction/subcompaction_state.cc
|
||||
db/convenience.cc
|
||||
db/coro_db.cc
|
||||
db/db_filesnapshot.cc
|
||||
db/db_impl/compacted_db_impl.cc
|
||||
db/db_impl/db_impl.cc
|
||||
@@ -845,7 +846,6 @@ set(SOURCES
|
||||
db/snapshot_impl.cc
|
||||
db/table_cache.cc
|
||||
db/table_properties_collector.cc
|
||||
db/transaction_log_impl.cc
|
||||
db/trim_history_scheduler.cc
|
||||
db/version_builder.cc
|
||||
db/version_edit.cc
|
||||
@@ -853,7 +853,9 @@ set(SOURCES
|
||||
db/version_set.cc
|
||||
db/version_util.cc
|
||||
db/wal_edit.cc
|
||||
db/wal_iterator_impl.cc
|
||||
db/wal_manager.cc
|
||||
db/wide/lazy_wide_columns.cc
|
||||
db/wide/read_path_blob_resolver.cc
|
||||
db/wide/wide_column_serialization.cc
|
||||
db/wide/wide_columns.cc
|
||||
@@ -1560,6 +1562,7 @@ if(WITH_TESTS)
|
||||
db/version_set_test.cc
|
||||
db/wal_manager_test.cc
|
||||
db/wal_edit_test.cc
|
||||
db/wide/db_lazy_entity_test.cc
|
||||
db/wide/db_wide_basic_test.cc
|
||||
db/wide/db_wide_blob_direct_write_test.cc
|
||||
db/wide/wide_column_serialization_test.cc
|
||||
|
||||
+42
@@ -1,6 +1,48 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 11.9.1 (08/18/2026)
|
||||
### Public API Changes
|
||||
* Added an immutable `external_table_config` option that is persisted in OPTIONS files and passed to external table factories during bootstrap.
|
||||
|
||||
## 11.9.0 (08/14/2026)
|
||||
### New Features
|
||||
* Added an integrated and improved compression auto-skip feature (new CompressionOptions, still experimental) that saves CPU by skipping compression when and where the overall ratio is inadequate. This improves on the previous auto-skip compression manager by (a) taking achieved compression ratios into account, (b) using a continuously evolving payoff estimate, and (c) combining natively with parallel compression.
|
||||
* Added the `blob_file_writable_file_max_buffer_size` mutable column family option. When set to a non-zero value, blob-file writers use it as their `WritableFileWriter` max buffer size instead of inheriting `DBOptions::writable_file_max_buffer_size`.
|
||||
* Added an experimental lazy wide-column read API, `DB::GetEntityLazy()` / `DB::MultiGetEntityLazy()` (see `include/rocksdb/lazy_wide_columns.h`). The returned `LazyWideColumns` leaves blob-backed columns as unresolved references and, like an iterator, stays valid after the call returns, so the caller resolves only the columns -- and only the byte ranges -- it needs; a byte-range read of an uncompressed blob (separate-file or embedded) reads just those bytes from storage. Adds `Env::IOActivity::kLazyResolve` and `rocksdb.blobdb.lazy.*` statistics. Requires `max_open_files == -1`.
|
||||
* Added DB property `rocksdb.num-running-remote-compactions`, reporting the number of compaction service jobs currently waiting in `CompactionService::Wait()`.
|
||||
* Added DB property `rocksdb.num-unscheduled-compactions`, reporting compactions waiting in the DB's internal compaction queue but not yet assigned to a background job.
|
||||
* Added `DB::AbortCompactions()` and `DB::ResumeCompactions()` for per-column-family compaction abort and resume, with the same active-abort semantics as the DB-wide `AbortAllCompactions()`/`ResumeAllCompactions()` but scoped to a single column family. Calls are reference-counted independently per column family and compose with the DB-wide abort.
|
||||
|
||||
### Public API Changes
|
||||
* Add IndexFactory compatibility names for the existing user-defined index API.
|
||||
* Added the experimental `CoroDB` capability, exposed by `DB::GetCoroDB()`, with lazy native coroutine variants of `Get` and `MultiGet`. The existing `DB::GetAsync()` and `DB::MultiGetAsync()` APIs dispatch through it.
|
||||
* Added `rocksdb_batched_multi_get_pinned_cf()` to return batch-owned pinned MultiGet values and per-key errors without allocating one wrapper per found value.
|
||||
* 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.
|
||||
* `ReadOptions::table_filter` is now a *pointer* to a caller-owned `std::function` instead of a `std::function` value, to improve efficiency in copying and destroying `ReadOptions` objects (which RocksDB does internally). This is a breaking change for users of this relatively obscure option, who will now need to declare a `std::function` that outlives the read. As a side effect of `ReadOptions` being trivially copyable, new but legitimate compiler warnings may surface.
|
||||
* Removed the experimental `CreateAutoSkipCompressionManager()`. Auto-skip compression is now a first-class feature of the block-based table builder, controlled by the new experimental `CompressionOptions::auto_skip`.
|
||||
* Added `CoroStackableDB` for implementing wrappers over native coroutine reads and used it to add coroutine `Get` and `MultiGet` support to `DBWithTTL` and WritePrepared `TransactionDB`. A stackable wrapper advertises coroutine support only when its wrapped DB supports it; TTL value decoding and transaction visibility semantics are preserved.
|
||||
* Added a `kSstFileWriter` value to `TableFileCreationReason`, which `SstFileWriter` now reports instead of the catch-all `kMisc`. This, along with new `db_name` and `is_remote_compaction` fields in `FilterBuildingContext`, improves the contextual information available to custom filter and compression strategies.
|
||||
* Renamed `TransactionLogIterator` to `WalIterator` (and `include/rocksdb/transaction_log.h` to `include/rocksdb/wal_iterator.h`), because it iterates the write-ahead log and is unrelated to `TransactionDB`. The old names still work. `DB::GetUpdatesSince()` is unchanged. In Java, `WalIterator` is the new name and `TransactionLogIterator` remains as a subclass. Also documented existing behavior that was previously unstated: only writes that reached the WAL are visible (so `WriteOptions::disableWAL` and `IngestExternalFile()` leave holes that end iteration), what each iterator state means, and that `GetUpdatesSince()` silently starts later when the requested sequence number is unavailable.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a bug where `BlobFileBuilder` passed an SST path to `OnBlobFileCompleted()` instead of the completed blob path during BlobDB garbage-collection compactions, causing incorrect `SstFileManager` accounting and compaction failures on filesystems where the SST was not yet pathname-visible.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -86,11 +86,20 @@ else
|
||||
LIB_MODE?=shared
|
||||
endif
|
||||
|
||||
$(info $$DEBUG_LEVEL is $(DEBUG_LEVEL), $$LIB_MODE is $(LIB_MODE))
|
||||
# Set by the gen_check_scripts wrappers to mark a recursive make reusing the
|
||||
# parent's make_config.mk: silence the parse-time banners and skip
|
||||
# build_detect_platform (below).
|
||||
$(if $(ROCKSDB_RECURSIVE_MAKE_SAME_CONFIG),,$(info $$DEBUG_LEVEL is $(DEBUG_LEVEL), $$LIB_MODE is $(LIB_MODE)))
|
||||
|
||||
# Detect what platform we're building on.
|
||||
# Export some common variables that might have been passed as Make variables
|
||||
# instead of environment variables.
|
||||
#
|
||||
# Skipped by the gen_check_scripts wrappers (which run concurrently under
|
||||
# gnu_parallel): each would otherwise rewrite the shared make_config.mk
|
||||
# non-atomically and a concurrent reader could catch it mid-write ("missing
|
||||
# separator"). Requires an existing make_config.mk, so a normal build still detects.
|
||||
ifeq ($(and $(ROCKSDB_RECURSIVE_MAKE_SAME_CONFIG),$(wildcard make_config.mk)),)
|
||||
dummy := $(shell (export ROCKSDB_ROOT="$(CURDIR)"; \
|
||||
export CXXFLAGS="$(EXTRA_CXXFLAGS)"; \
|
||||
export LDFLAGS="$(EXTRA_LDFLAGS)"; \
|
||||
@@ -104,8 +113,9 @@ dummy := $(shell (export ROCKSDB_ROOT="$(CURDIR)"; \
|
||||
export ROCKSDB_CXX_STANDARD="$(ROCKSDB_CXX_STANDARD)"; \
|
||||
export USE_FOLLY="$(USE_FOLLY)"; \
|
||||
export USE_FOLLY_LITE="$(USE_FOLLY_LITE)"; \
|
||||
"$(CURDIR)/build_tools/build_detect_platform" "$(CURDIR)/make_config.mk"))
|
||||
# this file is generated by the previous line to set build flags and sources
|
||||
"$(CURDIR)/build_tools/build_detect_platform" "$(CURDIR)/make_config.mk" $(if $(ROCKSDB_RECURSIVE_MAKE_SAME_CONFIG),2>/dev/null)))
|
||||
endif
|
||||
# this file (generated above, or reused when skipped) sets build flags and sources
|
||||
include make_config.mk
|
||||
|
||||
# Figure out optimize level.
|
||||
@@ -181,7 +191,7 @@ ifeq ($(filter -DROCKSDB_ASSERT_STATUS_CHECKED,$(OPT)),)
|
||||
endif
|
||||
endif
|
||||
|
||||
$(warning Warning: Compiling in debug mode. Don't use the resulting binary in production)
|
||||
$(if $(ROCKSDB_RECURSIVE_MAKE_SAME_CONFIG),,$(warning Warning: Compiling in debug mode. Don't use the resulting binary in production))
|
||||
endif
|
||||
|
||||
# `USE_LTO=1` enables link-time optimizations. Among other things, this enables
|
||||
@@ -579,9 +589,12 @@ TESTS += $(PLUGIN_TESTS)
|
||||
ifneq ($(filter check-headers, $(MAKECMDGOALS)),)
|
||||
# TODO: add/support JNI headers
|
||||
DEV_HEADER_DIRS := $(sort include/ $(dir $(ALL_SOURCES)))
|
||||
FOLLY_DEPENDENT_HEADERS := \
|
||||
include/rocksdb/coro_db.h \
|
||||
include/rocksdb/utilities/coro_stackable_db.h
|
||||
# Some headers like in port/ are platform-specific
|
||||
DEV_HEADERS_TO_CHECK := $(shell $(FIND) $(DEV_HEADER_DIRS) -type f -name '*.h' | grep -E -v 'port/|plugin/|range_tree/|secondary_index/')
|
||||
PUBLIC_HEADERS_TO_CHECK := $(shell $(FIND) include/ -type f -name '*.h')
|
||||
DEV_HEADERS_TO_CHECK := $(filter-out $(FOLLY_DEPENDENT_HEADERS), $(shell $(FIND) $(DEV_HEADER_DIRS) -type f -name '*.h' | grep -E -v 'port/|plugin/|range_tree/|secondary_index/'))
|
||||
PUBLIC_HEADERS_TO_CHECK := $(filter-out $(FOLLY_DEPENDENT_HEADERS), $(shell $(FIND) include/ -type f -name '*.h'))
|
||||
else
|
||||
DEV_HEADERS_TO_CHECK :=
|
||||
PUBLIC_HEADERS_TO_CHECK :=
|
||||
@@ -949,6 +962,57 @@ gen_parallel_tests:
|
||||
$(AM_V_at)$(FIND) t -type f -name 'run-*' -exec rm -f {} \;
|
||||
$(MAKE) $(parallel_tests)
|
||||
|
||||
# Auxiliary (non-unit-test) checks that are part of `make check`. Kept as a
|
||||
# single list so the same set can run as parallel jobs alongside the C++ unit
|
||||
# tests (see gen_check_scripts + check_0) and serially in the J=1 fallback.
|
||||
CHECK_EXTRA_TARGETS =
|
||||
ifneq ($(PLATFORM), OS_AIX)
|
||||
CHECK_EXTRA_TARGETS += check-all-python
|
||||
ifndef ASSERT_STATUS_CHECKED # not yet working with these tests
|
||||
CHECK_EXTRA_TARGETS += ldb_tests db_crashtest_tests rocksdb_dump_test
|
||||
endif
|
||||
endif
|
||||
ifndef SKIP_FORMAT_BUCK_CHECKS
|
||||
CHECK_EXTRA_TARGETS += check-format check-buck-targets check-sources check-workflow-yaml check-c-api-gen
|
||||
endif
|
||||
|
||||
# Generate a t/run-<target> wrapper per enabled auxiliary check; check_0's
|
||||
# `$(FIND) t -name 'run-*'` then runs them as gnu_parallel jobs alongside the
|
||||
# unit tests, each framed with a gtest-style pass/fail line. Under CI sharding,
|
||||
# round-robin across shards so each check runs on exactly one shard. Kept
|
||||
# separate from gen_parallel_tests (which cleans t/run-* first) so the valgrind_*
|
||||
# targets, which also glob t/run-*, don't pick these up.
|
||||
.PHONY: gen_check_scripts
|
||||
gen_check_scripts:
|
||||
$(AM_V_at)mkdir -p t; \
|
||||
idx=0; \
|
||||
for tgt in $(CHECK_EXTRA_TARGETS); do \
|
||||
if [ -z "$(CI_TOTAL_SHARDS)" ] || [ $$(( idx % $(CI_TOTAL_SHARDS) )) -eq $(CI_SHARD_INDEX) ]; then \
|
||||
s=t/run-$$tgt; \
|
||||
printf '%s\n' \
|
||||
'#!/bin/sh' \
|
||||
'# Generated by `make gen_check_scripts`.' \
|
||||
"t=$$tgt" \
|
||||
'# Drop the parent jobserver from MAKEFLAGS (its fds are lost through' \
|
||||
'# gnu_parallel, causing a spurious warning); keep overrides after "--".' \
|
||||
'MAKEFLAGS=$$(printf "%s" "$$MAKEFLAGS" | sed -e "s/ --jobserver-auth=[^ ]*//g" -e "s/ --jobserver-fds=[^ ]*//g" -e "s/ -j[0-9]*//g")' \
|
||||
'export MAKEFLAGS' \
|
||||
'echo; echo "[----------]"; echo "[ RUN ] >>> make $$t <<<"' \
|
||||
'$(MAKE) --no-print-directory ROCKSDB_RECURSIVE_MAKE_SAME_CONFIG=1 "$$t"' \
|
||||
'rc=$$?' \
|
||||
'if [ "$$rc" = 0 ]; then' \
|
||||
' echo "[ OK ] >>> make $$t <<<"' \
|
||||
'else' \
|
||||
' echo "[ FAILED ] >>> make $$t <<<"' \
|
||||
'fi' \
|
||||
'echo "[----------]"' \
|
||||
'exit $$rc' \
|
||||
> $$s; \
|
||||
chmod a=rx $$s; \
|
||||
fi; \
|
||||
idx=$$((idx + 1)); \
|
||||
done
|
||||
|
||||
# Reorder input lines (one per test or per shard) so the longest-running
|
||||
# tests/shards start first under "make check" parallel scheduling. The trick
|
||||
# is to prefix matched names with a fake duration (100), sort numerically
|
||||
@@ -976,7 +1040,8 @@ gen_parallel_tests:
|
||||
slow_test_regexp = \
|
||||
^.*point_lock_manager_stress_test.*$$|^.*db_test.*$$|^.*external_sst_file_test.*$$|^.*compaction_service_test.*$$|^.*corruption_test.*$$|^.*comparator_db_test.*$$|^.*external_sst_file_basic_test.*$$|^.*rate_limiter_test.*$$|^.*db_compaction_test.*$$|^.*write_prepared_transaction_test.*$$|^.*db_merge_operator_test.*$$|\
|
||||
^.*db_dynamic_level_test.*$$|^.*db_bloom_filter_test.*$$|^.*error_handler_fs_test.*$$|^.*merge_helper_test.*$$|^.*transaction_test.*$$|^.*db_kv_checksum_test.*$$|^.*inlineskiplist_test.*$$|\
|
||||
^.*db_with_timestamp_basic_test.*$$|^.*table_test.*$$|^.*db_wal_test.*$$|^.*block_based_table_reader_test.*$$|^.*block_test.*$$
|
||||
^.*db_with_timestamp_basic_test.*$$|^.*table_test.*$$|^.*db_wal_test.*$$|^.*block_based_table_reader_test.*$$|^.*block_test.*$$|\
|
||||
^.*run-check-c-api-gen.*$$|^.*run-check-format.*$$
|
||||
prioritize_long_running_tests = \
|
||||
perl -pe 's,($(slow_test_regexp)),100 $$1,' \
|
||||
| sort -k1,1gr \
|
||||
@@ -1108,27 +1173,17 @@ check: all
|
||||
&& (build_tools/gnu_parallel --gnu --help 2>/dev/null) | \
|
||||
grep -q 'GNU Parallel'; \
|
||||
then \
|
||||
$(MAKE) gen_check_scripts; \
|
||||
$(MAKE) T="$$t" check_0; \
|
||||
else \
|
||||
for t in $(TESTS); do \
|
||||
echo "===== Running $$t (`date`)"; ./$$t || exit 1; done; \
|
||||
for t in $(CHECK_EXTRA_TARGETS); do \
|
||||
echo "===== Running $$t (`date`)"; $(MAKE) $$t || exit 1; done; \
|
||||
fi
|
||||
rm -rf $(TEST_TMPDIR)
|
||||
ifneq ($(PLATFORM), OS_AIX)
|
||||
$(PYTHON) tools/check_all_python.py
|
||||
ifndef ASSERT_STATUS_CHECKED # not yet working with these tests
|
||||
$(PYTHON) tools/ldb_test.py
|
||||
$(PYTHON) tools/db_crashtest_test.py
|
||||
sh tools/rocksdb_dump_test.sh
|
||||
endif
|
||||
endif
|
||||
ifndef SKIP_FORMAT_BUCK_CHECKS
|
||||
$(MAKE) check-format
|
||||
$(MAKE) check-buck-targets
|
||||
$(MAKE) check-sources
|
||||
$(MAKE) check-workflow-yaml
|
||||
$(MAKE) check-c-api-gen
|
||||
endif
|
||||
# NOTE: ^^^^^^^^ don't add new auxiliary checks here, but instead to
|
||||
# CHECK_EXTRA_TARGETS
|
||||
|
||||
# Check that the auto-generated C API files are up to date. It regenerates the
|
||||
# fragments and the inlined c.h/c.cc and compares them against a snapshot of the
|
||||
@@ -1149,8 +1204,8 @@ endif
|
||||
# Any non-empty value other than 0/no/false enables strict mode.
|
||||
CLANG_FORMAT_BINARY ?=
|
||||
# Backward-compatibility baseline for the C API (signature-level) check. CI
|
||||
# overrides this with the PR's merge target; locally it falls back to main /
|
||||
# origin/main and is skipped if neither resolves.
|
||||
# overrides this with the PR's merge target or the branch's pre-push tip;
|
||||
# locally it falls back to main or origin/main and is skipped if neither resolves.
|
||||
API_COMPAT_REF ?= main
|
||||
CHECK_C_API_GEN_STRICT ?=
|
||||
check-c-api-gen:
|
||||
@@ -1235,6 +1290,14 @@ ldb_tests: ldb
|
||||
db_crashtest_tests:
|
||||
$(PYTHON) tools/db_crashtest_test.py
|
||||
|
||||
.PHONY: check-all-python
|
||||
check-all-python:
|
||||
$(PYTHON) tools/check_all_python.py
|
||||
|
||||
.PHONY: rocksdb_dump_test
|
||||
rocksdb_dump_test: rocksdb_dump rocksdb_undump
|
||||
sh tools/rocksdb_dump_test.sh
|
||||
|
||||
include crash_test.mk
|
||||
|
||||
asan_check: clean
|
||||
@@ -1670,6 +1733,9 @@ db_readonly_with_timestamp_test: $(OBJ_DIR)/db/db_readonly_with_timestamp_test.o
|
||||
db_wide_basic_test: $(OBJ_DIR)/db/wide/db_wide_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_lazy_entity_test: $(OBJ_DIR)/db/wide/db_lazy_entity_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_wide_blob_direct_write_test: $(OBJ_DIR)/db/wide/db_wide_blob_direct_write_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -2374,22 +2440,27 @@ ROCKSDB_JAVADOCS_JAR = rocksdbjni-$(ROCKSDB_JAVA_VERSION)-javadoc.jar
|
||||
ROCKSDB_SOURCES_JAR = rocksdbjni-$(ROCKSDB_JAVA_VERSION)-sources.jar
|
||||
SHA256_CMD = sha256sum
|
||||
|
||||
# The *_DOWNLOAD_PREFIX defaults below point at the upstream projects; CI
|
||||
# overrides them to a mirror on the "third-party-deps" GitHub release (see
|
||||
# .github/actions/pre-steps/action.yml). When bumping a version, see
|
||||
# instructions in CI config comments for how to update that release.
|
||||
ZLIB_VER ?= 1.3.1
|
||||
ZLIB_SHA256 ?= 9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23
|
||||
ZLIB_DOWNLOAD_BASE ?= http://zlib.net
|
||||
ZLIB_DOWNLOAD_PREFIX ?= http://zlib.net/zlib-
|
||||
BZIP2_VER ?= 1.0.8
|
||||
BZIP2_SHA256 ?= ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269
|
||||
BZIP2_DOWNLOAD_BASE ?= http://sourceware.org/pub/bzip2
|
||||
BZIP2_DOWNLOAD_PREFIX ?= http://sourceware.org/pub/bzip2/bzip2-
|
||||
SNAPPY_VER ?= 1.2.2
|
||||
SNAPPY_SHA256 ?= 90f74bc1fbf78a6c56b3c4a082a05103b3a56bb17bca1a27e052ea11723292dc
|
||||
SNAPPY_DOWNLOAD_BASE ?= https://github.com/google/snappy/archive
|
||||
SNAPPY_DOWNLOAD_PREFIX ?= https://github.com/google/snappy/archive/
|
||||
LZ4_VER ?= 1.10.0
|
||||
LZ4_SHA256 ?= 537512904744b35e232912055ccf8ec66d768639ff3abe5788d90d792ec5f48b
|
||||
LZ4_DOWNLOAD_BASE ?= https://github.com/lz4/lz4/archive
|
||||
LZ4_DOWNLOAD_PREFIX ?= https://github.com/lz4/lz4/archive/v
|
||||
ZSTD_VER ?= 1.5.7
|
||||
ZSTD_SHA256 ?= 37d7284556b20954e56e1ca85b80226768902e2edabd3b649e9e72c0c9012ee3
|
||||
ZSTD_DOWNLOAD_BASE ?= https://github.com/facebook/zstd/archive
|
||||
CURL_SSL_OPTS ?= --tlsv1
|
||||
ZSTD_DOWNLOAD_PREFIX ?= https://github.com/facebook/zstd/archive/v
|
||||
# curl with the common options used to fetch the pinned third-party sources above
|
||||
CURL_DOWNLOAD = curl --fail --location --retry 3 --retry-all-errors
|
||||
|
||||
ifeq ($(PLATFORM), OS_MACOSX)
|
||||
ifeq (,$(findstring librocksdbjni-osx,$(ROCKSDBJNILIB)))
|
||||
@@ -2468,7 +2539,7 @@ BUILD_SIG_NONBUILD_GOALS := \
|
||||
check-sources check-workflow-yaml check-progress clang-tidy \
|
||||
tags tags0 package jclean checkout_folly build_folly \
|
||||
watch-log dump-log suggest-slow-tests list_all_tests gen-pc \
|
||||
gen_parallel_tests check-c-api-gen \
|
||||
gen_parallel_tests gen_check_scripts check-c-api-gen check-all-python \
|
||||
setup-hooks install-hooks uninstall-hooks uninstall db_crashtest_tests
|
||||
# Goals that clean before building (depend on or invoke `clean`): they manage
|
||||
# their own freshness, so the check must not block them (it would error before
|
||||
@@ -2530,7 +2601,7 @@ endif
|
||||
endif
|
||||
|
||||
zlib-$(ZLIB_VER).tar.gz:
|
||||
curl --fail --output zlib-$(ZLIB_VER).tar.gz --location ${ZLIB_DOWNLOAD_BASE}/zlib-$(ZLIB_VER).tar.gz
|
||||
$(CURL_DOWNLOAD) --output zlib-$(ZLIB_VER).tar.gz ${ZLIB_DOWNLOAD_PREFIX}$(ZLIB_VER).tar.gz
|
||||
ZLIB_SHA256_ACTUAL=`$(SHA256_CMD) zlib-$(ZLIB_VER).tar.gz | cut -d ' ' -f 1`; \
|
||||
if [ "$(ZLIB_SHA256)" != "$$ZLIB_SHA256_ACTUAL" ]; then \
|
||||
echo zlib-$(ZLIB_VER).tar.gz checksum mismatch, expected=\"$(ZLIB_SHA256)\" actual=\"$$ZLIB_SHA256_ACTUAL\"; \
|
||||
@@ -2548,7 +2619,7 @@ libz.a: zlib-$(ZLIB_VER).tar.gz
|
||||
cp zlib-$(ZLIB_VER)/libz.a .
|
||||
|
||||
bzip2-$(BZIP2_VER).tar.gz:
|
||||
curl --fail --output bzip2-$(BZIP2_VER).tar.gz --location ${CURL_SSL_OPTS} ${BZIP2_DOWNLOAD_BASE}/bzip2-$(BZIP2_VER).tar.gz
|
||||
$(CURL_DOWNLOAD) --output bzip2-$(BZIP2_VER).tar.gz ${BZIP2_DOWNLOAD_PREFIX}$(BZIP2_VER).tar.gz
|
||||
BZIP2_SHA256_ACTUAL=`$(SHA256_CMD) bzip2-$(BZIP2_VER).tar.gz | cut -d ' ' -f 1`; \
|
||||
if [ "$(BZIP2_SHA256)" != "$$BZIP2_SHA256_ACTUAL" ]; then \
|
||||
echo bzip2-$(BZIP2_VER).tar.gz checksum mismatch, expected=\"$(BZIP2_SHA256)\" actual=\"$$BZIP2_SHA256_ACTUAL\"; \
|
||||
@@ -2562,7 +2633,7 @@ libbz2.a: bzip2-$(BZIP2_VER).tar.gz
|
||||
cp bzip2-$(BZIP2_VER)/libbz2.a .
|
||||
|
||||
snappy-$(SNAPPY_VER).tar.gz:
|
||||
curl --fail --output snappy-$(SNAPPY_VER).tar.gz --location ${CURL_SSL_OPTS} ${SNAPPY_DOWNLOAD_BASE}/$(SNAPPY_VER).tar.gz
|
||||
$(CURL_DOWNLOAD) --output snappy-$(SNAPPY_VER).tar.gz ${SNAPPY_DOWNLOAD_PREFIX}$(SNAPPY_VER).tar.gz
|
||||
SNAPPY_SHA256_ACTUAL=`$(SHA256_CMD) snappy-$(SNAPPY_VER).tar.gz | cut -d ' ' -f 1`; \
|
||||
if [ "$(SNAPPY_SHA256)" != "$$SNAPPY_SHA256_ACTUAL" ]; then \
|
||||
echo snappy-$(SNAPPY_VER).tar.gz checksum mismatch, expected=\"$(SNAPPY_SHA256)\" actual=\"$$SNAPPY_SHA256_ACTUAL\"; \
|
||||
@@ -2577,7 +2648,7 @@ libsnappy.a: snappy-$(SNAPPY_VER).tar.gz
|
||||
cp snappy-$(SNAPPY_VER)/build/libsnappy.a .
|
||||
|
||||
lz4-$(LZ4_VER).tar.gz:
|
||||
curl --fail --output lz4-$(LZ4_VER).tar.gz --location ${CURL_SSL_OPTS} ${LZ4_DOWNLOAD_BASE}/v$(LZ4_VER).tar.gz
|
||||
$(CURL_DOWNLOAD) --output lz4-$(LZ4_VER).tar.gz ${LZ4_DOWNLOAD_PREFIX}$(LZ4_VER).tar.gz
|
||||
LZ4_SHA256_ACTUAL=`$(SHA256_CMD) lz4-$(LZ4_VER).tar.gz | cut -d ' ' -f 1`; \
|
||||
if [ "$(LZ4_SHA256)" != "$$LZ4_SHA256_ACTUAL" ]; then \
|
||||
echo lz4-$(LZ4_VER).tar.gz checksum mismatch, expected=\"$(LZ4_SHA256)\" actual=\"$$LZ4_SHA256_ACTUAL\"; \
|
||||
@@ -2591,7 +2662,7 @@ liblz4.a: lz4-$(LZ4_VER).tar.gz
|
||||
cp lz4-$(LZ4_VER)/lib/liblz4.a .
|
||||
|
||||
zstd-$(ZSTD_VER).tar.gz:
|
||||
curl --fail --output zstd-$(ZSTD_VER).tar.gz --location ${CURL_SSL_OPTS} ${ZSTD_DOWNLOAD_BASE}/v$(ZSTD_VER).tar.gz
|
||||
$(CURL_DOWNLOAD) --output zstd-$(ZSTD_VER).tar.gz ${ZSTD_DOWNLOAD_PREFIX}$(ZSTD_VER).tar.gz
|
||||
ZSTD_SHA256_ACTUAL=`$(SHA256_CMD) zstd-$(ZSTD_VER).tar.gz | cut -d ' ' -f 1`; \
|
||||
if [ "$(ZSTD_SHA256)" != "$$ZSTD_SHA256_ACTUAL" ]; then \
|
||||
echo zstd-$(ZSTD_VER).tar.gz checksum mismatch, expected=\"$(ZSTD_SHA256)\" actual=\"$$ZSTD_SHA256_ACTUAL\"; \
|
||||
|
||||
@@ -141,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;
|
||||
@@ -1296,6 +1305,16 @@ uint64_t rocksdb_options_get_blob_file_size(rocksdb_options_t* opt) {
|
||||
return opt->rep.blob_file_size;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_blob_file_writable_file_max_buffer_size(
|
||||
rocksdb_options_t* opt, uint64_t v) {
|
||||
opt->rep.blob_file_writable_file_max_buffer_size = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_options_get_blob_file_writable_file_max_buffer_size(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.blob_file_writable_file_max_buffer_size;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_blob_compression_type(rocksdb_options_t* opt, int v) {
|
||||
opt->rep.blob_compression_type =
|
||||
static_cast<decltype(opt->rep.blob_compression_type)>(v);
|
||||
|
||||
@@ -55,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);
|
||||
|
||||
@@ -381,6 +387,14 @@ rocksdb_options_set_preserve_internal_time_seconds(rocksdb_options_t* opt,
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_options_get_preserve_internal_time_seconds(rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_options_set_blob_file_writable_file_max_buffer_size(
|
||||
rocksdb_options_t* opt, uint64_t v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_options_get_blob_file_writable_file_max_buffer_size(
|
||||
rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_enable_blob_direct_write(
|
||||
rocksdb_options_t* opt, unsigned char v);
|
||||
|
||||
|
||||
@@ -647,6 +647,12 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
|
||||
CheckCondition(rocksdb_options_get_blob_direct_write_partitions(obj) == 1);
|
||||
rocksdb_options_set_blob_direct_write_partitions(obj, 0);
|
||||
CheckCondition(rocksdb_options_get_blob_direct_write_partitions(obj) == 0);
|
||||
rocksdb_options_set_blob_file_writable_file_max_buffer_size(obj, 1);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_blob_file_writable_file_max_buffer_size(obj) == 1);
|
||||
rocksdb_options_set_blob_file_writable_file_max_buffer_size(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_blob_file_writable_file_max_buffer_size(obj) == 0);
|
||||
rocksdb_options_set_block_protection_bytes_per_key(obj, 1);
|
||||
CheckCondition(rocksdb_options_get_block_protection_bytes_per_key(obj) == 1);
|
||||
rocksdb_options_set_block_protection_bytes_per_key(obj, 0);
|
||||
@@ -849,6 +855,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);
|
||||
|
||||
@@ -4,12 +4,19 @@
|
||||
Thorough multi-agent code review for RocksDB commits following CLAUDE.md guidelines,
|
||||
with structured codebase exploration, inter-agent debate, and verification.
|
||||
|
||||
IMPORTANT — Incremental output: After completing EACH major phase (Setup,
|
||||
Codebase Context, Initial Review, Debate, Synthesis), append your findings
|
||||
to review-findings.md using the Write tool. This ensures partial results are
|
||||
saved even if the review is interrupted by the turn limit. After the final
|
||||
synthesis, write the complete review to review-findings.md, replacing
|
||||
previous content. Also output the final review as your response text.
|
||||
IMPORTANT — Where the review goes: Your FINAL RESPONSE TEXT is the review.
|
||||
The PR comment is generated verbatim from your final response, so the
|
||||
complete, polished review MUST appear there in full. Do NOT end your turn with
|
||||
a meta-summary such as "the review has been written to review-findings.md" —
|
||||
paste the entire review as your response.
|
||||
|
||||
review-findings.md is a RECOVERY BACKSTOP, not the primary output: after
|
||||
completing EACH major phase (Setup, Codebase Context, Initial Review, Debate,
|
||||
Synthesis), append your current findings to it with the Write tool, and after
|
||||
the final synthesis overwrite it with the complete review. This preserves
|
||||
partial results if the turn limit interrupts the run (a recovery step reads
|
||||
this file); it does NOT substitute for outputting the full review as your
|
||||
final response.
|
||||
|
||||
## Workflow Steps
|
||||
|
||||
@@ -466,7 +473,9 @@ Team lead synthesizes the debate into a consensus document:
|
||||
- Include detailed bug analysis (root cause, vulnerable code paths)
|
||||
- Include suggested fixes
|
||||
- Note which findings were debated and the outcome
|
||||
- Write the complete review to review-findings.md and output as response
|
||||
- Output the complete review as your final response text (this becomes the PR
|
||||
comment verbatim), and also overwrite review-findings.md with the identical
|
||||
content as a recovery backstop
|
||||
|
||||
**Final report quality rules:**
|
||||
- The final report must be CLEAN and POLISHED. No stream-of-consciousness,
|
||||
@@ -483,9 +492,10 @@ Team lead synthesizes the debate into a consensus document:
|
||||
|
||||
**REQUIRED output structure (so the PR page stays scrollable):**
|
||||
|
||||
The final response (and contents of `review-findings.md`) MUST follow this
|
||||
exact structure. The summary appears first so reviewers can see HIGH findings
|
||||
at a glance; everything else is hidden behind a `<details>` block.
|
||||
Your final response — which becomes the PR comment verbatim — (and the
|
||||
identical contents of `review-findings.md`) MUST follow this exact structure.
|
||||
The summary appears first so reviewers can see HIGH findings at a glance;
|
||||
everything else is hidden behind a `<details>` block.
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
@@ -533,6 +543,9 @@ Rules for this structure:
|
||||
- Do NOT nest a `<details>` inside another `<details>`.
|
||||
- Do NOT add any text after the closing `</details>` — the comment-builder
|
||||
appends its own footer.
|
||||
- Your final response MUST be the review itself in this structure — never a
|
||||
meta-summary like "the review has been written to review-findings.md". The
|
||||
PR comment is generated verbatim from your final response.
|
||||
- If the review is partial (recovery path), still produce the summary block
|
||||
first, and put whatever was salvaged inside the `<details>` block.
|
||||
|
||||
@@ -571,9 +584,14 @@ Rules for this structure:
|
||||
- [ ] All callers enumerated with parameter range table
|
||||
|
||||
## Output Structure
|
||||
Write all review artifacts to the working directory root:
|
||||
- `review-findings.md` — Incremental findings (appended after each phase),
|
||||
then replaced with the final synthesized review at the end
|
||||
Your **final response text is the primary output** — it becomes the PR comment
|
||||
verbatim. The files below are working artifacts and recovery backstops only;
|
||||
they are NOT a substitute for outputting the full review in your final response.
|
||||
|
||||
Write these artifacts to the working directory root:
|
||||
- `review-findings.md` — Recovery backstop: incremental findings appended after
|
||||
each phase, overwritten with the final synthesized review at the end. Used
|
||||
only if the turn limit interrupts the run.
|
||||
- `context.md` — Codebase context (call chains, invariants)
|
||||
- `findings-*.md` — Per-agent findings (design, correctness, cross-component,
|
||||
invariant-adversary, caller-audit, performance, api, serialization, tests)
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
# Code Review Skill
|
||||
|
||||
This document defines the methodology for performing thorough, multi-perspective
|
||||
code reviews on RocksDB changes. It is used both by CI (GitHub Actions) and
|
||||
local review workflows.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting a review:
|
||||
- Read `CLAUDE.md` in the repository root for project-specific guidelines
|
||||
- Read any files in `claude_md/` for architecture context
|
||||
- Identify the commit/diff/PR to review and parse the changed files
|
||||
|
||||
## Review Methodology
|
||||
|
||||
Conduct the review from multiple perspectives, then synthesize findings into a
|
||||
single report. Each perspective catches different classes of bugs.
|
||||
|
||||
### Perspective 1: Codebase Context & Call-Chain Analysis
|
||||
|
||||
Before reviewing the diff itself, build deep context:
|
||||
- For each changed function/method, trace the call chain UPWARD 3-5 levels.
|
||||
Who consumes the changed behavior? What invariants do callers rely on?
|
||||
- Trace DOWNWARD into callees. What side effects do they have? What shared
|
||||
state do they mutate (counters, sequence numbers, flags, metadata)?
|
||||
- Identify the "sibling" or "reference" implementation that handles the same
|
||||
scenario the standard way. Compare behaviors.
|
||||
- For data written to shared structures (memtable, SST block, cache entry),
|
||||
find ALL readers and verify their visibility rules match the writer.
|
||||
|
||||
### Perspective 2: Correctness & Edge Cases
|
||||
|
||||
- Thread safety and concurrency (lock ordering, data races)
|
||||
- Error handling and Status propagation
|
||||
- Edge cases: empty inputs, overflow, boundary conditions
|
||||
- Data corruption scenarios
|
||||
- Behavioral contract changes: do return value semantics match callers?
|
||||
|
||||
### Perspective 3: Cross-Component & Adversarial Analysis
|
||||
|
||||
Check the change in ALL execution contexts:
|
||||
|
||||
| Context | Key difference |
|
||||
|---------|---------------|
|
||||
| WritePreparedTxnDB | read_callback_ controls visibility |
|
||||
| ReadOnly DB / SecondaryInstance | No mutable memtable |
|
||||
| CompactionService / Remote compaction | Different process |
|
||||
| User-defined timestamps | Extra key comparison dimension |
|
||||
| MemPurge | Memtable-to-memtable path |
|
||||
| BlobDB | Values in blob files, not inline |
|
||||
| Old snapshots | Seqno far behind current |
|
||||
| Concurrent writers | Lock-free vs locked paths |
|
||||
| FIFO / Universal compaction | Different invariants |
|
||||
| Prefix seek / total order seek | Different iterator behavior |
|
||||
|
||||
When the change claims a property ("logically redundant", "safe because X"),
|
||||
systematically try to break it: list preconditions, construct counterexamples.
|
||||
|
||||
### Perspective 4: Performance
|
||||
|
||||
RocksDB is a high-performance storage engine.
|
||||
- Memory allocation on hot paths
|
||||
- Unnecessary copies (prefer move semantics, Slice, string_view)
|
||||
- Cache efficiency and data locality
|
||||
- Loop optimization, branch prediction (LIKELY/UNLIKELY)
|
||||
|
||||
### Perspective 5: API, Compatibility & Testing
|
||||
|
||||
- Public API backwards compatibility
|
||||
- Serialization format correctness and versioning
|
||||
- Test coverage for edge cases, failure modes, and system-level interactions
|
||||
|
||||
## How to Explore the Codebase
|
||||
|
||||
Use available tools to explore deeply — do NOT just review the diff in isolation.
|
||||
The most critical bugs hide at component boundaries.
|
||||
|
||||
- Read the header files for changed implementations
|
||||
- Search for callers of changed functions (3-5 levels up)
|
||||
- Read existing tests to understand guaranteed behaviors
|
||||
- Check related implementations for conventions and patterns
|
||||
|
||||
## Output Format
|
||||
|
||||
### Summary
|
||||
Brief overall assessment (1-2 sentences).
|
||||
|
||||
### Issues Found
|
||||
Categorize by severity:
|
||||
- :red_circle: **Critical**: Must fix (correctness, data corruption, security)
|
||||
- :yellow_circle: **Suggestion**: Should consider (performance, edge cases)
|
||||
- :green_circle: **Nitpick**: Minor style issues
|
||||
|
||||
For each issue include:
|
||||
- File and line reference
|
||||
- Which review perspective found it
|
||||
- Description with root cause analysis
|
||||
- Suggested fix
|
||||
|
||||
### Cross-Component Analysis
|
||||
Execution context checks and assumption stress-test results.
|
||||
|
||||
### Positive Observations
|
||||
Good patterns, clever optimizations, or improvements.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Manual (Local) Code Review — Lightweight Wrapper
|
||||
|
||||
## What this is
|
||||
|
||||
A resource-light, **single-reviewer** variant of `claude_md/ci_review_prompt.md`
|
||||
for running a review by hand in a local Claude Code / Devmate session. It reuses
|
||||
the full CI review *methodology* but drops the multi-agent orchestration and the
|
||||
CI-only file-persistence machinery, to approximate the heavyweight result at a
|
||||
fraction of the cost.
|
||||
|
||||
**Scope: LOCAL manual review only. This file is NOT used by CI.** CI uses
|
||||
`claude_md/ci_review_prompt.md`.
|
||||
|
||||
## How to use
|
||||
|
||||
Read `claude_md/ci_review_prompt.md` for the complete methodology — the analysis
|
||||
techniques (caller-chain, callee side-effects, cross-component data consumers,
|
||||
alternative execution contexts, assumption stress-testing), the per-perspective
|
||||
reviewer instructions, the review checklist, and the anti-patterns table.
|
||||
|
||||
Then apply that methodology with the OVERRIDES below. **Where this file
|
||||
conflicts with `ci_review_prompt.md`, this file wins.**
|
||||
|
||||
## Overrides (this is what makes it lightweight)
|
||||
|
||||
### 1. Single reviewer — no team, no subagents
|
||||
- Do NOT create a team, spawn subagents, or run parallel/background agents.
|
||||
- Do NOT run the round-robin Debate phase or the Consensus phase.
|
||||
- Instead, YOU personally apply each reviewer role from `ci_review_prompt.md`
|
||||
(Design, Correctness, Cross-Component & Adversarial, Invariant Adversary,
|
||||
Caller-Context, Performance, API/Compat, Serialization, Test Coverage) as
|
||||
sequential *lenses* in one pass. Spend a few focused minutes per lens. Skip
|
||||
lenses that plainly don't apply (e.g., Serialization when there is no on-disk
|
||||
or wire-format change) and note the skip in one line.
|
||||
|
||||
### 2. No intermediate files
|
||||
- Do NOT write `context.md`, `findings-*.md`, `consensus.md`, or
|
||||
`review-findings.md`. Keep working notes in your reasoning, not on disk.
|
||||
- Rationale: those files exist only so CI can recover partial results when an
|
||||
ephemeral runner hits the turn limit — a local session has neither problem.
|
||||
`ci_review_prompt.md` already outputs the review as its final response, so
|
||||
skipping the file loses nothing here.
|
||||
|
||||
### 3. Right-size the effort
|
||||
- Do the Codebase Context phase, but proportionally: trace the 2-3 most
|
||||
load-bearing call chains and the highest-risk cross-component data consumers,
|
||||
rather than exhaustively enumerating every caller 3-5 levels up.
|
||||
- Prioritize the highest-signal lenses for RocksDB: **Correctness**,
|
||||
**Cross-Component & Adversarial**, and **Assumption Stress-Testing**. These
|
||||
catch the bugs that matter most; give them the most attention.
|
||||
- Escalation clause: if a thread looks dangerous, go deep on that specific
|
||||
thread even though the overall pass is modest. Modest is the default, not a cap.
|
||||
|
||||
### 4. Identify the review target locally
|
||||
- Unlike CI, the diff is NOT pre-pasted into this prompt. Determine what to
|
||||
review from the user's request against the local worktree — e.g. the current
|
||||
commit (`git show`), uncommitted changes (`git diff` / `git diff --staged`),
|
||||
a named commit range, or a PR — and read the changed files plus their
|
||||
surrounding context directly from the repo. When in doubt, include all changes
|
||||
not yet upstream, as in
|
||||
`git diff "$(git merge-base upstream/main HEAD 2> /dev/null || git merge-base origin/main HEAD 2> /dev/null)"`
|
||||
plus any untracked files (`git status`) that seem relevant.
|
||||
|
||||
## Reminder
|
||||
|
||||
The goal is a thorough *single* reviewer, not a 9-agent debate: depth where it
|
||||
matters, brevity everywhere else.
|
||||
@@ -44,6 +44,45 @@ Status VersionBlobFetcherBase::FetchBlob(const Slice& user_key,
|
||||
prefetch_buffer, blob_value, bytes_read);
|
||||
}
|
||||
|
||||
Status VersionBlobFetcherBase::FetchBlobRange(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
uint64_t range_offset,
|
||||
size_t range_length,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
// Only ever called when SupportsRangeRead() holds (a Version to read through,
|
||||
// no direct-write fallback); the direct-write path resolves whole records and
|
||||
// has no range variant.
|
||||
assert(SupportsRangeRead());
|
||||
assert(version_);
|
||||
return version_->GetBlobRange(read_options(), user_key, blob_index,
|
||||
range_offset, range_length, blob_value,
|
||||
bytes_read);
|
||||
}
|
||||
|
||||
Status VersionBlobFetcherBase::FetchBlobForceVerify(
|
||||
const Slice& user_key, const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer, PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
if (read_options().verify_checksums) {
|
||||
// Verification already happens on the normal path.
|
||||
return FetchBlob(user_key, blob_index, prefetch_buffer, blob_value,
|
||||
bytes_read);
|
||||
}
|
||||
|
||||
// Force verification via a transient fetcher over a verify-enabled copy of
|
||||
// the read options. `verify_read_options` outlives `verifying_fetcher` (both
|
||||
// are local to this call), so the borrowed-options VersionBlobFetcher is
|
||||
// safe.
|
||||
ReadOptions verify_read_options = read_options();
|
||||
verify_read_options.verify_checksums = true;
|
||||
VersionBlobFetcher verifying_fetcher(version_, verify_read_options,
|
||||
blob_file_cache_,
|
||||
allow_write_path_fallback_);
|
||||
return verifying_fetcher.FetchBlob(user_key, blob_index, prefetch_buffer,
|
||||
blob_value, bytes_read);
|
||||
}
|
||||
|
||||
Status EmbeddedAwareBlobFetcher::FetchBlob(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
|
||||
@@ -63,6 +63,37 @@ class VersionBlobFetcherBase : public BlobFetcher {
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const override;
|
||||
|
||||
// Fetches a byte sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed*, separate-file blob's value into *blob_value, reading only
|
||||
// those bytes on a cache miss (see Version::GetBlobRange /
|
||||
// BlobSource::GetBlobRange). Only valid when SupportsRangeRead() is true (a
|
||||
// Version to read through and no direct-write fallback); callers check that
|
||||
// first and take the whole-value FetchBlob path otherwise. Not routed through
|
||||
// the same-file (embedded) decorator -- same-file range reads are a later
|
||||
// phase.
|
||||
Status FetchBlobRange(const Slice& user_key, const BlobIndex& blob_index,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
PinnableSlice* blob_value, uint64_t* bytes_read) const;
|
||||
|
||||
// Fetches the whole blob with checksum verification forced on, regardless of
|
||||
// this fetcher's ReadOptions::verify_checksums. Used to honor a lazy read's
|
||||
// force_verify when verify_checksums is off: the caller wants the record's
|
||||
// checksum checked even though the global option would skip it. When
|
||||
// verify_checksums is already on this is just FetchBlob.
|
||||
//
|
||||
// TODO(lazy-blob-resolution-cleanup): fold FetchBlobRange /
|
||||
// FetchBlobForceVerify (and the whole-vs-range GetBlob twins throughout the
|
||||
// blob stack) into one parameterized primitive taking a BlobReadRequest -- a
|
||||
// range descriptor ({offset, len}; whole == [0, npos)) plus a 3-valued verify
|
||||
// policy (must-verify-if-present / verify-if-no-amplification / ignore). That
|
||||
// retires this method and the range/whole method pairs. See the lazy blob
|
||||
// resolution plan (deferred for feature velocity).
|
||||
Status FetchBlobForceVerify(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// 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
|
||||
@@ -73,6 +104,13 @@ class VersionBlobFetcherBase : public BlobFetcher {
|
||||
(allow_write_path_fallback_ && blob_file_cache_ != nullptr);
|
||||
}
|
||||
|
||||
// True if this fetcher can serve an I/O-saving byte-range read: it reads
|
||||
// through a Version (FetchBlobRange goes to Version::GetBlobRange) and is not
|
||||
// in direct-write fallback mode (whose reads resolve whole records instead).
|
||||
bool SupportsRangeRead() const {
|
||||
return version_ != nullptr && !allow_write_path_fallback_;
|
||||
}
|
||||
|
||||
protected:
|
||||
VersionBlobFetcherBase(const Version* version, BlobFileCache* blob_file_cache,
|
||||
bool allow_write_path_fallback)
|
||||
|
||||
@@ -41,14 +41,14 @@ BlobFileBuilder::BlobFileBuilder(
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
BlobFileCompletionCallback* blob_callback,
|
||||
BlobFileCreationReason creation_reason,
|
||||
std::vector<std::string>* blob_file_paths,
|
||||
std::vector<std::string>* output_file_paths,
|
||||
std::vector<BlobFileAddition>* blob_file_additions)
|
||||
: BlobFileBuilder([versions]() { return versions->NewFileNumber(); }, fs,
|
||||
immutable_options, mutable_cf_options, file_options,
|
||||
write_options, db_id, db_session_id, job_id,
|
||||
column_family_id, column_family_name, write_hint,
|
||||
io_tracer, blob_callback, creation_reason,
|
||||
blob_file_paths, blob_file_additions) {}
|
||||
output_file_paths, blob_file_additions) {}
|
||||
|
||||
BlobFileBuilder::BlobFileBuilder(
|
||||
std::function<uint64_t()> file_number_generator, FileSystem* fs,
|
||||
@@ -60,13 +60,15 @@ BlobFileBuilder::BlobFileBuilder(
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
BlobFileCompletionCallback* blob_callback,
|
||||
BlobFileCreationReason creation_reason,
|
||||
std::vector<std::string>* blob_file_paths,
|
||||
std::vector<std::string>* output_file_paths,
|
||||
std::vector<BlobFileAddition>* blob_file_additions)
|
||||
: file_number_generator_(std::move(file_number_generator)),
|
||||
fs_(fs),
|
||||
immutable_options_(immutable_options),
|
||||
min_blob_size_(mutable_cf_options->min_blob_size),
|
||||
blob_file_size_(mutable_cf_options->blob_file_size),
|
||||
blob_file_writable_file_max_buffer_size_(
|
||||
mutable_cf_options->blob_file_writable_file_max_buffer_size),
|
||||
blob_compression_type_(mutable_cf_options->blob_compression_type),
|
||||
// TODO with schema change: support custom compression manager and options
|
||||
// such as max_compressed_bytes_per_kb
|
||||
@@ -88,7 +90,7 @@ BlobFileBuilder::BlobFileBuilder(
|
||||
io_tracer_(io_tracer),
|
||||
blob_callback_(blob_callback),
|
||||
creation_reason_(creation_reason),
|
||||
blob_file_paths_(blob_file_paths),
|
||||
output_file_paths_(output_file_paths),
|
||||
blob_file_additions_(blob_file_additions),
|
||||
blob_count_(0),
|
||||
blob_bytes_(0) {
|
||||
@@ -97,14 +99,21 @@ BlobFileBuilder::BlobFileBuilder(
|
||||
assert(immutable_options_);
|
||||
assert(file_options_);
|
||||
assert(write_options_);
|
||||
assert(blob_file_paths_);
|
||||
assert(blob_file_paths_->empty());
|
||||
assert(output_file_paths_);
|
||||
assert(output_file_paths_->empty());
|
||||
assert(blob_file_additions_);
|
||||
assert(blob_file_additions_->empty());
|
||||
}
|
||||
|
||||
BlobFileBuilder::~BlobFileBuilder() = default;
|
||||
|
||||
std::string BlobFileBuilder::GetBlobFilePath(uint64_t blob_file_number) const {
|
||||
assert(immutable_options_);
|
||||
assert(!immutable_options_->cf_paths.empty());
|
||||
return BlobFileName(immutable_options_->cf_paths.front().path,
|
||||
blob_file_number);
|
||||
}
|
||||
|
||||
Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
|
||||
std::string* blob_index) {
|
||||
assert(blob_index);
|
||||
@@ -186,10 +195,7 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
|
||||
assert(file_number_generator_);
|
||||
const uint64_t blob_file_number = file_number_generator_();
|
||||
|
||||
assert(immutable_options_);
|
||||
assert(!immutable_options_->cf_paths.empty());
|
||||
std::string blob_file_path =
|
||||
BlobFileName(immutable_options_->cf_paths.front().path, blob_file_number);
|
||||
std::string blob_file_path = GetBlobFilePath(blob_file_number);
|
||||
|
||||
if (blob_callback_) {
|
||||
blob_callback_->OnBlobFileCreationStarted(
|
||||
@@ -204,6 +210,10 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
|
||||
fo_copy.open_contract = FileOpenContract::kNoReopenForWrite |
|
||||
FileOpenContract::kNoReadersWhileOpenForWrite;
|
||||
fo_copy.write_hint = write_hint_;
|
||||
if (blob_file_writable_file_max_buffer_size_ > 0) {
|
||||
fo_copy.writable_file_max_buffer_size =
|
||||
blob_file_writable_file_max_buffer_size_;
|
||||
}
|
||||
Status s = NewWritableFile(fs_, blob_file_path, &file, fo_copy);
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
@@ -214,11 +224,11 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
|
||||
}
|
||||
}
|
||||
|
||||
// Note: files get added to blob_file_paths_ right after the open, so they
|
||||
// can be cleaned up upon failure. Contrast this with blob_file_additions_,
|
||||
// which only contains successfully written files.
|
||||
assert(blob_file_paths_);
|
||||
blob_file_paths_->emplace_back(std::move(blob_file_path));
|
||||
// Note: blob files get added to output_file_paths_ right after the open, so
|
||||
// they can be cleaned up upon failure. Contrast this with
|
||||
// blob_file_additions_, which only contains successfully written files.
|
||||
assert(output_file_paths_);
|
||||
output_file_paths_->emplace_back(std::move(blob_file_path));
|
||||
|
||||
assert(file);
|
||||
file->SetIOPriority(write_options_->rate_limiter_priority);
|
||||
@@ -228,7 +238,7 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
|
||||
FileTypeSet tmp_set = immutable_options_->checksum_handoff_file_types;
|
||||
Statistics* const statistics = immutable_options_->stats;
|
||||
std::unique_ptr<WritableFileWriter> file_writer(new WritableFileWriter(
|
||||
std::move(file), blob_file_paths_->back(), *file_options_,
|
||||
std::move(file), output_file_paths_->back(), fo_copy,
|
||||
immutable_options_->clock, io_tracer_, statistics,
|
||||
Histograms::BLOB_DB_BLOB_FILE_WRITE_MICROS, immutable_options_->listeners,
|
||||
immutable_options_->file_checksum_gen_factory.get(),
|
||||
@@ -341,7 +351,7 @@ Status BlobFileBuilder::CloseBlobFile() {
|
||||
|
||||
if (blob_callback_) {
|
||||
s = blob_callback_->OnBlobFileCompleted(
|
||||
blob_file_paths_->back(), column_family_name_, job_id_,
|
||||
GetBlobFilePath(blob_file_number), column_family_name_, job_id_,
|
||||
blob_file_number, creation_reason_, s, checksum_value, checksum_method,
|
||||
blob_count_, blob_bytes_);
|
||||
}
|
||||
@@ -385,9 +395,10 @@ void BlobFileBuilder::Abandon(const Status& s) {
|
||||
if (blob_callback_) {
|
||||
// BlobFileBuilder::Abandon() is called because of error while writing to
|
||||
// Blob files. So we can ignore the below error.
|
||||
const uint64_t blob_file_number = writer_->get_log_number();
|
||||
blob_callback_
|
||||
->OnBlobFileCompleted(blob_file_paths_->back(), column_family_name_,
|
||||
job_id_, writer_->get_log_number(),
|
||||
->OnBlobFileCompleted(GetBlobFilePath(blob_file_number),
|
||||
column_family_name_, job_id_, blob_file_number,
|
||||
creation_reason_, s, "", "", blob_count_,
|
||||
blob_bytes_)
|
||||
.PermitUncheckedError();
|
||||
|
||||
@@ -48,7 +48,7 @@ class BlobFileBuilder {
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
BlobFileCompletionCallback* blob_callback,
|
||||
BlobFileCreationReason creation_reason,
|
||||
std::vector<std::string>* blob_file_paths,
|
||||
std::vector<std::string>* output_file_paths,
|
||||
std::vector<BlobFileAddition>* blob_file_additions);
|
||||
|
||||
BlobFileBuilder(std::function<uint64_t()> file_number_generator,
|
||||
@@ -63,7 +63,7 @@ class BlobFileBuilder {
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
BlobFileCompletionCallback* blob_callback,
|
||||
BlobFileCreationReason creation_reason,
|
||||
std::vector<std::string>* blob_file_paths,
|
||||
std::vector<std::string>* output_file_paths,
|
||||
std::vector<BlobFileAddition>* blob_file_additions);
|
||||
|
||||
BlobFileBuilder(const BlobFileBuilder&) = delete;
|
||||
@@ -76,6 +76,7 @@ class BlobFileBuilder {
|
||||
void Abandon(const Status& s);
|
||||
|
||||
private:
|
||||
std::string GetBlobFilePath(uint64_t blob_file_number) const;
|
||||
bool IsBlobFileOpen() const;
|
||||
Status OpenBlobFileIfNeeded();
|
||||
Status CompressBlobIfNeeded(Slice* blob,
|
||||
@@ -93,6 +94,7 @@ class BlobFileBuilder {
|
||||
const ImmutableOptions* immutable_options_;
|
||||
uint64_t min_blob_size_;
|
||||
uint64_t blob_file_size_;
|
||||
uint64_t blob_file_writable_file_max_buffer_size_;
|
||||
CompressionType blob_compression_type_;
|
||||
std::unique_ptr<Compressor> blob_compressor_;
|
||||
mutable Compressor::ManagedWorkingArea blob_compressor_wa_;
|
||||
@@ -108,7 +110,7 @@ class BlobFileBuilder {
|
||||
std::shared_ptr<IOTracer> io_tracer_;
|
||||
BlobFileCompletionCallback* blob_callback_;
|
||||
BlobFileCreationReason creation_reason_;
|
||||
std::vector<std::string>* blob_file_paths_;
|
||||
std::vector<std::string>* output_file_paths_;
|
||||
std::vector<BlobFileAddition>* blob_file_additions_;
|
||||
std::unique_ptr<BlobLogWriter> writer_;
|
||||
uint64_t blob_count_;
|
||||
|
||||
@@ -100,7 +100,8 @@ BlobFilePartitionManager::BlobFilePartitionManager(
|
||||
FileNumberAllocator file_number_allocator, FileSystem* fs,
|
||||
SystemClock* clock, Statistics* statistics, const FileOptions& file_options,
|
||||
std::string db_path, std::string column_family_name,
|
||||
uint64_t blob_file_size, bool use_fsync, BlobFileCache* blob_file_cache,
|
||||
uint64_t blob_file_size, bool use_fsync,
|
||||
uint64_t blob_writer_max_buffer_size, BlobFileCache* blob_file_cache,
|
||||
BlobFileCompletionCallback* blob_callback,
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners,
|
||||
FileChecksumGenFactory* file_checksum_gen_factory,
|
||||
@@ -120,6 +121,7 @@ BlobFilePartitionManager::BlobFilePartitionManager(
|
||||
column_family_name_(std::move(column_family_name)),
|
||||
blob_file_size_(blob_file_size),
|
||||
use_fsync_(use_fsync),
|
||||
blob_writer_max_buffer_size_(blob_writer_max_buffer_size),
|
||||
blob_file_cache_(blob_file_cache),
|
||||
blob_callback_(blob_callback),
|
||||
listeners_(listeners),
|
||||
@@ -202,6 +204,11 @@ Status BlobFilePartitionManager::OpenNewBlobFile(Partition* partition,
|
||||
std::unique_ptr<FSWritableFile> file;
|
||||
FileOptions writer_file_options = file_options_;
|
||||
writer_file_options.open_contract = FileOpenContract::kNoReopenForWrite;
|
||||
const uint64_t max_buffer_size =
|
||||
blob_writer_max_buffer_size_.load(std::memory_order_relaxed);
|
||||
if (max_buffer_size > 0) {
|
||||
writer_file_options.writable_file_max_buffer_size = max_buffer_size;
|
||||
}
|
||||
Status s = NewWritableFile(fs_, blob_file_path, &file, writer_file_options);
|
||||
if (!s.ok()) {
|
||||
RemoveFilePartitionMapping(blob_file_number);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
@@ -61,7 +62,8 @@ class BlobFilePartitionManager {
|
||||
SystemClock* clock, Statistics* statistics,
|
||||
const FileOptions& file_options, std::string db_path,
|
||||
std::string column_family_name, uint64_t blob_file_size, bool use_fsync,
|
||||
BlobFileCache* blob_file_cache, BlobFileCompletionCallback* blob_callback,
|
||||
uint64_t blob_writer_max_buffer_size, BlobFileCache* blob_file_cache,
|
||||
BlobFileCompletionCallback* blob_callback,
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners,
|
||||
FileChecksumGenFactory* file_checksum_gen_factory,
|
||||
const FileTypeSet& checksum_handoff_file_types,
|
||||
@@ -129,6 +131,11 @@ class BlobFilePartitionManager {
|
||||
// protected by a live memtable / old SuperVersion.
|
||||
bool IsTrackedBlobFileNumber(uint64_t file_number) const;
|
||||
|
||||
void SetBlobWriterMaxBufferSize(uint64_t blob_writer_max_buffer_size) {
|
||||
blob_writer_max_buffer_size_.store(blob_writer_max_buffer_size,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// Increments / decrements memtable-held protection on sealed blob files.
|
||||
void ProtectSealedBlobFileNumbers(const std::vector<uint64_t>& file_numbers);
|
||||
void UnprotectSealedBlobFileNumbers(
|
||||
@@ -260,6 +267,7 @@ class BlobFilePartitionManager {
|
||||
// File creation policy and shared blob-file infrastructure.
|
||||
uint64_t blob_file_size_;
|
||||
bool use_fsync_;
|
||||
std::atomic<uint64_t> blob_writer_max_buffer_size_;
|
||||
BlobFileCache* blob_file_cache_;
|
||||
BlobFileCompletionCallback* blob_callback_;
|
||||
std::vector<std::shared_ptr<EventListener>> listeners_;
|
||||
|
||||
@@ -416,6 +416,77 @@ Status BlobFileReader::GetBlob(
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlobFileReader::GetBlobRange(const ReadOptions& read_options,
|
||||
const Slice& user_key, uint64_t offset,
|
||||
uint64_t value_size, uint64_t range_offset,
|
||||
size_t range_length,
|
||||
MemoryAllocator* allocator,
|
||||
std::unique_ptr<BlobContents>* result,
|
||||
uint64_t* bytes_read) const {
|
||||
assert(result);
|
||||
|
||||
// Partial reads are only meaningful for uncompressed blobs: a strict
|
||||
// sub-range of a compressed record cannot be decompressed in isolation.
|
||||
// Callers (BlobSource::GetBlobRange) must ensure this; enforce it here too.
|
||||
if (compression_type_ != kNoCompression) {
|
||||
return Status::Corruption("Cannot range-read a compressed blob");
|
||||
}
|
||||
|
||||
const uint64_t key_size = user_key.size();
|
||||
|
||||
// Validate that the full value region is within the file (same check GetBlob
|
||||
// performs); this guards the sub-range read below.
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
|
||||
has_footer_)) {
|
||||
return Status::Corruption("Invalid blob offset");
|
||||
}
|
||||
|
||||
// The requested sub-range must lie within the value.
|
||||
if (range_offset > value_size || range_length > value_size - range_offset) {
|
||||
return Status::InvalidArgument("Blob range out of bounds");
|
||||
}
|
||||
|
||||
// Read only the requested bytes, at the value's file position plus the range
|
||||
// offset. Unlike GetBlob there is no record-header adjustment (we do not read
|
||||
// the key/header) and no whole-record checksum verification (a strict
|
||||
// sub-range cannot cover it -- callers that require verification take the
|
||||
// full-read path instead).
|
||||
const uint64_t read_offset = offset + range_offset;
|
||||
const size_t read_size = range_length;
|
||||
|
||||
Slice range_slice;
|
||||
Buffer buf;
|
||||
AlignedBuffer direct_io_buffer;
|
||||
|
||||
TEST_SYNC_POINT("BlobFileReader::GetBlobRange:ReadFromFile");
|
||||
PERF_COUNTER_ADD(blob_read_count, 1);
|
||||
PERF_COUNTER_ADD(blob_read_byte, read_size);
|
||||
PERF_TIMER_GUARD(blob_read_time);
|
||||
|
||||
{
|
||||
const Status s =
|
||||
ReadFromFile(file_reader_.get(), read_options, read_offset, read_size,
|
||||
statistics_, &range_slice, &buf, &direct_io_buffer);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("BlobFileReader::GetBlobRange:TamperWithResult",
|
||||
&range_slice);
|
||||
|
||||
// The blob is uncompressed, so the bytes read are the requested value bytes;
|
||||
// copy them into an owned BlobContents (no decompression).
|
||||
BlobContentsCreator::Create(result, /*out_charge=*/nullptr, range_slice,
|
||||
kNoCompression, allocator);
|
||||
|
||||
if (bytes_read) {
|
||||
*bytes_read = read_size;
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void BlobFileReader::MultiGetBlob(
|
||||
const ReadOptions& read_options, MemoryAllocator* allocator,
|
||||
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>&
|
||||
|
||||
@@ -64,6 +64,23 @@ class BlobFileReader {
|
||||
std::unique_ptr<BlobContents>* result,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// Reads a byte sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed* blob's value directly from the file, reading only those
|
||||
// bytes. Unlike GetBlob it does not read the record header/key, does not
|
||||
// verify the whole-record checksum (a strict sub-range cannot cover it), and
|
||||
// never decompresses -- so it is only valid when this reader's blob file is
|
||||
// uncompressed (checked). `offset` and `value_size` are the blob value's file
|
||||
// offset and full size (from the BlobIndex); the caller must ensure
|
||||
// range_offset + range_length <= value_size. On success `*result` owns a copy
|
||||
// of the requested bytes and `*bytes_read` (when non-null) is the number of
|
||||
// bytes read from the file.
|
||||
Status GetBlobRange(const ReadOptions& read_options, const Slice& user_key,
|
||||
uint64_t offset, uint64_t value_size,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
MemoryAllocator* allocator,
|
||||
std::unique_ptr<BlobContents>* result,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// offsets must be sorted in ascending order by caller.
|
||||
void MultiGetBlob(
|
||||
const ReadOptions& read_options, MemoryAllocator* allocator,
|
||||
|
||||
@@ -76,6 +76,54 @@ Status ReadAndVerifySimpleGen2BlobRecord(
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file,
|
||||
uint64_t record_offset, size_t payload_size,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
CompressionType expected_compression,
|
||||
char* buf) {
|
||||
assert(file != nullptr);
|
||||
assert(buf != nullptr);
|
||||
|
||||
// A strict sub-range of a compressed payload cannot be decompressed in
|
||||
// isolation; callers resolve such payloads whole and slice instead.
|
||||
if (expected_compression != kNoCompression) {
|
||||
return Status::Corruption("Cannot range-read a compressed blob");
|
||||
}
|
||||
|
||||
// The requested sub-range must lie within the payload (the trailer that
|
||||
// follows the payload is never part of a range read).
|
||||
if (range_offset > payload_size ||
|
||||
range_length > payload_size - range_offset) {
|
||||
return Status::InvalidArgument("Blob range out of bounds");
|
||||
}
|
||||
|
||||
Slice result;
|
||||
IOOptions opts;
|
||||
IODebugContext dbg;
|
||||
Status s = file->PrepareIOOptions(read_options, opts, &dbg);
|
||||
if (s.ok()) {
|
||||
s = file->Read(opts, record_offset + range_offset, range_length, &result,
|
||||
buf, nullptr, &dbg);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (result.size() != range_length) {
|
||||
return Status::Corruption("Could not read complete blob range");
|
||||
}
|
||||
// With mmap reads the data lands outside `buf`; copy it in so the caller can
|
||||
// rely on `buf` owning the bytes (the only copy on the mmap path).
|
||||
if (result.data() != buf) {
|
||||
memcpy(buf, result.data(), range_length);
|
||||
}
|
||||
|
||||
// No trailer read and no checksum verification: a strict sub-range cannot
|
||||
// cover the record's checksum (callers that require it read the whole
|
||||
// record).
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
IOStatus WriteSimpleGen2BlobRecord(WritableFileWriter* file,
|
||||
const WriteOptions& write_options,
|
||||
ChecksumType checksum_type,
|
||||
|
||||
@@ -73,6 +73,24 @@ Status ReadAndVerifySimpleGen2BlobRecord(
|
||||
ChecksumType checksum_type, uint32_t base_context_checksum,
|
||||
CompressionType expected_compression, char* buf);
|
||||
|
||||
// Reads a byte sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed* SimpleGen2Blob payload directly into `buf` (capacity >=
|
||||
// range_length), reading only those bytes. Unlike
|
||||
// ReadAndVerifySimpleGen2BlobRecord it does not read the trailer and does not
|
||||
// verify the record's checksum (a strict sub-range cannot cover it); callers
|
||||
// that require verification read the whole record instead. `payload_size` is
|
||||
// the full payload size (from the blob index); the caller must ensure
|
||||
// range_offset + range_length <= payload_size. `expected_compression` must be
|
||||
// kNoCompression (a strict sub-range of a compressed payload cannot be
|
||||
// decompressed in isolation).
|
||||
//
|
||||
// On success, buf[0, range_length) holds the requested payload bytes.
|
||||
Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file,
|
||||
uint64_t record_offset, size_t payload_size,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
CompressionType expected_compression, char* buf);
|
||||
|
||||
// Writes a SimpleGen2Blob record for `payload` at the current end of `file`,
|
||||
// which the caller asserts is byte offset `record_offset`. Appends the payload
|
||||
// bytes followed by the 5-byte trailer (compression marker + context-modified
|
||||
|
||||
+283
-48
@@ -36,6 +36,102 @@ Status AppendBlobRefreshRetryFailure(const Status& stale_status,
|
||||
stale_status, "; refresh retry failed: ", retry_status.ToString());
|
||||
}
|
||||
|
||||
// Runs read(reader) against the blob-file reader for `file_number`. On a
|
||||
// Corruption -- which a stale cached reader (e.g. the physical file was
|
||||
// replaced) can surface -- evicts the cached reader, reopens it uncached,
|
||||
// retries read() once, and refreshes the cache on success. Shared by the
|
||||
// single-blob read paths (GetBlob / GetBlobRange) so the stale-file recovery
|
||||
// lives in one place.
|
||||
//
|
||||
// `read` is invoked with a BlobFileReader* and returns a Status; because it may
|
||||
// run twice, it must re-initialize any output it populates on each call (and is
|
||||
// taken by const reference rather than forwarded, since it is not moved-from).
|
||||
template <typename ReadFn>
|
||||
Status ReadBlobWithReaderRetry(BlobFileCache* blob_file_cache,
|
||||
const ReadOptions& read_options,
|
||||
uint64_t file_number, const ReadFn& read) {
|
||||
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
||||
Status s = blob_file_cache->GetBlobFileReader(read_options, file_number,
|
||||
&blob_file_reader);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
assert(blob_file_reader.GetValue());
|
||||
|
||||
s = read(blob_file_reader.GetValue());
|
||||
if (!s.IsCorruption()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
const Status stale_status = s;
|
||||
blob_file_reader.Reset();
|
||||
blob_file_cache->Evict(file_number);
|
||||
|
||||
std::unique_ptr<BlobFileReader> fresh_reader;
|
||||
s = blob_file_cache->OpenBlobFileReaderUncached(
|
||||
read_options, file_number, &fresh_reader,
|
||||
/*allow_footer_skip_retry=*/false);
|
||||
if (!s.ok()) {
|
||||
return AppendBlobRefreshRetryFailure(stale_status, s);
|
||||
}
|
||||
|
||||
s = read(fresh_reader.get());
|
||||
if (!s.ok()) {
|
||||
return AppendBlobRefreshRetryFailure(stale_status, s);
|
||||
}
|
||||
|
||||
CacheHandleGuard<BlobFileReader> ignored_reader;
|
||||
blob_file_cache
|
||||
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
|
||||
.PermitUncheckedError();
|
||||
return s;
|
||||
}
|
||||
|
||||
// Records lazy wide-column read metrics for one actual storage read issued
|
||||
// while resolving a lazy result (attributed via Env::IOActivity::kLazyResolve):
|
||||
// `bytes_read` bytes were read from storage. Covers both whole-column and
|
||||
// partial reads; not called for cache hits (no storage read).
|
||||
void RecordLazyRead(Statistics* statistics, uint64_t bytes_read) {
|
||||
RecordTick(statistics, BLOB_DB_LAZY_READ_COUNT);
|
||||
RecordTick(statistics, BLOB_DB_LAZY_READ_BYTES, bytes_read);
|
||||
}
|
||||
|
||||
// Additionally records the partial-read metrics for one actual partial
|
||||
// (byte-range) read: `bytes_read` bytes were fetched instead of the column's
|
||||
// full `value_size`. Call alongside RecordLazyRead (a partial read is also a
|
||||
// lazy read).
|
||||
void RecordLazyPartialRead(Statistics* statistics, uint64_t bytes_read,
|
||||
uint64_t value_size) {
|
||||
RecordTick(statistics, BLOB_DB_LAZY_PARTIAL_READ_COUNT);
|
||||
if (value_size > bytes_read) {
|
||||
RecordTick(statistics, BLOB_DB_LAZY_PARTIAL_BYTES_SAVED,
|
||||
value_size - bytes_read);
|
||||
}
|
||||
}
|
||||
|
||||
// Pins the sub-range [range_offset, range_offset + range_length) of a cache-hit
|
||||
// whole blob value into *value (clamped: an offset at/past the end yields
|
||||
// empty, length is clamped to the remainder), keeping the cached bytes alive by
|
||||
// transferring the cache handle into *value. Shared by the range-read cache-hit
|
||||
// paths (GetBlobRange / GetSimpleGen2BlobRange).
|
||||
void PinCacheHitSubRange(CacheHandleGuard<BlobContents>* blob_handle,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
PinnableSlice* value) {
|
||||
const Slice full = blob_handle->GetValue()->data();
|
||||
Slice sub;
|
||||
if (range_offset < full.size()) {
|
||||
const size_t off = static_cast<size_t>(range_offset);
|
||||
const size_t avail = full.size() - off;
|
||||
const size_t len = range_length > avail ? avail : range_length;
|
||||
sub = Slice(full.data() + off, len);
|
||||
} // else: offset at/past end -> empty (not an error)
|
||||
|
||||
value->Reset();
|
||||
constexpr Cleanable* cleanable = nullptr;
|
||||
value->PinSlice(sub, cleanable);
|
||||
blob_handle->TransferTo(value);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BlobSource::BlobSource(const ImmutableOptions& immutable_options,
|
||||
@@ -228,66 +324,37 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
std::unique_ptr<BlobContents> blob_contents;
|
||||
|
||||
{
|
||||
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
||||
s = blob_file_cache_->GetBlobFileReader(read_options, file_number,
|
||||
&blob_file_reader);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
assert(blob_file_reader.GetValue());
|
||||
|
||||
if (compression_type != blob_file_reader.GetValue()->GetCompressionType()) {
|
||||
return Status::Corruption("Compression type mismatch when reading blob");
|
||||
}
|
||||
|
||||
MemoryAllocator* const allocator =
|
||||
(blob_cache_ && read_options.fill_cache)
|
||||
? blob_cache_.get()->memory_allocator()
|
||||
: nullptr;
|
||||
|
||||
uint64_t read_size = 0;
|
||||
s = blob_file_reader.GetValue()->GetBlob(
|
||||
read_options, user_key, offset, value_size, compression_type,
|
||||
prefetch_buffer, allocator, &blob_contents, &read_size);
|
||||
if (s.IsCorruption()) {
|
||||
const Status stale_status = s;
|
||||
blob_file_reader.Reset();
|
||||
blob_file_cache_->Evict(file_number);
|
||||
|
||||
std::unique_ptr<BlobFileReader> fresh_reader;
|
||||
s = blob_file_cache_->OpenBlobFileReaderUncached(
|
||||
read_options, file_number, &fresh_reader,
|
||||
/*allow_footer_skip_retry=*/false);
|
||||
if (!s.ok()) {
|
||||
return AppendBlobRefreshRetryFailure(stale_status, s);
|
||||
}
|
||||
|
||||
if (compression_type != fresh_reader->GetCompressionType()) {
|
||||
return Status::Corruption(
|
||||
"Compression type mismatch when reading blob");
|
||||
}
|
||||
|
||||
blob_contents.reset();
|
||||
read_size = 0;
|
||||
s = fresh_reader->GetBlob(read_options, user_key, offset, value_size,
|
||||
compression_type, prefetch_buffer, allocator,
|
||||
&blob_contents, &read_size);
|
||||
if (!s.ok()) {
|
||||
s = AppendBlobRefreshRetryFailure(stale_status, s);
|
||||
} else {
|
||||
CacheHandleGuard<BlobFileReader> ignored_reader;
|
||||
blob_file_cache_
|
||||
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
s = ReadBlobWithReaderRetry(
|
||||
blob_file_cache_, read_options, file_number,
|
||||
[&](BlobFileReader* reader) {
|
||||
if (compression_type != reader->GetCompressionType()) {
|
||||
return Status::Corruption(
|
||||
"Compression type mismatch when reading blob");
|
||||
}
|
||||
blob_contents.reset();
|
||||
read_size = 0;
|
||||
return reader->GetBlob(read_options, user_key, offset, value_size,
|
||||
compression_type, prefetch_buffer, allocator,
|
||||
&blob_contents, &read_size);
|
||||
});
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (bytes_read) {
|
||||
*bytes_read = read_size;
|
||||
}
|
||||
// Whole-column read on the lazy resolve path (partial reads go through
|
||||
// GetBlobRange). Counts the storage read; partial reads are not counted
|
||||
// here.
|
||||
if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
|
||||
RecordLazyRead(statistics_, read_size);
|
||||
}
|
||||
}
|
||||
|
||||
if (blob_cache_ && read_options.fill_cache) {
|
||||
@@ -308,6 +375,92 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
return s;
|
||||
}
|
||||
|
||||
Status BlobSource::GetBlobRange(const ReadOptions& read_options,
|
||||
const Slice& user_key, uint64_t file_number,
|
||||
uint64_t offset, uint64_t file_size,
|
||||
uint64_t value_size,
|
||||
CompressionType compression_type,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
PinnableSlice* value, uint64_t* bytes_read) {
|
||||
assert(value);
|
||||
// Partial reads are for uncompressed blobs only; the caller decides this (a
|
||||
// compressed column takes the whole-value GetBlob path and slices).
|
||||
assert(compression_type == kNoCompression);
|
||||
// Range reads are (currently) only issued while resolving a lazy result, so
|
||||
// the lazy read stats below are recorded unconditionally (unlike GetBlob,
|
||||
// which gates on the activity). Enforce that invariant here.
|
||||
assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
|
||||
|
||||
Status s;
|
||||
|
||||
const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
|
||||
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
|
||||
// First, probe the blob cache for the whole value. On a hit, slice the
|
||||
// requested sub-range out of the cached value while pinning the cache handle
|
||||
// (zero-copy, no disk read). A partial read never inserts into the cache.
|
||||
if (blob_cache_) {
|
||||
Slice key = cache_key.AsSlice();
|
||||
s = GetBlobFromCache(key, &blob_handle);
|
||||
if (s.ok()) {
|
||||
PinCacheHitSubRange(&blob_handle, range_offset, range_length, value);
|
||||
if (bytes_read) {
|
||||
*bytes_read = 0; // served from cache; no disk read
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
assert(blob_handle.IsEmpty());
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
}
|
||||
|
||||
// Cache miss: read only the requested sub-range from the file. The result is
|
||||
// never inserted into the blob cache (see the header comment).
|
||||
std::unique_ptr<BlobContents> blob_contents;
|
||||
{
|
||||
// No cache-fill allocator: a partial value is never inserted into the
|
||||
// cache.
|
||||
constexpr MemoryAllocator* allocator = nullptr;
|
||||
|
||||
uint64_t read_size = 0;
|
||||
// Reuses the stale-reader retry shared with GetBlob. A range read skips
|
||||
// whole-record checksum verification, so the retry handles the stale-file
|
||||
// case (offset/size mismatch surfacing as Corruption), not a payload
|
||||
// checksum failure.
|
||||
s = ReadBlobWithReaderRetry(
|
||||
blob_file_cache_, read_options, file_number,
|
||||
[&](BlobFileReader* reader) {
|
||||
if (compression_type != reader->GetCompressionType()) {
|
||||
return Status::Corruption(
|
||||
"Compression type mismatch when reading blob");
|
||||
}
|
||||
blob_contents.reset();
|
||||
read_size = 0;
|
||||
return reader->GetBlobRange(read_options, user_key, offset,
|
||||
value_size, range_offset, range_length,
|
||||
allocator, &blob_contents, &read_size);
|
||||
});
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (bytes_read) {
|
||||
*bytes_read = read_size;
|
||||
}
|
||||
RecordLazyRead(statistics_, read_size);
|
||||
RecordLazyPartialRead(statistics_, read_size, value_size);
|
||||
}
|
||||
|
||||
PinOwnedBlob(&blob_contents, value);
|
||||
|
||||
assert(s.ok());
|
||||
return s;
|
||||
}
|
||||
|
||||
Status BlobSource::GetSimpleGen2Blob(
|
||||
const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file, uint64_t record_offset, uint64_t payload_size,
|
||||
@@ -379,6 +532,11 @@ Status BlobSource::GetSimpleGen2Blob(
|
||||
if (bytes_read) {
|
||||
*bytes_read = record_size;
|
||||
}
|
||||
// Whole-column read on the lazy resolve path (partial reads go through
|
||||
// GetSimpleGen2BlobRange).
|
||||
if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
|
||||
RecordLazyRead(statistics_, record_size);
|
||||
}
|
||||
|
||||
if (blob_cache_ && read_options.fill_cache) {
|
||||
// If filling cache is allowed and a cache is configured, try to put the
|
||||
@@ -398,6 +556,83 @@ Status BlobSource::GetSimpleGen2Blob(
|
||||
return s;
|
||||
}
|
||||
|
||||
Status BlobSource::GetSimpleGen2BlobRange(
|
||||
const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file, uint64_t record_offset, uint64_t payload_size,
|
||||
ChecksumType /*checksum_type*/, uint32_t /*base_context_checksum*/,
|
||||
CompressionType expected_compression, uint64_t range_offset,
|
||||
size_t range_length, PinnableSlice* value, uint64_t* bytes_read) {
|
||||
assert(value);
|
||||
assert(file);
|
||||
// Partial reads are for uncompressed payloads only; the caller decides this
|
||||
// (a compressed column takes the whole-payload GetSimpleGen2Blob path +
|
||||
// slice).
|
||||
assert(expected_compression == kNoCompression);
|
||||
// Range reads are (currently) only issued while resolving a lazy result, so
|
||||
// the lazy read stats below are recorded unconditionally (unlike
|
||||
// GetSimpleGen2Blob, which gates on the activity). Enforce that invariant
|
||||
// here.
|
||||
assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
|
||||
|
||||
const CacheKey cache_key =
|
||||
GetSimpleGen2BlobCacheKey(base_cache_key, record_offset);
|
||||
|
||||
Status s;
|
||||
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
|
||||
// First, probe the blob cache for the whole payload. On a hit, slice the
|
||||
// requested sub-range out of the cached payload while pinning the cache
|
||||
// handle (zero-copy, no disk read). A partial read never inserts into the
|
||||
// cache.
|
||||
if (blob_cache_) {
|
||||
Slice key = cache_key.AsSlice();
|
||||
s = GetBlobFromCache(key, &blob_handle);
|
||||
if (s.ok()) {
|
||||
PinCacheHitSubRange(&blob_handle, range_offset, range_length, value);
|
||||
if (bytes_read) {
|
||||
*bytes_read = 0; // served from cache; no disk read
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
assert(blob_handle.IsEmpty());
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
}
|
||||
|
||||
// Cache miss: read only the requested sub-range from the file. The result is
|
||||
// never inserted into the blob cache (a partial payload cannot represent the
|
||||
// whole-record cache entry).
|
||||
CacheAllocationPtr buf = AllocateBlock(range_length, /*allocator=*/nullptr);
|
||||
s = ReadSimpleGen2BlobRange(read_options, file, record_offset,
|
||||
static_cast<size_t>(payload_size), range_offset,
|
||||
range_length, expected_compression, buf.get());
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
std::unique_ptr<BlobContents> blob_contents(
|
||||
new BlobContents(std::move(buf), range_length));
|
||||
|
||||
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, range_length);
|
||||
PERF_COUNTER_ADD(blob_read_count, 1);
|
||||
PERF_COUNTER_ADD(blob_read_byte, range_length);
|
||||
RecordLazyRead(statistics_, range_length);
|
||||
RecordLazyPartialRead(statistics_, range_length, payload_size);
|
||||
if (bytes_read) {
|
||||
*bytes_read = range_length;
|
||||
}
|
||||
|
||||
PinOwnedBlob(&blob_contents, value);
|
||||
|
||||
assert(s.ok());
|
||||
return s;
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetBlob(const ReadOptions& read_options,
|
||||
autovector<BlobFileReadRequests>& blob_reqs,
|
||||
uint64_t* bytes_read) {
|
||||
|
||||
@@ -61,6 +61,31 @@ class BlobSource {
|
||||
FilePrefetchBuffer* prefetch_buffer, PinnableSlice* value,
|
||||
uint64_t* bytes_read);
|
||||
|
||||
// Reads a byte sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed* blob's value, reading only those bytes on a cache miss.
|
||||
//
|
||||
// First probes the blob cache for the whole value. On a hit, pins the cache
|
||||
// handle into *value and points it at the requested sub-range (zero-copy, no
|
||||
// disk I/O). On a miss, reads only the requested bytes from the blob file
|
||||
// (via BlobFileReader::GetBlobRange -- no record-header/key read and no
|
||||
// whole-record checksum verification) and pins the owned buffer into *value.
|
||||
//
|
||||
// Unlike GetBlob, a partial read never inserts into the blob cache: the cache
|
||||
// entry is keyed per (file, offset) and holds the *whole* BlobContents, so a
|
||||
// partial value would violate that invariant. `compression_type` must be
|
||||
// kNoCompression (a strict sub-range of a compressed blob cannot be
|
||||
// decompressed in isolation); callers that need a compressed column, a
|
||||
// whole-column read, or checksum verification use GetBlob and slice instead.
|
||||
//
|
||||
// The caller must ensure range_offset + range_length <= value_size. On a miss
|
||||
// *bytes_read (when non-null) is the number of bytes read from the file; on a
|
||||
// hit it is 0.
|
||||
Status GetBlobRange(const ReadOptions& read_options, const Slice& user_key,
|
||||
uint64_t file_number, uint64_t offset, uint64_t file_size,
|
||||
uint64_t value_size, CompressionType compression_type,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
PinnableSlice* value, uint64_t* bytes_read);
|
||||
|
||||
// Reads a SimpleGen2Blob payload (see db/blob/blob_gen2_format.h) through the
|
||||
// blob value cache and BLOB_DB_* statistics. This is the counterpart to
|
||||
// GetBlob() for the second-generation blob record format, which is read
|
||||
@@ -96,6 +121,31 @@ class BlobSource {
|
||||
CompressionType expected_compression,
|
||||
PinnableSlice* value, uint64_t* bytes_read);
|
||||
|
||||
// Reads a byte sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed* SimpleGen2Blob payload, reading only those bytes on a cache
|
||||
// miss. This is the embedded (same-file) counterpart of GetBlobRange().
|
||||
//
|
||||
// First probes the blob cache for the whole payload (keyed exactly as
|
||||
// GetSimpleGen2Blob). On a hit, pins the cache handle into *value and points
|
||||
// it at the requested sub-range (zero-copy, no disk I/O). On a miss, reads
|
||||
// only the requested bytes from the file (via ReadSimpleGen2BlobRange -- no
|
||||
// trailer read and no checksum verification) and pins the owned buffer.
|
||||
//
|
||||
// Like GetBlobRange, a partial read never inserts into the blob cache (the
|
||||
// cache entry holds the whole payload). `expected_compression` must be
|
||||
// kNoCompression. The caller must ensure range_offset + range_length <=
|
||||
// payload_size. On a miss *bytes_read (when non-null) is the number of bytes
|
||||
// read from the file; on a hit it is 0.
|
||||
Status GetSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file,
|
||||
uint64_t record_offset, uint64_t payload_size,
|
||||
ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum,
|
||||
CompressionType expected_compression,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
PinnableSlice* value, uint64_t* bytes_read);
|
||||
|
||||
// Read multiple blobs from the underlying cache or blob file(s).
|
||||
//
|
||||
// If successful, returns ok and sets "result" in the elements of "blob_reqs"
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -63,6 +65,75 @@ TEST_F(DBBlobBasicTest, GetBlob) {
|
||||
.IsIncomplete());
|
||||
}
|
||||
|
||||
TEST_F(DBBlobBasicTest, BlobFileWritableFileMaxBufferSize) {
|
||||
constexpr uint64_t kBlobWriterBufferSize = 128 * 1024;
|
||||
|
||||
Options options = GetDefaultOptions();
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 0;
|
||||
options.writable_file_max_buffer_size = 1024 * 1024;
|
||||
options.blob_file_writable_file_max_buffer_size = kBlobWriterBufferSize;
|
||||
|
||||
Reopen(options);
|
||||
|
||||
std::atomic<int> matching_blob_writer_count{0};
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"WritableFileWriter::WritableFileWriter:0", [&](void* arg) {
|
||||
const uint64_t max_buffer_size =
|
||||
static_cast<uint64_t>(reinterpret_cast<uintptr_t>(arg));
|
||||
if (max_buffer_size == kBlobWriterBufferSize) {
|
||||
++matching_blob_writer_count;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(Put("key", std::string(1024, 'v')));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_GE(matching_blob_writer_count.load(), 1);
|
||||
}
|
||||
|
||||
TEST_F(DBBlobBasicTest,
|
||||
DirectWriteBlobFileWritableFileMaxBufferSizeSetOptions) {
|
||||
constexpr uint64_t kBlobWriterBufferSize = 64 * 1024;
|
||||
|
||||
Options options = GetDefaultOptions();
|
||||
options.enable_blob_files = true;
|
||||
options.enable_blob_direct_write = true;
|
||||
options.allow_concurrent_memtable_write = false;
|
||||
options.min_blob_size = 0;
|
||||
options.blob_file_size = 200;
|
||||
options.writable_file_max_buffer_size = 1024 * 1024;
|
||||
|
||||
Reopen(options);
|
||||
|
||||
std::atomic<int> matching_blob_writer_count{0};
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"WritableFileWriter::WritableFileWriter:0", [&](void* arg) {
|
||||
const uint64_t max_buffer_size =
|
||||
static_cast<uint64_t>(reinterpret_cast<uintptr_t>(arg));
|
||||
if (max_buffer_size == kBlobWriterBufferSize) {
|
||||
++matching_blob_writer_count;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(Put("first", std::string(300, 'a')));
|
||||
ASSERT_OK(db_->SetOptions({{"blob_file_writable_file_max_buffer_size",
|
||||
std::to_string(kBlobWriterBufferSize)}}));
|
||||
ASSERT_OK(Put("second", std::string(300, 'b')));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_GE(matching_blob_writer_count.load(), 1);
|
||||
}
|
||||
|
||||
TEST_F(DBBlobBasicTest, EmptyValueNotStoredAsBlob) {
|
||||
// Regression test for crash when empty blob value is evicted to
|
||||
// CompressedSecondaryCache (T261142690). Empty values should always be
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "db/arena_wrapped_db_iter.h"
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/column_family.h"
|
||||
#include "db/db_iter.h"
|
||||
@@ -138,8 +139,11 @@ class DBBlobIndexTest : public DBTestBase {
|
||||
bool resolve_direct_write_value, const Version* current,
|
||||
PinnableSlice* value, PinnableWideColumns* columns, Status* s,
|
||||
bool* is_blob_index, bool* value_found = nullptr) {
|
||||
VersionBlobFetcher blob_fetcher(current, read_options,
|
||||
cfd()->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/true);
|
||||
return DBImpl::MaybeResolveDirectWriteValue(
|
||||
read_options, key, resolve_direct_write_value, current, cfd(), value,
|
||||
read_options, key, resolve_direct_write_value, blob_fetcher, value,
|
||||
columns, s, is_blob_index, value_found);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
@@ -34,6 +37,21 @@ class SameFileBlobReader {
|
||||
virtual Status GetSameFileBlob(const ReadOptions& read_options,
|
||||
const BlobIndex& blob_index,
|
||||
PinnableSlice* value) const = 0;
|
||||
|
||||
// Resolves a byte sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed* same-file blob reference into `value`, reading only those
|
||||
// bytes on a cache miss (the embedded counterpart of a separate-file blob
|
||||
// range read). Pins the payload zero-copy (a sliced cached record, or an
|
||||
// owned sub-range buffer). Unlike GetSameFileBlob it skips whole-record
|
||||
// checksum verification (a strict sub-range cannot cover it) and never
|
||||
// populates the blob cache. The caller must ensure range_offset +
|
||||
// range_length <= blob_index.size(). read_tier == kBlockCacheTier without a
|
||||
// cached record yields Status::Incomplete.
|
||||
virtual Status GetSameFileBlobRange(const ReadOptions& read_options,
|
||||
const BlobIndex& blob_index,
|
||||
uint64_t range_offset,
|
||||
size_t range_length,
|
||||
PinnableSlice* value) const = 0;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
@@ -182,7 +183,6 @@ using ROCKSDB_NAMESPACE::TraceReader;
|
||||
using ROCKSDB_NAMESPACE::Transaction;
|
||||
using ROCKSDB_NAMESPACE::TransactionDB;
|
||||
using ROCKSDB_NAMESPACE::TransactionDBOptions;
|
||||
using ROCKSDB_NAMESPACE::TransactionLogIterator;
|
||||
using ROCKSDB_NAMESPACE::TransactionOptions;
|
||||
using ROCKSDB_NAMESPACE::UserCollectedProperties;
|
||||
using ROCKSDB_NAMESPACE::UserDefinedIndexFactory;
|
||||
@@ -190,6 +190,7 @@ using ROCKSDB_NAMESPACE::VectorWalPtr;
|
||||
using ROCKSDB_NAMESPACE::WaitForCompactOptions;
|
||||
using ROCKSDB_NAMESPACE::WalFile;
|
||||
using ROCKSDB_NAMESPACE::WalFilter;
|
||||
using ROCKSDB_NAMESPACE::WalIterator;
|
||||
using ROCKSDB_NAMESPACE::WALRecoveryMode;
|
||||
using ROCKSDB_NAMESPACE::WritableFile;
|
||||
using ROCKSDB_NAMESPACE::WriteBatch;
|
||||
@@ -412,12 +413,16 @@ struct rocksdb_readoptions_t {
|
||||
void* table_filter_state = nullptr;
|
||||
void (*table_filter_destructor)(void*) = nullptr;
|
||||
rocksdb_readoptions_table_filter_cb table_filter_callback = nullptr;
|
||||
// Owns the std::function that rep.table_filter points to. rep.table_filter is
|
||||
// a pointer to a caller-owned std::function, and this struct is that owner.
|
||||
std::function<bool(const TableProperties&)> table_filter_func;
|
||||
std::shared_ptr<UserDefinedIndexFactory> table_index_factory;
|
||||
|
||||
~rocksdb_readoptions_t() { ClearTableFilter(); }
|
||||
|
||||
void ClearTableFilter() {
|
||||
rep.table_filter = {};
|
||||
rep.table_filter = nullptr;
|
||||
table_filter_func = {};
|
||||
if (table_filter_destructor != nullptr) {
|
||||
(*table_filter_destructor)(table_filter_state);
|
||||
}
|
||||
@@ -435,11 +440,12 @@ struct rocksdb_readoptions_t {
|
||||
if (callback != nullptr) {
|
||||
auto cb = table_filter_callback;
|
||||
auto filter_state = table_filter_state;
|
||||
rep.table_filter = [cb, filter_state](const TableProperties& props) {
|
||||
table_filter_func = [cb, filter_state](const TableProperties& props) {
|
||||
return cb(filter_state,
|
||||
reinterpret_cast<const rocksdb_table_properties_t*>(
|
||||
&props)) != 0;
|
||||
};
|
||||
rep.table_filter = &table_filter_func;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -480,10 +486,10 @@ struct rocksdb_writablefile_t {
|
||||
WritableFile* rep;
|
||||
};
|
||||
struct rocksdb_wal_iterator_t {
|
||||
TransactionLogIterator* rep;
|
||||
WalIterator* rep;
|
||||
};
|
||||
struct rocksdb_wal_readoptions_t {
|
||||
TransactionLogIterator::ReadOptions rep;
|
||||
WalIterator::ReadOptions rep;
|
||||
};
|
||||
|
||||
struct rocksdb_wal_files_t {
|
||||
@@ -594,6 +600,63 @@ struct rocksdb_perfcontext_t {
|
||||
struct rocksdb_pinnableslice_t {
|
||||
PinnableSlice rep;
|
||||
};
|
||||
struct rocksdb_pinnable_multi_get_t {
|
||||
static constexpr size_t kNotFound = std::numeric_limits<size_t>::max();
|
||||
|
||||
explicit rocksdb_pinnable_multi_get_t(size_t result_count)
|
||||
: result_indexes(result_count, kNotFound) {}
|
||||
|
||||
void ReservePayloads(const std::vector<Status>& statuses) {
|
||||
size_t value_count = 0;
|
||||
size_t error_count = 0;
|
||||
for (const Status& status : statuses) {
|
||||
value_count += status.ok();
|
||||
error_count += !status.ok() && !status.IsNotFound();
|
||||
}
|
||||
values.reserve(value_count);
|
||||
errors.reserve(error_count);
|
||||
}
|
||||
|
||||
void StoreResults(std::vector<PinnableSlice>* multi_get_values,
|
||||
const std::vector<Status>& statuses) {
|
||||
ReservePayloads(statuses);
|
||||
for (size_t i = 0; i < statuses.size(); ++i) {
|
||||
if (statuses[i].ok()) {
|
||||
result_indexes[i] = values.size();
|
||||
values.emplace_back(std::move((*multi_get_values)[i]));
|
||||
} else if (!statuses[i].IsNotFound()) {
|
||||
result_indexes[i] = kNotFound - errors.size() - 1;
|
||||
errors.emplace_back(statuses[i].ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int GetResult(size_t index, const char** value, size_t* value_size,
|
||||
const char** error, size_t* error_size) const {
|
||||
if (index >= result_indexes.size()) {
|
||||
return rocksdb_pinnable_multi_get_out_of_bounds;
|
||||
}
|
||||
const size_t result_index = result_indexes[index];
|
||||
if (result_index == kNotFound) {
|
||||
return rocksdb_pinnable_multi_get_not_found;
|
||||
}
|
||||
if (result_index < values.size()) {
|
||||
*value = values[result_index].data();
|
||||
*value_size = values[result_index].size();
|
||||
return rocksdb_pinnable_multi_get_found;
|
||||
}
|
||||
const std::string& message = errors[kNotFound - result_index - 1];
|
||||
*error = message.c_str();
|
||||
*error_size = message.size();
|
||||
return rocksdb_pinnable_multi_get_error;
|
||||
}
|
||||
|
||||
// Found entries store a value index. Error entries count back from
|
||||
// kNotFound - 1, leaving kNotFound to represent a missing key.
|
||||
std::vector<size_t> result_indexes;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<std::string> errors;
|
||||
};
|
||||
struct rocksdb_transactiondb_options_t {
|
||||
TransactionDBOptions rep;
|
||||
};
|
||||
@@ -2862,6 +2925,44 @@ void rocksdb_batched_multi_get_cf_slice(
|
||||
delete[] statuses;
|
||||
}
|
||||
|
||||
rocksdb_pinnable_multi_get_t* rocksdb_batched_multi_get_pinned_cf(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
rocksdb_column_family_handle_t* column_family, size_t num_keys,
|
||||
const rocksdb_slice_t* keys, unsigned char sorted_input) {
|
||||
auto results = std::make_unique<rocksdb_pinnable_multi_get_t>(num_keys);
|
||||
if (num_keys == 0) {
|
||||
return results.release();
|
||||
}
|
||||
const Slice* key_slices = reinterpret_cast<const Slice*>(keys);
|
||||
std::vector<PinnableSlice> values(num_keys);
|
||||
std::vector<Status> statuses(num_keys);
|
||||
db->rep->MultiGet(options->rep, column_family->rep, num_keys, key_slices,
|
||||
values.data(), statuses.data(), sorted_input != 0);
|
||||
results->StoreResults(&values, statuses);
|
||||
return results.release();
|
||||
}
|
||||
|
||||
size_t rocksdb_pinnable_multi_get_count(
|
||||
const rocksdb_pinnable_multi_get_t* multi_get) {
|
||||
return multi_get->result_indexes.size();
|
||||
}
|
||||
|
||||
int rocksdb_pinnable_multi_get_result(
|
||||
const rocksdb_pinnable_multi_get_t* multi_get, size_t index,
|
||||
const char** value, size_t* value_size, const char** error,
|
||||
size_t* error_size) {
|
||||
*value = nullptr;
|
||||
*value_size = 0;
|
||||
*error = nullptr;
|
||||
*error_size = 0;
|
||||
return multi_get->GetResult(index, value, value_size, error, error_size);
|
||||
}
|
||||
|
||||
void rocksdb_pinnable_multi_get_destroy(
|
||||
rocksdb_pinnable_multi_get_t* multi_get) {
|
||||
delete multi_get;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_key_may_exist(rocksdb_t* db,
|
||||
const rocksdb_readoptions_t* options,
|
||||
const char* key, size_t key_len,
|
||||
@@ -2921,8 +3022,8 @@ rocksdb_iterator_t* rocksdb_create_iterator(
|
||||
rocksdb_wal_iterator_t* rocksdb_get_updates_since(
|
||||
rocksdb_t* db, uint64_t seq_number,
|
||||
const rocksdb_wal_readoptions_t* options, char** errptr) {
|
||||
std::unique_ptr<TransactionLogIterator> iter;
|
||||
TransactionLogIterator::ReadOptions ro;
|
||||
std::unique_ptr<WalIterator> iter;
|
||||
WalIterator::ReadOptions ro;
|
||||
if (options != nullptr) {
|
||||
ro = options->rep;
|
||||
}
|
||||
@@ -9107,6 +9208,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;
|
||||
@@ -10262,6 +10372,16 @@ uint64_t rocksdb_options_get_blob_file_size(rocksdb_options_t* opt) {
|
||||
return opt->rep.blob_file_size;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_blob_file_writable_file_max_buffer_size(
|
||||
rocksdb_options_t* opt, uint64_t v) {
|
||||
opt->rep.blob_file_writable_file_max_buffer_size = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_options_get_blob_file_writable_file_max_buffer_size(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.blob_file_writable_file_max_buffer_size;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_blob_compression_type(rocksdb_options_t* opt, int v) {
|
||||
opt->rep.blob_compression_type =
|
||||
static_cast<decltype(opt->rep.blob_compression_type)>(v);
|
||||
|
||||
+178
@@ -189,6 +189,57 @@ static void CheckMultiGetValues(size_t num_keys, char** values,
|
||||
}
|
||||
}
|
||||
|
||||
static void CheckPinnableMultiGetFound(
|
||||
const rocksdb_pinnable_multi_get_t* multi_get, size_t index,
|
||||
const char* expected, size_t expected_size) {
|
||||
const char* value;
|
||||
const char* error;
|
||||
size_t value_size;
|
||||
size_t error_size;
|
||||
int result = rocksdb_pinnable_multi_get_result(
|
||||
multi_get, index, &value, &value_size, &error, &error_size);
|
||||
CheckCondition(result == rocksdb_pinnable_multi_get_found);
|
||||
CheckCondition(value_size == expected_size);
|
||||
CheckCondition(error == NULL);
|
||||
CheckCondition(error_size == 0);
|
||||
if (expected_size > 0) {
|
||||
CheckCondition(value != NULL);
|
||||
CheckCondition(memcmp(value, expected, expected_size) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void CheckPinnableMultiGetNotFound(
|
||||
const rocksdb_pinnable_multi_get_t* multi_get, size_t index) {
|
||||
const char* value;
|
||||
const char* error;
|
||||
size_t value_size;
|
||||
size_t error_size;
|
||||
int result = rocksdb_pinnable_multi_get_result(
|
||||
multi_get, index, &value, &value_size, &error, &error_size);
|
||||
CheckCondition(result == rocksdb_pinnable_multi_get_not_found);
|
||||
CheckCondition(value == NULL);
|
||||
CheckCondition(value_size == 0);
|
||||
CheckCondition(error == NULL);
|
||||
CheckCondition(error_size == 0);
|
||||
}
|
||||
|
||||
static void CheckPinnableMultiGetError(
|
||||
const rocksdb_pinnable_multi_get_t* multi_get, size_t index,
|
||||
const char* expected) {
|
||||
const char* value;
|
||||
const char* error;
|
||||
size_t value_size;
|
||||
size_t error_size;
|
||||
int result = rocksdb_pinnable_multi_get_result(
|
||||
multi_get, index, &value, &value_size, &error, &error_size);
|
||||
CheckCondition(result == rocksdb_pinnable_multi_get_error);
|
||||
CheckCondition(value == NULL);
|
||||
CheckCondition(value_size == 0);
|
||||
CheckCondition(error != NULL);
|
||||
CheckCondition(error_size == strlen(error));
|
||||
CheckCondition(strstr(error, expected) != NULL);
|
||||
}
|
||||
|
||||
static void CheckIter(rocksdb_iterator_t* iter, const char* key,
|
||||
const char* val) {
|
||||
size_t len;
|
||||
@@ -834,6 +885,39 @@ static char* MergeOperatorPartialMerge(void* arg, const char* key,
|
||||
return result;
|
||||
}
|
||||
|
||||
static char* FailingMergeOperatorFullMerge(
|
||||
void* arg, const char* key, size_t key_length, const char* existing_value,
|
||||
size_t existing_value_length, const char* const* operands_list,
|
||||
const size_t* operands_list_length, int num_operands,
|
||||
unsigned char* success, size_t* new_value_length) {
|
||||
(void)arg;
|
||||
(void)key;
|
||||
(void)key_length;
|
||||
(void)existing_value;
|
||||
(void)existing_value_length;
|
||||
(void)operands_list;
|
||||
(void)operands_list_length;
|
||||
(void)num_operands;
|
||||
*success = 0;
|
||||
*new_value_length = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char* FailingMergeOperatorPartialMerge(
|
||||
void* arg, const char* key, size_t key_length,
|
||||
const char* const* operands_list, const size_t* operands_list_length,
|
||||
int num_operands, unsigned char* success, size_t* new_value_length) {
|
||||
(void)arg;
|
||||
(void)key;
|
||||
(void)key_length;
|
||||
(void)operands_list;
|
||||
(void)operands_list_length;
|
||||
(void)num_operands;
|
||||
*success = 0;
|
||||
*new_value_length = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void CheckTxnGet(rocksdb_transaction_t* txn,
|
||||
const rocksdb_readoptions_t* options, const char* key,
|
||||
const char* expected) {
|
||||
@@ -2639,12 +2723,17 @@ int main(int argc, char** argv) {
|
||||
// use dbpathname2 as the cf_path for "cf1"
|
||||
rocksdb_dbpath_t* dbpath2;
|
||||
char dbpathname2[200];
|
||||
rocksdb_mergeoperator_t* failing_merge_operator;
|
||||
snprintf(dbpathname2, sizeof(dbpathname2),
|
||||
"%s/rocksdb_c_test-%" PRIu64 "-dbpath2", GetTempDir(),
|
||||
GetTestId());
|
||||
dbpath2 = rocksdb_dbpath_create(dbpathname2, 1024 * 1024);
|
||||
const rocksdb_dbpath_t* cf_paths[1] = {dbpath2};
|
||||
rocksdb_options_set_cf_paths(cf_options_2, cf_paths, 1);
|
||||
failing_merge_operator = rocksdb_mergeoperator_create(
|
||||
NULL, MergeOperatorDestroy, FailingMergeOperatorFullMerge,
|
||||
FailingMergeOperatorPartialMerge, NULL, MergeOperatorName);
|
||||
rocksdb_options_set_merge_operator(cf_options_2, failing_merge_operator);
|
||||
|
||||
const char* cf_names[2] = {"default", "cf1"};
|
||||
const rocksdb_options_t* cf_opts[2] = {cf_options_1, cf_options_2};
|
||||
@@ -3167,6 +3256,95 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
rocksdb_slice_t pinned_keys[6] = {{"box", 3}, {"missing", 7},
|
||||
{"empty", 5}, {"merge-error", 11},
|
||||
{"box", 3}, {"owned", 5}};
|
||||
rocksdb_pinnable_multi_get_t* multi_get;
|
||||
const char* value;
|
||||
const char* error;
|
||||
size_t value_size;
|
||||
size_t error_size;
|
||||
int result;
|
||||
|
||||
rocksdb_put_cf(db, woptions, handles[1], "empty", 5, "", 0, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_put_cf(db, woptions, handles[1], "merge-error", 11, "base", 4,
|
||||
&err);
|
||||
CheckNoError(err);
|
||||
rocksdb_merge_cf(db, woptions, handles[1], "merge-error", 11, "operand",
|
||||
7, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_put_cf(db, woptions, handles[1], "owned", 5, "before", 6, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
multi_get = rocksdb_batched_multi_get_pinned_cf(db, roptions, handles[1],
|
||||
6, pinned_keys, 0);
|
||||
CheckCondition(rocksdb_pinnable_multi_get_count(multi_get) == 6);
|
||||
CheckPinnableMultiGetFound(multi_get, 0, "c", 1);
|
||||
CheckPinnableMultiGetNotFound(multi_get, 1);
|
||||
CheckPinnableMultiGetFound(multi_get, 2, "", 0);
|
||||
CheckPinnableMultiGetError(multi_get, 3, "Merge operator failed");
|
||||
CheckPinnableMultiGetFound(multi_get, 4, "c", 1);
|
||||
CheckPinnableMultiGetFound(multi_get, 5, "before", 6);
|
||||
|
||||
value = "unchanged";
|
||||
error = "unchanged";
|
||||
value_size = 9;
|
||||
error_size = 9;
|
||||
result = rocksdb_pinnable_multi_get_result(
|
||||
multi_get, 6, &value, &value_size, &error, &error_size);
|
||||
CheckCondition(result == rocksdb_pinnable_multi_get_out_of_bounds);
|
||||
CheckCondition(value == NULL);
|
||||
CheckCondition(value_size == 0);
|
||||
CheckCondition(error == NULL);
|
||||
CheckCondition(error_size == 0);
|
||||
|
||||
result = rocksdb_pinnable_multi_get_result(
|
||||
multi_get, 5, &value, &value_size, &error, &error_size);
|
||||
CheckCondition(result == rocksdb_pinnable_multi_get_found);
|
||||
const char* borrowed_value = value;
|
||||
size_t borrowed_value_size = value_size;
|
||||
result = rocksdb_pinnable_multi_get_result(
|
||||
multi_get, 3, &value, &value_size, &error, &error_size);
|
||||
CheckCondition(result == rocksdb_pinnable_multi_get_error);
|
||||
const char* borrowed_error = error;
|
||||
size_t borrowed_error_size = error_size;
|
||||
rocksdb_put_cf(db, woptions, handles[1], "owned", 5, "after", 5, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(borrowed_value_size == 6);
|
||||
CheckCondition(memcmp(borrowed_value, "before", 6) == 0);
|
||||
CheckCondition(borrowed_error_size == strlen(borrowed_error));
|
||||
CheckCondition(strstr(borrowed_error, "Merge operator failed") != NULL);
|
||||
rocksdb_pinnable_multi_get_destroy(multi_get);
|
||||
}
|
||||
|
||||
{
|
||||
rocksdb_slice_t sorted_keys[4] = {
|
||||
{"box", 3}, {"box", 3}, {"empty", 5}, {"missing", 7}};
|
||||
rocksdb_pinnable_multi_get_t* multi_get =
|
||||
rocksdb_batched_multi_get_pinned_cf(db, roptions, handles[1], 4,
|
||||
sorted_keys, 1);
|
||||
CheckCondition(rocksdb_pinnable_multi_get_count(multi_get) == 4);
|
||||
CheckPinnableMultiGetFound(multi_get, 0, "c", 1);
|
||||
CheckPinnableMultiGetFound(multi_get, 1, "c", 1);
|
||||
CheckPinnableMultiGetFound(multi_get, 2, "", 0);
|
||||
CheckPinnableMultiGetNotFound(multi_get, 3);
|
||||
rocksdb_pinnable_multi_get_destroy(multi_get);
|
||||
|
||||
multi_get = rocksdb_batched_multi_get_pinned_cf(db, roptions, handles[1],
|
||||
0, sorted_keys, 1);
|
||||
CheckCondition(rocksdb_pinnable_multi_get_count(multi_get) == 0);
|
||||
rocksdb_pinnable_multi_get_destroy(multi_get);
|
||||
}
|
||||
|
||||
rocksdb_delete_cf(db, woptions, handles[1], "empty", 5, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_delete_cf(db, woptions, handles[1], "merge-error", 11, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_delete_cf(db, woptions, handles[1], "owned", 5, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
{
|
||||
unsigned char value_found = 0;
|
||||
|
||||
|
||||
+45
-2
@@ -29,6 +29,7 @@
|
||||
#include "trace_replay/block_cache_tracer.h"
|
||||
#include "util/cast_util.h"
|
||||
#include "util/hash_containers.h"
|
||||
#include "util/mutexlock.h"
|
||||
#include "util/thread_local.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -520,6 +521,32 @@ class ColumnFamilyData {
|
||||
bool queued_for_flush() { return queued_for_flush_; }
|
||||
bool queued_for_compaction() { return queued_for_compaction_; }
|
||||
|
||||
int compaction_aborted() const {
|
||||
return compaction_aborted_.obj_.load(std::memory_order_acquire);
|
||||
}
|
||||
void inc_compaction_aborted(InstrumentedMutex* db_mutex) {
|
||||
db_mutex->AssertHeld();
|
||||
compaction_aborted_.obj_.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
int dec_compaction_aborted(InstrumentedMutex* db_mutex) {
|
||||
db_mutex->AssertHeld();
|
||||
return compaction_aborted_.obj_.fetch_sub(1, std::memory_order_acq_rel) - 1;
|
||||
}
|
||||
|
||||
int num_in_flight_compactions(InstrumentedMutex* db_mutex) const {
|
||||
db_mutex->AssertHeld();
|
||||
return num_in_flight_compactions_;
|
||||
}
|
||||
void inc_num_in_flight_compactions(InstrumentedMutex* db_mutex) {
|
||||
db_mutex->AssertHeld();
|
||||
++num_in_flight_compactions_;
|
||||
}
|
||||
void dec_num_in_flight_compactions(InstrumentedMutex* db_mutex) {
|
||||
db_mutex->AssertHeld();
|
||||
assert(num_in_flight_compactions_ > 0);
|
||||
--num_in_flight_compactions_;
|
||||
}
|
||||
|
||||
static std::pair<WriteStallCondition, WriteStallCause>
|
||||
GetWriteStallConditionAndCause(
|
||||
int num_unflushed_memtables, int num_l0_files,
|
||||
@@ -711,10 +738,26 @@ class ColumnFamilyData {
|
||||
// If true --> this ColumnFamily is currently present in DBImpl::flush_queue_
|
||||
bool queued_for_flush_;
|
||||
|
||||
// If true --> this ColumnFamily is currently present in
|
||||
// DBImpl::compaction_queue_
|
||||
// If true, this ColumnFamily is in DBImpl::compaction_queue_ or its parked
|
||||
// compaction set.
|
||||
bool queued_for_compaction_;
|
||||
|
||||
// Read outside the DB mutex by compaction workers (acquire-loaded roughly
|
||||
// every 1024 records in ProcessKeyValueCompaction). Given its own cache line
|
||||
// via CacheAlignedWrapper to avoid false sharing with the DB-mutex-written
|
||||
// fields packed around it.
|
||||
CacheAlignedWrapper<std::atomic<int>> compaction_aborted_{};
|
||||
|
||||
// Per-CF count of compactions "in flight" for AbortCompactions(): a picked
|
||||
// Compaction (or one CompactFiles call), tracked from
|
||||
// BeginInFlightCompaction() until EndInFlightCompaction() at the end of
|
||||
// BackgroundCompaction, so it covers the NotifyOnCompactionCompleted window
|
||||
// that CompactionPicker's compactions_in_progress_ set does not. NOT the same
|
||||
// as DBImpl::num_running_compactions_ (which counts BackgroundCallCompaction
|
||||
// worker invocations, backs the public rocksdb.num-running-compactions
|
||||
// property, and excludes CompactFiles). Protected by the DB mutex.
|
||||
int num_in_flight_compactions_{0};
|
||||
|
||||
uint64_t prev_compaction_needed_bytes_;
|
||||
|
||||
// if the database was opened with 2pc enabled
|
||||
|
||||
@@ -152,7 +152,8 @@ CompactionJob::CompactionJob(
|
||||
const std::atomic<int>& compaction_aborted, const std::string& db_id,
|
||||
const std::string& db_session_id, std::string full_history_ts_low,
|
||||
std::string trim_ts, BlobFileCompletionCallback* blob_callback,
|
||||
int* bg_compaction_scheduled, int* bg_bottom_compaction_scheduled)
|
||||
int* bg_compaction_scheduled, int* bg_bottom_compaction_scheduled,
|
||||
std::atomic<int>* num_running_remote_compactions)
|
||||
: compact_(new CompactionState(compaction)),
|
||||
internal_stats_(compaction->compaction_reason(), 1),
|
||||
db_options_(db_options),
|
||||
@@ -198,7 +199,8 @@ CompactionJob::CompactionJob(
|
||||
blob_callback_(blob_callback),
|
||||
extra_num_subcompaction_threads_reserved_(0),
|
||||
bg_compaction_scheduled_(bg_compaction_scheduled),
|
||||
bg_bottom_compaction_scheduled_(bg_bottom_compaction_scheduled) {
|
||||
bg_bottom_compaction_scheduled_(bg_bottom_compaction_scheduled),
|
||||
num_running_remote_compactions_(num_running_remote_compactions) {
|
||||
assert(job_stats_ != nullptr);
|
||||
assert(log_buffer_ != nullptr);
|
||||
assert(job_context);
|
||||
@@ -1668,8 +1670,12 @@ Status CompactionJob::ProcessKeyValue(
|
||||
const uint64_t kCronEveryMask = (1 << 10) - 1;
|
||||
[[maybe_unused]] const std::optional<const Slice> end = sub_compact->end;
|
||||
|
||||
// Check for abort signal before starting key processing
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
// Check for abort signal before starting key processing. The DB-wide
|
||||
// (AbortAllCompactions) and per-CF (AbortCompactions) counters compose here;
|
||||
// either aborts this job. See DB::AbortCompactions() in db.h for how the
|
||||
// abort knobs relate to each other and to the other compaction controls.
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0 ||
|
||||
cfd->compaction_aborted() > 0) {
|
||||
return Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
}
|
||||
|
||||
@@ -1691,7 +1697,8 @@ Status CompactionJob::ProcessKeyValue(
|
||||
// Periodic cron operations: stats update, abort check.
|
||||
if ((num_records & kCronEveryMask) == kCronEveryMask) {
|
||||
// Check for abort signal periodically
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0 ||
|
||||
cfd->compaction_aborted() > 0) {
|
||||
status = Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
break;
|
||||
}
|
||||
@@ -2613,7 +2620,8 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
bottommost_level_, TableFileCreationReason::kCompaction,
|
||||
0 /* oldest_key_time */, current_time, db_id_, db_session_id_,
|
||||
sub_compact->compaction->max_output_file_size(), file_number,
|
||||
proximal_after_seqno_ /*last_level_inclusive_max_seqno_threshold*/);
|
||||
proximal_after_seqno_ /*last_level_inclusive_max_seqno_threshold*/,
|
||||
dbname_ /*db_name*/, IsRemoteCompaction() /*is_remote_compaction*/);
|
||||
|
||||
outputs.NewBuilder(tboptions);
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/memtablerep.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
#include "util/autovector.h"
|
||||
#include "util/stop_watch.h"
|
||||
#include "util/thread_local.h"
|
||||
@@ -166,7 +166,8 @@ class CompactionJob {
|
||||
std::string full_history_ts_low = "", std::string trim_ts = "",
|
||||
BlobFileCompletionCallback* blob_callback = nullptr,
|
||||
int* bg_compaction_scheduled = nullptr,
|
||||
int* bg_bottom_compaction_scheduled = nullptr);
|
||||
int* bg_bottom_compaction_scheduled = nullptr,
|
||||
std::atomic<int>* num_running_remote_compactions = nullptr);
|
||||
|
||||
virtual ~CompactionJob();
|
||||
|
||||
@@ -500,6 +501,12 @@ class CompactionJob {
|
||||
int* bg_compaction_scheduled_;
|
||||
int* bg_bottom_compaction_scheduled_;
|
||||
|
||||
// Stores the pointer to DBImpl::num_running_remote_compactions_, backing the
|
||||
// public rocksdb.num-running-remote-compactions property. Null when the job
|
||||
// has no owning DBImpl to report to (compaction service workers, unit tests).
|
||||
// Updated without the DB mutex held.
|
||||
std::atomic<int>* num_running_remote_compactions_;
|
||||
|
||||
// Stores the sequence number to time mapping gathered from all input files
|
||||
// it also collects the smallest_seqno -> oldest_ancester_time from the SST.
|
||||
SeqnoToTimeMapping seqno_to_time_mapping_;
|
||||
@@ -523,6 +530,10 @@ class CompactionJob {
|
||||
// Get table file name in where it's outputting to, which should also be in
|
||||
// `output_directory_`.
|
||||
virtual std::string GetTableFileName(uint64_t file_number);
|
||||
|
||||
// True if this job builds output files as a remote (offloaded)
|
||||
// CompactionService worker. Overridden by CompactionServiceCompactionJob.
|
||||
virtual bool IsRemoteCompaction() const { return false; }
|
||||
// The rate limiter priority (io_priority) is determined dynamically here.
|
||||
// The Compaction Read and Write priorities are the same for different
|
||||
// scenarios, such as write stalled.
|
||||
@@ -730,6 +741,7 @@ class CompactionServiceCompactionJob : private CompactionJob {
|
||||
private:
|
||||
// Get table file name in output_path
|
||||
std::string GetTableFileName(uint64_t file_number) override;
|
||||
bool IsRemoteCompaction() const override { return true; }
|
||||
// Specific the compaction output path, otherwise it uses default DB path
|
||||
const std::string output_path_;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "monitoring/thread_status_util.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "rocksdb/utilities/options_type.h"
|
||||
#include "util/defer.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
class SubcompactionState;
|
||||
@@ -114,9 +115,6 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
|
||||
break;
|
||||
}
|
||||
|
||||
std::string debug_str_before_wait =
|
||||
compaction->input_version()->DebugString(/*hex=*/true);
|
||||
|
||||
// TODO: Update CompactionService API to support abort and resume
|
||||
// functionality. Currently, remote compaction jobs cannot be aborted via
|
||||
// AbortAllCompactions() because the CompactionService interface lacks methods
|
||||
@@ -129,18 +127,29 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
|
||||
"[%s] [JOB %d] Waiting for remote compaction...",
|
||||
compaction->column_family_data()->GetName().c_str(), job_id_);
|
||||
std::string compaction_result_binary;
|
||||
CompactionServiceJobStatus compaction_status =
|
||||
db_options_.compaction_service->Wait(response.scheduled_job_id,
|
||||
&compaction_result_binary);
|
||||
CompactionServiceJobStatus compaction_status;
|
||||
{
|
||||
// Increment rocksdb.num-running-remote-compactions while this compaction
|
||||
// service job is waiting in CompactionService::Wait().
|
||||
std::atomic<int>* const num_running = num_running_remote_compactions_;
|
||||
if (num_running != nullptr) {
|
||||
num_running->fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
Defer decrement_num_running([num_running]() {
|
||||
if (num_running != nullptr) {
|
||||
num_running->fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
});
|
||||
compaction_status = db_options_.compaction_service->Wait(
|
||||
response.scheduled_job_id, &compaction_result_binary);
|
||||
}
|
||||
|
||||
if (compaction_status != CompactionServiceJobStatus::kSuccess) {
|
||||
ROCKS_LOG_ERROR(
|
||||
db_options_.info_log,
|
||||
"[%s] [JOB %d] Wait() status is not kSuccess. "
|
||||
"\nDebugString Before Wait():\n%s"
|
||||
"\nDebugString After Wait():\n%s",
|
||||
compaction->column_family_data()->GetName().c_str(), job_id_,
|
||||
debug_str_before_wait.c_str(),
|
||||
compaction->input_version()->DebugString(/*hex=*/true).c_str());
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
// 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 "db/db_test_util.h"
|
||||
#include "file/file_util.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/advanced_compression.h"
|
||||
#include "rocksdb/filter_policy.h"
|
||||
#include "rocksdb/utilities/object_registry.h"
|
||||
#include "rocksdb/utilities/options_util.h"
|
||||
#include "table/unique_id_impl.h"
|
||||
#include "utilities/merge_operators/string_append/stringappend.h"
|
||||
@@ -360,6 +364,7 @@ class CompactionServiceTest : public DBTestBase {
|
||||
|
||||
TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
Options options = CurrentOptions();
|
||||
options.max_open_files = -1;
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
Statistics* primary_statistics = GetPrimaryStatistics();
|
||||
@@ -480,6 +485,113 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// A CompressionManager that records, per GetCompressorForSST() call, whether
|
||||
// the FilterBuildingContext reports the SST as being built by a remote
|
||||
// (offloaded) compaction worker. Observations go into process-global counters
|
||||
// because the remote worker reconstructs its own manager instance from the
|
||||
// persisted OPTIONS file (same process in this test).
|
||||
struct RemoteAwareObservations {
|
||||
std::atomic<int> num_remote{0};
|
||||
std::atomic<int> num_non_remote{0};
|
||||
void Reset() {
|
||||
num_remote.store(0);
|
||||
num_non_remote.store(0);
|
||||
}
|
||||
};
|
||||
|
||||
RemoteAwareObservations& GetRemoteAwareObservations() {
|
||||
static RemoteAwareObservations obs;
|
||||
return obs;
|
||||
}
|
||||
|
||||
class RemoteAwareCompressionManager : public CompressionManagerWrapper {
|
||||
public:
|
||||
RemoteAwareCompressionManager()
|
||||
: CompressionManagerWrapper(GetBuiltinV2CompressionManager()) {}
|
||||
|
||||
static const char* kClassName() { return "RemoteAwareCompressionManager"; }
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
std::unique_ptr<Compressor> GetCompressorForSST(
|
||||
const FilterBuildingContext& context, const CompressionOptions& opts,
|
||||
CompressionType preferred) override {
|
||||
if (context.is_remote_compaction) {
|
||||
GetRemoteAwareObservations().num_remote.fetch_add(1);
|
||||
} else {
|
||||
GetRemoteAwareObservations().num_non_remote.fetch_add(1);
|
||||
}
|
||||
return CompressionManagerWrapper::GetCompressorForSST(context, opts,
|
||||
preferred);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(CompactionServiceTest, IsRemoteCompactionInCompressorContext) {
|
||||
// Register so the remote worker's DB::OpenAndCompact can reconstruct the
|
||||
// custom CompressionManager from the persisted OPTIONS file
|
||||
// (CompactionServiceOptionsOverride does not carry a compression_manager).
|
||||
ObjectLibrary::Default()->AddFactory<CompressionManager>(
|
||||
RemoteAwareCompressionManager::kClassName(),
|
||||
[](const std::string& /*uri*/, std::unique_ptr<CompressionManager>* guard,
|
||||
std::string* /*errmsg*/) {
|
||||
*guard = std::make_unique<RemoteAwareCompressionManager>();
|
||||
return guard->get();
|
||||
});
|
||||
|
||||
auto& obs = GetRemoteAwareObservations();
|
||||
obs.Reset();
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.compression_manager =
|
||||
std::make_shared<RemoteAwareCompressionManager>();
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
GenerateTestData();
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
VerifyTestData();
|
||||
|
||||
auto my_cs = GetCompactionService();
|
||||
ASSERT_GE(my_cs->GetCompactionNum(), 1);
|
||||
|
||||
// The offloaded worker built the compaction output SSTs and therefore must
|
||||
// have observed is_remote_compaction == true in GetCompressorForSST().
|
||||
ASSERT_GT(obs.num_remote.load(), 0);
|
||||
// The primary's own flushes, seen by the same manager, must report false
|
||||
ASSERT_GT(obs.num_non_remote.load(), 0);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, IsRemoteCompactionFalseForLocalCompaction) {
|
||||
// Same custom manager, but no compaction_service: everything runs locally,
|
||||
// so GetCompressorForSST() must never see is_remote_compaction == true.
|
||||
auto& obs = GetRemoteAwareObservations();
|
||||
obs.Reset();
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.compression_manager =
|
||||
std::make_shared<RemoteAwareCompressionManager>();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
Random rnd(301);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 100; j++) {
|
||||
ASSERT_OK(Put(Key(i * 100 + j), rnd.RandomString(64)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
// Local flushes and compactions must all report not-remote.
|
||||
ASSERT_GT(obs.num_non_remote.load(), 0);
|
||||
ASSERT_EQ(obs.num_remote.load(), 0);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, SkipWALRecoveryInOpenAndCompact) {
|
||||
// Verify that OpenAndCompact skips WAL recovery when opening the secondary
|
||||
// instance. WAL replay is unnecessary for remote compaction since it only
|
||||
@@ -1761,6 +1873,109 @@ TEST_F(CompactionServiceTest, SubCompaction) {
|
||||
ASSERT_GE(compaction_num, 2);
|
||||
}
|
||||
|
||||
class PropertySamplingCompactionService : public MyTestCompactionService {
|
||||
public:
|
||||
using MyTestCompactionService::MyTestCompactionService;
|
||||
|
||||
void SetPrimaryDB(DB* db) { db_ = db; }
|
||||
|
||||
CompactionServiceJobStatus Wait(const std::string& scheduled_job_id,
|
||||
std::string* result) override {
|
||||
EXPECT_TRUE(
|
||||
db_->GetIntProperty(DB::Properties::kNumRunningRemoteCompactions,
|
||||
&num_running_while_waiting_));
|
||||
return MyTestCompactionService::Wait(scheduled_job_id, result);
|
||||
}
|
||||
|
||||
uint64_t GetNumRunningWhileWaiting() const {
|
||||
return num_running_while_waiting_;
|
||||
}
|
||||
|
||||
private:
|
||||
DB* db_ = nullptr;
|
||||
uint64_t num_running_while_waiting_ = 0;
|
||||
};
|
||||
|
||||
TEST_F(CompactionServiceTest, NumRunningRemoteCompactionsProperty) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.env = env_;
|
||||
auto statistics = CreateDBStatistics();
|
||||
auto compaction_service = std::make_shared<PropertySamplingCompactionService>(
|
||||
dbname_, options, statistics,
|
||||
std::vector<std::shared_ptr<EventListener>>{},
|
||||
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>{});
|
||||
options.compaction_service = compaction_service;
|
||||
DestroyAndReopen(options);
|
||||
compaction_service->SetPrimaryDB(db_.get());
|
||||
|
||||
// Overlapping key ranges across files so the compaction is not a trivial
|
||||
// move, which would bypass the compaction service entirely.
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 10; j++) {
|
||||
ASSERT_OK(Put(Key(j), "value" + std::to_string(i * 10 + j)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
// No remote compaction is in flight yet.
|
||||
uint64_t num_running = 0;
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningRemoteCompactions,
|
||||
&num_running));
|
||||
ASSERT_EQ(num_running, 0U);
|
||||
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_GE(compaction_service->GetCompactionNum(), 1);
|
||||
ASSERT_GE(compaction_service->GetNumRunningWhileWaiting(), 1U);
|
||||
|
||||
// The counter is back to 0 once the remote jobs are installed.
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningRemoteCompactions,
|
||||
&num_running));
|
||||
ASSERT_EQ(num_running, 0U);
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, NumRunningRemoteCompactionsPropertyLocalOnly) {
|
||||
// Without a compaction_service configured, the property stays 0.
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Overlapping key ranges across files so the compaction is not a trivial
|
||||
// move, which would bypass the compaction service entirely.
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 10; j++) {
|
||||
ASSERT_OK(Put(Key(j), "value" + std::to_string(i * 10 + j)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
std::atomic<int> property_samples{0};
|
||||
std::atomic_bool observed_non_zero{false};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::ProcessKeyValueCompaction()::Processing",
|
||||
[&](void* /*arg*/) {
|
||||
uint64_t num_running = 0;
|
||||
EXPECT_TRUE(db_->GetIntProperty(
|
||||
DB::Properties::kNumRunningRemoteCompactions, &num_running));
|
||||
property_samples.fetch_add(1);
|
||||
if (num_running != 0) {
|
||||
observed_non_zero.store(true);
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_GT(property_samples.load(), 0);
|
||||
ASSERT_FALSE(observed_non_zero.load());
|
||||
|
||||
uint64_t num_running = 0;
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningRemoteCompactions,
|
||||
&num_running));
|
||||
ASSERT_EQ(num_running, 0U);
|
||||
}
|
||||
|
||||
class PartialDeleteCompactionFilter : public CompactionFilter {
|
||||
public:
|
||||
CompactionFilter::Decision FilterV2(
|
||||
@@ -1779,6 +1994,7 @@ class PartialDeleteCompactionFilter : public CompactionFilter {
|
||||
|
||||
TEST_F(CompactionServiceTest, CompactionFilter) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
std::unique_ptr<CompactionFilter> delete_comp_filter(
|
||||
new PartialDeleteCompactionFilter());
|
||||
options.compaction_filter = delete_comp_filter.get();
|
||||
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
// 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 "rocksdb/rocksdb_namespace.h"
|
||||
|
||||
#if USE_COROUTINES
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
|
||||
#include "folly/Executor.h"
|
||||
#include "folly/coro/Nothrow.h"
|
||||
#include "folly/executors/IOExecutor.h"
|
||||
#include "folly/io/async/EventBase.h"
|
||||
#include "rocksdb/coro_db.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "table/multiget_context.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp) {
|
||||
assert(db != nullptr);
|
||||
CoroDB* coro_db = db->GetCoroDB();
|
||||
auto* file_system = db->GetFileSystem();
|
||||
auto* read_executor =
|
||||
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
|
||||
Status status;
|
||||
if (coro_db == nullptr || read_event_base == nullptr) {
|
||||
status = db->Get(options, column_family, key, value, timestamp);
|
||||
} else {
|
||||
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base),
|
||||
coro_db->GetCoroutine(options, column_family, key, value, timestamp)));
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, std::string* value,
|
||||
std::string* timestamp) {
|
||||
assert(value != nullptr);
|
||||
PinnableSlice pinnable_value(value);
|
||||
assert(!pinnable_value.IsPinned());
|
||||
Status status = co_await folly::coro::co_nothrow(
|
||||
CoGet(db, options, column_family, key, &pinnable_value, timestamp));
|
||||
if (status.ok() && pinnable_value.IsPinned()) {
|
||||
value->assign(pinnable_value.data(), pinnable_value.size());
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
|
||||
const Slice& key, std::string* value) {
|
||||
assert(db != nullptr);
|
||||
return CoGet(db, options, db->DefaultColumnFamily(), key, value);
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
|
||||
const Slice& key, std::string* value,
|
||||
std::string* timestamp) {
|
||||
assert(db != nullptr);
|
||||
return CoGet(db, options, db->DefaultColumnFamily(), key, value, timestamp);
|
||||
}
|
||||
|
||||
folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
|
||||
DB* db, const ReadOptions& options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
const std::vector<Slice>& keys, std::vector<std::string>* values,
|
||||
std::vector<std::string>* timestamps) {
|
||||
const size_t num_keys = keys.size();
|
||||
std::vector<Status> statuses(num_keys);
|
||||
std::vector<PinnableSlice> pinnable_values(num_keys);
|
||||
|
||||
values->resize(num_keys);
|
||||
if (timestamps != nullptr) {
|
||||
timestamps->resize(num_keys);
|
||||
}
|
||||
co_await folly::coro::co_nothrow(CoMultiGet(
|
||||
db, options, num_keys,
|
||||
const_cast<ColumnFamilyHandle**>(column_families.data()), keys.data(),
|
||||
pinnable_values.data(),
|
||||
timestamps == nullptr ? nullptr : timestamps->data(), statuses.data(),
|
||||
/*sorted_input=*/false));
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
if (statuses[i].ok()) {
|
||||
(*values)[i].assign(pinnable_values[i].data(), pinnable_values[i].size());
|
||||
}
|
||||
}
|
||||
co_return statuses;
|
||||
}
|
||||
|
||||
folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
|
||||
DB* db, const ReadOptions& options, const std::vector<Slice>& keys,
|
||||
std::vector<std::string>* values, std::vector<std::string>* timestamps) {
|
||||
assert(db != nullptr);
|
||||
std::vector<ColumnFamilyHandle*> column_families(keys.size(),
|
||||
db->DefaultColumnFamily());
|
||||
co_return co_await folly::coro::co_nothrow(
|
||||
CoMultiGet(db, options, column_families, keys, values, timestamps));
|
||||
}
|
||||
|
||||
folly::coro::Task<void> CoroDB::CoMultiGet(
|
||||
DB* db, const ReadOptions& options, size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps, Status* statuses,
|
||||
bool sorted_input) {
|
||||
assert(db != nullptr);
|
||||
CoroDB* coro_db = db->GetCoroDB();
|
||||
auto* file_system = db->GetFileSystem();
|
||||
auto* read_executor =
|
||||
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
|
||||
if (coro_db == nullptr || read_event_base == nullptr) {
|
||||
db->MultiGet(options, num_keys, column_families, keys, values, timestamps,
|
||||
statuses, sorted_input);
|
||||
} else {
|
||||
co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base),
|
||||
coro_db->MultiGetCoroutine(options, num_keys, column_families, keys,
|
||||
values, timestamps, statuses,
|
||||
sorted_input)));
|
||||
}
|
||||
}
|
||||
|
||||
folly::coro::Task<void> CoroDB::CoMultiGet(
|
||||
DB* db, const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
size_t num_keys, const Slice* keys, PinnableSlice* values,
|
||||
std::string* timestamps, Status* statuses, bool sorted_input) {
|
||||
if (num_keys > MultiGetContext::MAX_BATCH_SIZE) {
|
||||
std::vector<ColumnFamilyHandle*> column_families(num_keys, column_family);
|
||||
co_await folly::coro::co_nothrow(
|
||||
CoMultiGet(db, options, num_keys, column_families.data(), keys, values,
|
||||
timestamps, statuses, sorted_input));
|
||||
} else {
|
||||
std::array<ColumnFamilyHandle*, MultiGetContext::MAX_BATCH_SIZE>
|
||||
column_families;
|
||||
std::fill(column_families.begin(), column_families.begin() + num_keys,
|
||||
column_family);
|
||||
co_await folly::coro::co_nothrow(
|
||||
CoMultiGet(db, options, num_keys, column_families.data(), keys, values,
|
||||
timestamps, statuses, sorted_input));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
#endif // USE_COROUTINES
|
||||
+6
-3
@@ -4020,7 +4020,8 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL0) {
|
||||
AssertMultiGetIOBatchSize(3, 1);
|
||||
}
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 3 : 0);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
@@ -4047,7 +4048,8 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1) {
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 6 : 3);
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 6 : 0);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
@@ -4200,7 +4202,8 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL2WithRangeOverlapL0L1) {
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 3 : 2);
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 3 : 0);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
|
||||
@@ -1800,6 +1800,7 @@ static std::map<TableFileCreationReason, std::string>
|
||||
{TableFileCreationReason::kFlush, "kFlush"},
|
||||
{TableFileCreationReason::kMisc, "kMisc"},
|
||||
{TableFileCreationReason::kRecovery, "kRecovery"},
|
||||
{TableFileCreationReason::kSstFileWriter, "kSstFileWriter"},
|
||||
};
|
||||
|
||||
class TestingContextCustomFilterPolicy
|
||||
@@ -1814,6 +1815,15 @@ class TestingContextCustomFilterPolicy
|
||||
const FilterBuildingContext& context) const override {
|
||||
test_report_ += "cf=";
|
||||
test_report_ += context.column_family_name;
|
||||
// Token rather than path, so expectations don't depend on test directory
|
||||
test_report_ += ",db=";
|
||||
if (context.db_name.empty()) {
|
||||
test_report_ += "none";
|
||||
} else if (context.db_name == Slice(expected_db_name_)) {
|
||||
test_report_ += "expected";
|
||||
} else {
|
||||
test_report_ += "UNEXPECTED:" + context.db_name.ToString();
|
||||
}
|
||||
test_report_ += ",s=";
|
||||
test_report_ +=
|
||||
OptionsHelper::compaction_style_to_string[context.compaction_style];
|
||||
@@ -1836,8 +1846,13 @@ class TestingContextCustomFilterPolicy
|
||||
return rv;
|
||||
}
|
||||
|
||||
void SetExpectedDbName(std::string db_name) {
|
||||
expected_db_name_ = std::move(db_name);
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::string test_report_;
|
||||
std::string expected_db_name_;
|
||||
};
|
||||
} // anonymous namespace
|
||||
|
||||
@@ -1859,6 +1874,13 @@ TEST_F(DBBloomFilterTest, ContextCustomFilterPolicy) {
|
||||
|
||||
ASSERT_OK(TryReopen(options));
|
||||
CreateAndReopenWithCF({fifo ? "abe" : "bob"}, options);
|
||||
policy->SetExpectedDbName(dbname_);
|
||||
|
||||
const char* expected_flush_report =
|
||||
fifo
|
||||
? "cf=abe,db=expected,s=kCompactionStyleFIFO,n=7,l=0,b=0,r=kFlush\n"
|
||||
: "cf=bob,db=expected,s=kCompactionStyleLevel,n=7,l=0,b=0,r="
|
||||
"kFlush\n";
|
||||
|
||||
const int maxKey = 10000;
|
||||
for (int i = 0; i < maxKey / 2; i++) {
|
||||
@@ -1867,17 +1889,13 @@ TEST_F(DBBloomFilterTest, ContextCustomFilterPolicy) {
|
||||
// Add a large key to make the file contain wide range
|
||||
ASSERT_OK(Put(1, Key(maxKey + 55555), Key(maxKey + 55555)));
|
||||
ASSERT_OK(Flush(1));
|
||||
EXPECT_EQ(policy->DumpTestReport(),
|
||||
fifo ? "cf=abe,s=kCompactionStyleFIFO,n=7,l=0,b=0,r=kFlush\n"
|
||||
: "cf=bob,s=kCompactionStyleLevel,n=7,l=0,b=0,r=kFlush\n");
|
||||
EXPECT_EQ(policy->DumpTestReport(), expected_flush_report);
|
||||
|
||||
for (int i = maxKey / 2; i < maxKey; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), Key(i)));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
EXPECT_EQ(policy->DumpTestReport(),
|
||||
fifo ? "cf=abe,s=kCompactionStyleFIFO,n=7,l=0,b=0,r=kFlush\n"
|
||||
: "cf=bob,s=kCompactionStyleLevel,n=7,l=0,b=0,r=kFlush\n");
|
||||
EXPECT_EQ(policy->DumpTestReport(), expected_flush_report);
|
||||
|
||||
// Check that they can be found
|
||||
for (int i = 0; i < maxKey; i++) {
|
||||
@@ -1904,7 +1922,8 @@ TEST_F(DBBloomFilterTest, ContextCustomFilterPolicy) {
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), handles_[1], nullptr,
|
||||
nullptr));
|
||||
EXPECT_EQ(policy->DumpTestReport(),
|
||||
"cf=bob,s=kCompactionStyleLevel,n=7,l=1,b=1,r=kCompaction\n");
|
||||
"cf=bob,db=expected,s=kCompactionStyleLevel,n=7,l=1,b=1,r="
|
||||
"kCompaction\n");
|
||||
|
||||
// Check that we now have one filter, about 9.2% FP rate (5 bits per key)
|
||||
for (int i = 0; i < maxKey; i++) {
|
||||
@@ -1926,7 +1945,8 @@ TEST_F(DBBloomFilterTest, ContextCustomFilterPolicy) {
|
||||
}
|
||||
// Note: kCompactionStyleLevel is default, ignored if num_levels == -1
|
||||
EXPECT_EQ(policy->DumpTestReport(),
|
||||
"cf=abe,s=kCompactionStyleLevel,n=-1,l=-1,b=0,r=kMisc\n");
|
||||
"cf=abe,db=none,s=kCompactionStyleLevel,n=-1,l=-1,b=0,r="
|
||||
"kSstFileWriter\n");
|
||||
}
|
||||
|
||||
// Destroy
|
||||
@@ -3978,7 +3998,8 @@ static std::vector<std::string> RangeQueryKeys(
|
||||
ReadOptions ro;
|
||||
ro.iterate_lower_bound = &lb;
|
||||
ro.iterate_upper_bound = &ub;
|
||||
ro.table_filter = factory.GetTableFilterForRangeQuery(lb, ub);
|
||||
auto filter = factory.GetTableFilterForRangeQuery(lb, ub);
|
||||
ro.table_filter = &filter;
|
||||
auto it = db.NewIterator(ro);
|
||||
std::vector<std::string> ret;
|
||||
for (it->Seek(lb); it->Valid(); it->Next()) {
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
@@ -14,6 +17,7 @@
|
||||
#include "options/options_helper.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/listener.h"
|
||||
#include "rocksdb/sst_file_writer.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testharness.h"
|
||||
@@ -91,6 +95,19 @@ class AbortSynchronizer {
|
||||
}
|
||||
}
|
||||
|
||||
void TriggerAbort(DBImpl* db, ColumnFamilyHandle* column_family) {
|
||||
if (abort_triggered_.load() && abort_completed_.load()) {
|
||||
Reset();
|
||||
}
|
||||
|
||||
if (!abort_triggered_.exchange(true)) {
|
||||
abort_thread_ = std::thread([this, db, column_family]() {
|
||||
db->AbortCompactions(column_family);
|
||||
SignalAbortCompleted();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the abort thread to complete.
|
||||
// Call this AFTER compaction returns to ensure the abort thread has finished
|
||||
// before proceeding with Resume or other operations.
|
||||
@@ -459,6 +476,483 @@ TEST_F(DBCompactionAbortTest, AbortScheduledAutomaticCompactionBeforePick) {
|
||||
VerifyDataIntegrity(/*num_keys=*/100);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortCompactionsIsScopedToColumnFamily) {
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 100;
|
||||
options.max_background_compactions = 2;
|
||||
options.max_subcompactions = 1;
|
||||
options.disable_auto_compactions = true;
|
||||
CreateAndReopenWithCF({"target", "sibling"}, options);
|
||||
|
||||
Random rnd(301);
|
||||
for (int cf = 1; cf <= 2; ++cf) {
|
||||
for (int file = 0; file < 4; ++file) {
|
||||
for (int key = 0; key < 100; ++key) {
|
||||
ASSERT_OK(Put(cf, Key(key), rnd.RandomString(1000)));
|
||||
}
|
||||
ASSERT_OK(Flush(cf));
|
||||
}
|
||||
}
|
||||
|
||||
AbortSynchronizer abort_sync;
|
||||
std::atomic<int> compactions_started{0};
|
||||
std::mutex sibling_mutex;
|
||||
std::condition_variable sibling_cv;
|
||||
bool sibling_started = false;
|
||||
bool allow_sibling_to_finish = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImpl::AbortCompactions:FlagSet",
|
||||
"DBCompactionAbortTest::ScopedAbort:WaitForAbort"},
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::ProcessKeyValueCompaction:Start", [&](void* /*arg*/) {
|
||||
if (compactions_started.fetch_add(1) == 0) {
|
||||
std::unique_lock<std::mutex> lock(sibling_mutex);
|
||||
sibling_started = true;
|
||||
sibling_cv.notify_all();
|
||||
sibling_cv.wait(lock, [&]() { return allow_sibling_to_finish; });
|
||||
return;
|
||||
}
|
||||
abort_sync.TriggerAbort(dbfull(), handles_[1]);
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"DBCompactionAbortTest::ScopedAbort:WaitForAbort", nullptr);
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status sibling_status;
|
||||
std::thread sibling_thread([&]() {
|
||||
sibling_status = dbfull()->CompactRange(CompactRangeOptions(), handles_[2],
|
||||
nullptr, nullptr);
|
||||
});
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(sibling_mutex);
|
||||
sibling_cv.wait(lock, [&]() { return sibling_started; });
|
||||
}
|
||||
|
||||
Status target_status = dbfull()->CompactRange(CompactRangeOptions(),
|
||||
handles_[1], nullptr, nullptr);
|
||||
abort_sync.WaitForAbortCompletion();
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(sibling_mutex);
|
||||
allow_sibling_to_finish = true;
|
||||
}
|
||||
sibling_cv.notify_all();
|
||||
sibling_thread.join();
|
||||
CleanupSyncPoints();
|
||||
|
||||
ASSERT_TRUE(target_status.IsIncomplete());
|
||||
ASSERT_TRUE(target_status.IsCompactionAborted());
|
||||
ASSERT_OK(sibling_status);
|
||||
target_status = dbfull()->CompactRange(CompactRangeOptions(), handles_[1],
|
||||
nullptr, nullptr);
|
||||
ASSERT_TRUE(target_status.IsIncomplete());
|
||||
ASSERT_TRUE(target_status.IsCompactionAborted());
|
||||
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), handles_[1], nullptr,
|
||||
nullptr));
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, PerColumnFamilyAndGlobalAbortCountersCompose) {
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 100;
|
||||
options.disable_auto_compactions = true;
|
||||
CreateAndReopenWithCF({"target", "sibling"}, options);
|
||||
|
||||
for (int cf = 1; cf <= 2; ++cf) {
|
||||
for (int file = 0; file < 2; ++file) {
|
||||
ASSERT_OK(Put(cf, Key(file), "value"));
|
||||
ASSERT_OK(Flush(cf));
|
||||
}
|
||||
}
|
||||
|
||||
dbfull()->AbortAllCompactions();
|
||||
dbfull()->AbortCompactions(handles_[1]);
|
||||
dbfull()->AbortCompactions(handles_[1]);
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
|
||||
Status sibling_status = dbfull()->CompactRange(CompactRangeOptions(),
|
||||
handles_[2], nullptr, nullptr);
|
||||
ASSERT_TRUE(sibling_status.IsIncomplete());
|
||||
ASSERT_TRUE(sibling_status.IsCompactionAborted());
|
||||
|
||||
dbfull()->ResumeAllCompactions();
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), handles_[2], nullptr,
|
||||
nullptr));
|
||||
Status target_status = dbfull()->CompactRange(CompactRangeOptions(),
|
||||
handles_[1], nullptr, nullptr);
|
||||
ASSERT_TRUE(target_status.IsIncomplete());
|
||||
ASSERT_TRUE(target_status.IsCompactionAborted());
|
||||
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), handles_[1], nullptr,
|
||||
nullptr));
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortCompactionsRejectsCompactFilesTrivialMove) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
CreateAndReopenWithCF({"target"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "value"));
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
ColumnFamilyMetaData metadata;
|
||||
dbfull()->GetColumnFamilyMetaData(handles_[1], &metadata);
|
||||
ASSERT_EQ(metadata.levels[0].files.size(), 1);
|
||||
const std::vector<std::string> input_files = {
|
||||
metadata.levels[0].files.front().name};
|
||||
|
||||
CompactionOptions compaction_options;
|
||||
compaction_options.allow_trivial_move = true;
|
||||
dbfull()->AbortCompactions(handles_[1]);
|
||||
Status status =
|
||||
dbfull()->CompactFiles(compaction_options, handles_[1], input_files, 1);
|
||||
ASSERT_TRUE(status.IsIncomplete());
|
||||
ASSERT_TRUE(status.IsCompactionAborted());
|
||||
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
ASSERT_OK(
|
||||
dbfull()->CompactFiles(compaction_options, handles_[1], input_files, 1));
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, ResumeRestoresParkedAutomaticCompaction) {
|
||||
constexpr int kCompactionTrigger = 4;
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
||||
options.max_background_compactions = 1;
|
||||
options.disable_auto_compactions = false;
|
||||
CreateAndReopenWithCF({"target", "sibling"}, options);
|
||||
|
||||
AbortSynchronizer abort_sync;
|
||||
std::mutex parked_mutex;
|
||||
std::condition_variable parked_cv;
|
||||
bool compaction_parked = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImpl::AbortCompactions:FlagSet",
|
||||
"DBCompactionAbortTest::ParkedCompaction:WaitForAbort"},
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"BackgroundCallCompaction:0", [&](void* /*arg*/) {
|
||||
abort_sync.TriggerAbort(dbfull(), handles_[1]);
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"DBCompactionAbortTest::ParkedCompaction:WaitForAbort", nullptr);
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::PickCompactionFromQueue:Parked", [&](void* /*arg*/) {
|
||||
std::lock_guard<std::mutex> guard(parked_mutex);
|
||||
compaction_parked = true;
|
||||
parked_cv.notify_all();
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Random rnd(301);
|
||||
for (int file = 0; file < kCompactionTrigger; ++file) {
|
||||
for (int key = 0; key < 100; ++key) {
|
||||
ASSERT_OK(Put(1, Key(key), rnd.RandomString(1000)));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(parked_mutex);
|
||||
parked_cv.wait(lock, [&]() { return compaction_parked; });
|
||||
}
|
||||
|
||||
CleanupSyncPoints();
|
||||
abort_sync.WaitForAbortCompletion();
|
||||
const int l0_files_before_resume = NumTableFilesAtLevel(0, 1);
|
||||
ASSERT_GE(l0_files_before_resume, kCompactionTrigger);
|
||||
|
||||
WaitForCompactOptions wait_options;
|
||||
wait_options.timeout = std::chrono::milliseconds(1);
|
||||
ASSERT_TRUE(dbfull()->WaitForCompact(wait_options).IsTimedOut());
|
||||
|
||||
for (int file = 0; file < kCompactionTrigger; ++file) {
|
||||
for (int key = 0; key < 100; ++key) {
|
||||
ASSERT_OK(Put(2, Key(key), rnd.RandomString(1000)));
|
||||
}
|
||||
ASSERT_OK(Flush(2));
|
||||
}
|
||||
ASSERT_OK(dbfull()->TEST_WaitForBackgroundWork());
|
||||
ASSERT_LT(NumTableFilesAtLevel(0, 2), kCompactionTrigger);
|
||||
ASSERT_EQ(NumTableFilesAtLevel(0, 1), l0_files_before_resume);
|
||||
|
||||
std::atomic<bool> target_reenqueued{false};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::RestoreParkedCompaction:cfd", [&](void* arg) {
|
||||
auto* cfd = static_cast<ColumnFamilyData*>(arg);
|
||||
if (cfd->GetName() == "target") {
|
||||
target_reenqueued.store(true);
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
ASSERT_TRUE(target_reenqueued.load());
|
||||
CleanupSyncPoints();
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
ASSERT_LT(NumTableFilesAtLevel(0, 1), l0_files_before_resume);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, DropColumnFamilyReleasesParkedCompaction) {
|
||||
constexpr int kCompactionTrigger = 4;
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
||||
options.max_background_compactions = 1;
|
||||
options.disable_auto_compactions = false;
|
||||
CreateAndReopenWithCF({"target"}, options);
|
||||
|
||||
AbortSynchronizer abort_sync;
|
||||
std::mutex parked_mutex;
|
||||
std::condition_variable parked_cv;
|
||||
bool compaction_parked = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImpl::AbortCompactions:FlagSet",
|
||||
"DBCompactionAbortTest::DropParkedCompaction:WaitForAbort"},
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"BackgroundCallCompaction:0", [&](void* /*arg*/) {
|
||||
abort_sync.TriggerAbort(dbfull(), handles_[1]);
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"DBCompactionAbortTest::DropParkedCompaction:WaitForAbort",
|
||||
nullptr);
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::PickCompactionFromQueue:Parked", [&](void* /*arg*/) {
|
||||
std::lock_guard<std::mutex> guard(parked_mutex);
|
||||
compaction_parked = true;
|
||||
parked_cv.notify_all();
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Random rnd(301);
|
||||
for (int file = 0; file < kCompactionTrigger; ++file) {
|
||||
for (int key = 0; key < 100; ++key) {
|
||||
ASSERT_OK(Put(1, Key(key), rnd.RandomString(1000)));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(parked_mutex);
|
||||
parked_cv.wait(lock, [&]() { return compaction_parked; });
|
||||
}
|
||||
|
||||
CleanupSyncPoints();
|
||||
abort_sync.WaitForAbortCompletion();
|
||||
ASSERT_OK(dbfull()->DropColumnFamily(handles_[1]));
|
||||
|
||||
WaitForCompactOptions wait_options;
|
||||
wait_options.timeout = std::chrono::seconds(10);
|
||||
ASSERT_OK(dbfull()->WaitForCompact(wait_options));
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, DropColumnFamilyBeforeAbortedQueueEntryIsParked) {
|
||||
constexpr int kCompactionTrigger = 4;
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
||||
options.max_background_compactions = 1;
|
||||
options.disable_auto_compactions = false;
|
||||
CreateAndReopenWithCF({"target"}, options);
|
||||
|
||||
std::mutex worker_mutex;
|
||||
std::condition_variable worker_cv;
|
||||
bool worker_started = false;
|
||||
bool allow_worker_to_pick = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"BackgroundCallCompaction:0", [&](void* /*arg*/) {
|
||||
std::unique_lock<std::mutex> lock(worker_mutex);
|
||||
worker_started = true;
|
||||
worker_cv.notify_all();
|
||||
worker_cv.wait(lock, [&]() { return allow_worker_to_pick; });
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
for (int file = 0; file < kCompactionTrigger; ++file) {
|
||||
ASSERT_OK(Put(1, Key(file), "value"));
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(worker_mutex);
|
||||
worker_cv.wait(lock, [&]() { return worker_started; });
|
||||
}
|
||||
|
||||
dbfull()->AbortCompactions(handles_[1]);
|
||||
const Status drop_status = dbfull()->DropColumnFamily(handles_[1]);
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(worker_mutex);
|
||||
allow_worker_to_pick = true;
|
||||
}
|
||||
worker_cv.notify_all();
|
||||
|
||||
WaitForCompactOptions wait_options;
|
||||
wait_options.timeout = std::chrono::seconds(10);
|
||||
const Status wait_status = dbfull()->WaitForCompact(wait_options);
|
||||
CleanupSyncPoints();
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
|
||||
ASSERT_OK(drop_status);
|
||||
ASSERT_OK(wait_status);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, NewCompactionWorkWhileAbortedIsParked) {
|
||||
constexpr int kCompactionTrigger = 4;
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
||||
options.max_background_compactions = 1;
|
||||
options.disable_auto_compactions = false;
|
||||
CreateAndReopenWithCF({"target"}, options);
|
||||
|
||||
dbfull()->AbortCompactions(handles_[1]);
|
||||
for (int file = 0; file < kCompactionTrigger; ++file) {
|
||||
ASSERT_OK(Put(1, Key(file), "value"));
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
|
||||
const int l0_files_before_resume = NumTableFilesAtLevel(0, 1);
|
||||
WaitForCompactOptions wait_options;
|
||||
wait_options.timeout = std::chrono::milliseconds(1);
|
||||
const Status wait_status = dbfull()->WaitForCompact(wait_options);
|
||||
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
ASSERT_TRUE(wait_status.IsTimedOut());
|
||||
ASSERT_GE(l0_files_before_resume, kCompactionTrigger);
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
ASSERT_LT(NumTableFilesAtLevel(0, 1), l0_files_before_resume);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortWaitsForCompactionCompletionCallback) {
|
||||
constexpr int kCompactionTrigger = 4;
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
||||
options.max_background_compactions = 1;
|
||||
options.disable_auto_compactions = false;
|
||||
options.listeners.emplace_back(std::make_shared<EventListener>());
|
||||
CreateAndReopenWithCF({"target"}, options);
|
||||
|
||||
std::mutex callback_mutex;
|
||||
std::condition_variable callback_cv;
|
||||
bool callback_started = false;
|
||||
bool allow_callback_to_finish = false;
|
||||
enum class AbortState { kNotObserved, kWaiting, kComplete };
|
||||
AbortState abort_state = AbortState::kNotObserved;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::NotifyOnCompactionCompleted::UnlockMutex", [&](void* /*arg*/) {
|
||||
std::unique_lock<std::mutex> lock(callback_mutex);
|
||||
callback_started = true;
|
||||
callback_cv.notify_all();
|
||||
callback_cv.wait(lock, [&]() { return allow_callback_to_finish; });
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::AbortCompactions:Waiting", [&](void* /*arg*/) {
|
||||
std::lock_guard<std::mutex> guard(callback_mutex);
|
||||
abort_state = AbortState::kWaiting;
|
||||
callback_cv.notify_all();
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::AbortCompactions:Complete", [&](void* /*arg*/) {
|
||||
std::lock_guard<std::mutex> guard(callback_mutex);
|
||||
abort_state = AbortState::kComplete;
|
||||
callback_cv.notify_all();
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
for (int file = 0; file < kCompactionTrigger; ++file) {
|
||||
ASSERT_OK(Put(1, Key(file), "value"));
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(callback_mutex);
|
||||
callback_cv.wait(lock, [&]() { return callback_started; });
|
||||
}
|
||||
|
||||
std::thread abort_thread([&]() { dbfull()->AbortCompactions(handles_[1]); });
|
||||
bool returned_while_callback_blocked = false;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(callback_mutex);
|
||||
callback_cv.wait(lock,
|
||||
[&]() { return abort_state != AbortState::kNotObserved; });
|
||||
returned_while_callback_blocked = abort_state == AbortState::kComplete;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(callback_mutex);
|
||||
allow_callback_to_finish = true;
|
||||
}
|
||||
callback_cv.notify_all();
|
||||
abort_thread.join();
|
||||
CleanupSyncPoints();
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
|
||||
ASSERT_FALSE(returned_while_callback_blocked);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest,
|
||||
DroppedColumnFamilyFilesPurgedAfterTrackedCompaction) {
|
||||
constexpr int kCompactionTrigger = 4;
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
||||
options.max_background_compactions = 1;
|
||||
options.disable_auto_compactions = false;
|
||||
CreateAndReopenWithCF({"target"}, options);
|
||||
|
||||
std::mutex compaction_mutex;
|
||||
std::condition_variable compaction_cv;
|
||||
bool compaction_started = false;
|
||||
bool allow_compaction_to_finish = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::Run():Start", [&](void* /*arg*/) {
|
||||
std::unique_lock<std::mutex> lock(compaction_mutex);
|
||||
compaction_started = true;
|
||||
compaction_cv.notify_all();
|
||||
compaction_cv.wait(lock, [&]() { return allow_compaction_to_finish; });
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Random rnd(301);
|
||||
for (int file = 0; file < kCompactionTrigger; ++file) {
|
||||
for (int key = 0; key < 100; ++key) {
|
||||
ASSERT_OK(Put(1, Key(key), rnd.RandomString(1000)));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(compaction_mutex);
|
||||
compaction_cv.wait(lock, [&]() { return compaction_started; });
|
||||
}
|
||||
|
||||
const int sst_files_before_drop = GetSstFileCount(dbname_);
|
||||
const Status drop_status = dbfull()->DropColumnFamily(handles_[1]);
|
||||
Status destroy_handle_status;
|
||||
if (drop_status.ok()) {
|
||||
destroy_handle_status = dbfull()->DestroyColumnFamilyHandle(handles_[1]);
|
||||
if (destroy_handle_status.ok()) {
|
||||
handles_.erase(handles_.begin() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(compaction_mutex);
|
||||
allow_compaction_to_finish = true;
|
||||
}
|
||||
compaction_cv.notify_all();
|
||||
const Status wait_status = dbfull()->TEST_WaitForCompact();
|
||||
CleanupSyncPoints();
|
||||
|
||||
ASSERT_OK(drop_status);
|
||||
ASSERT_OK(destroy_handle_status);
|
||||
ASSERT_OK(wait_status);
|
||||
ASSERT_GT(sst_files_before_drop, 0);
|
||||
ASSERT_EQ(0, GetSstFileCount(dbname_));
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, ResumeCompactionsAfterCloseIsNoOp) {
|
||||
auto* const default_column_family = dbfull()->DefaultColumnFamily();
|
||||
dbfull()->AbortCompactions(default_column_family);
|
||||
|
||||
ASSERT_OK(dbfull()->Close());
|
||||
dbfull()->ResumeCompactions(default_column_family);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortAndVerifyNoOutputFiles) {
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
|
||||
@@ -813,6 +813,23 @@ TEST_F(DBCompactionTest, CompactRangeBottomPri) {
|
||||
int low_pri_count = 0;
|
||||
int bottom_pri_count = 0;
|
||||
bool bottom_running_seen = false;
|
||||
std::atomic<bool> bottom_property_seen = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::Run():Start", [&](void*) {
|
||||
uint64_t total = 0;
|
||||
uint64_t bottom = 0;
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningCompactions,
|
||||
&total));
|
||||
ASSERT_TRUE(db_->GetIntProperty(
|
||||
DB::Properties::kNumRunningBottomCompactions, &bottom));
|
||||
ASSERT_GE(total, bottom);
|
||||
ASSERT_GE(total, 1);
|
||||
ASSERT_LE(total, 2);
|
||||
ASSERT_LE(bottom, 1);
|
||||
if (bottom > 0) {
|
||||
bottom_property_seen = true;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BackgroundCallCompaction:1", [&](void*) {
|
||||
if (dbfull()->TEST_NumRunningBottomCompactions() > 0) {
|
||||
@@ -838,6 +855,7 @@ TEST_F(DBCompactionTest, CompactRangeBottomPri) {
|
||||
ASSERT_EQ(1, low_pri_count);
|
||||
ASSERT_EQ(1, bottom_pri_count);
|
||||
ASSERT_TRUE(bottom_running_seen);
|
||||
ASSERT_TRUE(bottom_property_seen);
|
||||
ASSERT_EQ("0,0,2", FilesPerLevel(0));
|
||||
|
||||
// Recompact bottom most level uses bottom pool
|
||||
@@ -6156,6 +6174,7 @@ TEST_F(DBCompactionTest, CompactRangeFlushOverlappingMemtable) {
|
||||
TEST_F(DBCompactionTest, CompactionStatsTest) {
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 2;
|
||||
options.max_background_compactions = 1;
|
||||
CompactionStatsCollector* collector = new CompactionStatsCollector();
|
||||
options.listeners.emplace_back(collector);
|
||||
DestroyAndReopen(options);
|
||||
@@ -6163,9 +6182,13 @@ TEST_F(DBCompactionTest, CompactionStatsTest) {
|
||||
// Verify that the internal statistics for num_running_compactions and
|
||||
// num_running_compaction_sorted_runs start and end at valid states
|
||||
uint64_t num_running_compactions = 0;
|
||||
uint64_t num_running_bottom_compactions = 0;
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningCompactions,
|
||||
&num_running_compactions));
|
||||
ASSERT_EQ(num_running_compactions, 0);
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningBottomCompactions,
|
||||
&num_running_bottom_compactions));
|
||||
ASSERT_EQ(0, num_running_compactions);
|
||||
ASSERT_EQ(0, num_running_bottom_compactions);
|
||||
uint64_t num_running_compaction_sorted_runs = 0;
|
||||
ASSERT_TRUE(
|
||||
db_->GetIntProperty(DB::Properties::kNumRunningCompactionSortedRuns,
|
||||
@@ -6174,6 +6197,18 @@ TEST_F(DBCompactionTest, CompactionStatsTest) {
|
||||
// Check that the stat actually gets changed some time between the start and
|
||||
// end of compaction
|
||||
std::atomic<bool> sorted_runs_count_incremented = false;
|
||||
std::atomic<bool> running_compaction_seen = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::Run():Start", [&](void*) {
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningCompactions,
|
||||
&num_running_compactions));
|
||||
ASSERT_TRUE(
|
||||
db_->GetIntProperty(DB::Properties::kNumRunningBottomCompactions,
|
||||
&num_running_bottom_compactions));
|
||||
ASSERT_EQ(1, num_running_compactions);
|
||||
ASSERT_EQ(0, num_running_bottom_compactions);
|
||||
running_compaction_seen = true;
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionMergingIterator::UpdateInternalStats",
|
||||
[&](void*) { sorted_runs_count_incremented = true; });
|
||||
@@ -6197,11 +6232,15 @@ TEST_F(DBCompactionTest, CompactionStatsTest) {
|
||||
// to process
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningCompactions,
|
||||
&num_running_compactions));
|
||||
ASSERT_EQ(num_running_compactions, 0);
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningBottomCompactions,
|
||||
&num_running_bottom_compactions));
|
||||
ASSERT_EQ(0, num_running_compactions);
|
||||
ASSERT_EQ(0, num_running_bottom_compactions);
|
||||
ASSERT_TRUE(
|
||||
db_->GetIntProperty(DB::Properties::kNumRunningCompactionSortedRuns,
|
||||
&num_running_compaction_sorted_runs));
|
||||
ASSERT_EQ(num_running_compaction_sorted_runs, 0);
|
||||
ASSERT_TRUE(running_compaction_seen);
|
||||
ASSERT_TRUE(sorted_runs_count_incremented);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
|
||||
+71
-1
@@ -8,6 +8,7 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "db/db_impl/db_impl.h"
|
||||
@@ -20,8 +21,8 @@
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/metadata.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/types.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/file_checksum_helper.h"
|
||||
#include "util/mutexlock.h"
|
||||
@@ -195,14 +196,64 @@ Status DBImpl::GetCurrentWalFile(std::unique_ptr<WalFile>* current_wal_file) {
|
||||
return wal_manager_.GetLiveWalFile(current_logfile_number, current_wal_file);
|
||||
}
|
||||
|
||||
Status DBImpl::AppendColumnFamilyDropsToManifest(
|
||||
const std::string& manifest_path, uint64_t manifest_size,
|
||||
const std::vector<uint32_t>& cf_ids) {
|
||||
const WriteOptions write_options;
|
||||
// Snapshot manifest_preallocation_size_ under the DB mutex per VersionSet's
|
||||
// contract (the field is mutated by SetDBOptions under this mutex).
|
||||
uint64_t preallocation_size = 0;
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
preallocation_size = versions_->manifest_preallocation_size_;
|
||||
}
|
||||
return versions_->AppendColumnFamilyDropsToManifest(
|
||||
manifest_path, manifest_size, cf_ids, write_options, preallocation_size);
|
||||
}
|
||||
|
||||
Status DBImpl::GetLiveFilesStorageInfo(
|
||||
const LiveFilesStorageInfoOptions& opts,
|
||||
std::vector<LiveFileStorageInfo>* files) {
|
||||
return GetLiveFilesStorageInfoImpl(opts, /*include_cf_ids=*/{}, files,
|
||||
/*excluded_cf_ids=*/nullptr);
|
||||
}
|
||||
|
||||
Status DBImpl::GetLiveFilesStorageInfoForSubsetCheckpoint(
|
||||
const LiveFilesStorageInfoOptions& opts,
|
||||
const std::vector<uint32_t>& include_cf_ids,
|
||||
std::vector<LiveFileStorageInfo>* files,
|
||||
std::vector<uint32_t>* excluded_cf_ids) {
|
||||
// Internal contract: caller must coalesce ids and always include the default
|
||||
// CF (it cannot be dropped, so a subset checkpoint without it would produce
|
||||
// an unopenable DB). CheckpointImpl enforces this before calling here.
|
||||
assert(!include_cf_ids.empty());
|
||||
assert(std::find(include_cf_ids.begin(), include_cf_ids.end(),
|
||||
default_cf_handle_->GetID()) != include_cf_ids.end());
|
||||
assert(excluded_cf_ids != nullptr);
|
||||
return GetLiveFilesStorageInfoImpl(opts, include_cf_ids, files,
|
||||
excluded_cf_ids);
|
||||
}
|
||||
|
||||
Status DBImpl::GetLiveFilesStorageInfoImpl(
|
||||
const LiveFilesStorageInfoOptions& opts,
|
||||
const std::vector<uint32_t>& include_cf_ids,
|
||||
std::vector<LiveFileStorageInfo>* files,
|
||||
std::vector<uint32_t>* excluded_cf_ids) {
|
||||
// To avoid returning partial results, only move results to files on success.
|
||||
assert(files);
|
||||
files->clear();
|
||||
std::vector<LiveFileStorageInfo> results;
|
||||
|
||||
const bool cf_subset = !include_cf_ids.empty();
|
||||
assert(!cf_subset || excluded_cf_ids != nullptr);
|
||||
const std::unordered_set<uint32_t> include_cf_id_set(include_cf_ids.begin(),
|
||||
include_cf_ids.end());
|
||||
// Tracks which requested ids remain to be observed as live under the DB
|
||||
// mutex; any id still present after the enumeration was either dropped
|
||||
// before or during the checkpoint (race) and must not silently disappear
|
||||
// from the checkpoint.
|
||||
std::unordered_set<uint32_t> unseen_include_cf_ids = include_cf_id_set;
|
||||
|
||||
// NOTE: This implementation was largely migrated from Checkpoint.
|
||||
|
||||
VectorWalPtr live_wal_files;
|
||||
@@ -282,6 +333,14 @@ Status DBImpl::GetLiveFilesStorageInfo(
|
||||
if (cfd->IsDropped()) {
|
||||
continue;
|
||||
}
|
||||
if (cf_subset &&
|
||||
include_cf_id_set.find(cfd->GetID()) == include_cf_id_set.end()) {
|
||||
excluded_cf_ids->push_back(cfd->GetID());
|
||||
continue;
|
||||
}
|
||||
if (cf_subset) {
|
||||
unseen_include_cf_ids.erase(cfd->GetID());
|
||||
}
|
||||
VersionStorageInfo& vsi = *cfd->current()->storage_info();
|
||||
auto& cf_paths = cfd->ioptions().cf_paths;
|
||||
|
||||
@@ -343,6 +402,17 @@ Status DBImpl::GetLiveFilesStorageInfo(
|
||||
}
|
||||
}
|
||||
|
||||
// Release-build validation: every id the subset-checkpoint caller asked for
|
||||
// must have been observed as live under the mutex. If any is missing, it was
|
||||
// dropped before we ran (stale handle) or lost a race with a concurrent
|
||||
// drop; either way the checkpoint would silently omit it, violating the
|
||||
// caller's contract that the checkpoint contains the requested CFs.
|
||||
if (cf_subset && !unseen_include_cf_ids.empty()) {
|
||||
mutex_.Unlock();
|
||||
return Status::InvalidArgument(
|
||||
"Requested column family id was not live at checkpoint time");
|
||||
}
|
||||
|
||||
// Capture some final info before releasing mutex
|
||||
const uint64_t manifest_number = versions_->manifest_file_number();
|
||||
const uint64_t manifest_size = versions_->manifest_file_size();
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
|
||||
#include "util/coro_utils.h"
|
||||
|
||||
#if defined(USE_COROUTINES) && defined(WITH_COROUTINES)
|
||||
#include "util/coro_stats_util.h"
|
||||
#endif // USE_COROUTINES && WITH_COROUTINES
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
|
||||
@@ -14,6 +18,10 @@ namespace ROCKSDB_NAMESPACE {
|
||||
DEFINE_SYNC_AND_ASYNC(Status, CompactedDBImpl::Get)
|
||||
(const ReadOptions& _read_options, ColumnFamilyHandle*, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGet) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
@@ -102,6 +110,10 @@ DEFINE_SYNC_AND_ASYNC(void, CompactedDBImpl::MultiGet)
|
||||
ColumnFamilyHandle** /*column_families*/, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps, Status* statuses,
|
||||
const bool /*sorted_input*/) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
assert(user_comparator_);
|
||||
Status s;
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
|
||||
+369
-124
@@ -15,13 +15,6 @@
|
||||
#include <alloca.h>
|
||||
#endif
|
||||
|
||||
#if USE_COROUTINES
|
||||
#include "folly/Executor.h"
|
||||
#include "folly/coro/Invoke.h"
|
||||
#include "folly/executors/IOExecutor.h"
|
||||
#include "folly/io/async/EventBase.h"
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstdio>
|
||||
#include <deque>
|
||||
@@ -67,8 +60,9 @@
|
||||
#include "db/range_tombstone_fragmenter.h"
|
||||
#include "db/table_cache.h"
|
||||
#include "db/table_properties_collector.h"
|
||||
#include "db/transaction_log_impl.h"
|
||||
#include "db/version_set.h"
|
||||
#include "db/wal_iterator_impl.h"
|
||||
#include "db/wide/lazy_wide_columns_helper.h"
|
||||
#include "db/wide/wide_column_serialization.h"
|
||||
#include "db/wide/wide_columns_helper.h"
|
||||
#include "db/write_batch_internal.h"
|
||||
@@ -91,6 +85,7 @@
|
||||
#include "options/cf_options.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "options/options_parser.h"
|
||||
#include "rocksdb/lazy_wide_columns.h"
|
||||
#include "util/udt_util.h"
|
||||
#ifdef ROCKSDB_JEMALLOC
|
||||
#include "port/jemalloc_helper.h"
|
||||
@@ -135,6 +130,15 @@
|
||||
#include "util/udt_util.h"
|
||||
#include "utilities/trace/replayer_impl.h"
|
||||
|
||||
#if USE_COROUTINES
|
||||
#include "folly/Executor.h"
|
||||
#include "folly/coro/Nothrow.h"
|
||||
#include "folly/coro/Task.h"
|
||||
#include "folly/executors/IOExecutor.h"
|
||||
#include "folly/io/async/EventBase.h"
|
||||
#include "rocksdb/coro_db.h"
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
const std::string kDefaultColumnFamilyName("default");
|
||||
@@ -244,6 +248,11 @@ DBImpl::DBImpl(const DBOptions& options, const std::string& dbname,
|
||||
blob_callback_(immutable_db_options_.sst_file_manager.get(), &mutex_,
|
||||
&error_handler_, &event_logger_,
|
||||
immutable_db_options_.listeners, dbname_) {
|
||||
#if USE_COROUTINES
|
||||
immutable_db_options_.fs->SetReadIOExecutorThreads(
|
||||
immutable_db_options_.read_io_executor_threads);
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
// !batch_per_trx_ implies seq_per_batch_ because it is only unset for
|
||||
// WriteUnprepared, which should use seq_per_batch_.
|
||||
assert(batch_per_txn_ || seq_per_batch_);
|
||||
@@ -665,8 +674,9 @@ void DBImpl::MaybeInitBlobDirectWriteColumnFamily(
|
||||
[vs = versions_.get()]() { return vs->NewFileNumber(); }, fs_.get(),
|
||||
immutable_db_options_.clock, stats_, file_options_, dbname_,
|
||||
column_family_name, cf_options.blob_file_size,
|
||||
immutable_db_options_.use_fsync, cfd->blob_file_cache(), &blob_callback_,
|
||||
immutable_db_options_.listeners,
|
||||
immutable_db_options_.use_fsync,
|
||||
cf_options.blob_file_writable_file_max_buffer_size,
|
||||
cfd->blob_file_cache(), &blob_callback_, immutable_db_options_.listeners,
|
||||
immutable_db_options_.file_checksum_gen_factory.get(),
|
||||
immutable_db_options_.checksum_handoff_file_types, io_tracer_, db_id_,
|
||||
db_session_id_, immutable_db_options_.info_log.get());
|
||||
@@ -878,6 +888,13 @@ Status DBImpl::CloseHelper() {
|
||||
cfd->UnrefAndTryDelete();
|
||||
}
|
||||
|
||||
for (auto* cfd : parked_compaction_cfds_) {
|
||||
assert(cfd->queued_for_compaction());
|
||||
cfd->set_queued_for_compaction(false);
|
||||
cfd->UnrefAndTryDelete();
|
||||
}
|
||||
parked_compaction_cfds_.clear();
|
||||
|
||||
if (default_cf_handle_ != nullptr || persist_stats_cf_handle_ != nullptr) {
|
||||
// we need to delete handle outside of lock because it does its own locking
|
||||
mutex_.Unlock();
|
||||
@@ -1675,7 +1692,13 @@ Status DBImpl::SetOptions(
|
||||
// options to file, otherwise there will be a deadlock with writer
|
||||
// thread.
|
||||
for (const auto& cfd_opts : column_family_datas) {
|
||||
InstallSuperVersionForConfigChange(cfd_opts.first, &sv_context);
|
||||
auto* cfd = cfd_opts.first;
|
||||
InstallSuperVersionForConfigChange(cfd, &sv_context);
|
||||
if (auto* blob_partition_manager = cfd->blob_partition_manager()) {
|
||||
blob_partition_manager->SetBlobWriterMaxBufferSize(
|
||||
cfd->GetLatestMutableCFOptions()
|
||||
.blob_file_writable_file_max_buffer_size);
|
||||
}
|
||||
}
|
||||
|
||||
persist_options_status =
|
||||
@@ -2532,6 +2555,17 @@ static void CleanupSuperVersionHandle(void* arg1, void* /*arg2*/) {
|
||||
delete sv_handle;
|
||||
}
|
||||
|
||||
void DBImpl::TransferSuperVersionPin(SuperVersion* super_version,
|
||||
Cleanable* pin) {
|
||||
assert(super_version != nullptr);
|
||||
assert(pin != nullptr);
|
||||
super_version->Ref();
|
||||
SuperVersionHandle* sv_handle = new SuperVersionHandle(
|
||||
this, &mutex_, super_version,
|
||||
immutable_db_options_.avoid_unnecessary_blocking_io);
|
||||
pin->RegisterCleanup(CleanupSuperVersionHandle, sv_handle, nullptr);
|
||||
}
|
||||
|
||||
struct GetMergeOperandsState {
|
||||
GetMergeOperandsState(MergeContext _merge_context,
|
||||
PinnedIteratorsManager _pinned_iters_mgr)
|
||||
@@ -2702,6 +2736,163 @@ Status DBImpl::GetEntity(const ReadOptions& _read_options,
|
||||
return GetImpl(read_options, key, get_impl_options);
|
||||
}
|
||||
|
||||
Status DBImpl::GetEntityLazyImpl(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, LazyWideColumns* result) {
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(column_family);
|
||||
auto cfd = cfh->cfd();
|
||||
|
||||
// Point-lookup output: the entity buffer owned by the result. GetImpl fills
|
||||
// it with the (unresolved) entity and hands back the resolution context.
|
||||
PinnableWideColumns* entity_buffer =
|
||||
LazyWideColumnsHelper::EntityBuffer(result);
|
||||
|
||||
const SameFileBlobReader* same_file_reader = nullptr;
|
||||
const Version* version = nullptr;
|
||||
Cleanable pin;
|
||||
|
||||
GetImplOptions get_impl_options;
|
||||
get_impl_options.column_family = column_family;
|
||||
get_impl_options.columns = entity_buffer;
|
||||
get_impl_options.lazy_columns_pin = &pin;
|
||||
get_impl_options.lazy_columns_version = &version;
|
||||
get_impl_options.lazy_columns_same_file_reader = &same_file_reader;
|
||||
|
||||
Status s = GetImpl(read_options, key, get_impl_options);
|
||||
if (!s.ok()) {
|
||||
result->Reset();
|
||||
return s;
|
||||
}
|
||||
|
||||
s = LazyWideColumnsHelper::Finalize(
|
||||
result, key, version, read_options, cfd->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/cfd->blob_partition_manager() != nullptr,
|
||||
same_file_reader, std::move(pin));
|
||||
if (!s.ok()) {
|
||||
result->Reset();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status DBImpl::GetEntityLazy(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, LazyWideColumns* result) {
|
||||
// Start from an empty result so a reused LazyWideColumns is left empty (not
|
||||
// stale) on every early-return / error path below.
|
||||
if (result != nullptr) {
|
||||
result->Reset();
|
||||
}
|
||||
if (!column_family) {
|
||||
return Status::InvalidArgument(
|
||||
"Cannot call GetEntityLazy without a column family handle");
|
||||
}
|
||||
if (!result) {
|
||||
return Status::InvalidArgument(
|
||||
"Cannot call GetEntityLazy without a LazyWideColumns object");
|
||||
}
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGetEntity) {
|
||||
return Status::InvalidArgument(
|
||||
"Can only call GetEntityLazy with `ReadOptions::io_activity` set to "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGetEntity`");
|
||||
}
|
||||
// Lazy resolution keeps blob references (including same-file/embedded ones)
|
||||
// resolvable after the call returns; that relies on immortal, pinned table
|
||||
// readers, which the DB only guarantees when max_open_files == -1.
|
||||
if (mutable_db_options_.max_open_files != -1) {
|
||||
return Status::InvalidArgument(
|
||||
"GetEntityLazy requires the DB to be opened with max_open_files == -1");
|
||||
}
|
||||
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kGetEntity;
|
||||
}
|
||||
|
||||
return GetEntityLazyImpl(read_options, column_family, key, result);
|
||||
}
|
||||
|
||||
void DBImpl::MultiGetEntityLazy(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
size_t num_keys, const Slice* keys,
|
||||
LazyWideColumnsBatch* result, Status* statuses,
|
||||
bool /* sorted_input */) {
|
||||
// Start from an empty batch so a reused LazyWideColumnsBatch is left empty
|
||||
// (not stale) on every early-return / error path below.
|
||||
if (result != nullptr) {
|
||||
result->Reset();
|
||||
}
|
||||
if (num_keys == 0) {
|
||||
return;
|
||||
}
|
||||
Status arg_status;
|
||||
if (!column_family) {
|
||||
arg_status = Status::InvalidArgument(
|
||||
"Cannot call MultiGetEntityLazy without a column family handle");
|
||||
} else if (!result) {
|
||||
arg_status = Status::InvalidArgument(
|
||||
"Cannot call MultiGetEntityLazy without a LazyWideColumnsBatch object");
|
||||
} else if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kMultiGetEntity) {
|
||||
arg_status = Status::InvalidArgument(
|
||||
"Can only call MultiGetEntityLazy with `ReadOptions::io_activity` set "
|
||||
"to `Env::IOActivity::kUnknown` or `Env::IOActivity::kMultiGetEntity`");
|
||||
} else if (mutable_db_options_.max_open_files != -1) {
|
||||
arg_status = Status::InvalidArgument(
|
||||
"MultiGetEntityLazy requires the DB to be opened with max_open_files "
|
||||
"== "
|
||||
"-1");
|
||||
}
|
||||
if (!arg_status.ok()) {
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] = arg_status;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
LazyWideColumnsHelper::InitBatch(result, num_keys);
|
||||
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kMultiGetEntity;
|
||||
}
|
||||
|
||||
// The current phase implements MultiGetEntityLazy as a loop of single-key
|
||||
// lazy lookups. Each iteration acquires (and hands back to the result) its
|
||||
// own SuperVersion reference, and each single-key lookup independently
|
||||
// derives its read sequence number. To still present one consistent
|
||||
// point-in-time view across all keys (as MultiGet guarantees), pin an
|
||||
// explicit snapshot for the duration of the loop when the caller did not
|
||||
// supply one: it fixes the read sequence number for every key and prevents
|
||||
// compaction from dropping a version some later key still needs to observe.
|
||||
//
|
||||
// TODO(lazy-blob-resolution-phase3): replace this loop with a genuinely
|
||||
// batched read that acquires a single SuperVersion and one consistent
|
||||
// (implicit) sequence number for the whole batch -- the mechanism batched
|
||||
// MultiGet already uses -- and holds one shared pin per column family instead
|
||||
// of one per key. That removes the need for an explicit snapshot here (which
|
||||
// takes the DB mutex and adds a snapshot list entry) and enables
|
||||
// coalescing/parallelizing the per-key work.
|
||||
const Snapshot* snapshot = read_options.snapshot;
|
||||
const bool own_snapshot = snapshot == nullptr;
|
||||
if (own_snapshot) {
|
||||
snapshot = GetSnapshot();
|
||||
read_options.snapshot = snapshot;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] =
|
||||
GetEntityLazyImpl(read_options, column_family, keys[i], &(*result)[i]);
|
||||
}
|
||||
// Link the populated entities back to the batch so batch reads can validate
|
||||
// column ownership.
|
||||
LazyWideColumnsHelper::FinalizeBatch(result);
|
||||
|
||||
if (own_snapshot) {
|
||||
ReleaseSnapshot(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
Status DBImpl::GetEntity(const ReadOptions& _read_options, const Slice& key,
|
||||
PinnableAttributeGroups* result) {
|
||||
if (!result) {
|
||||
@@ -2822,9 +3013,10 @@ static Status DecodeDirectWriteBlobIndex(const Slice& blob_index_slice,
|
||||
return status;
|
||||
}
|
||||
|
||||
Status DBImpl::ResolveDirectWritePlainValue(
|
||||
const ReadOptions& read_options, const Slice& key, const Version* current,
|
||||
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns) {
|
||||
Status DBImpl::ResolveDirectWritePlainValue(const Slice& key,
|
||||
const BlobFetcher& blob_fetcher,
|
||||
PinnableSlice* value,
|
||||
PinnableWideColumns* columns) {
|
||||
Slice blob_index_slice;
|
||||
std::string blob_index_storage;
|
||||
if (value != nullptr) {
|
||||
@@ -2862,9 +3054,9 @@ Status DBImpl::ResolveDirectWritePlainValue(
|
||||
BlobIndex blob_idx;
|
||||
Status status = DecodeDirectWriteBlobIndex(blob_index_slice, &blob_idx);
|
||||
if (status.ok()) {
|
||||
status = BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
|
||||
read_options, key, blob_idx, current, cfd->blob_file_cache(),
|
||||
nullptr /* prefetch_buffer */, target, nullptr /* bytes_read */);
|
||||
status =
|
||||
blob_fetcher.FetchBlob(key, blob_idx, nullptr /* prefetch_buffer */,
|
||||
target, nullptr /* bytes_read */);
|
||||
if (status.ok() && columns != nullptr) {
|
||||
columns->SetPlainValue(std::move(*target));
|
||||
}
|
||||
@@ -2872,10 +3064,8 @@ Status DBImpl::ResolveDirectWritePlainValue(
|
||||
return status;
|
||||
}
|
||||
|
||||
Status DBImpl::ResolveDirectWriteWideColumns(const ReadOptions& read_options,
|
||||
const Slice& key,
|
||||
const Version* current,
|
||||
ColumnFamilyData* cfd,
|
||||
Status DBImpl::ResolveDirectWriteWideColumns(const Slice& key,
|
||||
const BlobFetcher& blob_fetcher,
|
||||
PinnableWideColumns* columns) {
|
||||
assert(columns != nullptr);
|
||||
|
||||
@@ -2919,9 +3109,8 @@ Status DBImpl::ResolveDirectWriteWideColumns(const ReadOptions& read_options,
|
||||
} else {
|
||||
extra_buffers.emplace_front();
|
||||
PinnableSlice& blob_value = extra_buffers.front();
|
||||
s = BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
|
||||
read_options, key, blob_idx, current, cfd->blob_file_cache(),
|
||||
nullptr /* prefetch_buffer */, &blob_value, nullptr /* bytes_read */);
|
||||
s = blob_fetcher.FetchBlob(key, blob_idx, nullptr /* prefetch_buffer */,
|
||||
&blob_value, nullptr /* bytes_read */);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -3117,9 +3306,9 @@ Status DBImpl::PostprocessMemtableValueRead(
|
||||
|
||||
bool DBImpl::MaybeResolveDirectWriteValue(
|
||||
const ReadOptions& read_options, const Slice& key,
|
||||
bool resolve_direct_write_value, const Version* current,
|
||||
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index, bool* value_found) {
|
||||
bool resolve_direct_write_value, const BlobFetcher& blob_fetcher,
|
||||
PinnableSlice* value, PinnableWideColumns* columns, Status* s,
|
||||
bool* is_blob_index, bool* value_found) {
|
||||
if (!s->ok() || !resolve_direct_write_value || (!value && !columns)) {
|
||||
return false;
|
||||
}
|
||||
@@ -3164,15 +3353,14 @@ bool DBImpl::MaybeResolveDirectWriteValue(
|
||||
}
|
||||
|
||||
if (needs_plain_value_resolution) {
|
||||
*s = ResolveDirectWritePlainValue(read_options, key, current, cfd, value,
|
||||
columns);
|
||||
*s = ResolveDirectWritePlainValue(key, blob_fetcher, value, columns);
|
||||
assert(is_blob_index != nullptr);
|
||||
*is_blob_index = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
assert(columns != nullptr);
|
||||
*s = ResolveDirectWriteWideColumns(read_options, key, current, cfd, columns);
|
||||
*s = ResolveDirectWriteWideColumns(key, blob_fetcher, columns);
|
||||
|
||||
if (is_blob_index != nullptr) {
|
||||
*is_blob_index = false;
|
||||
@@ -3184,15 +3372,16 @@ bool DBImpl::MaybeResolveDirectWriteValue(
|
||||
void DBImpl::PostprocessDirectWriteValueRead(
|
||||
const ReadOptions& read_options, const Slice& key,
|
||||
const std::string* timestamp, bool resolve_direct_write_value,
|
||||
const Version* current, ColumnFamilyData* cfd, PinnableSlice* value,
|
||||
const BlobFetcher* blob_fetcher, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
|
||||
bool* value_found) {
|
||||
if (resolve_direct_write_value) {
|
||||
assert(blob_fetcher != nullptr);
|
||||
std::string blob_lookup_key_storage;
|
||||
const bool value_resolved = MaybeResolveDirectWriteValue(
|
||||
read_options,
|
||||
GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage),
|
||||
resolve_direct_write_value, current, cfd, value, columns, s,
|
||||
resolve_direct_write_value, *blob_fetcher, value, columns, s,
|
||||
is_blob_index, value_found);
|
||||
if (!value_resolved && value != nullptr) {
|
||||
value->PinSelf();
|
||||
@@ -4011,6 +4200,9 @@ Status DBImpl::DropColumnFamilyImpl(ColumnFamilyHandle* column_family) {
|
||||
if (s.ok() && cfd->blob_partition_manager() != nullptr) {
|
||||
UnregisterBlobDirectWriteColumnFamily();
|
||||
}
|
||||
if (s.ok()) {
|
||||
RestoreParkedCompaction(cfd);
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
auto& moptions = cfd->GetLatestMutableCFOptions();
|
||||
@@ -5207,9 +5399,9 @@ void DBImpl::ReleaseOptionsFileNumber(
|
||||
}
|
||||
}
|
||||
|
||||
Status DBImpl::GetUpdatesSince(
|
||||
SequenceNumber seq, std::unique_ptr<TransactionLogIterator>* iter,
|
||||
const TransactionLogIterator::ReadOptions& read_options) {
|
||||
Status DBImpl::GetUpdatesSince(SequenceNumber seq,
|
||||
std::unique_ptr<WalIterator>* iter,
|
||||
const WalIterator::ReadOptions& read_options) {
|
||||
RecordTick(stats_, GET_UPDATES_SINCE_CALLS);
|
||||
if (seq_per_batch_) {
|
||||
return Status::NotSupported(
|
||||
@@ -5830,7 +6022,7 @@ void DeleteOptionsFilesHelper(const std::map<uint64_t, std::string>& filenames,
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Status DBImpl::DeleteObsoleteOptionsFiles() {
|
||||
Status DBImpl::DeleteObsoleteOptionsFiles(bool schedule_only) {
|
||||
std::vector<std::string> filenames;
|
||||
// use ordered map to store keep the filenames sorted from the newest
|
||||
// to the oldest.
|
||||
@@ -5855,8 +6047,23 @@ Status DBImpl::DeleteObsoleteOptionsFiles() {
|
||||
|
||||
// Keeps the latest 2 Options file
|
||||
const size_t kNumOptionsFilesKept = 2;
|
||||
DeleteOptionsFilesHelper(options_filenames, kNumOptionsFilesKept,
|
||||
immutable_db_options_.info_log, GetEnv());
|
||||
if (options_filenames.size() > kNumOptionsFilesKept) {
|
||||
if (schedule_only) {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
for (auto iter =
|
||||
std::next(options_filenames.begin(), kNumOptionsFilesKept);
|
||||
iter != options_filenames.end(); ++iter) {
|
||||
const uint64_t file_number =
|
||||
std::numeric_limits<uint64_t>::max() - iter->first;
|
||||
SchedulePendingPurge(iter->second, GetName(), kOptionsFile, file_number,
|
||||
/*job_id=*/0);
|
||||
}
|
||||
SchedulePurge();
|
||||
} else {
|
||||
DeleteOptionsFilesHelper(options_filenames, kNumOptionsFilesKept,
|
||||
immutable_db_options_.info_log, GetEnv());
|
||||
}
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -5896,18 +6103,42 @@ Status DBImpl::RenameTempFileToOptionsFile(const std::string& file_name,
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
int my_disable_delete_obsolete_files;
|
||||
enum class ObsoleteOptionsFileCleanup {
|
||||
kSkip,
|
||||
kDeleteNow,
|
||||
kSchedule,
|
||||
};
|
||||
ObsoleteOptionsFileCleanup obsolete_options_file_cleanup =
|
||||
ObsoleteOptionsFileCleanup::kSkip;
|
||||
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
versions_->options_file_number_ = options_file_number;
|
||||
versions_->options_file_size_ = options_file_size;
|
||||
my_disable_delete_obsolete_files = disable_delete_obsolete_files_;
|
||||
if (!disable_delete_obsolete_files_ && !is_remote_compaction_enabled) {
|
||||
if (immutable_db_options_.avoid_unnecessary_blocking_io &&
|
||||
!reject_new_background_jobs_) {
|
||||
// DB::Open() sets `opened_successfully_` after WriteOptionsFile()
|
||||
// returns, then schedules the deferred OPTIONS-file purge.
|
||||
obsolete_options_file_cleanup =
|
||||
opened_successfully_ ? ObsoleteOptionsFileCleanup::kSchedule
|
||||
: ObsoleteOptionsFileCleanup::kSkip;
|
||||
} else {
|
||||
obsolete_options_file_cleanup =
|
||||
ObsoleteOptionsFileCleanup::kDeleteNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!my_disable_delete_obsolete_files && !is_remote_compaction_enabled) {
|
||||
// TODO: Should we check for errors here?
|
||||
DeleteObsoleteOptionsFiles().PermitUncheckedError();
|
||||
if (obsolete_options_file_cleanup != ObsoleteOptionsFileCleanup::kSkip) {
|
||||
Status obsolete_options_status =
|
||||
DeleteObsoleteOptionsFiles(obsolete_options_file_cleanup ==
|
||||
ObsoleteOptionsFileCleanup::kSchedule);
|
||||
if (!obsolete_options_status.ok()) {
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"Unable to delete obsolete OPTIONS files: %s",
|
||||
obsolete_options_status.ToString().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7550,9 +7781,24 @@ const IOStatsContext* CurrentIOStatsContextForAsyncCallback(
|
||||
}
|
||||
|
||||
#if USE_COROUTINES
|
||||
void InstallCoroutineStatsConfigToTLS(
|
||||
const CoroutineStatsConfig& stats_config) {
|
||||
#ifndef NPERF_CONTEXT
|
||||
if (stats_config.per_level_perf_context_enabled) {
|
||||
get_perf_context()->EnablePerLevelPerfContext();
|
||||
} else {
|
||||
get_perf_context()->per_level_perf_context_enabled = false;
|
||||
}
|
||||
#endif
|
||||
#ifndef NIOSTATS_CONTEXT
|
||||
get_iostats_context()->disable_iostats = stats_config.iostats_disabled;
|
||||
#endif
|
||||
SetPerfLevel(stats_config.perf_level);
|
||||
}
|
||||
|
||||
std::optional<CoroutineStatsConfig> CaptureCoroutineStatsConfigForCallback(
|
||||
bool stats_enabled) {
|
||||
if (!stats_enabled) {
|
||||
if (!stats_enabled || !IsCoroutineStatsEnabled()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return CaptureCoroutineStatsConfig();
|
||||
@@ -7562,50 +7808,49 @@ std::optional<CoroutineStatsConfig> CaptureCoroutineStatsConfigForCallback(
|
||||
|
||||
} // namespace
|
||||
|
||||
void DBImpl::GetAsync(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp,
|
||||
Status& status, AsyncCallback& callback) {
|
||||
void DB::GetAsync(const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp, Status& status,
|
||||
AsyncCallback& callback) {
|
||||
const bool stats_enabled = callback.EnableStats();
|
||||
#if USE_COROUTINES
|
||||
auto* read_executor =
|
||||
immutable_db_options_.env->GetFileSystem()->GetReadExecutor();
|
||||
if (read_executor != nullptr) {
|
||||
auto* read_event_base = read_executor->getEventBase();
|
||||
assert(read_event_base != nullptr);
|
||||
auto task =
|
||||
[](std::optional<CoroutineStatsConfig> task_stats_config, DBImpl* db,
|
||||
ReadOptions task_options, ColumnFamilyHandle* task_column_family,
|
||||
Slice task_key, PinnableSlice* task_value,
|
||||
std::string* task_timestamp, Status& task_status,
|
||||
AsyncCallback& task_callback) mutable -> folly::coro::Task<void> {
|
||||
std::optional<CoroutineStatsContextScope> coroutine_stats_scope;
|
||||
if (task_stats_config.has_value()) {
|
||||
coroutine_stats_scope.emplace(std::move(*task_stats_config),
|
||||
db->immutable_db_options_.env);
|
||||
}
|
||||
task_status = co_await folly::coro::co_nothrow(
|
||||
db->GetCoroutine(task_options, task_column_family, task_key,
|
||||
task_value, task_timestamp));
|
||||
if (coroutine_stats_scope.has_value()) {
|
||||
coroutine_stats_scope->Finalize();
|
||||
}
|
||||
task_callback.OnComplete(
|
||||
CurrentPerfContextForAsyncCallback(coroutine_stats_scope.has_value()),
|
||||
CurrentIOStatsContextForAsyncCallback(
|
||||
coroutine_stats_scope.has_value()));
|
||||
}(CaptureCoroutineStatsConfigForCallback(stats_enabled), this, options,
|
||||
column_family, key, value,
|
||||
timestamp, status, callback);
|
||||
folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task))
|
||||
.start();
|
||||
return;
|
||||
CoroDB* coro_db = GetCoroDB();
|
||||
if (coro_db != nullptr) {
|
||||
auto* read_executor = GetFileSystem()->GetReadExecutor();
|
||||
if (read_executor != nullptr) {
|
||||
auto* read_event_base = read_executor->getEventBase();
|
||||
assert(read_event_base != nullptr);
|
||||
auto stats_config = CaptureCoroutineStatsConfigForCallback(stats_enabled);
|
||||
auto task =
|
||||
[](std::optional<CoroutineStatsConfig> task_stats_config,
|
||||
CoroDB* task_db, ReadOptions task_options,
|
||||
ColumnFamilyHandle* task_column_family, Slice task_key,
|
||||
PinnableSlice* task_value, std::string* task_timestamp,
|
||||
Status& task_status,
|
||||
AsyncCallback& task_callback) mutable -> folly::coro::Task<void> {
|
||||
if (task_stats_config.has_value()) {
|
||||
InstallCoroutineStatsConfigToTLS(*task_stats_config);
|
||||
}
|
||||
task_status = co_await folly::coro::co_nothrow(
|
||||
task_db->GetCoroutine(task_options, task_column_family, task_key,
|
||||
task_value, task_timestamp));
|
||||
|
||||
const bool task_stats_enabled = task_stats_config.has_value();
|
||||
task_callback.OnComplete(
|
||||
CurrentPerfContextForAsyncCallback(task_stats_enabled),
|
||||
CurrentIOStatsContextForAsyncCallback(task_stats_enabled));
|
||||
}(std::move(stats_config), coro_db, options, column_family, key, value,
|
||||
timestamp, status, callback);
|
||||
folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task))
|
||||
.start();
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
if (stats_enabled) {
|
||||
// Mirror behavior of coroutine version where each coroutine gets a clean
|
||||
// context
|
||||
// Match coroutine reads: callback stats describe only this request.
|
||||
ResetThreadLocalStatsForAsyncCallback();
|
||||
}
|
||||
status = Get(options, column_family, key, value, timestamp);
|
||||
@@ -7613,52 +7858,52 @@ void DBImpl::GetAsync(const ReadOptions& options,
|
||||
CurrentIOStatsContextForAsyncCallback(stats_enabled));
|
||||
}
|
||||
|
||||
void DBImpl::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) {
|
||||
void DB::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 bool stats_enabled = callback.EnableStats();
|
||||
#if USE_COROUTINES
|
||||
auto* read_executor =
|
||||
immutable_db_options_.env->GetFileSystem()->GetReadExecutor();
|
||||
if (read_executor != nullptr) {
|
||||
auto* read_event_base = read_executor->getEventBase();
|
||||
assert(read_event_base != nullptr);
|
||||
auto task = [](std::optional<CoroutineStatsConfig> task_stats_config,
|
||||
DBImpl* db, ReadOptions task_options, size_t task_num_keys,
|
||||
ColumnFamilyHandle** task_column_families,
|
||||
const Slice* task_keys, PinnableSlice* task_values,
|
||||
std::string* task_timestamps, Status* task_statuses,
|
||||
bool task_sorted_input, AsyncCallback& task_callback) mutable
|
||||
-> folly::coro::Task<void> {
|
||||
std::optional<CoroutineStatsContextScope> coroutine_stats_scope;
|
||||
if (task_stats_config.has_value()) {
|
||||
coroutine_stats_scope.emplace(std::move(*task_stats_config),
|
||||
db->immutable_db_options_.env);
|
||||
}
|
||||
co_await folly::coro::co_nothrow(db->MultiGetCoroutine(
|
||||
task_options, task_num_keys, task_column_families, task_keys,
|
||||
task_values, task_timestamps, task_statuses, task_sorted_input));
|
||||
if (coroutine_stats_scope.has_value()) {
|
||||
coroutine_stats_scope->Finalize();
|
||||
}
|
||||
task_callback.OnComplete(
|
||||
CurrentPerfContextForAsyncCallback(coroutine_stats_scope.has_value()),
|
||||
CurrentIOStatsContextForAsyncCallback(
|
||||
coroutine_stats_scope.has_value()));
|
||||
}(CaptureCoroutineStatsConfigForCallback(stats_enabled), this, options,
|
||||
num_keys, column_families, keys, values, timestamps, statuses,
|
||||
sorted_input, callback);
|
||||
folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task))
|
||||
.start();
|
||||
return;
|
||||
CoroDB* coro_db = GetCoroDB();
|
||||
if (coro_db != nullptr) {
|
||||
auto* read_executor = GetFileSystem()->GetReadExecutor();
|
||||
if (read_executor != nullptr) {
|
||||
auto* read_event_base = read_executor->getEventBase();
|
||||
assert(read_event_base != nullptr);
|
||||
auto stats_config = CaptureCoroutineStatsConfigForCallback(stats_enabled);
|
||||
auto task =
|
||||
[](std::optional<CoroutineStatsConfig> task_stats_config,
|
||||
CoroDB* task_db, ReadOptions task_options, size_t task_num_keys,
|
||||
ColumnFamilyHandle** task_column_families, const Slice* task_keys,
|
||||
PinnableSlice* task_values, std::string* task_timestamps,
|
||||
Status* task_statuses, bool task_sorted_input,
|
||||
AsyncCallback& task_callback) mutable -> folly::coro::Task<void> {
|
||||
if (task_stats_config.has_value()) {
|
||||
InstallCoroutineStatsConfigToTLS(*task_stats_config);
|
||||
}
|
||||
co_await folly::coro::co_nothrow(task_db->MultiGetCoroutine(
|
||||
task_options, task_num_keys, task_column_families, task_keys,
|
||||
task_values, task_timestamps, task_statuses, task_sorted_input));
|
||||
|
||||
const bool task_stats_enabled = task_stats_config.has_value();
|
||||
task_callback.OnComplete(
|
||||
CurrentPerfContextForAsyncCallback(task_stats_enabled),
|
||||
CurrentIOStatsContextForAsyncCallback(task_stats_enabled));
|
||||
}(std::move(stats_config), coro_db, options, num_keys, column_families,
|
||||
keys, values, timestamps,
|
||||
statuses, sorted_input,
|
||||
callback);
|
||||
folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task))
|
||||
.start();
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
if (stats_enabled) {
|
||||
// Mirror behavior of coroutine version where each coroutine gets a clean
|
||||
// context
|
||||
// Match coroutine reads: callback stats describe only this request.
|
||||
ResetThreadLocalStatsForAsyncCallback();
|
||||
}
|
||||
MultiGet(options, num_keys, column_families, keys, values, timestamps,
|
||||
|
||||
+166
-46
@@ -21,6 +21,7 @@
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "db/column_family.h"
|
||||
@@ -53,6 +54,7 @@
|
||||
#include "memtable/wbwi_memtable.h"
|
||||
#include "monitoring/instrumented_mutex.h"
|
||||
#include "options/db_options.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/attribute_groups.h"
|
||||
#include "rocksdb/db.h"
|
||||
@@ -60,10 +62,10 @@
|
||||
#include "rocksdb/memtablerep.h"
|
||||
#include "rocksdb/status.h"
|
||||
#include "rocksdb/trace_reader_writer.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/user_write_callback.h"
|
||||
#include "rocksdb/utilities/replayer.h"
|
||||
#include "rocksdb/utilities/write_batch_with_index.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
#include "rocksdb/write_buffer_manager.h"
|
||||
#include "table/merging_iterator.h"
|
||||
#include "util/autovector.h"
|
||||
@@ -73,11 +75,17 @@
|
||||
#include "util/stop_watch.h"
|
||||
#include "util/thread_local.h"
|
||||
|
||||
#if USE_COROUTINES
|
||||
#include "rocksdb/coro_db.h"
|
||||
#endif
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class Arena;
|
||||
class ArenaWrappedDBIter;
|
||||
class Cleanable;
|
||||
class FileIngestionHandleImpl;
|
||||
class SameFileBlobReader;
|
||||
class InMemoryStatsHistoryIterator;
|
||||
class MemTable;
|
||||
class PersistentStatsHistoryIterator;
|
||||
@@ -196,14 +204,22 @@ struct DBOpenLogRecordReadReporter : public log::Reader::Reporter {
|
||||
//
|
||||
// Since it's a very large class, the definition of the functions is
|
||||
// divided in several db_impl_*.cc files, besides db_impl.cc.
|
||||
class DBImpl : public DB {
|
||||
#if USE_COROUTINES
|
||||
class DBImpl : public DB,
|
||||
public CoroDB
|
||||
#else
|
||||
class DBImpl : public DB
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
DBImpl(const DBOptions& options, const std::string& dbname,
|
||||
const bool seq_per_batch = false, const bool batch_per_txn = true,
|
||||
bool read_only = false);
|
||||
// No copying allowed
|
||||
// No copying or moving allowed
|
||||
DBImpl(const DBImpl&) = delete;
|
||||
void operator=(const DBImpl&) = delete;
|
||||
DBImpl(DBImpl&&) = delete;
|
||||
void operator=(DBImpl&&) = delete;
|
||||
|
||||
virtual ~DBImpl();
|
||||
|
||||
@@ -269,14 +285,10 @@ class DBImpl : public DB {
|
||||
bool HasAnyBlobDirectWriteColumnFamily();
|
||||
|
||||
using DB::Get;
|
||||
using DB::GetAsync;
|
||||
DECLARE_SYNC_ASYNC_AND_CALLBACK(
|
||||
Status, Get, GetAsync,
|
||||
(const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value, std::string* timestamp,
|
||||
Status& status, AsyncCallback& callback),
|
||||
const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value, std::string* timestamp);
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, Get, const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp);
|
||||
|
||||
using DB::GetEntity;
|
||||
Status GetEntity(const ReadOptions& options,
|
||||
@@ -285,6 +297,10 @@ class DBImpl : public DB {
|
||||
Status GetEntity(const ReadOptions& options, const Slice& key,
|
||||
PinnableAttributeGroups* result) override;
|
||||
|
||||
Status GetEntityLazy(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
LazyWideColumns* result) override;
|
||||
|
||||
using DB::GetMergeOperands;
|
||||
Status GetMergeOperands(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
@@ -301,7 +317,6 @@ class DBImpl : public DB {
|
||||
}
|
||||
|
||||
using DB::MultiGet;
|
||||
using DB::MultiGetAsync;
|
||||
// This MultiGet is a batched version, which may be faster than calling Get
|
||||
// multiple times, especially if the keys have some spatial locality that
|
||||
// enables them to be queried in the same SST files/set of files. The larger
|
||||
@@ -309,16 +324,13 @@ class DBImpl : public DB {
|
||||
// The values and statuses parameters are arrays with number of elements
|
||||
// equal to keys.size(). This allows the storage for those to be alloacted
|
||||
// by the caller on the stack for small batches
|
||||
DECLARE_SYNC_ASYNC_AND_CALLBACK(
|
||||
void, MultiGet, MultiGetAsync,
|
||||
(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input, AsyncCallback& callback),
|
||||
const ReadOptions& _read_options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input = false);
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(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);
|
||||
|
||||
void MultiGetWithCallback(
|
||||
const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
@@ -340,6 +352,11 @@ class DBImpl : public DB {
|
||||
const Slice* keys,
|
||||
PinnableAttributeGroups* results) override;
|
||||
|
||||
void MultiGetEntityLazy(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, size_t num_keys,
|
||||
const Slice* keys, LazyWideColumnsBatch* result,
|
||||
Status* statuses, bool sorted_input) override;
|
||||
|
||||
void MultiGetEntityWithCallback(
|
||||
const ReadOptions& read_options, ColumnFamilyHandle* column_family,
|
||||
ReadCallback* callback,
|
||||
@@ -477,6 +494,8 @@ class DBImpl : public DB {
|
||||
void DisableManualCompaction() override;
|
||||
void AbortAllCompactions() override;
|
||||
void ResumeAllCompactions() override;
|
||||
void AbortCompactions(ColumnFamilyHandle* column_family) override;
|
||||
void ResumeCompactions(ColumnFamilyHandle* column_family) override;
|
||||
|
||||
using DB::SetOptions;
|
||||
Status SetOptions(
|
||||
@@ -573,10 +592,10 @@ class DBImpl : public DB {
|
||||
Status GetCurrentWalFile(std::unique_ptr<WalFile>* current_wal_file) override;
|
||||
Status GetCreationTimeOfOldestFile(uint64_t* creation_time) override;
|
||||
|
||||
Status GetUpdatesSince(
|
||||
SequenceNumber seq_number, std::unique_ptr<TransactionLogIterator>* iter,
|
||||
const TransactionLogIterator::ReadOptions& read_options =
|
||||
TransactionLogIterator::ReadOptions()) override;
|
||||
Status GetUpdatesSince(SequenceNumber seq_number,
|
||||
std::unique_ptr<WalIterator>* iter,
|
||||
const WalIterator::ReadOptions& read_options =
|
||||
WalIterator::ReadOptions()) override;
|
||||
Status DeleteFilesInRanges(ColumnFamilyHandle* column_family,
|
||||
const RangeOpt* ranges, size_t n,
|
||||
bool include_end = true);
|
||||
@@ -589,6 +608,35 @@ class DBImpl : public DB {
|
||||
const LiveFilesStorageInfoOptions& opts,
|
||||
std::vector<LiveFileStorageInfo>* files) override;
|
||||
|
||||
// Variant of GetLiveFilesStorageInfo used by subset-CF Checkpoint. Only
|
||||
// table/blob files of the given column families are captured; the rest are
|
||||
// reported in excluded_cf_ids so the caller can reconcile the copied
|
||||
// MANIFEST (typically via AppendColumnFamilyDropsToManifest below). Caller
|
||||
// must supply a non-empty include_cf_ids containing the default CF id and a
|
||||
// non-null excluded_cf_ids; both are internal-contract preconditions.
|
||||
Status GetLiveFilesStorageInfoForSubsetCheckpoint(
|
||||
const LiveFilesStorageInfoOptions& opts,
|
||||
const std::vector<uint32_t>& include_cf_ids,
|
||||
std::vector<LiveFileStorageInfo>* files,
|
||||
std::vector<uint32_t>* excluded_cf_ids);
|
||||
|
||||
// Shared body of GetLiveFilesStorageInfo and its subset-checkpoint variant.
|
||||
// include_cf_ids empty -> no filter; otherwise records skipped CF ids in
|
||||
// *excluded_cf_ids (which must be non-null in that case).
|
||||
Status GetLiveFilesStorageInfoImpl(
|
||||
const LiveFilesStorageInfoOptions& opts,
|
||||
const std::vector<uint32_t>& include_cf_ids,
|
||||
std::vector<LiveFileStorageInfo>* files,
|
||||
std::vector<uint32_t>* excluded_cf_ids);
|
||||
|
||||
// Appends kColumnFamilyDrop records to the MANIFEST file at manifest_path for
|
||||
// the given column family ids. Used by Checkpoint to make a subset-CF
|
||||
// checkpoint's copied MANIFEST consistent with the reduced file set. Operates
|
||||
// only on the given file; does not mutate this DB's live state.
|
||||
Status AppendColumnFamilyDropsToManifest(const std::string& manifest_path,
|
||||
uint64_t manifest_size,
|
||||
const std::vector<uint32_t>& cf_ids);
|
||||
|
||||
Status GetPreparedFileInfoForExternalSstIngestion(
|
||||
const std::string& file_path,
|
||||
std::shared_ptr<const PreparedFileInfo>* file_info) override;
|
||||
@@ -729,6 +777,24 @@ class DBImpl : public DB {
|
||||
PinnableSlice* merge_operands = nullptr;
|
||||
GetMergeOperandsOptions* get_merge_operands_options = nullptr;
|
||||
int* number_of_operands = nullptr;
|
||||
|
||||
// Lazy wide-column mode (used by GetEntityLazy / MultiGetEntityLazy). When
|
||||
// `lazy_columns_pin` is non-null, the SST point-lookup path leaves a
|
||||
// wide-column entity's blob references *unresolved* (see
|
||||
// GetContext::SaveWideColumnEntityToColumns), so the caller can resolve
|
||||
// them on demand later. On a successful lookup GetImpl then:
|
||||
// - sets `*lazy_columns_version` to the Version the entity was read from
|
||||
// (the resolution context for blob references), and
|
||||
// - transfers an extra SuperVersion reference by registering a cleanup on
|
||||
// `*lazy_columns_pin`, so the result may outlive this call (as an
|
||||
// iterator's result does).
|
||||
// The SameFileBlobReader for the SST that held the entity (if any) is
|
||||
// stored in `*lazy_columns_same_file_reader` by the read path, so same-file
|
||||
// ("embedded") references stay resolvable later. All three must be set
|
||||
// together. `columns` must also be set (the entity output).
|
||||
Cleanable* lazy_columns_pin = nullptr;
|
||||
const Version** lazy_columns_version = nullptr;
|
||||
const SameFileBlobReader** lazy_columns_same_file_reader = nullptr;
|
||||
};
|
||||
|
||||
DECLARE_SYNC_AND_ASYNC(Status, GetImpl, const ReadOptions& read_options,
|
||||
@@ -750,6 +816,17 @@ class DBImpl : public DB {
|
||||
const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
|
||||
// Shared implementation of GetEntityLazy / MultiGetEntityLazy for a single
|
||||
// key: performs a lazy point lookup for `key` (leaving blob references
|
||||
// unresolved) into `*result`, which takes ownership of a SuperVersion pin so
|
||||
// it may outlive this call. `read_options` must already be finalized (e.g.
|
||||
// io_activity set). Returns OK on success, NotFound (with an empty `*result`)
|
||||
// if there is no entry, or another non-OK status on error. REQUIRES:
|
||||
// max_open_files == -1 has already been validated by the caller.
|
||||
Status GetEntityLazyImpl(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
LazyWideColumns* result);
|
||||
|
||||
// If `snapshot` == kMaxSequenceNumber, set a recent one inside the file.
|
||||
ArenaWrappedDBIter* NewIteratorImpl(const ReadOptions& options,
|
||||
ColumnFamilyHandleImpl* cfh,
|
||||
@@ -901,6 +978,13 @@ class DBImpl : public DB {
|
||||
void CleanupIteratorSuperVersion(SuperVersion* super_version,
|
||||
bool background_purge);
|
||||
|
||||
// Transfer an extra reference to `super_version` into `pin` (via a cleanup
|
||||
// callback), so `pin`'s owner keeps the SuperVersion -- and thus the data it
|
||||
// references -- alive after the current call returns, as an iterator does.
|
||||
// Shared by the lazy wide-column read path (GetEntityLazy) across the
|
||||
// primary, read-only, and secondary GetImpl overrides.
|
||||
void TransferSuperVersionPin(SuperVersion* super_version, Cleanable* pin);
|
||||
|
||||
LogsWithPrepTracker* logs_with_prep_tracker() {
|
||||
return &logs_with_prep_tracker_;
|
||||
}
|
||||
@@ -1418,6 +1502,10 @@ class DBImpl : public DB {
|
||||
|
||||
bool seq_per_batch() const { return seq_per_batch_; }
|
||||
|
||||
#if USE_COROUTINES
|
||||
CoroDB* GetCoroDB() override { return this; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
const std::string dbname_;
|
||||
const bool read_only_;
|
||||
@@ -1562,7 +1650,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);
|
||||
@@ -2801,9 +2889,24 @@ class DBImpl : public DB {
|
||||
ColumnFamilyData* PopFirstFromCompactionQueue();
|
||||
FlushRequest PopFirstFromFlushQueue();
|
||||
|
||||
struct CompactionQueueThrottled {};
|
||||
struct CompactionQueueParked {};
|
||||
struct CompactionQueuePicked {
|
||||
ColumnFamilyData* cfd;
|
||||
};
|
||||
using CompactionQueuePickResult =
|
||||
std::variant<CompactionQueuePicked, CompactionQueueThrottled,
|
||||
CompactionQueueParked>;
|
||||
|
||||
// Pick the first unthrottled compaction with task token from queue.
|
||||
ColumnFamilyData* PickCompactionFromQueue(
|
||||
CompactionQueuePickResult PickCompactionFromQueue(
|
||||
std::unique_ptr<TaskLimiterToken>* token, LogBuffer* log_buffer);
|
||||
bool IsCompactionAborted(ColumnFamilyData* cfd) const;
|
||||
bool MustWaitForCompaction(ColumnFamilyData* cfd);
|
||||
bool HasPendingManualCompaction(ColumnFamilyData* cfd);
|
||||
bool RestoreParkedCompaction(ColumnFamilyData* cfd);
|
||||
void BeginInFlightCompaction(ColumnFamilyData* cfd);
|
||||
bool EndInFlightCompaction(ColumnFamilyData* cfd);
|
||||
|
||||
IOStatus SyncWalImpl(bool include_current_wal,
|
||||
const WriteOptions& write_options,
|
||||
@@ -3084,39 +3187,34 @@ class DBImpl : public DB {
|
||||
// Resolves a plain value whose current payload is an encoded direct-write
|
||||
// blob index, either through `value` directly or through the default-column
|
||||
// view layered on `columns`.
|
||||
static Status ResolveDirectWritePlainValue(const ReadOptions& read_options,
|
||||
const Slice& key,
|
||||
const Version* current,
|
||||
ColumnFamilyData* cfd,
|
||||
static Status ResolveDirectWritePlainValue(const Slice& key,
|
||||
const BlobFetcher& blob_fetcher,
|
||||
PinnableSlice* value,
|
||||
PinnableWideColumns* columns);
|
||||
// Resolves each unresolved direct-write blob-valued column in `columns` and
|
||||
// rebuilds the serialized wide-column entity in place.
|
||||
static Status ResolveDirectWriteWideColumns(const ReadOptions& read_options,
|
||||
const Slice& key,
|
||||
const Version* current,
|
||||
ColumnFamilyData* cfd,
|
||||
static Status ResolveDirectWriteWideColumns(const Slice& key,
|
||||
const BlobFetcher& blob_fetcher,
|
||||
PinnableWideColumns* columns);
|
||||
// Dispatches between plain-value and wide-column direct-write resolution for
|
||||
// read results observed before flush makes the corresponding blob file
|
||||
// visible through normal Version metadata.
|
||||
static bool MaybeResolveDirectWriteValue(
|
||||
const ReadOptions& read_options, const Slice& key,
|
||||
bool resolve_direct_write_value, const Version* current,
|
||||
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index, bool* value_found = nullptr);
|
||||
bool resolve_direct_write_value, const BlobFetcher& blob_fetcher,
|
||||
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,
|
||||
const BlobFetcher* blob_fetcher, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
|
||||
bool* value_found = nullptr);
|
||||
// Resolves memtable read results that still carry blob references through
|
||||
// either a raw blob-index payload in `value` or unresolved blob columns in
|
||||
// `columns`. Unlike the direct-write helper above, this path only depends on
|
||||
// a BlobFetcher and therefore works for read-only/secondary DBs. Sets
|
||||
// `columns`. This generic path is also used by 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
|
||||
@@ -3190,6 +3288,10 @@ class DBImpl : public DB {
|
||||
// DB mutex in compaction code paths.
|
||||
std::atomic<int> compaction_aborted_ = 0;
|
||||
|
||||
// Number of AbortCompactions() callers waiting for a targeted compaction to
|
||||
// finish. Protected by the DB mutex.
|
||||
int per_cf_compaction_abort_waiters_ = 0;
|
||||
|
||||
// This condition variable is signaled on these conditions:
|
||||
// * whenever bg_compaction_scheduled_ goes down to 0
|
||||
// * if AnyManualCompaction, whenever a compaction finishes, even if it hasn't
|
||||
@@ -3399,7 +3501,7 @@ class DBImpl : public DB {
|
||||
// cfd->imm()->IsFlushPending()
|
||||
// A column family is inserted into compaction_queue_ when it satisfied
|
||||
// condition cfd->NeedsCompaction()
|
||||
// Column families in this list are all Ref()-erenced
|
||||
// Column families in these collections are all Ref()-erenced.
|
||||
// TODO(icanadi) Provide some kind of ReferencedColumnFamily class that will
|
||||
// do RAII on ColumnFamilyData
|
||||
// Column families are in this queue when they need to be flushed or
|
||||
@@ -3414,9 +3516,23 @@ class DBImpl : public DB {
|
||||
// invariant(column family present in flush_queue_ <==>
|
||||
// ColumnFamilyData::pending_flush_ == true)
|
||||
std::deque<FlushRequest> flush_queue_;
|
||||
// invariant(column family present in compaction_queue_ <==>
|
||||
// ColumnFamilyData::pending_compaction_ == true)
|
||||
// invariant((column family in compaction_queue_ or
|
||||
// column family in parked_compaction_cfds_)
|
||||
// <=> queued_for_compaction() == true)
|
||||
//
|
||||
// "Parked" means a column family whose compaction was aborted via
|
||||
// AbortCompactions() while it was queued. When an aborted CF reaches the
|
||||
// front of compaction_queue_, PickCompactionFromQueue() moves it to
|
||||
// parked_compaction_cfds_ instead of scheduling it. The CF does not
|
||||
// immediately lose its place -- only when it reaches the front of the
|
||||
// queue is it parked. This preserves fairness for CFs behind it and
|
||||
// avoids restoring the scheduler credit (which would cause a hot re-pick
|
||||
// loop). On the final ResumeCompactions(), RestoreParkedCompaction()
|
||||
// re-enqueues the CF (or releases its ref if it no longer needs
|
||||
// compaction). Each parked CF carries exactly one Ref() from the
|
||||
// original AddToCompactionQueue() call.
|
||||
std::deque<ColumnFamilyData*> compaction_queue_;
|
||||
std::unordered_set<ColumnFamilyData*> parked_compaction_cfds_;
|
||||
|
||||
// A map to store file numbers and filenames of the files to be purged
|
||||
std::unordered_map<uint64_t, PurgeFileInfo> purge_files_;
|
||||
@@ -3447,6 +3563,10 @@ class DBImpl : public DB {
|
||||
// stores the number of BOTTOM-priority compactions currently running
|
||||
int num_running_bottom_compactions_ = 0;
|
||||
|
||||
// Number of compaction service jobs currently waiting in
|
||||
// CompactionService::Wait(), counted per executed subcompaction.
|
||||
std::atomic<int> num_running_remote_compactions_ = 0;
|
||||
|
||||
// number of background memtable flush jobs, submitted to the HIGH pool
|
||||
int bg_flush_scheduled_ = 0;
|
||||
|
||||
@@ -3770,7 +3890,7 @@ inline Status DBImpl::FailIfTsMismatchCf(ColumnFamilyHandle* column_family,
|
||||
inline Status DBImpl::FailIfTableFilterWithRangeConversion(
|
||||
const ReadOptions& read_options,
|
||||
const MutableCFOptions& mutable_cf_options) const {
|
||||
if (read_options.table_filter &&
|
||||
if (HasTableFilter(read_options) &&
|
||||
mutable_cf_options.min_tombstones_for_range_conversion > 0) {
|
||||
return Status::InvalidArgument(
|
||||
"ReadOptions::table_filter is not supported when "
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
|
||||
// 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 <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <deque>
|
||||
#include <unordered_map>
|
||||
@@ -1195,11 +1196,15 @@ Status DBImpl::CompactRange(const CompactRangeOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice* begin_without_ts,
|
||||
const Slice* end_without_ts) {
|
||||
auto* cfd =
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(column_family)->cfd();
|
||||
assert(cfd);
|
||||
|
||||
if (manual_compaction_paused_.load(std::memory_order_acquire) > 0) {
|
||||
return Status::Incomplete(Status::SubCode::kManualCompactionPaused);
|
||||
}
|
||||
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
if (IsCompactionAborted(cfd)) {
|
||||
return Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
}
|
||||
|
||||
@@ -1610,6 +1615,21 @@ Status DBImpl::CompactFiles(const CompactionOptions& compact_options,
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(column_family)->cfd();
|
||||
assert(cfd);
|
||||
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
BeginInFlightCompaction(cfd);
|
||||
}
|
||||
Defer end_in_flight_compaction([&]() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
// The caller holds a live handle to this CF, so this can never be the final
|
||||
// unref (the return can only be true when the last reference is dropped).
|
||||
[[maybe_unused]] const bool cfd_deleted = EndInFlightCompaction(cfd);
|
||||
assert(!cfd_deleted);
|
||||
if (per_cf_compaction_abort_waiters_ > 0) {
|
||||
bg_cv_.SignalAll();
|
||||
}
|
||||
});
|
||||
|
||||
Status s;
|
||||
JobContext job_context(next_job_id_.fetch_add(1), true);
|
||||
LogBuffer log_buffer(InfoLogLevel::INFO_LEVEL,
|
||||
@@ -1738,8 +1758,7 @@ Status DBImpl::CompactFilesImpl(
|
||||
return Status::ShutdownInProgress();
|
||||
}
|
||||
|
||||
// triggered by AbortAllCompactions
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
if (IsCompactionAborted(cfd)) {
|
||||
return Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
}
|
||||
|
||||
@@ -1866,7 +1885,7 @@ Status DBImpl::CompactFilesImpl(
|
||||
|
||||
c.reset();
|
||||
bg_compaction_scheduled_--;
|
||||
if (bg_compaction_scheduled_ == 0) {
|
||||
if (bg_compaction_scheduled_ == 0 || per_cf_compaction_abort_waiters_ > 0) {
|
||||
bg_cv_.SignalAll();
|
||||
}
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
@@ -1896,7 +1915,7 @@ Status DBImpl::CompactFilesImpl(
|
||||
kManualCompactionCanceledFalse_, compaction_aborted_, db_id_,
|
||||
db_session_id_, c->column_family_data()->GetFullHistoryTsLow(),
|
||||
c->trim_ts(), &blob_callback_, &bg_compaction_scheduled_,
|
||||
&bg_bottom_compaction_scheduled_);
|
||||
&bg_bottom_compaction_scheduled_, &num_running_remote_compactions_);
|
||||
|
||||
// Creating a compaction influences the compaction score because the score
|
||||
// takes running compactions into account (by skipping files that are already
|
||||
@@ -2001,7 +2020,7 @@ Status DBImpl::CompactFilesImpl(
|
||||
c.reset();
|
||||
|
||||
bg_compaction_scheduled_--;
|
||||
if (bg_compaction_scheduled_ == 0) {
|
||||
if (bg_compaction_scheduled_ == 0 || per_cf_compaction_abort_waiters_ > 0) {
|
||||
bg_cv_.SignalAll();
|
||||
}
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
@@ -2471,12 +2490,10 @@ Status DBImpl::RunManualCompaction(
|
||||
return manual.status;
|
||||
}
|
||||
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
// All compactions are being aborted. Return immediately.
|
||||
int counter = compaction_aborted_.load(std::memory_order_acquire);
|
||||
ROCKS_LOG_INFO(
|
||||
immutable_db_options_.info_log,
|
||||
"RunManualCompaction: Aborting due to compaction_aborted_=%d", counter);
|
||||
if (IsCompactionAborted(cfd)) {
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"[%s] RunManualCompaction: aborting compaction",
|
||||
cfd->GetName().c_str());
|
||||
manual.status = Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
manual.done = true;
|
||||
return manual.status;
|
||||
@@ -2506,7 +2523,7 @@ Status DBImpl::RunManualCompaction(
|
||||
// and `CompactRangeOptions::canceled` might not work well together.
|
||||
while (bg_bottom_compaction_scheduled_ > 0 ||
|
||||
bg_compaction_scheduled_ > 0) {
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
if (IsCompactionAborted(cfd)) {
|
||||
// Pretend the error came from compaction so the below cleanup/error
|
||||
// handling code can process it.
|
||||
manual.done = true;
|
||||
@@ -2542,6 +2559,11 @@ Status DBImpl::RunManualCompaction(
|
||||
// true.
|
||||
while (!manual.done) {
|
||||
assert(HasPendingManualCompaction());
|
||||
if (IsCompactionAborted(cfd) && !scheduled && !manual.in_progress) {
|
||||
manual.done = true;
|
||||
manual.status = Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
break;
|
||||
}
|
||||
manual_conflict = false;
|
||||
Compaction* compaction = nullptr;
|
||||
if (ShouldntRunManualCompaction(&manual) || (manual.in_progress == true) ||
|
||||
@@ -2631,7 +2653,7 @@ Status DBImpl::RunManualCompaction(
|
||||
if (!scheduled) {
|
||||
// There is nothing scheduled to wait on, so any cancellation can end the
|
||||
// manual now.
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
if (IsCompactionAborted(cfd)) {
|
||||
// Stop waiting since it was canceled. Pretend the error came from
|
||||
// compaction so the below cleanup/error handling code can process it.
|
||||
manual.done = true;
|
||||
@@ -3299,6 +3321,52 @@ void DBImpl::EnableManualCompaction() {
|
||||
manual_compaction_paused_.fetch_sub(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool DBImpl::IsCompactionAborted(ColumnFamilyData* cfd) const {
|
||||
return compaction_aborted_.load(std::memory_order_acquire) > 0 ||
|
||||
cfd->compaction_aborted() > 0;
|
||||
}
|
||||
|
||||
bool DBImpl::MustWaitForCompaction(ColumnFamilyData* cfd) {
|
||||
mutex_.AssertHeld();
|
||||
// num_in_flight_compactions_ and the picker's compactions_in_progress_ set
|
||||
// cover DIFFERENT windows and neither subsumes the other:
|
||||
// - the counter covers a picked Compaction from pick through the end of
|
||||
// BackgroundCompaction (including NotifyOnCompactionCompleted), which the
|
||||
// picker set does not -- it is cleared during Install's manifest write.
|
||||
// - the picker set covers the bottom-priority handoff: when a LOW thread
|
||||
// forwards a bottom compaction, its EndInFlightCompaction() dips the
|
||||
// counter to 0 while the compaction is still registered and queued for the
|
||||
// BOTTOM pool, and the picker set keeps AbortCompactions() waiting across
|
||||
// that gap. (The re-registered "intended" bottom compaction inherits the
|
||||
// original compaction_reason(), so it is not filtered out below.)
|
||||
if (cfd->num_in_flight_compactions(&mutex_) > 0) {
|
||||
return true;
|
||||
}
|
||||
for (const auto* compaction :
|
||||
*cfd->compaction_picker()->compactions_in_progress()) {
|
||||
const auto reason = compaction->compaction_reason();
|
||||
// These are picker reservations for conflict tracking, not CompactionJobs,
|
||||
// so they never observe the abort atomic in ProcessKeyValue().
|
||||
if (reason != CompactionReason::kExternalSstIngestion &&
|
||||
reason != CompactionReason::kRefitLevel) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void DBImpl::BeginInFlightCompaction(ColumnFamilyData* cfd) {
|
||||
mutex_.AssertHeld();
|
||||
cfd->Ref();
|
||||
cfd->inc_num_in_flight_compactions(&mutex_);
|
||||
}
|
||||
|
||||
bool DBImpl::EndInFlightCompaction(ColumnFamilyData* cfd) {
|
||||
mutex_.AssertHeld();
|
||||
cfd->dec_num_in_flight_compactions(&mutex_);
|
||||
return cfd->UnrefAndTryDelete();
|
||||
}
|
||||
|
||||
void DBImpl::AbortAllCompactions() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
|
||||
@@ -3325,6 +3393,83 @@ void DBImpl::AbortAllCompactions() {
|
||||
}
|
||||
}
|
||||
|
||||
void DBImpl::AbortCompactions(ColumnFamilyHandle* column_family) {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
if (shutting_down_.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert(column_family != nullptr);
|
||||
auto* cfh = static_cast_with_check<ColumnFamilyHandleImpl>(column_family);
|
||||
assert(cfh->db() == this);
|
||||
auto* cfd = cfh->cfd();
|
||||
assert(cfd != nullptr);
|
||||
|
||||
cfd->inc_compaction_aborted(&mutex_);
|
||||
TEST_SYNC_POINT("DBImpl::AbortCompactions:FlagSet");
|
||||
|
||||
for (const auto* manual_compaction : manual_compaction_dequeue_) {
|
||||
if (manual_compaction->cfd == cfd) {
|
||||
manual_compaction->canceled.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
bg_cv_.SignalAll();
|
||||
|
||||
// Wait for this CF's running compactions to finish or abort. If the DB
|
||||
// is shutting down, return early -- the counter stays incremented but the
|
||||
// CFD and DB will be torn down anyway. Callers that use SCOPE_EXIT for
|
||||
// the matching ResumeCompactions() are safe: the resume is a no-op on a
|
||||
// DB that is already closed.
|
||||
++per_cf_compaction_abort_waiters_;
|
||||
while (!shutting_down_.load(std::memory_order_acquire) &&
|
||||
(MustWaitForCompaction(cfd) || HasPendingManualCompaction(cfd))) {
|
||||
TEST_SYNC_POINT("DBImpl::AbortCompactions:Waiting");
|
||||
bg_cv_.Wait();
|
||||
}
|
||||
--per_cf_compaction_abort_waiters_;
|
||||
TEST_SYNC_POINT("DBImpl::AbortCompactions:Complete");
|
||||
}
|
||||
|
||||
void DBImpl::ResumeCompactions(ColumnFamilyHandle* column_family) {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
if (shutting_down_.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert(column_family != nullptr);
|
||||
auto* cfh = static_cast_with_check<ColumnFamilyHandleImpl>(column_family);
|
||||
assert(cfh->db() == this);
|
||||
auto* cfd = cfh->cfd();
|
||||
assert(cfd != nullptr);
|
||||
|
||||
const int before = cfd->compaction_aborted();
|
||||
if (before <= 0) {
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"ResumeCompactions called without prior AbortCompactions "
|
||||
"for column family %s (counter=%d)",
|
||||
cfd->GetName().c_str(), before);
|
||||
return;
|
||||
}
|
||||
|
||||
const int current = cfd->dec_compaction_aborted(&mutex_);
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"[%s] ResumeCompactions: counter %d -> %d",
|
||||
cfd->GetName().c_str(), before, current);
|
||||
if (current == 0) {
|
||||
// TODO: Aborting an automatic compaction after PickCompaction()
|
||||
// releases its inputs without recomputing the compaction score, so this
|
||||
// EnqueuePendingCompaction() can observe a stale sub-trigger score and
|
||||
// queue nothing, leaving the CF idle until the next flush. Pre-existing
|
||||
// (ResumeAllCompactions() has the same gap); fix separately for both APIs
|
||||
// by recomputing the score on the abort path.
|
||||
if (!RestoreParkedCompaction(cfd) && !cfd->IsDropped()) {
|
||||
EnqueuePendingCompaction(cfd);
|
||||
}
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
bg_cv_.SignalAll();
|
||||
}
|
||||
}
|
||||
|
||||
void DBImpl::ResumeAllCompactions() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
int before = compaction_aborted_.load(std::memory_order_acquire);
|
||||
@@ -3350,6 +3495,7 @@ void DBImpl::ResumeAllCompactions() {
|
||||
// If this is the last resume call (abort counter back to 0), schedule
|
||||
// compactions that may have been waiting
|
||||
if (current == 0) {
|
||||
// The stale-score TODO in ResumeCompactions() also applies here.
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
}
|
||||
}
|
||||
@@ -3576,6 +3722,27 @@ void DBImpl::AddToCompactionQueue(ColumnFamilyData* cfd) {
|
||||
++unscheduled_compactions_;
|
||||
}
|
||||
|
||||
bool DBImpl::RestoreParkedCompaction(ColumnFamilyData* cfd) {
|
||||
mutex_.AssertHeld();
|
||||
auto it = parked_compaction_cfds_.find(cfd);
|
||||
if (it == parked_compaction_cfds_.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
parked_compaction_cfds_.erase(it);
|
||||
assert(cfd->queued_for_compaction());
|
||||
if (!cfd->IsDropped() && cfd->NeedsCompaction()) {
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::RestoreParkedCompaction:cfd",
|
||||
static_cast<void*>(cfd));
|
||||
compaction_queue_.push_back(cfd);
|
||||
++unscheduled_compactions_;
|
||||
} else {
|
||||
cfd->set_queued_for_compaction(false);
|
||||
cfd->UnrefAndTryDelete();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ColumnFamilyData* DBImpl::PopFirstFromCompactionQueue() {
|
||||
assert(!compaction_queue_.empty());
|
||||
auto cfd = *compaction_queue_.begin();
|
||||
@@ -3603,22 +3770,37 @@ DBImpl::FlushRequest DBImpl::PopFirstFromFlushQueue() {
|
||||
return flush_req;
|
||||
}
|
||||
|
||||
ColumnFamilyData* DBImpl::PickCompactionFromQueue(
|
||||
DBImpl::CompactionQueuePickResult DBImpl::PickCompactionFromQueue(
|
||||
std::unique_ptr<TaskLimiterToken>* token, LogBuffer* log_buffer) {
|
||||
assert(!compaction_queue_.empty());
|
||||
assert(*token == nullptr);
|
||||
autovector<ColumnFamilyData*> throttled_candidates;
|
||||
ColumnFamilyData* cfd = nullptr;
|
||||
CompactionQueuePickResult result = CompactionQueueThrottled{};
|
||||
while (!compaction_queue_.empty()) {
|
||||
auto first_cfd = *compaction_queue_.begin();
|
||||
compaction_queue_.pop_front();
|
||||
assert(first_cfd->queued_for_compaction());
|
||||
if (first_cfd->IsDropped()) {
|
||||
first_cfd->set_queued_for_compaction(false);
|
||||
result = CompactionQueuePicked{first_cfd};
|
||||
break;
|
||||
}
|
||||
if (first_cfd->compaction_aborted() > 0) {
|
||||
// Parking consumes this worker's credit. Remaining queue entries retain
|
||||
// their own credits and are scheduled when this worker returns.
|
||||
[[maybe_unused]] const bool inserted =
|
||||
parked_compaction_cfds_.insert(first_cfd).second;
|
||||
assert(inserted);
|
||||
TEST_SYNC_POINT("DBImpl::PickCompactionFromQueue:Parked");
|
||||
result = CompactionQueueParked{};
|
||||
break;
|
||||
}
|
||||
if (!RequestCompactionToken(first_cfd, false, token, log_buffer)) {
|
||||
throttled_candidates.push_back(first_cfd);
|
||||
continue;
|
||||
}
|
||||
cfd = first_cfd;
|
||||
cfd->set_queued_for_compaction(false);
|
||||
first_cfd->set_queued_for_compaction(false);
|
||||
result = CompactionQueuePicked{first_cfd};
|
||||
break;
|
||||
}
|
||||
// Add throttled compaction candidates back to queue in the original order.
|
||||
@@ -3626,7 +3808,7 @@ ColumnFamilyData* DBImpl::PickCompactionFromQueue(
|
||||
iter != throttled_candidates.rend(); ++iter) {
|
||||
compaction_queue_.push_front(*iter);
|
||||
}
|
||||
return cfd;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DBImpl::EnqueuePendingFlush(const FlushRequest& flush_req) {
|
||||
@@ -3673,13 +3855,21 @@ bool DBImpl::EnqueuePendingFlush(const FlushRequest& flush_req) {
|
||||
|
||||
void DBImpl::EnqueuePendingCompaction(ColumnFamilyData* cfd) {
|
||||
mutex_.AssertHeld();
|
||||
if (reject_new_background_jobs_) {
|
||||
if (reject_new_background_jobs_ || cfd->IsDropped()) {
|
||||
return;
|
||||
}
|
||||
if (!cfd->queued_for_compaction() && cfd->NeedsCompaction()) {
|
||||
TEST_SYNC_POINT_CALLBACK("EnqueuePendingCompaction::cfd",
|
||||
static_cast<void*>(cfd));
|
||||
AddToCompactionQueue(cfd);
|
||||
if (cfd->compaction_aborted() > 0) {
|
||||
cfd->Ref();
|
||||
cfd->set_queued_for_compaction(true);
|
||||
[[maybe_unused]] const bool inserted =
|
||||
parked_compaction_cfds_.insert(cfd).second;
|
||||
assert(inserted);
|
||||
} else {
|
||||
AddToCompactionQueue(cfd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4139,7 +4329,7 @@ void DBImpl::BackgroundCallCompaction(PrepickedCompaction* prepicked_compaction,
|
||||
prepicked_compaction->task_token.reset();
|
||||
}
|
||||
|
||||
if (made_progress ||
|
||||
if (made_progress || per_cf_compaction_abort_waiters_ > 0 ||
|
||||
(bg_compaction_scheduled_ == 0 &&
|
||||
bg_bottom_compaction_scheduled_ == 0) ||
|
||||
HasPendingManualCompaction() || unscheduled_compactions_ == 0) {
|
||||
@@ -4181,7 +4371,29 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
prepicked_compaction->compaction != nullptr) {
|
||||
c.reset(prepicked_compaction->compaction);
|
||||
}
|
||||
// Track this CF as having an in-flight compaction for the duration of this
|
||||
// BackgroundCompaction (which covers NotifyOnCompactionCompleted). The Defer
|
||||
// ends tracking on every return path. BeginInFlightCompaction()'s Ref keeps a
|
||||
// dropped CF alive across any c.reset() until then; if this ends the CF's
|
||||
// last reference, the FindObsoleteFiles() pass in BackgroundCallCompaction
|
||||
// cleans up its files.
|
||||
ColumnFamilyData* in_flight_cfd = nullptr;
|
||||
Defer end_in_flight_compaction([&]() {
|
||||
if (in_flight_cfd != nullptr) {
|
||||
EndInFlightCompaction(in_flight_cfd);
|
||||
}
|
||||
});
|
||||
if (c != nullptr) {
|
||||
in_flight_cfd = c->column_family_data();
|
||||
BeginInFlightCompaction(in_flight_cfd);
|
||||
}
|
||||
bool is_prepicked = is_manual || c;
|
||||
ColumnFamilyData* prepicked_cfd = nullptr;
|
||||
if (c) {
|
||||
prepicked_cfd = c->column_family_data();
|
||||
} else if (manual_compaction != nullptr) {
|
||||
prepicked_cfd = manual_compaction->cfd;
|
||||
}
|
||||
|
||||
// (manual_compaction->in_progress == false);
|
||||
bool trivial_move_disallowed =
|
||||
@@ -4209,6 +4421,9 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
// can schedule the still-queued work.
|
||||
unscheduled_compactions_++;
|
||||
}
|
||||
} else if (prepicked_cfd != nullptr &&
|
||||
prepicked_cfd->compaction_aborted() > 0) {
|
||||
status = Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
} else if (is_manual &&
|
||||
manual_compaction->canceled.load(std::memory_order_acquire)) {
|
||||
status = Status::Incomplete(Status::SubCode::kManualCompactionPaused);
|
||||
@@ -4313,13 +4528,18 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
ColumnFamilyData* cfd = nullptr;
|
||||
|
||||
if (!need_repick) {
|
||||
cfd = PickCompactionFromQueue(&task_token, log_buffer);
|
||||
if (cfd == nullptr) {
|
||||
auto pick_result = PickCompactionFromQueue(&task_token, log_buffer);
|
||||
if (std::holds_alternative<CompactionQueueThrottled>(pick_result)) {
|
||||
// Can't find any executable task from the compaction queue.
|
||||
// All tasks have been throttled by compaction thread limiter.
|
||||
++unscheduled_compactions_;
|
||||
return Status::Busy();
|
||||
}
|
||||
if (std::holds_alternative<CompactionQueueParked>(pick_result)) {
|
||||
return Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
}
|
||||
cfd = std::get<CompactionQueuePicked>(pick_result).cfd;
|
||||
assert(cfd != nullptr);
|
||||
|
||||
// We unreference here because the following code will take a Ref() on
|
||||
// this cfd if it is going to use it (Compaction class holds a
|
||||
@@ -4360,6 +4580,10 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
mutable_cf_options, mutable_db_options_, job_context->snapshot_seqs,
|
||||
job_context->snapshot_checker, log_buffer,
|
||||
thread_pri == Env::Priority::BOTTOM /* require_max_output_level */));
|
||||
if (c != nullptr && in_flight_cfd == nullptr) {
|
||||
in_flight_cfd = cfd;
|
||||
BeginInFlightCompaction(in_flight_cfd);
|
||||
}
|
||||
if (thread_pri == Env::Priority::LOW) {
|
||||
TEST_SYNC_POINT("DBImpl::BackgroundCompaction():AfterPickCompaction");
|
||||
} else if (thread_pri == Env::Priority::BOTTOM) {
|
||||
@@ -4840,7 +5064,7 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
compaction_aborted_, db_id_, db_session_id_,
|
||||
c->column_family_data()->GetFullHistoryTsLow(), c->trim_ts(),
|
||||
&blob_callback_, &bg_compaction_scheduled_,
|
||||
&bg_bottom_compaction_scheduled_);
|
||||
&bg_bottom_compaction_scheduled_, &num_running_remote_compactions_);
|
||||
compaction_job.Prepare(std::nullopt /*subcompact to be computed*/);
|
||||
|
||||
std::unique_ptr<std::list<uint64_t>::iterator> min_options_file_number_elem;
|
||||
@@ -5082,6 +5306,14 @@ bool DBImpl::HasPendingManualCompaction() {
|
||||
return (!manual_compaction_dequeue_.empty());
|
||||
}
|
||||
|
||||
bool DBImpl::HasPendingManualCompaction(ColumnFamilyData* cfd) {
|
||||
return std::any_of(manual_compaction_dequeue_.begin(),
|
||||
manual_compaction_dequeue_.end(),
|
||||
[cfd](const ManualCompactionState* manual) {
|
||||
return manual->cfd == cfd;
|
||||
});
|
||||
}
|
||||
|
||||
void DBImpl::AddManualCompaction(DBImpl::ManualCompactionState* m) {
|
||||
assert(manual_compaction_paused_ == 0);
|
||||
manual_compaction_dequeue_.push_back(m);
|
||||
@@ -5432,6 +5664,7 @@ Status DBImpl::WaitForCompact(
|
||||
}
|
||||
if ((bg_bottom_compaction_scheduled_ || bg_compaction_scheduled_ ||
|
||||
bg_flush_scheduled_ || unscheduled_compactions_ ||
|
||||
!parked_compaction_cfds_.empty() ||
|
||||
(wait_for_compact_options.wait_for_purge && bg_purge_scheduled_) ||
|
||||
unscheduled_flushes_ || error_handler_.IsRecoveryInProgress()) &&
|
||||
(error_handler_.GetBGError().ok())) {
|
||||
|
||||
@@ -55,6 +55,8 @@ Env::IOActivity GetCurrentThreadIOActivityForMetadataRead() {
|
||||
case ThreadStatus::OperationType::
|
||||
OP_GET_FILE_CHECKSUMS_FROM_CURRENT_MANIFEST:
|
||||
return Env::IOActivity::kGetFileChecksumsFromCurrentManifest;
|
||||
case ThreadStatus::OperationType::OP_LAZY_RESOLVE:
|
||||
return Env::IOActivity::kLazyResolve;
|
||||
default:
|
||||
return Env::IOActivity::kUnknown;
|
||||
}
|
||||
|
||||
@@ -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. ");
|
||||
@@ -2171,7 +2176,7 @@ Status DBImpl::WriteLevel0TableForRecovery(int job_id, ColumnFamilyData* cfd,
|
||||
false /* is_bottommost */, TableFileCreationReason::kRecovery,
|
||||
0 /* oldest_key_time */, 0 /* file_creation_time */, db_id_,
|
||||
db_session_id_, 0 /* target_file_size */, meta.fd.GetNumber(),
|
||||
kMaxSequenceNumber);
|
||||
kMaxSequenceNumber, dbname_ /* db_name */);
|
||||
Version* version = cfd->current();
|
||||
version->Ref();
|
||||
TableProperties temp_table_proerties;
|
||||
@@ -2872,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();
|
||||
@@ -2966,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 {
|
||||
|
||||
@@ -128,7 +128,8 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
/*value_found*/ nullptr,
|
||||
/*key_exists*/ nullptr, /*seq*/ nullptr, &read_cb,
|
||||
/*is_blob*/ nullptr,
|
||||
/*do_merge=*/get_impl_options.get_value);
|
||||
/*do_merge=*/get_impl_options.get_value,
|
||||
get_impl_options.lazy_columns_same_file_reader);
|
||||
RecordTick(stats_, MEMTABLE_MISS);
|
||||
}
|
||||
{
|
||||
@@ -156,6 +157,17 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
PERF_COUNTER_ADD(get_read_bytes, size);
|
||||
}
|
||||
}
|
||||
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy): hand back the Version the entity's blob
|
||||
// references must be resolved against, and transfer a SuperVersion pin into
|
||||
// the result so it -- and deferred blob reads -- stay valid after this call
|
||||
// (as an iterator's pin does). Read-only borrows the current SuperVersion
|
||||
// without a reference, so the pin takes its own.
|
||||
if (get_impl_options.lazy_columns_version != nullptr) {
|
||||
*get_impl_options.lazy_columns_version = super_version->current;
|
||||
}
|
||||
TransferSuperVersionPin(super_version, get_impl_options.lazy_columns_pin);
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
|
||||
|
||||
@@ -1362,9 +1362,6 @@ Status DB::OpenAndCompact(
|
||||
db_options.statistics = override_options.statistics;
|
||||
db_options.listeners = override_options.listeners;
|
||||
db_options.compaction_service = nullptr;
|
||||
// We will close the DB after the compaction anyway.
|
||||
// Open as many files as needed for the compaction.
|
||||
db_options.max_open_files = -1;
|
||||
db_options.info_log = override_options.info_log;
|
||||
|
||||
// 4. Filter CFs that are needed for OpenAndCompact()
|
||||
|
||||
@@ -185,11 +185,22 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
/*value_found*/ nullptr,
|
||||
/*key_exists*/ nullptr, /*seq*/ nullptr, &read_cb,
|
||||
/*is_blob*/ nullptr,
|
||||
/*do_merge=*/get_impl_options.get_value);
|
||||
/*do_merge=*/get_impl_options.get_value,
|
||||
get_impl_options.lazy_columns_same_file_reader);
|
||||
RecordTick(stats_, MEMTABLE_MISS);
|
||||
}
|
||||
{
|
||||
PERF_TIMER_GUARD(get_post_process_time);
|
||||
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy): hand back the resolution Version and take
|
||||
// a SuperVersion pin for the result before the borrowed reference below
|
||||
// is released, so the result -- and deferred blob reads -- stay valid
|
||||
// after this call (as an iterator's pin does).
|
||||
if (get_impl_options.lazy_columns_version != nullptr) {
|
||||
*get_impl_options.lazy_columns_version = super_version->current;
|
||||
}
|
||||
TransferSuperVersionPin(super_version, get_impl_options.lazy_columns_pin);
|
||||
}
|
||||
#if defined(WITH_COROUTINES)
|
||||
CleanupSuperVersion(super_version);
|
||||
#else
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
#include "util/coro_utils.h"
|
||||
|
||||
#if defined(USE_COROUTINES) && defined(WITH_COROUTINES)
|
||||
#include "folly/executors/IOExecutor.h"
|
||||
#include "folly/io/async/EventBase.h"
|
||||
#include "util/coro_stats_util.h"
|
||||
#endif // USE_COROUTINES && WITH_COROUTINES
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
@@ -26,6 +25,10 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::Get)
|
||||
(const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value, std::string* timestamp) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
assert(value != nullptr);
|
||||
value->Reset();
|
||||
|
||||
@@ -41,19 +44,8 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::Get)
|
||||
read_options.io_activity = Env::IOActivity::kGet;
|
||||
}
|
||||
|
||||
#ifdef WITH_COROUTINES
|
||||
auto* read_executor =
|
||||
immutable_db_options_.env->GetFileSystem()->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);
|
||||
CO_RETURN CO_AWAIT(GetImpl, read_options, column_family, key, value,
|
||||
timestamp);
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
@@ -250,8 +242,9 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
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);
|
||||
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()) &&
|
||||
@@ -266,8 +259,9 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
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);
|
||||
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);
|
||||
}
|
||||
@@ -307,14 +301,16 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
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);
|
||||
get_impl_options.get_value,
|
||||
get_impl_options.lazy_columns_same_file_reader);
|
||||
if (get_impl_options.get_value && resolve_direct_write_value) {
|
||||
assert(memtable_blob_fetcher_ptr != nullptr);
|
||||
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,
|
||||
resolve_direct_write_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_MISS);
|
||||
@@ -407,6 +403,19 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
PERF_COUNTER_ADD(get_read_bytes, size);
|
||||
}
|
||||
|
||||
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy): the entity's blob references were left
|
||||
// unresolved. Hand back the Version they must be resolved against and
|
||||
// transfer an extra SuperVersion reference into the result's pin, so the
|
||||
// result -- and thus deferred blob reads -- stay valid after this call
|
||||
// returns (as an iterator's SuperVersion pin does). The reference is
|
||||
// released when the pin's cleanup runs (result destruction); the
|
||||
// borrowed reference for this call is dropped by the sv_cleanup Defer.
|
||||
if (get_impl_options.lazy_columns_version != nullptr) {
|
||||
*get_impl_options.lazy_columns_version = sv->current;
|
||||
}
|
||||
TransferSuperVersionPin(sv, get_impl_options.lazy_columns_pin);
|
||||
}
|
||||
RecordInHistogram(stats_, BYTES_PER_READ, size);
|
||||
}
|
||||
CO_RETURN s;
|
||||
@@ -574,12 +583,13 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::MultiGetImpl)
|
||||
key->is_blob_index = false;
|
||||
*key->s = Status::Aborted();
|
||||
} else {
|
||||
assert(memtable_blob_fetcher_ptr != nullptr);
|
||||
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,
|
||||
/*resolve_direct_write_value=*/true, *memtable_blob_fetcher_ptr,
|
||||
key->value, key->columns, key->s, &key->is_blob_index);
|
||||
}
|
||||
}
|
||||
@@ -791,6 +801,10 @@ 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) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kMultiGet) {
|
||||
Status s = Status::InvalidArgument(
|
||||
@@ -807,22 +821,10 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGet)
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kMultiGet;
|
||||
}
|
||||
#ifdef WITH_COROUTINES
|
||||
auto* read_executor =
|
||||
immutable_db_options_.env->GetFileSystem()->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);
|
||||
CO_AWAIT(MultiGetCommon, read_options, num_keys, column_families, keys,
|
||||
values,
|
||||
/* columns */ nullptr, timestamps, statuses, sorted_input);
|
||||
CO_RETURN;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
+4
-3
@@ -26,6 +26,7 @@
|
||||
#include "logging/logging.h"
|
||||
#include "memory/arena.h"
|
||||
#include "monitoring/perf_context_imp.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "port/likely.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/io_dispatcher.h"
|
||||
@@ -78,8 +79,8 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
|
||||
merge_operator_(ioptions.merge_operator.get()),
|
||||
iter_(iter),
|
||||
// Enable the direct-write blob fallback only when this CF has a
|
||||
// write-path partition manager; otherwise the fetcher stays on the plain
|
||||
// Version::GetBlob fast path.
|
||||
// write-path partition manager; otherwise the fetcher stays on the
|
||||
// version-backed path.
|
||||
blob_state_(version, read_options, cfd ? cfd->blob_file_cache() : nullptr,
|
||||
cfd != nullptr && cfd->blob_partition_manager() != nullptr),
|
||||
read_callback_(read_callback),
|
||||
@@ -122,7 +123,7 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
|
||||
// prefix_same_as_start do not guarantee complete scans, so conversion
|
||||
// must stay disabled for the iterator lifetime.
|
||||
min_tombstones_for_range_conversion_(
|
||||
active_mem != nullptr && !read_options.table_filter &&
|
||||
active_mem != nullptr && !HasTableFilter(read_options) &&
|
||||
(expect_total_order_inner_iter_ || prefix_same_as_start_) &&
|
||||
HasFullTimestampVisibility(read_options)
|
||||
? mutable_cf_options.min_tombstones_for_range_conversion
|
||||
|
||||
+18
-16
@@ -3230,16 +3230,18 @@ TEST_P(DBIteratorTest, TableFilter) {
|
||||
{
|
||||
std::set<uint64_t> unseen{1, 2, 3};
|
||||
ReadOptions opts;
|
||||
opts.table_filter = [&](const TableProperties& props) {
|
||||
auto it = unseen.find(props.num_entries);
|
||||
if (it == unseen.end()) {
|
||||
ADD_FAILURE() << "saw table properties with an unexpected "
|
||||
<< props.num_entries << " entries";
|
||||
} else {
|
||||
unseen.erase(it);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
std::function<bool(const TableProperties&)> filter =
|
||||
[&](const TableProperties& props) {
|
||||
auto it = unseen.find(props.num_entries);
|
||||
if (it == unseen.end()) {
|
||||
ADD_FAILURE() << "saw table properties with an unexpected "
|
||||
<< props.num_entries << " entries";
|
||||
} else {
|
||||
unseen.erase(it);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
opts.table_filter = &filter;
|
||||
auto iter = NewIterator(opts);
|
||||
iter->SeekToFirst();
|
||||
ASSERT_EQ(IterStatus(iter), "a->1");
|
||||
@@ -3264,9 +3266,9 @@ TEST_P(DBIteratorTest, TableFilter) {
|
||||
// during iteration.
|
||||
{
|
||||
ReadOptions opts;
|
||||
opts.table_filter = [](const TableProperties& props) {
|
||||
return props.num_entries != 2;
|
||||
};
|
||||
std::function<bool(const TableProperties&)> filter =
|
||||
[](const TableProperties& props) { return props.num_entries != 2; };
|
||||
opts.table_filter = &filter;
|
||||
auto iter = NewIterator(opts);
|
||||
iter->SeekToFirst();
|
||||
ASSERT_EQ(IterStatus(iter), "a->1");
|
||||
@@ -7151,9 +7153,9 @@ TEST_P(ReadPathRangeTombstoneTest, TableFilterNotAllowed) {
|
||||
|
||||
{
|
||||
ReadOptions filtered_ro;
|
||||
filtered_ro.table_filter = [](const TableProperties& props) {
|
||||
return props.num_entries != 2;
|
||||
};
|
||||
std::function<bool(const TableProperties&)> filter =
|
||||
[](const TableProperties& props) { return props.num_entries != 2; };
|
||||
filtered_ro.table_filter = &filter;
|
||||
// Hiding the two-delete SST would otherwise leave this iterator with a
|
||||
// partial SST view plus the previously converted memtable tombstone,
|
||||
// allowing hidden SST state to affect the filtered read result.
|
||||
|
||||
+104
-28
@@ -18,14 +18,12 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class DBTestXactLogIterator : public DBTestBase {
|
||||
class DBWalIteratorTest : public DBTestBase {
|
||||
public:
|
||||
DBTestXactLogIterator()
|
||||
: DBTestBase("db_log_iter_test", /*env_do_fsync=*/true) {}
|
||||
DBWalIteratorTest() : DBTestBase("db_log_iter_test", /*env_do_fsync=*/true) {}
|
||||
|
||||
std::unique_ptr<TransactionLogIterator> OpenTransactionLogIter(
|
||||
const SequenceNumber seq) {
|
||||
std::unique_ptr<TransactionLogIterator> iter;
|
||||
std::unique_ptr<WalIterator> OpenWalIter(const SequenceNumber seq) {
|
||||
std::unique_ptr<WalIterator> iter;
|
||||
Status status = dbfull()->GetUpdatesSince(seq, &iter);
|
||||
EXPECT_OK(status);
|
||||
EXPECT_TRUE(iter->Valid());
|
||||
@@ -34,8 +32,8 @@ class DBTestXactLogIterator : public DBTestBase {
|
||||
};
|
||||
|
||||
namespace {
|
||||
SequenceNumber ReadRecords(std::unique_ptr<TransactionLogIterator>& iter,
|
||||
int& count, bool expect_ok = true) {
|
||||
SequenceNumber ReadRecords(std::unique_ptr<WalIterator>& iter, int& count,
|
||||
bool expect_ok = true) {
|
||||
count = 0;
|
||||
SequenceNumber lastSequence = 0;
|
||||
BatchResult res;
|
||||
@@ -56,14 +54,14 @@ SequenceNumber ReadRecords(std::unique_ptr<TransactionLogIterator>& iter,
|
||||
}
|
||||
|
||||
void ExpectRecords(const int expected_no_records,
|
||||
std::unique_ptr<TransactionLogIterator>& iter) {
|
||||
std::unique_ptr<WalIterator>& iter) {
|
||||
int num_records;
|
||||
ReadRecords(iter, num_records);
|
||||
ASSERT_EQ(num_records, expected_no_records);
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
TEST_F(DBTestXactLogIterator, TransactionLogIterator) {
|
||||
TEST_F(DBWalIteratorTest, Basic) {
|
||||
do {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
@@ -73,7 +71,7 @@ TEST_F(DBTestXactLogIterator, TransactionLogIterator) {
|
||||
ASSERT_OK(Put(1, "key2", DummyString(1024)));
|
||||
ASSERT_EQ(dbfull()->GetLatestSequenceNumber(), 3U);
|
||||
{
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
ExpectRecords(3, iter);
|
||||
}
|
||||
ReopenWithColumnFamilies({"default", "pikachu"}, options);
|
||||
@@ -84,14 +82,14 @@ TEST_F(DBTestXactLogIterator, TransactionLogIterator) {
|
||||
ASSERT_OK(Put(0, "key6", DummyString(1024)));
|
||||
}
|
||||
{
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
ExpectRecords(6, iter);
|
||||
}
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
#ifndef NDEBUG // sync point is not included with DNDEBUG build
|
||||
TEST_F(DBTestXactLogIterator, TransactionLogIteratorRace) {
|
||||
TEST_F(DBWalIteratorTest, Race) {
|
||||
static const int LOG_ITERATOR_RACE_TEST_COUNT = 2;
|
||||
static const char* sync_points[LOG_ITERATOR_RACE_TEST_COUNT][4] = {
|
||||
{"WalManager::GetSortedWalFiles:1", "WalManager::PurgeObsoleteFiles:1",
|
||||
@@ -123,7 +121,7 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorRace) {
|
||||
ASSERT_OK(dbfull()->FlushWAL(false));
|
||||
|
||||
{
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
ExpectRecords(4, iter);
|
||||
}
|
||||
|
||||
@@ -140,14 +138,14 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorRace) {
|
||||
ASSERT_OK(dbfull()->FlushWAL(false));
|
||||
{
|
||||
// this iter would miss "key4" if not fixed
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
ExpectRecords(5, iter);
|
||||
}
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBTestXactLogIterator, TransactionLogIteratorCheckWhenArchive) {
|
||||
TEST_F(DBWalIteratorTest, CheckWhenArchive) {
|
||||
RelaxedAtomic<bool> callback_hit{};
|
||||
do {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearTrace();
|
||||
@@ -173,7 +171,7 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorCheckWhenArchive) {
|
||||
callback_hit.StoreRelaxed(false);
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"WalManager::PurgeObsoleteFiles:1", [&](void*) {
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
ExpectRecords(4, iter);
|
||||
callback_hit.StoreRelaxed(true);
|
||||
});
|
||||
@@ -192,12 +190,12 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorCheckWhenArchive) {
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_F(DBTestXactLogIterator, TransactionLogIteratorStallAtLastRecord) {
|
||||
TEST_F(DBWalIteratorTest, StallAtLastRecord) {
|
||||
do {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("key1", DummyString(1024)));
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
iter->Next();
|
||||
@@ -210,7 +208,7 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorStallAtLastRecord) {
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBTestXactLogIterator, TransactionLogIteratorCheckAfterRestart) {
|
||||
TEST_F(DBWalIteratorTest, CheckAfterRestart) {
|
||||
do {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
@@ -218,12 +216,12 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorCheckAfterRestart) {
|
||||
ASSERT_OK(Put("key2", DummyString(1023)));
|
||||
ASSERT_OK(dbfull()->Flush(FlushOptions()));
|
||||
Reopen(options);
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
ExpectRecords(2, iter);
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBTestXactLogIterator, TransactionLogIteratorCorruptedLog) {
|
||||
TEST_F(DBWalIteratorTest, CorruptedLog) {
|
||||
do {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
@@ -254,18 +252,18 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorCorruptedLog) {
|
||||
|
||||
// Try to read from the beginning. Should stop before the gap and read less
|
||||
// than 1025 entries
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
int count = 0;
|
||||
SequenceNumber last_sequence_read = ReadRecords(iter, count, false);
|
||||
ASSERT_LT(last_sequence_read, 1025U);
|
||||
|
||||
// Try to read past the gap, should be able to seek to key1025
|
||||
auto iter2 = OpenTransactionLogIter(last_sequence_read + 1);
|
||||
auto iter2 = OpenWalIter(last_sequence_read + 1);
|
||||
ExpectRecords(1, iter2);
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBTestXactLogIterator, TransactionLogIteratorBatchOperations) {
|
||||
TEST_F(DBWalIteratorTest, BatchOperations) {
|
||||
do {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
@@ -280,12 +278,12 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorBatchOperations) {
|
||||
ASSERT_OK(Flush(0));
|
||||
ReopenWithColumnFamilies({"default", "pikachu"}, options);
|
||||
ASSERT_OK(Put(1, "key4", DummyString(1024)));
|
||||
auto iter = OpenTransactionLogIter(3);
|
||||
auto iter = OpenWalIter(3);
|
||||
ExpectRecords(2, iter);
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBTestXactLogIterator, TransactionLogIteratorBlobs) {
|
||||
TEST_F(DBWalIteratorTest, Blobs) {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
@@ -301,7 +299,7 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorBlobs) {
|
||||
ReopenWithColumnFamilies({"default", "pikachu"}, options);
|
||||
}
|
||||
|
||||
auto res = OpenTransactionLogIter(0)->GetBatch();
|
||||
auto res = OpenWalIter(0)->GetBatch();
|
||||
struct Handler : public WriteBatch::Handler {
|
||||
std::string seen;
|
||||
Status PutCF(uint32_t cf, const Slice& key, const Slice& value) override {
|
||||
@@ -332,6 +330,84 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorBlobs) {
|
||||
"Delete(0, key2)",
|
||||
handler.seen);
|
||||
}
|
||||
|
||||
// The tests below pin the semantics documented on WalIterator and
|
||||
// DB::GetUpdatesSince, including the sharp edges, so that changing any of
|
||||
// them is a deliberate and visible decision rather than an accident.
|
||||
|
||||
// A caught-up iterator is usable, not finished: Next() is how a consumer
|
||||
// tails a DB.
|
||||
TEST_F(DBWalIteratorTest, CaughtUpThenResumes) {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("key1", DummyString(128)));
|
||||
|
||||
auto iter = OpenWalIter(0);
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
iter->Next();
|
||||
|
||||
// Caught up: !Valid(), but status() is OK and the iterator is not spent.
|
||||
ASSERT_TRUE(!iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
// A later write to the same WAL is picked up by calling Next() again,
|
||||
// without rebuilding the iterator.
|
||||
ASSERT_OK(Put("key2", DummyString(128)));
|
||||
iter->Next();
|
||||
ASSERT_TRUE(iter->Valid()) << iter->status().ToString();
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_EQ(2U, iter->GetBatch().sequence);
|
||||
}
|
||||
|
||||
// Writes that bypass the WAL still consume sequence numbers, so they leave
|
||||
// holes that this API surfaces as the end of the run.
|
||||
TEST_F(DBWalIteratorTest, GapFromDisableWAL) {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
WriteOptions no_wal;
|
||||
no_wal.disableWAL = true;
|
||||
ASSERT_OK(Put("key1", DummyString(128))); // seq 1, in the WAL
|
||||
ASSERT_OK(Put("key2", DummyString(128), no_wal)); // seq 2, not in the WAL
|
||||
ASSERT_OK(Put("key3", DummyString(128))); // seq 3, in the WAL
|
||||
|
||||
auto iter = OpenWalIter(0);
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(1U, iter->GetBatch().sequence);
|
||||
|
||||
// The run stops at the hole rather than silently jumping to seq 3.
|
||||
iter->Next();
|
||||
ASSERT_TRUE(!iter->Valid());
|
||||
ASSERT_TRUE(iter->status().IsNotFound()) << iter->status().ToString();
|
||||
|
||||
// And the iterator is spent: further Next() calls do nothing and the
|
||||
// status never changes.
|
||||
iter->Next();
|
||||
iter->Next();
|
||||
ASSERT_TRUE(!iter->Valid());
|
||||
ASSERT_TRUE(iter->status().IsNotFound());
|
||||
}
|
||||
|
||||
// GetUpdatesSince is permissive about its starting point: if the requested
|
||||
// sequence number is not in the WAL it silently starts later and still
|
||||
// reports OK. This is the path by which a consumer recovering from a spent
|
||||
// iterator can lose data without noticing.
|
||||
TEST_F(DBWalIteratorTest, SilentlySkipsUnavailableStart) {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
WriteOptions no_wal;
|
||||
no_wal.disableWAL = true;
|
||||
ASSERT_OK(Put("key1", DummyString(128), no_wal)); // seq 1, not in the WAL
|
||||
ASSERT_OK(Put("key2", DummyString(128))); // seq 2, in the WAL
|
||||
|
||||
std::unique_ptr<WalIterator> iter;
|
||||
ASSERT_OK(dbfull()->GetUpdatesSince(1, &iter));
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
// Asked for seq 1, silently given seq 2, with no error anywhere.
|
||||
ASSERT_EQ(2U, iter->GetBatch().sequence);
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -566,6 +566,25 @@ TEST_F(DBOptionsTest, SetOptionsAndReopen) {
|
||||
ASSERT_OK(TryReopen(options));
|
||||
}
|
||||
|
||||
TEST_F(DBOptionsTest, SetBlobFileWritableFileMaxBufferSize) {
|
||||
constexpr uint64_t kBlobWriterBufferSize = 128 * 1024;
|
||||
|
||||
Options options;
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 0;
|
||||
Reopen(options);
|
||||
|
||||
ASSERT_EQ(db_->GetOptions().blob_file_writable_file_max_buffer_size, 0U);
|
||||
|
||||
ASSERT_OK(db_->SetOptions({{"blob_file_writable_file_max_buffer_size",
|
||||
std::to_string(kBlobWriterBufferSize)}}));
|
||||
|
||||
ASSERT_EQ(db_->GetOptions().blob_file_writable_file_max_buffer_size,
|
||||
kBlobWriterBufferSize);
|
||||
}
|
||||
|
||||
TEST_F(DBOptionsTest, EnableAutoCompactionAndTriggerStall) {
|
||||
const std::string kValue(1024, 'v');
|
||||
for (int method_type = 0; method_type < 2; method_type++) {
|
||||
|
||||
@@ -1148,6 +1148,60 @@ TEST_F(DBPropertiesTest, EstimatePendingCompBytes) {
|
||||
ASSERT_EQ(int_num, 0U);
|
||||
}
|
||||
|
||||
TEST_F(DBPropertiesTest, NumUnscheduledCompactions) {
|
||||
if (env_ == nullptr) {
|
||||
ADD_FAILURE() << "Missing test environment";
|
||||
return;
|
||||
}
|
||||
auto& env = *env_;
|
||||
env.SetBackgroundThreads(1, Env::HIGH);
|
||||
env.SetBackgroundThreads(1, Env::LOW);
|
||||
|
||||
Options options = CurrentOptions();
|
||||
WriteOptions writeOpt = WriteOptions();
|
||||
writeOpt.disableWAL = true;
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.level0_file_num_compaction_trigger = 2;
|
||||
options.max_background_compactions = 1;
|
||||
options.max_background_flushes = 1;
|
||||
options.write_buffer_size = 1000000;
|
||||
DestroyAndReopen(options);
|
||||
CreateAndReopenWithCF({"one"}, options);
|
||||
|
||||
uint64_t int_num;
|
||||
|
||||
ASSERT_TRUE(dbfull()->GetIntProperty(
|
||||
DB::Properties::kNumUnscheduledCompactions, &int_num));
|
||||
ASSERT_EQ(int_num, 0U);
|
||||
|
||||
// Keep one scheduled compaction from completing.
|
||||
test::SleepingBackgroundTask sleeping_task_low;
|
||||
env.Schedule(&test::SleepingBackgroundTask::DoSleepTask, &sleeping_task_low,
|
||||
Env::Priority::LOW);
|
||||
sleeping_task_low.WaitUntilSleeping();
|
||||
|
||||
std::string big_value(1000000 * 2, 'x');
|
||||
for (int cf = 0; cf < 2; cf++) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
ASSERT_OK(dbfull()->Put(writeOpt, handles_[cf], "k" + std::to_string(i),
|
||||
big_value));
|
||||
ASSERT_OK(Flush(cf));
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_TRUE(dbfull()->GetIntProperty(
|
||||
DB::Properties::kNumUnscheduledCompactions, &int_num));
|
||||
ASSERT_GT(int_num, 0U);
|
||||
|
||||
sleeping_task_low.WakeUp();
|
||||
sleeping_task_low.WaitUntilDone();
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
ASSERT_TRUE(dbfull()->GetIntProperty(
|
||||
DB::Properties::kNumUnscheduledCompactions, &int_num));
|
||||
ASSERT_EQ(int_num, 0U);
|
||||
}
|
||||
|
||||
TEST_F(DBPropertiesTest, EstimateCompressionRatio) {
|
||||
if (!Snappy_Supported()) {
|
||||
return;
|
||||
|
||||
@@ -749,8 +749,7 @@ TEST_F(DBSecondaryTest, OptionsOverrideTest) {
|
||||
"blob_compaction_readahead_size=4194304;"
|
||||
"some_invalid_option=ignore_me;"
|
||||
"env=this_should_not_fail;"
|
||||
"max_open_files=100;", // this should be always overriden as
|
||||
// -1 in remote compaction
|
||||
"max_open_files=100;",
|
||||
&override_options.options_map));
|
||||
|
||||
bool verified = false;
|
||||
@@ -761,7 +760,7 @@ TEST_F(DBSecondaryTest, OptionsOverrideTest) {
|
||||
auto secondary_db_options = secondary_db->GetOptions();
|
||||
// DBOption
|
||||
ASSERT_EQ(secondary_db_options.compaction_readahead_size, 8388608);
|
||||
ASSERT_EQ(secondary_db_options.max_open_files, -1);
|
||||
ASSERT_EQ(secondary_db_options.max_open_files, 100);
|
||||
// CFOption
|
||||
ASSERT_EQ(secondary_db_options.blob_compaction_readahead_size, 4194304);
|
||||
verified = true;
|
||||
|
||||
@@ -701,6 +701,44 @@ TEST_F(DBSSTTest, DBWithSstFileManagerForBlobFilesWithGC) {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(DBSSTTest, SstFileManagerTracksMultipleCompactionBlobOutputs) {
|
||||
constexpr uint64_t kBlobFileSize = 1024;
|
||||
constexpr size_t kValueSize = 600;
|
||||
|
||||
std::shared_ptr<SstFileManager> sst_file_manager(NewSstFileManager(env_));
|
||||
auto* sfm = static_cast<SstFileManagerImpl*>(sst_file_manager.get());
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.sst_file_manager = sst_file_manager;
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 0;
|
||||
options.blob_file_size = kBlobFileSize;
|
||||
options.blob_compression_type = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.enable_blob_garbage_collection = true;
|
||||
options.blob_garbage_collection_age_cutoff = 1.0;
|
||||
options.blob_garbage_collection_force_threshold = 0.0;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string value(kValueSize, 'a');
|
||||
ASSERT_OK(Put("key1", value));
|
||||
ASSERT_OK(Put("key3", value));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("key2", value));
|
||||
ASSERT_OK(Put("key4", value));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
sfm->WaitForEmptyTrash();
|
||||
ASSERT_EQ(2U, GetBlobFileNumbers().size());
|
||||
|
||||
std::unordered_map<std::string, uint64_t> files_in_db;
|
||||
ASSERT_OK(GetAllDataFiles(kTableFile, &files_in_db));
|
||||
ASSERT_OK(GetAllDataFiles(kBlobFile, &files_in_db));
|
||||
ASSERT_EQ(files_in_db, sfm->GetTrackedFiles());
|
||||
}
|
||||
|
||||
class DBSSTTestRateLimit : public DBSSTTest,
|
||||
public ::testing::WithParamInterface<bool> {
|
||||
public:
|
||||
|
||||
+6
-5
@@ -3725,6 +3725,8 @@ class ModelDB : public DB {
|
||||
void DisableManualCompaction() override {}
|
||||
void AbortAllCompactions() override {}
|
||||
void ResumeAllCompactions() override {}
|
||||
void AbortCompactions(ColumnFamilyHandle* /*column_family*/) override {}
|
||||
void ResumeCompactions(ColumnFamilyHandle* /*column_family*/) override {}
|
||||
|
||||
Status WaitForCompact(
|
||||
const WaitForCompactOptions& /* wait_for_compact_options */) override {
|
||||
@@ -3804,11 +3806,10 @@ class ModelDB : public DB {
|
||||
return Status::NotSupported();
|
||||
}
|
||||
|
||||
Status GetUpdatesSince(
|
||||
ROCKSDB_NAMESPACE::SequenceNumber,
|
||||
std::unique_ptr<ROCKSDB_NAMESPACE::TransactionLogIterator>*,
|
||||
const TransactionLogIterator::ReadOptions& /*read_options*/ =
|
||||
TransactionLogIterator::ReadOptions()) override {
|
||||
Status GetUpdatesSince(ROCKSDB_NAMESPACE::SequenceNumber,
|
||||
std::unique_ptr<ROCKSDB_NAMESPACE::WalIterator>*,
|
||||
const WalIterator::ReadOptions& /*read_options*/ =
|
||||
WalIterator::ReadOptions()) override {
|
||||
return Status::NotSupported("Not supported in Model DB");
|
||||
}
|
||||
|
||||
|
||||
+16
-12
@@ -25,6 +25,7 @@
|
||||
|
||||
#if USE_COROUTINES
|
||||
#include "folly/coro/BlockingWait.h"
|
||||
#include "rocksdb/coro_db.h"
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -872,8 +873,8 @@ std::string DBTestBase::Get(const std::string& k, const Snapshot* snapshot,
|
||||
#if USE_COROUTINES
|
||||
if (use_coroutine) {
|
||||
PinnableSlice pinnable_value(&result);
|
||||
s = folly::coro::blockingWait(dbfull()->GetCoroutine(
|
||||
options, dbfull()->DefaultColumnFamily(), k, &pinnable_value,
|
||||
s = folly::coro::blockingWait(CoroDB::CoGet(
|
||||
dbfull(), options, dbfull()->DefaultColumnFamily(), k, &pinnable_value,
|
||||
/*timestamp=*/nullptr));
|
||||
if (s.ok() && pinnable_value.IsPinned()) {
|
||||
result.assign(pinnable_value.data(), pinnable_value.size());
|
||||
@@ -903,8 +904,9 @@ std::string DBTestBase::Get(int cf, const std::string& k,
|
||||
#if USE_COROUTINES
|
||||
if (use_coroutine) {
|
||||
PinnableSlice pinnable_value(&result);
|
||||
s = folly::coro::blockingWait(dbfull()->GetCoroutine(
|
||||
options, handles_[cf], k, &pinnable_value, /*timestamp=*/nullptr));
|
||||
s = folly::coro::blockingWait(CoroDB::CoGet(dbfull(), options, handles_[cf],
|
||||
k, &pinnable_value,
|
||||
/*timestamp=*/nullptr));
|
||||
if (s.ok() && pinnable_value.IsPinned()) {
|
||||
result.assign(pinnable_value.data(), pinnable_value.size());
|
||||
}
|
||||
@@ -956,9 +958,10 @@ std::vector<std::string> DBTestBase::MultiGet(std::vector<int> cfs,
|
||||
s.resize(cfs.size());
|
||||
#if USE_COROUTINES
|
||||
if (use_coroutine) {
|
||||
folly::coro::blockingWait(dbfull()->MultiGetCoroutine(
|
||||
options, cfs.size(), handles.data(), keys.data(), pin_values.data(),
|
||||
/*timestamps=*/nullptr, s.data(), /*sorted_input=*/false));
|
||||
folly::coro::blockingWait(CoroDB::CoMultiGet(
|
||||
dbfull(), options, cfs.size(), handles.data(), keys.data(),
|
||||
pin_values.data(), /*timestamps=*/nullptr, s.data(),
|
||||
/*sorted_input=*/false));
|
||||
} else
|
||||
#else
|
||||
(void)use_coroutine;
|
||||
@@ -1005,9 +1008,10 @@ std::vector<std::string> DBTestBase::MultiGet(const std::vector<std::string>& k,
|
||||
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));
|
||||
folly::coro::blockingWait(CoroDB::CoMultiGet(
|
||||
dbfull(), options, keys.size(), cfs.data(), keys.data(),
|
||||
pin_values.data(), /*timestamps=*/nullptr, statuses.data(),
|
||||
/*sorted_input=*/false));
|
||||
} else
|
||||
#else
|
||||
(void)use_coroutine;
|
||||
@@ -1038,8 +1042,8 @@ Status DBTestBase::Get(const std::string& k, PinnableSlice* v,
|
||||
#if USE_COROUTINES
|
||||
if (use_coroutine) {
|
||||
return folly::coro::blockingWait(
|
||||
dbfull()->GetCoroutine(options, dbfull()->DefaultColumnFamily(), k, v,
|
||||
/*timestamp=*/nullptr));
|
||||
CoroDB::CoGet(dbfull(), options, dbfull()->DefaultColumnFamily(), k, v,
|
||||
/*timestamp=*/nullptr));
|
||||
}
|
||||
#else
|
||||
(void)use_coroutine;
|
||||
|
||||
+10
-8
@@ -21,7 +21,7 @@
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "test_util/testutil.h"
|
||||
@@ -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);
|
||||
|
||||
@@ -2415,6 +2415,253 @@ TEST_F(ExternalSSTFileTest, SstFileWriterNonSharedKeys) {
|
||||
ASSERT_OK(DeprecatedAddFile({file_path}));
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, IngestFlushRequestNotLostBehindQueuedFlush) {
|
||||
// These waits only prevent broken test paths from hanging forever. Keep them
|
||||
// large enough to avoid treating slow CI scheduling as a regression.
|
||||
constexpr uint64_t kHangGuardMicros = 30 * 1000 * 1000;
|
||||
constexpr uint64_t kSetupWaitMicros = kHangGuardMicros;
|
||||
constexpr uint64_t kIngestWaitMicros = kHangGuardMicros;
|
||||
constexpr uint64_t kPollMicros = 1000;
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.atomic_flush = false;
|
||||
options.disable_auto_compactions = true;
|
||||
options.write_buffer_size = 1024;
|
||||
options.max_write_buffer_number = 8;
|
||||
options.min_write_buffer_number_to_merge = 2;
|
||||
env_->SetBackgroundThreads(1, Env::HIGH);
|
||||
Reopen(options);
|
||||
|
||||
std::string external_file_path;
|
||||
std::vector<std::pair<std::string, std::string>> external_file_data = {
|
||||
{"ingest_key", "ingested_value"}};
|
||||
ASSERT_OK(GenerateOneExternalFile(options, nullptr, external_file_data, -1,
|
||||
true /* sort_data */, &external_file_path,
|
||||
nullptr /* true_data */));
|
||||
|
||||
auto sleeping_task = std::make_shared<test::SleepingBackgroundTask>();
|
||||
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask,
|
||||
sleeping_task.get(), Env::Priority::HIGH);
|
||||
sleeping_task->WaitUntilSleeping();
|
||||
|
||||
std::atomic<int> atomic_flush_scheduled{0};
|
||||
std::atomic<int> get_live_files_done{0};
|
||||
std::atomic<bool> ingest_waiting_for_flush{false};
|
||||
std::atomic<bool> ingest_done{false};
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::AtomicFlushMemTables:AfterScheduleFlush", [&](void*) {
|
||||
atomic_flush_scheduled.fetch_add(1, std::memory_order_acq_rel);
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::FlushMemTable:BeforeWaitForBgFlush", [&](void*) {
|
||||
ingest_waiting_for_flush.store(true, std::memory_order_release);
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
auto wait_until = [&](const std::function<bool()>& predicate,
|
||||
uint64_t timeout_micros) {
|
||||
for (uint64_t waited_micros = 0; waited_micros < timeout_micros;
|
||||
waited_micros += kPollMicros) {
|
||||
if (predicate()) {
|
||||
return true;
|
||||
}
|
||||
env_->SleepForMicroseconds(kPollMicros);
|
||||
}
|
||||
return predicate();
|
||||
};
|
||||
|
||||
auto wake_sleeping_task = [&] {
|
||||
if (!sleeping_task->WokenUp()) {
|
||||
sleeping_task->WakeUp();
|
||||
}
|
||||
return !sleeping_task->TimedWaitUntilDone(kSetupWaitMicros);
|
||||
};
|
||||
|
||||
auto wait_or_cancel_background_work =
|
||||
[&](const std::function<bool()>& predicate, uint64_t timeout_micros) {
|
||||
if (wait_until(predicate, timeout_micros)) {
|
||||
return true;
|
||||
}
|
||||
dbfull()->CancelAllBackgroundWork(false /* wait */);
|
||||
if (wait_until(predicate, timeout_micros)) {
|
||||
return true;
|
||||
}
|
||||
dbfull()->CancelAllBackgroundWork(true /* wait */);
|
||||
return wait_until(predicate, timeout_micros);
|
||||
};
|
||||
|
||||
ASSERT_OK(Put("atomic_flush_key", "value"));
|
||||
|
||||
Status get_live_files_status[2];
|
||||
std::vector<LiveFileStorageInfo> live_files[2];
|
||||
ThreadGuard get_live_files_thread_1(port::Thread([&] {
|
||||
LiveFilesStorageInfoOptions lfsi_opts;
|
||||
lfsi_opts.wal_size_for_flush = 0;
|
||||
lfsi_opts.atomic_flush = true;
|
||||
get_live_files_status[0] =
|
||||
db_->GetLiveFilesStorageInfo(lfsi_opts, &live_files[0]);
|
||||
get_live_files_done.fetch_add(1, std::memory_order_acq_rel);
|
||||
}));
|
||||
ThreadGuard get_live_files_thread_2(port::Thread([&] {
|
||||
LiveFilesStorageInfoOptions lfsi_opts;
|
||||
lfsi_opts.wal_size_for_flush = 0;
|
||||
lfsi_opts.atomic_flush = true;
|
||||
get_live_files_status[1] =
|
||||
db_->GetLiveFilesStorageInfo(lfsi_opts, &live_files[1]);
|
||||
get_live_files_done.fetch_add(1, std::memory_order_acq_rel);
|
||||
}));
|
||||
ThreadGuard ingest_thread;
|
||||
|
||||
auto get_live_files_finished = [&] {
|
||||
return get_live_files_done.load(std::memory_order_acquire) == 2;
|
||||
};
|
||||
auto ingest_finished = [&] {
|
||||
return ingest_done.load(std::memory_order_acquire);
|
||||
};
|
||||
auto wait_for_get_live_files_finished = [&] {
|
||||
return wait_or_cancel_background_work(get_live_files_finished,
|
||||
kSetupWaitMicros);
|
||||
};
|
||||
auto wait_for_ingest_finished = [&](uint64_t timeout_micros) {
|
||||
return wait_or_cancel_background_work(ingest_finished, timeout_micros);
|
||||
};
|
||||
auto join_get_live_files_threads = [&] {
|
||||
if (get_live_files_thread_1.GetThread().joinable()) {
|
||||
get_live_files_thread_1.GetThread().join();
|
||||
}
|
||||
if (get_live_files_thread_2.GetThread().joinable()) {
|
||||
get_live_files_thread_2.GetThread().join();
|
||||
}
|
||||
};
|
||||
auto join_ingest_thread = [&] {
|
||||
if (ingest_thread.GetThread().joinable()) {
|
||||
ingest_thread.GetThread().join();
|
||||
}
|
||||
};
|
||||
|
||||
class CleanupGuard {
|
||||
public:
|
||||
explicit CleanupGuard(const std::function<void()>& cleanup)
|
||||
: cleanup_(cleanup) {}
|
||||
|
||||
~CleanupGuard() {
|
||||
if (armed_) {
|
||||
cleanup_();
|
||||
}
|
||||
}
|
||||
|
||||
void Disarm() { armed_ = false; }
|
||||
|
||||
private:
|
||||
std::function<void()> cleanup_;
|
||||
bool armed_ = true;
|
||||
};
|
||||
|
||||
CleanupGuard cleanup_guard([&] {
|
||||
wake_sleeping_task();
|
||||
wait_for_get_live_files_finished();
|
||||
if (ingest_thread.GetThread().joinable()) {
|
||||
wait_for_ingest_finished(kSetupWaitMicros);
|
||||
}
|
||||
join_get_live_files_threads();
|
||||
join_ingest_thread();
|
||||
});
|
||||
|
||||
const bool saw_both_atomic_flushes = wait_until(
|
||||
[&] {
|
||||
return atomic_flush_scheduled.load(std::memory_order_acquire) >= 2;
|
||||
},
|
||||
kSetupWaitMicros);
|
||||
|
||||
if (!saw_both_atomic_flushes) {
|
||||
const bool sleeping_task_done = wake_sleeping_task();
|
||||
const bool get_live_files_finished_after_cleanup =
|
||||
wait_for_get_live_files_finished();
|
||||
join_get_live_files_threads();
|
||||
EXPECT_OK(get_live_files_status[0]);
|
||||
EXPECT_OK(get_live_files_status[1]);
|
||||
EXPECT_TRUE(get_live_files_finished_after_cleanup);
|
||||
ASSERT_TRUE(saw_both_atomic_flushes)
|
||||
<< "Timed out waiting for both GetLiveFilesStorageInfo calls to "
|
||||
"schedule their flushes. scheduled="
|
||||
<< atomic_flush_scheduled.load(std::memory_order_acquire)
|
||||
<< " get_live_files_done="
|
||||
<< get_live_files_done.load(std::memory_order_acquire)
|
||||
<< " sleeping_task_done=" << sleeping_task_done;
|
||||
}
|
||||
|
||||
ASSERT_OK(Put("ingest_key", "memtable_value"));
|
||||
|
||||
Status ingest_status;
|
||||
ingest_thread = ThreadGuard(port::Thread([&] {
|
||||
IngestExternalFileOptions ifo;
|
||||
ingest_status = db_->IngestExternalFile({external_file_path}, ifo);
|
||||
ingest_done.store(true, std::memory_order_release);
|
||||
}));
|
||||
|
||||
const bool saw_ingest_wait = wait_until(
|
||||
[&] {
|
||||
return ingest_waiting_for_flush.load(std::memory_order_acquire) ||
|
||||
ingest_done.load(std::memory_order_acquire);
|
||||
},
|
||||
kSetupWaitMicros);
|
||||
const bool ingest_finished_before_wait =
|
||||
ingest_done.load(std::memory_order_acquire);
|
||||
|
||||
if (!saw_ingest_wait || ingest_finished_before_wait) {
|
||||
const bool sleeping_task_done = wake_sleeping_task();
|
||||
const bool get_live_files_finished_after_cleanup =
|
||||
wait_for_get_live_files_finished();
|
||||
join_get_live_files_threads();
|
||||
const bool ingest_finished_after_cleanup =
|
||||
wait_for_ingest_finished(kSetupWaitMicros);
|
||||
join_ingest_thread();
|
||||
EXPECT_OK(get_live_files_status[0]);
|
||||
EXPECT_OK(get_live_files_status[1]);
|
||||
EXPECT_OK(ingest_status);
|
||||
EXPECT_TRUE(get_live_files_finished_after_cleanup);
|
||||
EXPECT_TRUE(ingest_finished_after_cleanup);
|
||||
ASSERT_TRUE(sleeping_task_done);
|
||||
ASSERT_TRUE(saw_ingest_wait)
|
||||
<< "Timed out waiting for IngestExternalFile to enter its flush wait";
|
||||
ASSERT_FALSE(ingest_finished_before_wait)
|
||||
<< "IngestExternalFile finished before entering the expected flush "
|
||||
"wait";
|
||||
}
|
||||
|
||||
const bool sleeping_task_done = wake_sleeping_task();
|
||||
join_get_live_files_threads();
|
||||
|
||||
const bool ingest_completed_without_unblock =
|
||||
wait_until(ingest_finished, kIngestWaitMicros);
|
||||
|
||||
if (!ingest_completed_without_unblock) {
|
||||
FlushOptions unblock_flush_options;
|
||||
unblock_flush_options.wait = false;
|
||||
const Status unblock_flush_status = db_->Flush(unblock_flush_options);
|
||||
const bool ingest_finished_after_unblock =
|
||||
wait_for_ingest_finished(kIngestWaitMicros);
|
||||
join_ingest_thread();
|
||||
ASSERT_TRUE(sleeping_task_done);
|
||||
ASSERT_OK(get_live_files_status[0]);
|
||||
ASSERT_OK(get_live_files_status[1]);
|
||||
ASSERT_OK(unblock_flush_status);
|
||||
ASSERT_TRUE(ingest_finished_after_unblock);
|
||||
FAIL() << "IngestExternalFile remained blocked waiting for a flush that "
|
||||
"was deduped behind an older queued flush request";
|
||||
}
|
||||
|
||||
join_ingest_thread();
|
||||
ASSERT_TRUE(sleeping_task_done);
|
||||
ASSERT_OK(get_live_files_status[0]);
|
||||
ASSERT_OK(get_live_files_status[1]);
|
||||
ASSERT_OK(ingest_status);
|
||||
ASSERT_EQ("ingested_value", Get("ingest_key"));
|
||||
cleanup_guard.Disarm();
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, WithUnorderedWrite) {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->LoadDependency(
|
||||
|
||||
+2
-1
@@ -1042,7 +1042,8 @@ Status FlushJob::WriteLevel0Table() {
|
||||
meta_.fd.GetNumber(),
|
||||
preclude_last_level_min_seqno_ == kMaxSequenceNumber
|
||||
? preclude_last_level_min_seqno_
|
||||
: std::min(earliest_snapshot_, preclude_last_level_min_seqno_));
|
||||
: std::min(earliest_snapshot_, preclude_last_level_min_seqno_),
|
||||
dbname_ /*db_name*/);
|
||||
s = BuildTable(
|
||||
dbname_, versions_, db_options_, tboptions, file_options_,
|
||||
cfd_->table_cache(), iter.get(), std::move(range_del_iters), &meta_,
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/listener.h"
|
||||
#include "rocksdb/memtablerep.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
#include "util/autovector.h"
|
||||
#include "util/stop_watch.h"
|
||||
#include "util/thread_local.h"
|
||||
|
||||
@@ -308,9 +308,15 @@ static const std::string aggregated_table_properties =
|
||||
static const std::string aggregated_table_properties_at_level =
|
||||
aggregated_table_properties + "-at-level";
|
||||
static const std::string num_running_compactions = "num-running-compactions";
|
||||
static const std::string num_running_remote_compactions =
|
||||
"num-running-remote-compactions";
|
||||
static const std::string num_running_bottom_compactions =
|
||||
"num-running-bottom-compactions";
|
||||
static const std::string num_running_compaction_sorted_runs =
|
||||
"num-running-compaction-sorted-runs";
|
||||
static const std::string compaction_abort_count = "compaction-abort-count";
|
||||
static const std::string num_unscheduled_compactions =
|
||||
"num-unscheduled-compactions";
|
||||
static const std::string num_running_flushes = "num-running-flushes";
|
||||
static const std::string actual_delayed_write_rate =
|
||||
"actual-delayed-write-rate";
|
||||
@@ -361,10 +367,16 @@ const std::string DB::Properties::kCompactionPending =
|
||||
rocksdb_prefix + compaction_pending;
|
||||
const std::string DB::Properties::kNumRunningCompactions =
|
||||
rocksdb_prefix + num_running_compactions;
|
||||
const std::string DB::Properties::kNumRunningRemoteCompactions =
|
||||
rocksdb_prefix + num_running_remote_compactions;
|
||||
const std::string DB::Properties::kNumRunningBottomCompactions =
|
||||
rocksdb_prefix + num_running_bottom_compactions;
|
||||
const std::string DB::Properties::kNumRunningCompactionSortedRuns =
|
||||
rocksdb_prefix + num_running_compaction_sorted_runs;
|
||||
const std::string DB::Properties::kCompactionAbortCount =
|
||||
rocksdb_prefix + compaction_abort_count;
|
||||
const std::string DB::Properties::kNumUnscheduledCompactions =
|
||||
rocksdb_prefix + num_unscheduled_compactions;
|
||||
const std::string DB::Properties::kNumRunningFlushes =
|
||||
rocksdb_prefix + num_running_flushes;
|
||||
const std::string DB::Properties::kBackgroundErrors =
|
||||
@@ -594,12 +606,21 @@ const UnorderedMap<std::string, DBPropertyInfo>
|
||||
{DB::Properties::kNumRunningCompactions,
|
||||
{false, nullptr, &InternalStats::HandleNumRunningCompactions, nullptr,
|
||||
nullptr}},
|
||||
{DB::Properties::kNumRunningRemoteCompactions,
|
||||
{false, nullptr, &InternalStats::HandleNumRunningRemoteCompactions,
|
||||
nullptr, nullptr}},
|
||||
{DB::Properties::kNumRunningBottomCompactions,
|
||||
{false, nullptr, &InternalStats::HandleNumRunningBottomCompactions,
|
||||
nullptr, nullptr}},
|
||||
{DB::Properties::kNumRunningCompactionSortedRuns,
|
||||
{false, nullptr, &InternalStats::HandleNumRunningCompactionSortedRuns,
|
||||
nullptr, nullptr}},
|
||||
{DB::Properties::kCompactionAbortCount,
|
||||
{false, nullptr, &InternalStats::HandleCompactionAbortCount, nullptr,
|
||||
nullptr}},
|
||||
{DB::Properties::kNumUnscheduledCompactions,
|
||||
{false, nullptr, &InternalStats::HandleNumUnscheduledCompactions,
|
||||
nullptr, nullptr}},
|
||||
{DB::Properties::kActualDelayedWriteRate,
|
||||
{false, nullptr, &InternalStats::HandleActualDelayedWriteRate, nullptr,
|
||||
nullptr}},
|
||||
@@ -1286,6 +1307,23 @@ bool InternalStats::HandleNumRunningCompactions(uint64_t* value, DBImpl* db,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleNumRunningRemoteCompactions(uint64_t* value,
|
||||
DBImpl* db,
|
||||
Version* /*version*/) {
|
||||
const int num_running =
|
||||
db->num_running_remote_compactions_.load(std::memory_order_relaxed);
|
||||
assert(num_running >= 0);
|
||||
*value = static_cast<uint64_t>(num_running);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleNumRunningBottomCompactions(uint64_t* value,
|
||||
DBImpl* db,
|
||||
Version* /*version*/) {
|
||||
*value = static_cast<uint64_t>(db->num_running_bottom_compactions_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleNumRunningCompactionSortedRuns(uint64_t* value,
|
||||
DBImpl* db,
|
||||
Version* /*version*/) {
|
||||
@@ -1305,6 +1343,14 @@ bool InternalStats::HandleCompactionAbortCount(uint64_t* value, DBImpl* db,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleNumUnscheduledCompactions(uint64_t* value, DBImpl* db,
|
||||
Version* /*version*/) {
|
||||
db->mutex()->AssertHeld();
|
||||
assert(db->unscheduled_compactions_ >= 0);
|
||||
*value = static_cast<uint64_t>(db->unscheduled_compactions_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleBackgroundErrors(uint64_t* value, DBImpl* /*db*/,
|
||||
Version* /*version*/) {
|
||||
// Accumulated number of errors in background flushes or compactions.
|
||||
|
||||
@@ -850,10 +850,16 @@ class InternalStats {
|
||||
bool HandleCompactionPending(uint64_t* value, DBImpl* db, Version* version);
|
||||
bool HandleNumRunningCompactions(uint64_t* value, DBImpl* db,
|
||||
Version* version);
|
||||
bool HandleNumRunningRemoteCompactions(uint64_t* value, DBImpl* db,
|
||||
Version* version);
|
||||
bool HandleNumRunningBottomCompactions(uint64_t* value, DBImpl* db,
|
||||
Version* version);
|
||||
bool HandleNumRunningCompactionSortedRuns(uint64_t* value, DBImpl* db,
|
||||
Version* version);
|
||||
bool HandleCompactionAbortCount(uint64_t* value, DBImpl* db,
|
||||
Version* version);
|
||||
bool HandleNumUnscheduledCompactions(uint64_t* value, DBImpl* db,
|
||||
Version* version);
|
||||
bool HandleBackgroundErrors(uint64_t* value, DBImpl* db, Version* version);
|
||||
bool HandleCurSizeActiveMemTable(uint64_t* value, DBImpl* db,
|
||||
Version* version);
|
||||
|
||||
+6
-5
@@ -498,7 +498,6 @@ void MemTableList::PickMemtablesToFlush(uint64_t max_memtable_id,
|
||||
AutoThreadOperationStageUpdater stage_updater(
|
||||
ThreadStatus::STAGE_PICK_MEMTABLES_TO_FLUSH);
|
||||
const auto& memlist = current_->memlist_;
|
||||
bool atomic_flush = false;
|
||||
|
||||
// Note: every time MemTableList::Add(mem) is called, it adds the new mem
|
||||
// at the FRONT of the memlist (memlist.push_front(mem)). Therefore, by
|
||||
@@ -509,9 +508,6 @@ void MemTableList::PickMemtablesToFlush(uint64_t max_memtable_id,
|
||||
auto it = memlist.rbegin();
|
||||
for (; it != memlist.rend(); ++it) {
|
||||
ReadOnlyMemTable* m = *it;
|
||||
if (!atomic_flush && m->atomic_flush_seqno_ != kMaxSequenceNumber) {
|
||||
atomic_flush = true;
|
||||
}
|
||||
if (m->GetID() > max_memtable_id) {
|
||||
break;
|
||||
}
|
||||
@@ -542,7 +538,12 @@ void MemTableList::PickMemtablesToFlush(uint64_t max_memtable_id,
|
||||
// since they map to the same WAL and have the same NextLogNumber().
|
||||
assert(strcmp((*it)->Name(), "WBWIMemTable") != 0);
|
||||
}
|
||||
if (!atomic_flush || num_flush_not_started_ == 0) {
|
||||
// The flush request is complete only after every unstarted immutable
|
||||
// memtable has been picked. A request can be older than a newer immutable
|
||||
// memtable; clearing flush_requested_ after a partial or empty pick would
|
||||
// make the queued newer request look unnecessary and leave that memtable
|
||||
// unflushed.
|
||||
if (num_flush_not_started_ == 0) {
|
||||
flush_requested_ = false; // start-flush request is complete
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -402,11 +402,11 @@ class MemTableList {
|
||||
// Returns an estimate of the timestamp of the earliest key.
|
||||
uint64_t ApproximateOldestKeyTime() const;
|
||||
|
||||
// Request a flush of all existing memtables to storage. This will
|
||||
// cause future calls to IsFlushPending() to return true if this list is
|
||||
// non-empty (regardless of the min_write_buffer_number_to_merge
|
||||
// parameter). This flush request will persist until the next time
|
||||
// PickMemtablesToFlush() is called.
|
||||
// Request a flush of all existing memtables to storage. This will cause
|
||||
// future calls to IsFlushPending() to return true if this list is non-empty
|
||||
// (regardless of the min_write_buffer_number_to_merge parameter). This flush
|
||||
// request will persist until PickMemtablesToFlush() has picked all unstarted
|
||||
// memtables.
|
||||
void FlushRequested() {
|
||||
flush_requested_ = true;
|
||||
// If there are some memtables stored in imm() that don't trigger
|
||||
|
||||
@@ -890,8 +890,8 @@ TEST_F(MemTableListTest, FlushPendingTest) {
|
||||
ASSERT_TRUE(to_flush5.empty());
|
||||
ASSERT_EQ(1, list.NumNotFlushed());
|
||||
ASSERT_TRUE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
ASSERT_FALSE(list.HasFlushRequested());
|
||||
ASSERT_TRUE(list.IsFlushPending());
|
||||
ASSERT_TRUE(list.HasFlushRequested());
|
||||
|
||||
// Pick tables to flush. The tables to pick must have ID smaller than or
|
||||
// equal to 5. Therefore, only tables[5] will be selected.
|
||||
@@ -920,6 +920,52 @@ TEST_F(MemTableListTest, FlushPendingTest) {
|
||||
to_delete.clear();
|
||||
}
|
||||
|
||||
TEST_F(MemTableListTest, FlushRequestPersistsAfterPartialPick) {
|
||||
SequenceNumber seq = 1;
|
||||
|
||||
auto factory = std::make_shared<SkipListFactory>();
|
||||
options.memtable_factory = factory;
|
||||
ImmutableOptions ioptions(options);
|
||||
InternalKeyComparator cmp(BytewiseComparator());
|
||||
WriteBufferManager wb(options.db_write_buffer_size);
|
||||
MutableCFOptions mutable_cf_options(options);
|
||||
autovector<ReadOnlyMemTable*> to_delete;
|
||||
|
||||
MemTableList list(10 /* min_write_buffer_number_to_merge */,
|
||||
0 /* max_write_buffer_size_to_maintain */);
|
||||
|
||||
for (uint64_t memtable_id = 0; memtable_id < 2; ++memtable_id) {
|
||||
MemTable* mem = new MemTable(cmp, ioptions, mutable_cf_options, &wb,
|
||||
kMaxSequenceNumber, 0 /* column_family_id */);
|
||||
mem->SetID(memtable_id);
|
||||
mem->Ref();
|
||||
ASSERT_OK(mem->Add(++seq, kTypeValue, "key" + std::to_string(memtable_id),
|
||||
"value", nullptr /* kv_prot_info */));
|
||||
list.Add(mem, &to_delete);
|
||||
}
|
||||
|
||||
list.FlushRequested();
|
||||
ASSERT_TRUE(list.IsFlushPending());
|
||||
|
||||
autovector<ReadOnlyMemTable*> to_flush;
|
||||
list.PickMemtablesToFlush(0 /* max_memtable_id */, &to_flush);
|
||||
ASSERT_EQ(1, to_flush.size());
|
||||
ASSERT_TRUE(list.IsFlushPending());
|
||||
ASSERT_TRUE(list.HasFlushRequested());
|
||||
|
||||
autovector<ReadOnlyMemTable*> to_flush2;
|
||||
list.PickMemtablesToFlush(1 /* max_memtable_id */, &to_flush2);
|
||||
ASSERT_EQ(1, to_flush2.size());
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
ASSERT_FALSE(list.HasFlushRequested());
|
||||
|
||||
list.current()->Unref(&to_delete);
|
||||
ASSERT_EQ(2, to_delete.size());
|
||||
for (ReadOnlyMemTable* m : to_delete) {
|
||||
delete m;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MemTableListTest, EmptyAtomicFlushTest) {
|
||||
autovector<MemTableList*> lists;
|
||||
autovector<uint32_t> cf_ids;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "test_util/testutil.h"
|
||||
|
||||
@@ -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;
|
||||
|
||||
+69
-66
@@ -112,44 +112,45 @@ TEST_F(PerfContextTest, CoroutineStatsContextScopeCollectsStats) {
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -199,32 +200,33 @@ TEST_F(PerfContextTest, CoroutineStatsContextsRemainIsolatedWhenInterleaved) {
|
||||
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());
|
||||
{
|
||||
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);
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
};
|
||||
@@ -250,17 +252,18 @@ TEST_F(PerfContextTest, CoroutineStatsContextScopeCollectsGetCpuNanos) {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
clock->MockSleepForCPUNanos(7);
|
||||
scope.Finalize();
|
||||
EXPECT_EQ(29, get_perf_context()->get_cpu_nanos);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -482,7 +482,9 @@ class Repairer {
|
||||
current_time /* newest_key_time */, false /* is_bottommost */,
|
||||
TableFileCreationReason::kRecovery, 0 /* oldest_key_time */,
|
||||
0 /* file_creation_time */, "DB Repairer" /* db_id */, db_session_id_,
|
||||
0 /*target_file_size*/, meta.fd.GetNumber());
|
||||
0 /*target_file_size*/, meta.fd.GetNumber(),
|
||||
kMaxSequenceNumber /*last_level_inclusive_max_seqno_threshold*/,
|
||||
dbname_ /*db_name*/);
|
||||
|
||||
SeqnoToTimeMapping empty_seqno_to_time_mapping;
|
||||
status = BuildTable(
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
#include "rocksdb/comparator.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
#include "table/unique_id_impl.h"
|
||||
#include "util/string_util.h"
|
||||
|
||||
|
||||
+3
-2
@@ -19,6 +19,7 @@
|
||||
#include "logging/logging.h"
|
||||
#include "monitoring/file_read_sample.h"
|
||||
#include "monitoring/perf_context_imp.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "rocksdb/advanced_options.h"
|
||||
#include "rocksdb/statistics.h"
|
||||
#include "table/block_based/block_based_table_reader.h"
|
||||
@@ -391,8 +392,8 @@ InternalIterator* TableCache::NewIterator(
|
||||
open_options);
|
||||
InternalIterator* result = nullptr;
|
||||
if (s.ok()) {
|
||||
if (options.table_filter &&
|
||||
!options.table_filter(*table_reader->GetTableProperties())) {
|
||||
if (HasTableFilter(options) &&
|
||||
!(*options.table_filter)(*table_reader->GetTableProperties())) {
|
||||
result = NewEmptyInternalIterator<Slice>(arena);
|
||||
} else {
|
||||
result = table_reader->NewIterator(
|
||||
|
||||
+163
-17
@@ -2770,23 +2770,6 @@ Version::Version(ColumnFamilyData* column_family_data, VersionSet* vset,
|
||||
}
|
||||
}
|
||||
|
||||
Status Version::GetBlob(const ReadOptions& read_options, const Slice& user_key,
|
||||
const Slice& blob_index_slice,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* value, uint64_t* bytes_read) const {
|
||||
BlobIndex blob_index;
|
||||
|
||||
{
|
||||
Status s = blob_index.DecodeFrom(blob_index_slice);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
return GetBlob(read_options, user_key, blob_index, prefetch_buffer, value,
|
||||
bytes_read);
|
||||
}
|
||||
|
||||
Status Version::GetBlob(const ReadOptions& read_options, const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
@@ -2820,6 +2803,40 @@ Status Version::GetBlob(const ReadOptions& read_options, const Slice& user_key,
|
||||
return s;
|
||||
}
|
||||
|
||||
Status Version::GetBlobRange(const ReadOptions& read_options,
|
||||
const Slice& user_key, const BlobIndex& blob_index,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
PinnableSlice* value, uint64_t* bytes_read) const {
|
||||
assert(value);
|
||||
|
||||
if (blob_index.HasTTL() || blob_index.IsInlined()) {
|
||||
return Status::Corruption("Unexpected TTL/inlined blob index");
|
||||
}
|
||||
|
||||
// A strict sub-range of a compressed blob cannot be decompressed in
|
||||
// isolation; the caller resolves such columns whole and slices instead.
|
||||
if (blob_index.compression() != kNoCompression) {
|
||||
return Status::Corruption("Cannot range-read a compressed blob");
|
||||
}
|
||||
|
||||
const uint64_t blob_file_number = blob_index.file_number();
|
||||
|
||||
auto blob_file_meta = storage_info_.GetBlobFileMetaData(blob_file_number);
|
||||
if (!blob_file_meta) {
|
||||
// INTEGRITY CHECK -- see Version::GetBlob. No metadata (including
|
||||
// file_number 0, the same-file "embedded" sentinel) means a corrupt index
|
||||
// or a same-file reference that the lazy caller must not route here.
|
||||
return Status::Corruption("Invalid blob file number");
|
||||
}
|
||||
|
||||
assert(blob_source_);
|
||||
value->Reset();
|
||||
return blob_source_->GetBlobRange(
|
||||
read_options, user_key, blob_file_number, blob_index.offset(),
|
||||
blob_file_meta->GetBlobFileSize(), blob_index.size(),
|
||||
blob_index.compression(), range_offset, range_length, value, bytes_read);
|
||||
}
|
||||
|
||||
void Version::MultiGetBlob(
|
||||
const ReadOptions& read_options, MultiGetRange& range,
|
||||
std::unordered_map<uint64_t, BlobReadContexts>& blob_ctxs) {
|
||||
@@ -6545,6 +6562,135 @@ Status VersionSet::ReopenManifestForAppend(const std::string& manifest_path) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status VersionSet::AppendColumnFamilyDropsToManifest(
|
||||
const std::string& manifest_path, uint64_t manifest_size,
|
||||
const std::vector<uint32_t>& cf_ids, const WriteOptions& write_options,
|
||||
uint64_t manifest_preallocation_size) {
|
||||
if (cf_ids.empty()) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
// Rewrite the checkpoint MANIFEST via a bounded-memory buffered copy into a
|
||||
// .tmp file (append drops, sync, close), then atomically rename over the
|
||||
// original. Avoids depending on FileSystem::ReopenWritableFile (optional,
|
||||
// unsupported on some backends) and avoids reading the whole MANIFEST into
|
||||
// memory (which can reach ~1GB depending on configuration).
|
||||
const std::string tmp_path = manifest_path + ".tmp";
|
||||
|
||||
// Best-effort cleanup of any stale .tmp left by a prior aborted run.
|
||||
fs_->DeleteFile(tmp_path, IOOptions(), /*dbg=*/nullptr)
|
||||
.PermitUncheckedError();
|
||||
|
||||
std::unique_ptr<FSSequentialFile> src_file;
|
||||
{
|
||||
IOStatus io_s = fs_->NewSequentialFile(manifest_path, FileOptions(),
|
||||
&src_file, /*dbg=*/nullptr);
|
||||
if (!io_s.ok()) {
|
||||
return io_s;
|
||||
}
|
||||
}
|
||||
|
||||
FileOptions opt_file_opts = GetFileOptionsForManifestWrite();
|
||||
// Buffered writes only: direct writes would need alignment / tail-pad
|
||||
// reasoning that isn't worth the complexity for a checkpoint MANIFEST.
|
||||
opt_file_opts.use_direct_writes = false;
|
||||
|
||||
std::unique_ptr<FSWritableFile> dst_file;
|
||||
{
|
||||
IOStatus io_s = fs_->NewWritableFile(tmp_path, opt_file_opts, &dst_file,
|
||||
/*dbg=*/nullptr);
|
||||
if (!io_s.ok()) {
|
||||
return io_s;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy exactly manifest_size bytes through a fixed-size buffer.
|
||||
constexpr size_t kBufSize = 1 * 1024 * 1024;
|
||||
std::string buffer(kBufSize, '\0');
|
||||
uint64_t remaining = manifest_size;
|
||||
while (remaining > 0) {
|
||||
Slice result;
|
||||
const size_t to_read =
|
||||
static_cast<size_t>(std::min<uint64_t>(kBufSize, remaining));
|
||||
IOStatus read_s = src_file->Read(to_read, opt_file_opts.io_options, &result,
|
||||
buffer.data(), /*dbg=*/nullptr);
|
||||
if (!read_s.ok()) {
|
||||
return read_s;
|
||||
}
|
||||
if (result.empty()) {
|
||||
return Status::Corruption(
|
||||
"Unexpected EOF reading source checkpoint MANIFEST");
|
||||
}
|
||||
IOStatus write_s = dst_file->Append(result, opt_file_opts.io_options,
|
||||
/*dbg=*/nullptr);
|
||||
if (!write_s.ok()) {
|
||||
return write_s;
|
||||
}
|
||||
remaining -= result.size();
|
||||
}
|
||||
src_file.reset();
|
||||
|
||||
// Seeds initial_block_offset = manifest_size % kBlockSize so drop-record
|
||||
// framing continues from where the copied contents left off, matching the
|
||||
// position-based log reader.
|
||||
std::unique_ptr<log::Writer> writer =
|
||||
CreateManifestWriter(std::move(dst_file), tmp_path, opt_file_opts,
|
||||
manifest_preallocation_size, manifest_size);
|
||||
|
||||
// Always attempt to Close the writer on any exit path, folding any Close
|
||||
// error into the primary status. Guards against dropping the underlying
|
||||
// FSWritableFile with buffered data / no filesystem close hook.
|
||||
Status s;
|
||||
auto close_writer = [&]() {
|
||||
if (writer == nullptr) {
|
||||
return;
|
||||
}
|
||||
Status close_s = writer->Close(write_options);
|
||||
if (s.ok() && !close_s.ok()) {
|
||||
s = close_s;
|
||||
}
|
||||
writer.reset();
|
||||
};
|
||||
|
||||
for (uint32_t cf_id : cf_ids) {
|
||||
VersionEdit edit;
|
||||
edit.SetColumnFamily(cf_id);
|
||||
edit.DropColumnFamily();
|
||||
std::string record;
|
||||
if (!edit.EncodeTo(&record)) {
|
||||
s = Status::Corruption(
|
||||
"Unable to encode column family drop VersionEdit for checkpoint");
|
||||
close_writer();
|
||||
return s;
|
||||
}
|
||||
IOStatus add_s = writer->AddRecord(write_options, record);
|
||||
if (!add_s.ok()) {
|
||||
s = add_s;
|
||||
close_writer();
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
IOStatus sync_s = SyncManifest(db_options_, write_options, writer->file());
|
||||
if (!sync_s.ok()) {
|
||||
s = sync_s;
|
||||
close_writer();
|
||||
return s;
|
||||
}
|
||||
close_writer();
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// Replace the original with the rewritten .tmp.
|
||||
IOStatus rename_s =
|
||||
fs_->RenameFile(tmp_path, manifest_path, IOOptions(), /*dbg=*/nullptr);
|
||||
if (!rename_s.ok()) {
|
||||
return rename_s;
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status VersionSet::Recover(
|
||||
const std::vector<ColumnFamilyDescriptor>& column_families, bool read_only,
|
||||
std::string* db_id, bool no_error_if_files_missing, bool is_retry,
|
||||
|
||||
+26
-10
@@ -72,6 +72,7 @@ class Compaction;
|
||||
class LogBuffer;
|
||||
class LookupKey;
|
||||
class MemTable;
|
||||
class SameFileBlobReader;
|
||||
class Version;
|
||||
class VersionSet;
|
||||
class WriteBufferManager;
|
||||
@@ -969,21 +970,13 @@ class Version {
|
||||
PinnedIteratorsManager* pinned_iters_mgr, bool* value_found = nullptr,
|
||||
bool* key_exists = nullptr, SequenceNumber* seq = nullptr,
|
||||
ReadCallback* callback = nullptr, bool* is_blob = nullptr,
|
||||
bool do_merge = true);
|
||||
bool do_merge = true,
|
||||
const SameFileBlobReader** lazy_columns_same_file_reader = nullptr);
|
||||
|
||||
DECLARE_SYNC_AND_ASYNC(void, MultiGet, const ReadOptions&,
|
||||
MultiGetRange* range,
|
||||
ReadCallback* callback = nullptr);
|
||||
|
||||
// Interprets blob_index_slice as a blob reference, and (assuming the
|
||||
// corresponding blob file is part of this Version) retrieves the blob and
|
||||
// saves it in *value.
|
||||
// REQUIRES: blob_index_slice stores an encoded blob reference
|
||||
Status GetBlob(const ReadOptions& read_options, const Slice& user_key,
|
||||
const Slice& blob_index_slice,
|
||||
FilePrefetchBuffer* prefetch_buffer, PinnableSlice* value,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// Retrieves a blob using a blob reference and saves it in *value,
|
||||
// assuming the corresponding blob file is part of this Version.
|
||||
Status GetBlob(const ReadOptions& read_options, const Slice& user_key,
|
||||
@@ -991,6 +984,19 @@ class Version {
|
||||
FilePrefetchBuffer* prefetch_buffer, PinnableSlice* value,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// Retrieves a byte sub-range [range_offset, range_offset + range_length) of
|
||||
// an *uncompressed*, separate-file blob's value into *value, reading only
|
||||
// those bytes on a cache miss (see BlobSource::GetBlobRange). The blob file
|
||||
// must be part of this Version. Returns Corruption for a
|
||||
// TTL/inlined/same-file or compressed blob index (the lazy caller only routes
|
||||
// uncompressed separate-file references here; other cases take the
|
||||
// whole-value GetBlob path). The caller must ensure range_offset +
|
||||
// range_length <= blob_index.size().
|
||||
Status GetBlobRange(const ReadOptions& read_options, const Slice& user_key,
|
||||
const BlobIndex& blob_index, uint64_t range_offset,
|
||||
size_t range_length, PinnableSlice* value,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
struct BlobReadContext {
|
||||
BlobReadContext(const BlobIndex& blob_idx, KeyContext* key_ctx)
|
||||
: blob_index(blob_idx), key_context(key_ctx) {}
|
||||
@@ -1684,6 +1690,16 @@ class VersionSet {
|
||||
return manifest_preallocation_size_;
|
||||
}
|
||||
|
||||
// Appends a kColumnFamilyDrop record for each id in cf_ids to the MANIFEST
|
||||
// file at manifest_path, whose valid content length is manifest_size bytes.
|
||||
// Intended for post-processing a checkpoint's copied MANIFEST so that column
|
||||
// families whose SST/blob files were not copied are recorded as dropped and
|
||||
// thus not opened during recovery.
|
||||
Status AppendColumnFamilyDropsToManifest(
|
||||
const std::string& manifest_path, uint64_t manifest_size,
|
||||
const std::vector<uint32_t>& cf_ids, const WriteOptions& write_options,
|
||||
uint64_t manifest_preallocation_size);
|
||||
|
||||
protected:
|
||||
struct ManifestWriter;
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ DEFINE_SYNC_AND_ASYNC(void, Version::Get)
|
||||
PinnableWideColumns* columns, std::string* timestamp, Status* status,
|
||||
MergeContext* merge_context, SequenceNumber* max_covering_tombstone_seq,
|
||||
PinnedIteratorsManager* pinned_iters_mgr, bool* value_found, bool* key_exists,
|
||||
SequenceNumber* seq, ReadCallback* callback, bool* is_blob, bool do_merge) {
|
||||
SequenceNumber* seq, ReadCallback* callback, bool* is_blob, bool do_merge,
|
||||
const SameFileBlobReader** lazy_columns_same_file_reader) {
|
||||
Slice ikey = k.internal_key();
|
||||
Slice user_key = k.user_key();
|
||||
|
||||
@@ -48,7 +49,7 @@ DEFINE_SYNC_AND_ASYNC(void, Version::Get)
|
||||
do_merge ? timestamp : nullptr, value_found, merge_context, do_merge,
|
||||
max_covering_tombstone_seq, clock_, seq,
|
||||
merge_operator_ ? pinned_iters_mgr : nullptr, callback, is_blob_to_use,
|
||||
tracing_get_id, &blob_fetcher);
|
||||
tracing_get_id, &blob_fetcher, lazy_columns_same_file_reader);
|
||||
|
||||
// Pin blocks that we read to hold merge operands
|
||||
if (merge_operator_) {
|
||||
@@ -143,8 +144,9 @@ DEFINE_SYNC_AND_ASYNC(void, Version::Get)
|
||||
|
||||
constexpr uint64_t* bytes_read = nullptr;
|
||||
|
||||
*status = GetBlob(read_options, get_context.ukey_to_get_blob_value(),
|
||||
blob_index, prefetch_buffer, &result, bytes_read);
|
||||
*status = blob_fetcher.FetchBlob(get_context.ukey_to_get_blob_value(),
|
||||
blob_index, prefetch_buffer, &result,
|
||||
bytes_read);
|
||||
if (!status->ok()) {
|
||||
if (status->IsIncomplete()) {
|
||||
get_context.MarkKeyMayExist();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "db/transaction_log_impl.h"
|
||||
#include "db/wal_iterator_impl.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
TransactionLogIteratorImpl::TransactionLogIteratorImpl(
|
||||
WalIteratorImpl::WalIteratorImpl(
|
||||
const std::string& dir, const ImmutableDBOptions* options,
|
||||
const TransactionLogIterator::ReadOptions& read_options,
|
||||
const EnvOptions& soptions, const SequenceNumber seq,
|
||||
std::unique_ptr<VectorWalPtr> files, VersionSet const* const versions,
|
||||
const bool seq_per_batch, const std::shared_ptr<IOTracer>& io_tracer)
|
||||
const WalIterator::ReadOptions& read_options, const EnvOptions& soptions,
|
||||
const SequenceNumber seq, std::unique_ptr<VectorWalPtr> files,
|
||||
VersionSet const* const versions, [[maybe_unused]] const bool seq_per_batch,
|
||||
const std::shared_ptr<IOTracer>& io_tracer)
|
||||
: dir_(dir),
|
||||
options_(options),
|
||||
read_options_(read_options),
|
||||
@@ -26,7 +26,6 @@ TransactionLogIteratorImpl::TransactionLogIteratorImpl(
|
||||
starting_sequence_number_(seq),
|
||||
files_(std::move(files)),
|
||||
versions_(versions),
|
||||
seq_per_batch_(seq_per_batch),
|
||||
io_tracer_(io_tracer),
|
||||
started_(false),
|
||||
is_valid_(false),
|
||||
@@ -35,14 +34,16 @@ TransactionLogIteratorImpl::TransactionLogIteratorImpl(
|
||||
current_last_seq_(0) {
|
||||
assert(files_ != nullptr);
|
||||
assert(versions_ != nullptr);
|
||||
assert(!seq_per_batch_);
|
||||
// WalManager::GetUpdatesSince() rejects seq_per_batch before we get here, so
|
||||
// one update always consumes exactly one sequence number below.
|
||||
assert(!seq_per_batch);
|
||||
current_status_.PermitUncheckedError(); // Clear on start
|
||||
reporter_.env = options_->env;
|
||||
reporter_.info_log = options_->info_log.get();
|
||||
SeekToStartSequence(); // Seek till starting sequence
|
||||
}
|
||||
|
||||
Status TransactionLogIteratorImpl::OpenLogFile(
|
||||
Status WalIteratorImpl::OpenLogFile(
|
||||
const WalFile* log_file,
|
||||
std::unique_ptr<SequentialFileReader>* file_reader) {
|
||||
FileSystemPtr fs(options_->fs, io_tracer_);
|
||||
@@ -71,7 +72,7 @@ Status TransactionLogIteratorImpl::OpenLogFile(
|
||||
return s;
|
||||
}
|
||||
|
||||
BatchResult TransactionLogIteratorImpl::GetBatch() {
|
||||
BatchResult WalIteratorImpl::GetBatch() {
|
||||
assert(is_valid_); // cannot call in a non valid state.
|
||||
BatchResult result;
|
||||
result.sequence = current_batch_seq_;
|
||||
@@ -79,11 +80,11 @@ BatchResult TransactionLogIteratorImpl::GetBatch() {
|
||||
return result;
|
||||
}
|
||||
|
||||
Status TransactionLogIteratorImpl::status() { return current_status_; }
|
||||
Status WalIteratorImpl::status() { return current_status_; }
|
||||
|
||||
bool TransactionLogIteratorImpl::Valid() { return started_ && is_valid_; }
|
||||
bool WalIteratorImpl::Valid() { return started_ && is_valid_; }
|
||||
|
||||
bool TransactionLogIteratorImpl::RestrictedRead(Slice* record) {
|
||||
bool WalIteratorImpl::RestrictedRead(Slice* record) {
|
||||
// Don't read if no more complete entries to read from logs
|
||||
if (current_last_seq_ >= versions_->LastSequence()) {
|
||||
return false;
|
||||
@@ -91,13 +92,12 @@ bool TransactionLogIteratorImpl::RestrictedRead(Slice* record) {
|
||||
return current_log_reader_->ReadRecord(record, &scratch_);
|
||||
}
|
||||
|
||||
void TransactionLogIteratorImpl::SeekToStartSequence(uint64_t start_file_index,
|
||||
bool strict) {
|
||||
void WalIteratorImpl::SeekToStartSequence(uint64_t start_file_index,
|
||||
bool strict) {
|
||||
Slice record;
|
||||
started_ = false;
|
||||
is_valid_ = false;
|
||||
// Check invariant of TransactionLogIterator when SeekToStartSequence()
|
||||
// succeeds.
|
||||
// Check invariant of WalIterator when SeekToStartSequence() succeeds.
|
||||
const Defer defer([this]() {
|
||||
if (is_valid_) {
|
||||
assert(current_status_.ok());
|
||||
@@ -110,6 +110,7 @@ void TransactionLogIteratorImpl::SeekToStartSequence(uint64_t start_file_index,
|
||||
if (files_->size() <= start_file_index) {
|
||||
return;
|
||||
} else if (!current_status_.ok()) {
|
||||
// Already spent; see the comment on current_status_.
|
||||
return;
|
||||
}
|
||||
Status s =
|
||||
@@ -166,14 +167,15 @@ void TransactionLogIteratorImpl::SeekToStartSequence(uint64_t start_file_index,
|
||||
}
|
||||
}
|
||||
|
||||
void TransactionLogIteratorImpl::Next() {
|
||||
void WalIteratorImpl::Next() {
|
||||
if (!current_status_.ok()) {
|
||||
// Spent; the run ended and cannot be resumed. See WalIterator docs.
|
||||
return;
|
||||
}
|
||||
return NextImpl(false);
|
||||
}
|
||||
|
||||
void TransactionLogIteratorImpl::NextImpl(bool internal) {
|
||||
void WalIteratorImpl::NextImpl(bool internal) {
|
||||
Slice record;
|
||||
is_valid_ = false;
|
||||
if (!internal && !started_) {
|
||||
@@ -215,8 +217,13 @@ void TransactionLogIteratorImpl::NextImpl(bool internal) {
|
||||
} else {
|
||||
is_valid_ = false;
|
||||
if (current_last_seq_ == versions_->LastSequence()) {
|
||||
// Caught up. Not an error: the caller may call Next() again later to
|
||||
// pick up writes that have not happened yet.
|
||||
current_status_ = Status::OK();
|
||||
} else {
|
||||
// The DB has moved on but this iterator's set of WAL files is
|
||||
// exhausted, typically because the WAL was rotated after the file
|
||||
// list was collected. The caller must build a new iterator.
|
||||
const char* msg = "Create a new iterator to fetch the new tail.";
|
||||
current_status_ = Status::TryAgain(msg);
|
||||
}
|
||||
@@ -225,16 +232,15 @@ void TransactionLogIteratorImpl::NextImpl(bool internal) {
|
||||
}
|
||||
}
|
||||
|
||||
bool TransactionLogIteratorImpl::IsBatchExpected(
|
||||
const WriteBatch* batch, const SequenceNumber expected_seq) {
|
||||
bool WalIteratorImpl::IsBatchExpected(const WriteBatch* batch,
|
||||
const SequenceNumber expected_seq) {
|
||||
assert(batch);
|
||||
SequenceNumber batchSeq = WriteBatchInternal::Sequence(batch);
|
||||
if (batchSeq != expected_seq) {
|
||||
std::ostringstream oss;
|
||||
oss << "Discontinuity in log records. " << "Got seq=" << batchSeq << ", "
|
||||
<< "Expected seq=" << expected_seq << ", "
|
||||
<< "Last flushed seq=" << versions_->LastSequence() << ". "
|
||||
<< "Log iterator will reseek the correct batch.";
|
||||
<< "Last flushed seq=" << versions_->LastSequence() << ".";
|
||||
|
||||
reporter_.Info(oss.str().c_str());
|
||||
return false;
|
||||
@@ -242,7 +248,7 @@ bool TransactionLogIteratorImpl::IsBatchExpected(
|
||||
return true;
|
||||
}
|
||||
|
||||
void TransactionLogIteratorImpl::UpdateCurrentWriteBatch(const Slice& record) {
|
||||
void WalIteratorImpl::UpdateCurrentWriteBatch(const Slice& record) {
|
||||
std::unique_ptr<WriteBatch> batch(new WriteBatch());
|
||||
Status s = WriteBatchInternal::SetContents(batch.get(), record);
|
||||
s.PermitUncheckedError(); // TODO: What should we do with this error?
|
||||
@@ -250,26 +256,15 @@ void TransactionLogIteratorImpl::UpdateCurrentWriteBatch(const Slice& record) {
|
||||
SequenceNumber expected_seq = current_last_seq_ + 1;
|
||||
// If the iterator has started, then confirm that we get continuous batches
|
||||
if (started_ && !IsBatchExpected(batch.get(), expected_seq)) {
|
||||
// Seek to the batch having expected sequence number
|
||||
if (expected_seq < files_->at(current_file_index_)->StartSequence()) {
|
||||
// Expected batch must lie in the previous log file
|
||||
// Avoid underflow.
|
||||
if (current_file_index_ != 0) {
|
||||
current_file_index_--;
|
||||
}
|
||||
}
|
||||
starting_sequence_number_ = expected_seq;
|
||||
// currentStatus_ will be set to Ok if reseek succeeds
|
||||
// Note: this is still ok in seq_pre_batch_ && two_write_queuesp_ mode
|
||||
// that allows gaps in the WAL since it will still skip over the gap.
|
||||
// A run is contiguous, so a discontinuity ends it. Note that this is a
|
||||
// normal consequence of writes that bypass the WAL (WriteOptions::
|
||||
// disableWAL, IngestExternalFile()), not necessarily of data loss.
|
||||
is_valid_ = false;
|
||||
current_status_ = Status::NotFound("Gap in sequence numbers");
|
||||
// In seq_per_batch_ mode, gaps in the seq are possible so the strict mode
|
||||
// should be disabled
|
||||
return SeekToStartSequence(current_file_index_, !seq_per_batch_);
|
||||
return;
|
||||
}
|
||||
|
||||
current_batch_seq_ = WriteBatchInternal::Sequence(batch.get());
|
||||
assert(!seq_per_batch_);
|
||||
current_last_seq_ =
|
||||
current_batch_seq_ + WriteBatchInternal::Count(batch.get()) - 1;
|
||||
// currentBatchSeq_ can only change here
|
||||
@@ -280,7 +275,7 @@ void TransactionLogIteratorImpl::UpdateCurrentWriteBatch(const Slice& record) {
|
||||
current_status_ = Status::OK();
|
||||
}
|
||||
|
||||
Status TransactionLogIteratorImpl::OpenLogReader(const WalFile* log_file) {
|
||||
Status WalIteratorImpl::OpenLogReader(const WalFile* log_file) {
|
||||
std::unique_ptr<SequentialFileReader> file;
|
||||
Status s = OpenLogFile(log_file, &file);
|
||||
if (!s.ok()) {
|
||||
@@ -14,8 +14,8 @@
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/types.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -54,14 +54,14 @@ class WalFileImpl : public WalFile {
|
||||
uint64_t sizeFileBytes_;
|
||||
};
|
||||
|
||||
class TransactionLogIteratorImpl : public TransactionLogIterator {
|
||||
class WalIteratorImpl : public WalIterator {
|
||||
public:
|
||||
TransactionLogIteratorImpl(
|
||||
const std::string& dir, const ImmutableDBOptions* options,
|
||||
const TransactionLogIterator::ReadOptions& read_options,
|
||||
const EnvOptions& soptions, const SequenceNumber seqNum,
|
||||
std::unique_ptr<VectorWalPtr> files, VersionSet const* const versions,
|
||||
const bool seq_per_batch, const std::shared_ptr<IOTracer>& io_tracer);
|
||||
WalIteratorImpl(const std::string& dir, const ImmutableDBOptions* options,
|
||||
const WalIterator::ReadOptions& read_options,
|
||||
const EnvOptions& soptions, const SequenceNumber seqNum,
|
||||
std::unique_ptr<VectorWalPtr> files,
|
||||
VersionSet const* const versions, const bool seq_per_batch,
|
||||
const std::shared_ptr<IOTracer>& io_tracer);
|
||||
|
||||
bool Valid() override;
|
||||
|
||||
@@ -74,19 +74,20 @@ class TransactionLogIteratorImpl : public TransactionLogIterator {
|
||||
private:
|
||||
const std::string& dir_;
|
||||
const ImmutableDBOptions* options_;
|
||||
const TransactionLogIterator::ReadOptions read_options_;
|
||||
const WalIterator::ReadOptions read_options_;
|
||||
const EnvOptions& soptions_;
|
||||
SequenceNumber starting_sequence_number_;
|
||||
std::unique_ptr<VectorWalPtr> files_;
|
||||
// Used only to get latest seq. num
|
||||
// TODO(icanadi) can this be just a callback?
|
||||
VersionSet const* const versions_;
|
||||
const bool seq_per_batch_;
|
||||
std::shared_ptr<IOTracer> io_tracer_;
|
||||
|
||||
// State variables
|
||||
bool started_;
|
||||
bool is_valid_; // not valid when it starts of.
|
||||
// Once this is set to a non-OK status the iterator is spent: Next() is a
|
||||
// no-op and the status never changes again.
|
||||
Status current_status_;
|
||||
size_t current_file_index_;
|
||||
std::unique_ptr<WriteBatch> current_batch_;
|
||||
@@ -126,4 +127,5 @@ class TransactionLogIteratorImpl : public TransactionLogIterator {
|
||||
void UpdateCurrentWriteBatch(const Slice& record);
|
||||
Status OpenLogReader(const WalFile* file);
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+14
-12
@@ -16,7 +16,7 @@
|
||||
|
||||
#include "db/log_reader.h"
|
||||
#include "db/log_writer.h"
|
||||
#include "db/transaction_log_impl.h"
|
||||
#include "db/wal_iterator_impl.h"
|
||||
#include "db/write_batch_internal.h"
|
||||
#include "file/file_util.h"
|
||||
#include "file/filename.h"
|
||||
@@ -58,7 +58,7 @@ Status WalManager::GetSortedWalFiles(VectorWalPtr& files, bool need_seqnos,
|
||||
|
||||
// Reproduce the race condition where a log file is moved
|
||||
// to archived dir, between these two sync points, used in
|
||||
// (DBTest,TransactionLogIteratorRace)
|
||||
// (DBWalIteratorTest,Race)
|
||||
TEST_SYNC_POINT("WalManager::GetSortedWalFiles:1");
|
||||
TEST_SYNC_POINT("WalManager::GetSortedWalFiles:2");
|
||||
|
||||
@@ -100,10 +100,10 @@ Status WalManager::GetSortedWalFiles(VectorWalPtr& files, bool need_seqnos,
|
||||
return s;
|
||||
}
|
||||
|
||||
Status WalManager::GetUpdatesSince(
|
||||
SequenceNumber seq, std::unique_ptr<TransactionLogIterator>* iter,
|
||||
const TransactionLogIterator::ReadOptions& read_options,
|
||||
VersionSet* version_set) {
|
||||
Status WalManager::GetUpdatesSince(SequenceNumber seq,
|
||||
std::unique_ptr<WalIterator>* iter,
|
||||
const WalIterator::ReadOptions& read_options,
|
||||
VersionSet* version_set) {
|
||||
if (seq_per_batch_) {
|
||||
return Status::NotSupported();
|
||||
}
|
||||
@@ -123,9 +123,9 @@ Status WalManager::GetUpdatesSince(
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
iter->reset(new TransactionLogIteratorImpl(
|
||||
wal_dir_, &db_options_, read_options, file_options_, seq,
|
||||
std::move(wal_files), version_set, seq_per_batch_, io_tracer_));
|
||||
iter->reset(new WalIteratorImpl(wal_dir_, &db_options_, read_options,
|
||||
file_options_, seq, std::move(wal_files),
|
||||
version_set, seq_per_batch_, io_tracer_));
|
||||
return (*iter)->status();
|
||||
}
|
||||
|
||||
@@ -286,11 +286,13 @@ void WalManager::PurgeObsoleteWALFiles() {
|
||||
|
||||
void WalManager::ArchiveWALFile(const std::string& fname, uint64_t number) {
|
||||
auto archived_log_name = ArchivedLogFileName(wal_dir_, number);
|
||||
// The sync point below is used in (DBTest,TransactionLogIteratorRace)
|
||||
// The sync point below is used in
|
||||
// (DBWalIteratorTest,Race)
|
||||
TEST_SYNC_POINT("WalManager::PurgeObsoleteFiles:1");
|
||||
Status s = env_->RenameFile(fname, archived_log_name);
|
||||
IGNORE_STATUS_IF_ERROR(s);
|
||||
// The sync point below is used in (DBTest,TransactionLogIteratorRace)
|
||||
// The sync point below is used in
|
||||
// (DBWalIteratorTest,Race)
|
||||
TEST_SYNC_POINT("WalManager::PurgeObsoleteFiles:2");
|
||||
// The sync point below is used in
|
||||
// (CheckPointTest, CheckpointWithArchievedLog)
|
||||
@@ -329,7 +331,7 @@ Status WalManager::GetSortedWalsOfType(const std::string& path,
|
||||
|
||||
// Reproduce the race condition where a log file is moved
|
||||
// to archived dir, between these two sync points, used in
|
||||
// (DBTest,TransactionLogIteratorRace)
|
||||
// (DBWalIteratorTest,Race)
|
||||
TEST_SYNC_POINT("WalManager::GetSortedWalsOfType:1");
|
||||
TEST_SYNC_POINT("WalManager::GetSortedWalsOfType:2");
|
||||
|
||||
|
||||
+6
-6
@@ -23,8 +23,8 @@
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/status.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/types.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
#include "util/atomic.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -51,12 +51,12 @@ class WalManager {
|
||||
Status GetSortedWalFiles(VectorWalPtr& files, bool need_seqnos = true,
|
||||
bool include_archived = true);
|
||||
|
||||
// Allow user to tail transaction log to find all recent changes to the
|
||||
// Allow user to tail the WAL to find all recent changes to the
|
||||
// database that are newer than `seq_number`.
|
||||
Status GetUpdatesSince(
|
||||
SequenceNumber seq_number, std::unique_ptr<TransactionLogIterator>* iter,
|
||||
const TransactionLogIterator::ReadOptions& read_options,
|
||||
VersionSet* version_set);
|
||||
Status GetUpdatesSince(SequenceNumber seq_number,
|
||||
std::unique_ptr<WalIterator>* iter,
|
||||
const WalIterator::ReadOptions& read_options,
|
||||
VersionSet* version_set);
|
||||
|
||||
void PurgeObsoleteWALFiles();
|
||||
|
||||
|
||||
+11
-12
@@ -105,11 +105,10 @@ class WalManagerTest : public testing::Test {
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<TransactionLogIterator> OpenTransactionLogIter(
|
||||
const SequenceNumber seq) {
|
||||
std::unique_ptr<TransactionLogIterator> iter;
|
||||
std::unique_ptr<WalIterator> OpenWalIter(const SequenceNumber seq) {
|
||||
std::unique_ptr<WalIterator> iter;
|
||||
Status status = wal_manager_->GetUpdatesSince(
|
||||
seq, &iter, TransactionLogIterator::ReadOptions(), versions_.get());
|
||||
seq, &iter, WalIterator::ReadOptions(), versions_.get());
|
||||
EXPECT_OK(status);
|
||||
return iter;
|
||||
}
|
||||
@@ -206,7 +205,7 @@ std::vector<std::uint64_t> ListSpecificFiles(
|
||||
return file_numbers;
|
||||
}
|
||||
|
||||
int CountRecords(TransactionLogIterator* iter) {
|
||||
int CountRecords(WalIterator* iter) {
|
||||
int count = 0;
|
||||
SequenceNumber lastSequence = 0;
|
||||
BatchResult res;
|
||||
@@ -320,7 +319,7 @@ TEST_F(WalManagerTest, WALArchivalTtlClockGoesBackwards) {
|
||||
ListSpecificFiles(env_.get(), archive_dir, kWalFile).size());
|
||||
}
|
||||
|
||||
TEST_F(WalManagerTest, TransactionLogIteratorMoveOverZeroFiles) {
|
||||
TEST_F(WalManagerTest, WalIteratorMoveOverZeroFiles) {
|
||||
Init(nullptr /* clock_override */);
|
||||
RollTheLog(false);
|
||||
Put("key1", std::string(1024, 'a'));
|
||||
@@ -330,22 +329,22 @@ TEST_F(WalManagerTest, TransactionLogIteratorMoveOverZeroFiles) {
|
||||
|
||||
Put("key2", std::string(1024, 'a'));
|
||||
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
ASSERT_EQ(2, CountRecords(iter.get()));
|
||||
}
|
||||
|
||||
TEST_F(WalManagerTest, TransactionLogIteratorJustEmptyFile) {
|
||||
TEST_F(WalManagerTest, WalIteratorJustEmptyFile) {
|
||||
Init(nullptr /* clock_override */);
|
||||
RollTheLog(false);
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
// Check that an empty iterator is returned
|
||||
ASSERT_TRUE(!iter->Valid());
|
||||
}
|
||||
|
||||
TEST_F(WalManagerTest, TransactionLogIteratorNewFileWhileScanning) {
|
||||
TEST_F(WalManagerTest, WalIteratorNewFileWhileScanning) {
|
||||
Init(nullptr /* clock_override */);
|
||||
CreateArchiveLogs(2, 100);
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
CreateArchiveLogs(1, 100);
|
||||
int i = 0;
|
||||
for (; iter->Valid(); iter->Next()) {
|
||||
@@ -356,7 +355,7 @@ TEST_F(WalManagerTest, TransactionLogIteratorNewFileWhileScanning) {
|
||||
// TryAgain indicates a new iterator is needed to fetch the new data
|
||||
ASSERT_TRUE(iter->status().IsTryAgain());
|
||||
|
||||
iter = OpenTransactionLogIter(0);
|
||||
iter = OpenWalIter(0);
|
||||
i = 0;
|
||||
for (; iter->Valid(); iter->Next()) {
|
||||
i++;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,440 @@
|
||||
// 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 "rocksdb/lazy_wide_columns.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/wide/blob_column_resolver_util.h"
|
||||
#include "db/wide/lazy_wide_columns_helper.h"
|
||||
#include "db/wide/read_path_blob_resolver.h"
|
||||
#include "db/wide/wide_column_serialization.h"
|
||||
#include "db/wide/wide_columns_helper.h"
|
||||
#include "monitoring/thread_status_util.h"
|
||||
#include "rocksdb/cleanable.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/wide_columns.h"
|
||||
|
||||
// Current implementation of the lazy blob resolution API. Enumeration and
|
||||
// whole-column resolution are functional. Byte-range reads of an uncompressed
|
||||
// blob -- whether it lives in a separate blob file or is embedded (same-file)
|
||||
// in the SST -- are served by reading only the requested bytes from storage
|
||||
// (skipping checksum verification and cache-fill); other cases (compressed,
|
||||
// whole-column, already-cached, or force_verify) resolve the whole column and
|
||||
// slice it. Cross-key coalescing and async execution are future work; here
|
||||
// LazyWideColumnsBatch::MultiResolve simply routes each read to its owning
|
||||
// per-key result, and MultiGetEntityLazy fills the batch key-by-key.
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace {
|
||||
// Scope guard used while resolving a lazy result: sets the thread's
|
||||
// ThreadStatus operation to OP_LAZY_RESOLVE for the duration, then returns it
|
||||
// to OP_UNKNOWN on exit. A resolve is a self-contained top-level operation, so
|
||||
// it starts and ends with no active op rather than restoring a prior one --
|
||||
// thread operations do not stack, and any op lingering from the originating
|
||||
// GetEntity/MultiGetEntity call is stale. (This mirrors the SetThreadOperation
|
||||
// + ResetThreadStatus pattern other top-level ops such as compaction/DBOpen
|
||||
// use.)
|
||||
//
|
||||
// Setting the parallel operation type keeps the io_activity and thread
|
||||
// operation consistent while the deferred reads (which carry
|
||||
// Env::IOActivity::kLazyResolve) run: db_stress asserts every file read's
|
||||
// io_activity matches the one implied by the current thread operation (see
|
||||
// db_stress_env_wrapper.h / TEST_GetExpectedIOActivity), and ThreadStatus
|
||||
// reporting then attributes these reads to the lazy-resolve operation.
|
||||
//
|
||||
// TODO: unified RAII wrappers for thread status updates
|
||||
class LazyResolveThreadOpScope {
|
||||
public:
|
||||
LazyResolveThreadOpScope() {
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_LAZY_RESOLVE);
|
||||
}
|
||||
~LazyResolveThreadOpScope() {
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_UNKNOWN);
|
||||
}
|
||||
LazyResolveThreadOpScope(const LazyResolveThreadOpScope&) = delete;
|
||||
LazyResolveThreadOpScope& operator=(const LazyResolveThreadOpScope&) = delete;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
// Internal representation. Owns the serialized-entity backing buffer + inline
|
||||
// columns (via `entity_`), the decoded blob references, the per-column views,
|
||||
// the on-demand blob resolver, and the SuperVersion pin that keeps all of it
|
||||
// (blob files, immortal table readers) valid after the producing DB call
|
||||
// returns. The Rep is heap-allocated and never relocated (it lives behind a
|
||||
// unique_ptr that only ever transfers ownership), so pointers to it and to its
|
||||
// `columns_` elements stay stable across LazyWideColumns/Batch moves -- which
|
||||
// is what lets a LazyColumnReadRequest identify its column (and owning result)
|
||||
// by pointer without any move-time fix-ups.
|
||||
class LazyWideColumns::Rep {
|
||||
public:
|
||||
// Holds the serialized entity as a single backing buffer and, after
|
||||
// SetWideColumnValue (without resolution), the full sorted column set --
|
||||
// inline columns' value() are zero-copy Slices into that buffer; blob
|
||||
// columns' value() are the raw serialized BlobIndex bytes (the resolver reads
|
||||
// decoded indices from blob_columns_, not from here).
|
||||
//
|
||||
// Lifetime invariant: `entity_` is populated once (by the point lookup, via
|
||||
// EntityBuffer(), before Finalize) and is never mutated afterwards, and the
|
||||
// Rep is heap-allocated and never relocated. So this backing buffer is stable
|
||||
// for the result's whole lifetime -- which is what keeps valid every Slice
|
||||
// that points into it: the inline column values, the decoded
|
||||
// inlined-BlobIndex values stored in blob_columns_, the per-column
|
||||
// inline_data_ in columns_, and the resolver's cached bytes.
|
||||
PinnableWideColumns entity_;
|
||||
|
||||
// Decoded blob references (column_index -> BlobIndex), for the columns whose
|
||||
// value is a blob reference. Empty for a fully inline entity.
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns_;
|
||||
|
||||
// Per-column view returned by LazyWideColumns::operator[]. Derives from the
|
||||
// public LazyWideColumn (its ctor sets the protected base fields) and adds a
|
||||
// back-pointer to this Rep, used to validate that a read's column belongs to
|
||||
// this result (and, via owning_batch_rep_, to this batch). Nested (rather
|
||||
// than in an anonymous namespace) so it shares the enclosing class's linkage
|
||||
// -- an internal-linkage subobject type in this external-linkage class trips
|
||||
// -Wsubobject-linkage in unity builds.
|
||||
struct ColumnImpl : public LazyWideColumn {
|
||||
ColumnImpl(Rep* parent, size_t index, Slice name, const char* inline_data,
|
||||
std::optional<uint64_t> size)
|
||||
: parent_rep_(parent) {
|
||||
name_ = name;
|
||||
inline_data_ = inline_data;
|
||||
size_ = size;
|
||||
index_ = index;
|
||||
}
|
||||
Rep* parent_rep_ = nullptr;
|
||||
};
|
||||
|
||||
// One entry per column in `entity_.columns()`, in sorted order.
|
||||
std::vector<ColumnImpl> columns_;
|
||||
|
||||
// Identity of the owning batch's Rep (stable across batch move), or null for
|
||||
// a standalone result. Used to validate that a batch read targets an entity
|
||||
// of that batch. Stored as void* to avoid naming LazyWideColumnsBatch::Rep
|
||||
// here (it is only an identity token, never dereferenced).
|
||||
const void* owning_batch_rep_ = nullptr;
|
||||
|
||||
// Stable copy of the user key, referenced by the resolver for blob
|
||||
// verification (the originating key argument does not outlive the call).
|
||||
std::string user_key_;
|
||||
|
||||
// Pins the SuperVersion for this result's lifetime. Declared before
|
||||
// `resolver_` so the resolver (which references the Version) is destroyed
|
||||
// first.
|
||||
Cleanable pin_;
|
||||
|
||||
// On-demand blob resolver, bound to entity_.columns() + blob_columns_.
|
||||
// Emplaced at finalize time (needs the Version). Absent for an unpopulated
|
||||
// (e.g. NotFound) result.
|
||||
std::optional<ReadPathBlobResolver> resolver_;
|
||||
|
||||
~Rep() {
|
||||
// Releasing the SuperVersion pin (pin_) can trigger obsolete-file cleanup
|
||||
// I/O -- e.g. FindObsoleteFiles closing an obsolete WAL -- when this result
|
||||
// held the last reference to it. Mirror DBIter::~DBIter and run that
|
||||
// teardown with the thread operation reset to OP_UNKNOWN, so the incidental
|
||||
// I/O is not attributed to whatever read operation happens to be active on
|
||||
// the destroying thread (which otherwise misattributes I/O stats and trips
|
||||
// db_stress's io_activity invariant).
|
||||
const ThreadStatus::OperationType saved_op =
|
||||
ThreadStatusUtil::GetThreadOperation();
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_UNKNOWN);
|
||||
resolver_.reset(); // borrows the pinned Version; drop before the pin
|
||||
pin_.Reset(); // runs CleanupSuperVersionHandle if this was last ref
|
||||
ThreadStatusUtil::SetThreadOperation(saved_op);
|
||||
}
|
||||
|
||||
// Resolve one read against column `index` of this result (index < size()),
|
||||
// writing the per-read outcome to read.status / read.result. Shared by the
|
||||
// single-entity and batch MultiResolve paths.
|
||||
void ResolveOneRead(size_t index, LazyColumnReadRequest& read) {
|
||||
assert(resolver_);
|
||||
|
||||
// Delegate to the resolver, which owns the partial-vs-whole decision: for a
|
||||
// strict sub-range of an uncompressed blob reference (separate-file or
|
||||
// embedded, and not force_verify) it reads only the requested bytes,
|
||||
// skipping checksum and cache-fill; every other case resolves the whole
|
||||
// column (caching it) and slices. Repeated whole-column reads therefore
|
||||
// read the blob at most once; partial reads own their own bytes and are not
|
||||
// cached. A null read.result still resolves (to surface I/O/integrity
|
||||
// errors on read.status and honor force_verify), just without producing
|
||||
// output -- ResolveColumnRange handles that case.
|
||||
const Status s = resolver_->ResolveColumnRange(
|
||||
index, read.offset, read.length, read.force_verify, read.result);
|
||||
|
||||
if (read.status) {
|
||||
*read.status = s;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
LazyWideColumns::LazyWideColumns() = default;
|
||||
LazyWideColumns::~LazyWideColumns() = default;
|
||||
LazyWideColumns::LazyWideColumns(LazyWideColumns&&) noexcept = default;
|
||||
LazyWideColumns& LazyWideColumns::operator=(LazyWideColumns&&) noexcept =
|
||||
default;
|
||||
|
||||
size_t LazyWideColumns::size() const {
|
||||
return rep_ ? rep_->columns_.size() : 0;
|
||||
}
|
||||
|
||||
const LazyWideColumn& LazyWideColumns::operator[](size_t i) const {
|
||||
assert(rep_);
|
||||
assert(i < rep_->columns_.size());
|
||||
return rep_->columns_[i];
|
||||
}
|
||||
|
||||
Status LazyWideColumns::MultiResolve(size_t num_reads,
|
||||
LazyColumnReadRequest* reads) {
|
||||
LazyResolveThreadOpScope thread_op_scope;
|
||||
for (size_t i = 0; i < num_reads; ++i) {
|
||||
LazyColumnReadRequest& read = reads[i];
|
||||
if (read.status) {
|
||||
*read.status = Status::OK();
|
||||
}
|
||||
if (read.result) {
|
||||
read.result->Reset(); // failure paths below leave an empty result
|
||||
}
|
||||
// The column must belong to this result (it carries a back-pointer to the
|
||||
// Rep that owns it).
|
||||
if (read.column == nullptr ||
|
||||
static_cast<const Rep::ColumnImpl*>(read.column)->parent_rep_ !=
|
||||
rep_.get()) {
|
||||
if (read.status) {
|
||||
*read.status = Status::InvalidArgument(
|
||||
"Column does not belong to this LazyWideColumns");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
rep_->ResolveOneRead(read.column->index(), read);
|
||||
}
|
||||
// Every read was dispatched synchronously; per-read outcomes are in
|
||||
// read.status.
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status LazyWideColumns::ResolveColumnRange(const LazyWideColumn& column,
|
||||
uint64_t offset, size_t length,
|
||||
PinnableSlice* result) {
|
||||
// Sugar over a one-entry batch, so the batch path is the single real,
|
||||
// optimizable (coalescing/async) code path.
|
||||
Status status;
|
||||
LazyColumnReadRequest read;
|
||||
read.column = &column;
|
||||
read.offset = offset;
|
||||
read.length = length;
|
||||
read.result = result;
|
||||
read.status = &status;
|
||||
|
||||
const Status batch_status = MultiResolve(/*num_reads=*/1, &read);
|
||||
return batch_status.ok() ? status : batch_status;
|
||||
}
|
||||
|
||||
Status LazyWideColumns::ResolveColumn(const LazyWideColumn& column,
|
||||
PinnableSlice* result) {
|
||||
return ResolveColumnRange(column, /*offset=*/0, kLazyWholeColumn, result);
|
||||
}
|
||||
|
||||
void LazyWideColumns::Reset() { rep_.reset(); }
|
||||
|
||||
// ---- LazyWideColumnsBatch ----
|
||||
|
||||
// Internal representation. Owns the per-key LazyWideColumns. (In the current
|
||||
// phase each entity holds its own SuperVersion pin; cf_pins is reserved for the
|
||||
// future shared-pin, cross-CF design and is currently unused.)
|
||||
class LazyWideColumnsBatch::Rep {
|
||||
public:
|
||||
// One result per key of the MultiGetEntityLazy call, in key order.
|
||||
std::vector<LazyWideColumns> entities;
|
||||
|
||||
// TODO(lazy-blob-resolution-phase3): switch to one shared SuperVersion pin
|
||||
// per column family here (instead of one self-pin per entity), populated by a
|
||||
// batched MultiGetEntityLazy.
|
||||
std::map<uint32_t /* column_family_id */, Cleanable> cf_pins;
|
||||
};
|
||||
|
||||
LazyWideColumnsBatch::LazyWideColumnsBatch() = default;
|
||||
LazyWideColumnsBatch::~LazyWideColumnsBatch() = default;
|
||||
LazyWideColumnsBatch::LazyWideColumnsBatch(LazyWideColumnsBatch&&) noexcept =
|
||||
default;
|
||||
LazyWideColumnsBatch& LazyWideColumnsBatch::operator=(
|
||||
LazyWideColumnsBatch&&) noexcept = default;
|
||||
|
||||
size_t LazyWideColumnsBatch::size() const {
|
||||
return rep_ ? rep_->entities.size() : 0;
|
||||
}
|
||||
|
||||
const LazyWideColumns& LazyWideColumnsBatch::operator[](size_t i) const {
|
||||
assert(rep_);
|
||||
assert(i < rep_->entities.size());
|
||||
return rep_->entities[i];
|
||||
}
|
||||
|
||||
LazyWideColumns& LazyWideColumnsBatch::operator[](size_t i) {
|
||||
assert(rep_);
|
||||
assert(i < rep_->entities.size());
|
||||
return rep_->entities[i];
|
||||
}
|
||||
|
||||
Status LazyWideColumnsBatch::MultiResolve(size_t num_reads,
|
||||
LazyColumnReadRequest* reads) {
|
||||
LazyResolveThreadOpScope thread_op_scope;
|
||||
const void* this_rep = rep_.get();
|
||||
for (size_t i = 0; i < num_reads; ++i) {
|
||||
LazyColumnReadRequest& read = reads[i];
|
||||
if (read.status) {
|
||||
*read.status = Status::OK();
|
||||
}
|
||||
if (read.result) {
|
||||
read.result->Reset(); // failure paths below leave an empty result
|
||||
}
|
||||
if (read.column == nullptr) {
|
||||
if (read.status) {
|
||||
*read.status = Status::InvalidArgument("Null column in batch read");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Route the read to the entity that owns its column, and require that
|
||||
// entity to belong to this batch. The owning_batch_rep_ == nullptr check is
|
||||
// essential: a standalone column (from GetEntityLazy) has
|
||||
// owning_batch_rep_ == nullptr, and an empty/default-constructed batch has
|
||||
// this_rep == nullptr, so without it a foreign standalone column would slip
|
||||
// through the nullptr == nullptr comparison instead of being rejected.
|
||||
// TODO(lazy-blob-resolution-phase3): group reads per (CF, Version, blob
|
||||
// file) across entities and coalesce them; the request shape is unchanged.
|
||||
LazyWideColumns::Rep* entity_rep =
|
||||
static_cast<const LazyWideColumns::Rep::ColumnImpl*>(read.column)
|
||||
->parent_rep_;
|
||||
if (entity_rep == nullptr || entity_rep->owning_batch_rep_ == nullptr ||
|
||||
entity_rep->owning_batch_rep_ != this_rep) {
|
||||
if (read.status) {
|
||||
*read.status =
|
||||
Status::InvalidArgument("Column does not belong to this batch");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
entity_rep->ResolveOneRead(read.column->index(), read);
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void LazyWideColumnsBatch::Reset() { rep_.reset(); }
|
||||
|
||||
// ---- LazyWideColumnsHelper (DB-side construction) ----
|
||||
|
||||
PinnableWideColumns* LazyWideColumnsHelper::EntityBuffer(
|
||||
LazyWideColumns* result) {
|
||||
assert(result);
|
||||
if (!result->rep_) {
|
||||
result->rep_ = std::make_unique<LazyWideColumns::Rep>();
|
||||
}
|
||||
return &result->rep_->entity_;
|
||||
}
|
||||
|
||||
Status LazyWideColumnsHelper::Finalize(
|
||||
LazyWideColumns* result, const Slice& user_key, const Version* version,
|
||||
const ReadOptions& read_options, BlobFileCache* blob_file_cache,
|
||||
bool allow_write_path_fallback, const SameFileBlobReader* same_file_reader,
|
||||
Cleanable&& pin) {
|
||||
assert(result);
|
||||
assert(result->rep_);
|
||||
LazyWideColumns::Rep& rep = *result->rep_;
|
||||
|
||||
// Decode the blob references (if the entity still has unresolved ones -- it
|
||||
// does not for a fully inline entity, or one resolved eagerly on the memtable
|
||||
// path). When unresolved indices are present the entity occupies a single
|
||||
// backing buffer, so GetSerializedEntity() is valid.
|
||||
if (!PinnableWideColumnsHelper::GetUnresolvedBlobColumnIndices(rep.entity_)
|
||||
.empty()) {
|
||||
WideColumns unused_columns;
|
||||
const Status s = WideColumnSerialization::Deserialize(
|
||||
PinnableWideColumnsHelper::GetSerializedEntity(rep.entity_),
|
||||
unused_columns, &rep.blob_columns_);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
// Build the per-column views from the (sorted) column set + blob references.
|
||||
const WideColumns& columns = rep.entity_.columns();
|
||||
rep.columns_.clear();
|
||||
rep.columns_.reserve(columns.size());
|
||||
for (size_t i = 0; i < columns.size(); ++i) {
|
||||
const BlobIndex* blob_index =
|
||||
blob_resolver_util::FindBlobColumn(&rep.blob_columns_, i);
|
||||
const char* inline_data = nullptr;
|
||||
std::optional<uint64_t> size;
|
||||
if (blob_index == nullptr) {
|
||||
// Inline column: bytes available with no I/O. Slice guarantees a non-null
|
||||
// data(), so a non-null inline_data marks this as inline (not a
|
||||
// reference) even for an empty value.
|
||||
inline_data = columns[i].value().data();
|
||||
size = columns[i].value().size();
|
||||
} else if (blob_index->IsInlined()) {
|
||||
// TTL-inlined blob: an unresolved reference whose logical size is known
|
||||
// (the value lives in the index).
|
||||
size = blob_index->value().size();
|
||||
} else if (blob_index->compression() == kNoCompression) {
|
||||
// Uncompressed reference: on-disk size equals logical size.
|
||||
size = blob_index->size();
|
||||
}
|
||||
// else: compressed reference -- logical size unknown until resolved.
|
||||
rep.columns_.emplace_back(&rep, i, columns[i].name(), inline_data, size);
|
||||
}
|
||||
|
||||
// Take ownership of the SuperVersion pin and stand up the resolver bound to
|
||||
// the (address-stable, since Rep is heap-allocated) entity columns + blob
|
||||
// references. The resolver's deferred blob-byte reads are attributed to
|
||||
// Env::IOActivity::kLazyResolve (distinct from the kGetEntity/kMultiGetEntity
|
||||
// of the initial entity read that already completed via GetImpl).
|
||||
rep.pin_ = std::move(pin);
|
||||
rep.user_key_.assign(user_key.data(), user_key.size());
|
||||
ReadOptions resolve_read_options(read_options);
|
||||
resolve_read_options.io_activity = Env::IOActivity::kLazyResolve;
|
||||
rep.resolver_.emplace(version, resolve_read_options, blob_file_cache,
|
||||
allow_write_path_fallback);
|
||||
rep.resolver_->Reset(Slice(rep.user_key_), &rep.entity_.columns(),
|
||||
&rep.blob_columns_, same_file_reader);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void LazyWideColumnsHelper::InitBatch(LazyWideColumnsBatch* batch,
|
||||
size_t num_entities) {
|
||||
assert(batch);
|
||||
if (!batch->rep_) {
|
||||
batch->rep_ = std::make_unique<LazyWideColumnsBatch::Rep>();
|
||||
}
|
||||
batch->rep_->entities.clear();
|
||||
batch->rep_->entities.resize(num_entities);
|
||||
}
|
||||
|
||||
void LazyWideColumnsHelper::FinalizeBatch(LazyWideColumnsBatch* batch) {
|
||||
assert(batch);
|
||||
if (!batch->rep_) {
|
||||
return;
|
||||
}
|
||||
// Link each populated entity to this batch (by the batch Rep's stable
|
||||
// identity) so batch reads can validate that a column belongs to this batch.
|
||||
const void* batch_rep = batch->rep_.get();
|
||||
for (LazyWideColumns& entity : batch->rep_->entities) {
|
||||
if (entity.rep_) {
|
||||
entity.rep_->owning_batch_rep_ = batch_rep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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 <cstddef>
|
||||
|
||||
#include "rocksdb/cleanable.h"
|
||||
#include "rocksdb/lazy_wide_columns.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class BlobFileCache;
|
||||
class PinnableWideColumns;
|
||||
class SameFileBlobReader;
|
||||
class Version;
|
||||
struct ReadOptions;
|
||||
|
||||
// Internal (DB-side) helper for populating LazyWideColumns /
|
||||
// LazyWideColumnsBatch results without exposing their construction machinery on
|
||||
// the public API (the same pattern as PinnableWideColumnsHelper). Only the DB
|
||||
// read path uses this.
|
||||
class LazyWideColumnsHelper {
|
||||
public:
|
||||
// Creates (if needed) the internal representation of `result` and returns the
|
||||
// PinnableWideColumns to use as the columns output of a lazy point lookup.
|
||||
// The returned buffer is owned by `result`; after the lookup fills it, call
|
||||
// Finalize().
|
||||
static PinnableWideColumns* EntityBuffer(LazyWideColumns* result);
|
||||
|
||||
// Finalizes a lazy result after its EntityBuffer() was populated by a point
|
||||
// lookup: decodes any remaining blob references, builds the enumeration
|
||||
// metadata, constructs the on-demand resolver bound to the entity, and takes
|
||||
// ownership of the SuperVersion `pin`. `version` is the Version blob
|
||||
// references resolve against; `same_file_reader` (may be null) resolves
|
||||
// same-file/embedded references. Returns non-OK only on a decode error.
|
||||
static Status Finalize(LazyWideColumns* result, const Slice& user_key,
|
||||
const Version* version,
|
||||
const ReadOptions& read_options,
|
||||
BlobFileCache* blob_file_cache,
|
||||
bool allow_write_path_fallback,
|
||||
const SameFileBlobReader* same_file_reader,
|
||||
Cleanable&& pin);
|
||||
|
||||
// Prepares `batch` to hold `num_entities` empty per-key results (creating the
|
||||
// batch's representation). Use (*batch)[i] to access each for filling, then
|
||||
// call FinalizeBatch() once all entities are populated.
|
||||
static void InitBatch(LazyWideColumnsBatch* batch, size_t num_entities);
|
||||
|
||||
// Links every populated entity of `batch` back to the batch, so a batch read
|
||||
// can validate that a column belongs to this batch. Call after all entities
|
||||
// have been filled (and individually Finalize()d).
|
||||
static void FinalizeBatch(LazyWideColumnsBatch* batch);
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/blob/same_file_blob_reader.h"
|
||||
#include "db/version_set.h"
|
||||
#include "db/wide/blob_column_resolver_util.h"
|
||||
|
||||
@@ -21,15 +23,24 @@ ReadPathBlobResolver::ReadPathBlobResolver(const Version* version,
|
||||
|
||||
void ReadPathBlobResolver::Reset(
|
||||
const Slice& user_key, const std::vector<WideColumn>* columns,
|
||||
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns) {
|
||||
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns,
|
||||
const SameFileBlobReader* same_file_reader) {
|
||||
user_key_ = user_key;
|
||||
columns_ = columns;
|
||||
blob_columns_ = blob_columns;
|
||||
same_file_reader_ = same_file_reader;
|
||||
resolved_cache_.clear();
|
||||
}
|
||||
|
||||
Status ReadPathBlobResolver::ResolveColumn(size_t column_index,
|
||||
Slice* resolved_value) {
|
||||
return ResolveColumnInternal(column_index, /*force_verify=*/false,
|
||||
resolved_value);
|
||||
}
|
||||
|
||||
Status ReadPathBlobResolver::ResolveColumnInternal(size_t column_index,
|
||||
bool force_verify,
|
||||
Slice* resolved_value) {
|
||||
assert(columns_);
|
||||
assert(resolved_value);
|
||||
|
||||
@@ -64,9 +75,50 @@ Status ReadPathBlobResolver::ResolveColumn(size_t column_index,
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
constexpr uint64_t* bytes_read = nullptr;
|
||||
|
||||
status =
|
||||
blob_fetcher_.FetchBlob(user_key_, blob_index, prefetch_buffer,
|
||||
new_entry.second.get(), bytes_read);
|
||||
// TODO(lazy-blob-resolution-cleanup): the force_verify branching here
|
||||
// (and the whole-vs-range split in ResolveColumnRange) is really a
|
||||
// single 3-valued verify policy -- must-verify-if-present (today's
|
||||
// force_verify), verify-if-no-amplification (verify_checksums on: a
|
||||
// whole read verifies, a partial read skips), and ignore. Map
|
||||
// (verify_checksums, force_verify) to that internal enum once at this
|
||||
// boundary and thread it down, collapsing these branches. Deferred
|
||||
// for feature velocity; see the lazy blob resolution plan.
|
||||
if (force_verify && !blob_index.IsSameFile()) {
|
||||
// Honor force_verify for separate-file references: read and verify
|
||||
// the whole record even if ReadOptions::verify_checksums is off.
|
||||
status = blob_fetcher_.FetchBlobForceVerify(
|
||||
user_key_, blob_index, prefetch_buffer, new_entry.second.get(),
|
||||
bytes_read);
|
||||
} else if (force_verify && blob_index.IsSameFile()) {
|
||||
// Honor force_verify for same-file (embedded) references the same
|
||||
// way: force checksum verification on via a verify-enabled copy of
|
||||
// the read options. A same-file reference is only produced when the
|
||||
// entity was read from an SST that supplied its SameFileBlobReader,
|
||||
// so it is expected to be set; surface a clean error rather than
|
||||
// silently dropping force_verify (and taking the unverified path
|
||||
// below) if it somehow is not.
|
||||
assert(same_file_reader_ != nullptr);
|
||||
if (same_file_reader_ == nullptr) {
|
||||
status = Status::Corruption(
|
||||
"Cannot force-verify same-file blob: no same-file reader");
|
||||
} else {
|
||||
ReadOptions verify_read_options = blob_fetcher_.read_options();
|
||||
verify_read_options.verify_checksums = true;
|
||||
status = same_file_reader_->GetSameFileBlob(
|
||||
verify_read_options, blob_index, new_entry.second.get());
|
||||
}
|
||||
} else {
|
||||
// Route same-file ("embedded") references through the current SST's
|
||||
// SameFileBlobReader when one is set; separate-file references (and
|
||||
// the no-same-file-reader case) go straight to the Version-backed
|
||||
// fetcher. EffectiveFetcher() returns the plain base fetcher when
|
||||
// there is no same-file reader, adding no indirection.
|
||||
EmbeddedAwareBlobFetcher embedded_fetcher(&blob_fetcher_,
|
||||
same_file_reader_);
|
||||
status = embedded_fetcher.EffectiveFetcher()->FetchBlob(
|
||||
user_key_, blob_index, prefetch_buffer, new_entry.second.get(),
|
||||
bytes_read);
|
||||
}
|
||||
if (!status.ok()) {
|
||||
resolved_cache_.pop_back();
|
||||
} else {
|
||||
@@ -79,6 +131,104 @@ Status ReadPathBlobResolver::ResolveColumn(size_t column_index,
|
||||
return status;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Pin [range_offset, range_offset + range_length) of `whole` into *result,
|
||||
// clamped to `whole`'s size (offset at/past end -> empty). The bytes are not
|
||||
// copied or owned here: *result points into `whole`, whose storage must outlive
|
||||
// *result (the resolver's whole-column cache / entity buffer, which lives as
|
||||
// long as this resolver).
|
||||
void PinClampedSubRange(const Slice& whole, uint64_t range_offset,
|
||||
size_t range_length, PinnableSlice* result) {
|
||||
result->Reset();
|
||||
if (range_offset >= whole.size()) {
|
||||
result->PinSlice(Slice(), nullptr);
|
||||
return;
|
||||
}
|
||||
const size_t off = static_cast<size_t>(range_offset);
|
||||
const size_t avail = whole.size() - off;
|
||||
const size_t len = range_length > avail ? avail : range_length;
|
||||
result->PinSlice(Slice(whole.data() + off, len), nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status ReadPathBlobResolver::ResolveColumnRange(size_t column_index,
|
||||
uint64_t range_offset,
|
||||
size_t range_length,
|
||||
bool force_verify,
|
||||
PinnableSlice* result) {
|
||||
assert(columns_);
|
||||
|
||||
if (column_index >= columns_->size()) {
|
||||
return Status::InvalidArgument("Column index out of bounds");
|
||||
}
|
||||
|
||||
// No output buffer: the caller only wants to surface any I/O / integrity
|
||||
// error (and honor force_verify). Resolve the whole column and return; there
|
||||
// is nothing to slice into.
|
||||
if (result == nullptr) {
|
||||
Slice ignored;
|
||||
return ResolveColumnInternal(column_index, force_verify, &ignored);
|
||||
}
|
||||
|
||||
const BlobIndex* blob_index_ptr =
|
||||
blob_resolver_util::FindBlobColumn(blob_columns_, column_index);
|
||||
|
||||
// Decide whether the I/O-saving partial path applies. It requires a blob
|
||||
// reference that is: not already resolved (else we slice the cached whole
|
||||
// value), uncompressed (a strict sub-range of a compressed record can't be
|
||||
// decompressed in isolation), a strict sub-range (a whole-column read takes
|
||||
// the verifying + cache-filling path), and not force_verify. The read then
|
||||
// goes to either the separate-file range fetcher (needs a Version) or, for a
|
||||
// same-file / embedded reference, the current SST's SameFileBlobReader.
|
||||
if (blob_index_ptr != nullptr && !force_verify &&
|
||||
blob_resolver_util::FindInCache(resolved_cache_, column_index) ==
|
||||
nullptr) {
|
||||
const BlobIndex& blob_index = *blob_index_ptr;
|
||||
const bool separate_file_ok =
|
||||
!blob_index.IsSameFile() && blob_fetcher_.SupportsRangeRead();
|
||||
const bool same_file_ok =
|
||||
blob_index.IsSameFile() && same_file_reader_ != nullptr;
|
||||
if (!blob_index.IsInlined() && blob_index.compression() == kNoCompression &&
|
||||
(separate_file_ok || same_file_ok)) {
|
||||
const uint64_t value_size = blob_index.size();
|
||||
const bool strict_subrange =
|
||||
range_offset > 0 || range_length < value_size;
|
||||
if (strict_subrange) {
|
||||
result->Reset();
|
||||
if (range_offset >= value_size || range_length == 0) {
|
||||
// Nothing to read; empty (not an error), no I/O.
|
||||
result->PinSlice(Slice(), nullptr);
|
||||
return Status::OK();
|
||||
}
|
||||
const size_t avail = static_cast<size_t>(value_size - range_offset);
|
||||
const size_t actual_len = range_length > avail ? avail : range_length;
|
||||
if (blob_index.IsSameFile()) {
|
||||
return same_file_reader_->GetSameFileBlobRange(
|
||||
blob_fetcher_.read_options(), blob_index, range_offset,
|
||||
actual_len, result);
|
||||
}
|
||||
return blob_fetcher_.FetchBlobRange(user_key_, blob_index, range_offset,
|
||||
actual_len, result,
|
||||
/*bytes_read=*/nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Full path: resolve the whole column (inline value directly, blob reference
|
||||
// via the resolver's cache, filling it on a miss and verifying under
|
||||
// ReadOptions::verify_checksums or force_verify), then slice out the
|
||||
// requested range.
|
||||
Slice whole;
|
||||
Status s = ResolveColumnInternal(column_index, force_verify, &whole);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
PinClampedSubRange(whole, range_offset, range_length, result);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status ReadPathBlobResolver::ResolveColumns(
|
||||
const std::vector<size_t>& column_indices,
|
||||
std::vector<Slice>* resolved_values) {
|
||||
|
||||
@@ -21,13 +21,14 @@
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class Version;
|
||||
class SameFileBlobReader;
|
||||
|
||||
// TODO: ReadPathBlobResolver and CompactionBlobResolver (in
|
||||
// compaction_iterator.h) share significant logic for blob column resolution
|
||||
// and caching. A refactoring into a common base class or shared utility
|
||||
// could reduce duplication. The two classes differ in their blob-fetching
|
||||
// backends (Version::GetBlob vs BlobFetcher::FetchBlob) and contexts
|
||||
// (read path vs compaction path with stats tracking).
|
||||
// could reduce duplication. The two classes differ in fetcher ownership and
|
||||
// their surrounding contexts (read path vs compaction path with stats
|
||||
// tracking).
|
||||
//
|
||||
// Enables lazy (on-demand) resolution of blob column values in the read path.
|
||||
// When a wide-column entity contains blob references (V2 format), the resolver
|
||||
@@ -50,8 +51,17 @@ class ReadPathBlobResolver {
|
||||
// Reset the resolver for a new entity. Clears all cached values.
|
||||
// The columns and blob_columns pointers must remain valid for the lifetime
|
||||
// of the resolver (or until the next Reset call).
|
||||
//
|
||||
// same_file_reader: when non-null, same-file ("embedded") blob references are
|
||||
// resolved against it (the SST that held the entity) via an
|
||||
// EmbeddedAwareBlobFetcher; it must outlive the resolver (ensured, on the
|
||||
// lazy read path, by the same SuperVersion pin that keeps the Version alive,
|
||||
// combined with immortal table readers when max_open_files == -1). When null
|
||||
// (e.g. the DBIter path, which resolves embedded refs separately), only
|
||||
// separate-file references are resolvable.
|
||||
void Reset(const Slice& user_key, const std::vector<WideColumn>* columns,
|
||||
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns);
|
||||
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns,
|
||||
const SameFileBlobReader* same_file_reader = nullptr);
|
||||
|
||||
// Resolve the value for the column at the given index.
|
||||
// For blob columns, fetches the blob value from the blob file (or returns
|
||||
@@ -62,6 +72,29 @@ class ReadPathBlobResolver {
|
||||
// - I/O error occurred while fetching the blob
|
||||
Status ResolveColumn(size_t column_index, Slice* resolved_value);
|
||||
|
||||
// Resolve a byte sub-range [range_offset, range_offset + range_length) of the
|
||||
// column at `column_index` into *result (zero-copy). `range_length` may be
|
||||
// larger than the remaining bytes (or SIZE_MAX for "to the end"); it is
|
||||
// clamped to the column's logical size. An offset at/past the end yields an
|
||||
// empty result (not an error).
|
||||
//
|
||||
// Reads only the requested bytes -- skipping whole-record checksum
|
||||
// verification and blob-cache population -- only for a strict sub-range of an
|
||||
// uncompressed blob reference (in a separate blob file, or
|
||||
// embedded/same-file) that is not already resolved, when !force_verify and
|
||||
// the read is servable (a Version-backed range read for separate-file refs,
|
||||
// or a SameFileBlobReader for embedded refs). Every other case (inline
|
||||
// column, already-resolved column, inlined blob, compressed reference, a
|
||||
// whole-column read, or force_verify) resolves the whole column (caching it,
|
||||
// verifying under ReadOptions::verify_checksums) and slices the requested
|
||||
// range out of it.
|
||||
//
|
||||
// Unlike ResolveColumn, a partial read is NOT cached: *result owns (or pins)
|
||||
// its own bytes, independent of this resolver's whole-column cache.
|
||||
Status ResolveColumnRange(size_t column_index, uint64_t range_offset,
|
||||
size_t range_length, bool force_verify,
|
||||
PinnableSlice* result);
|
||||
|
||||
// Resolve multiple columns in the order provided by `column_indices`.
|
||||
// Resolved blob values are cached exactly as if ResolveColumn() were called
|
||||
// repeatedly.
|
||||
@@ -91,6 +124,13 @@ class ReadPathBlobResolver {
|
||||
}
|
||||
|
||||
private:
|
||||
// Shared implementation of ResolveColumn / the whole-column path of
|
||||
// ResolveColumnRange. When force_verify is set, a separate-file blob
|
||||
// reference is read with checksum verification forced on even if
|
||||
// ReadOptions::verify_checksums is off (see LazyColumnReadRequest).
|
||||
Status ResolveColumnInternal(size_t column_index, bool force_verify,
|
||||
Slice* resolved_value);
|
||||
|
||||
// Owns its ReadOptions: a resolver's lifetime is independent of the caller
|
||||
// that created it (e.g. it may outlive the originating ReadOptions once
|
||||
// returned as part of a lazy result).
|
||||
@@ -99,6 +139,9 @@ class ReadPathBlobResolver {
|
||||
Slice user_key_;
|
||||
const std::vector<WideColumn>* columns_ = nullptr;
|
||||
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns_ = nullptr;
|
||||
// Non-null on the lazy read path for entities with same-file blob references;
|
||||
// see Reset().
|
||||
const SameFileBlobReader* same_file_reader_ = nullptr;
|
||||
|
||||
// Cache for resolved blob values to avoid re-fetching.
|
||||
// Uses a vector of (column_index, PinnableSlice) pairs. Typical entities
|
||||
|
||||
@@ -381,6 +381,19 @@ class BatchedOpsStressTest : public StressTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reuse one of the sibling entities we just read (under read_opts_copy's
|
||||
// snapshot, and cross-checked above) as the lazy read's reference, rather
|
||||
// than issuing another eager read.
|
||||
const size_t lazy_idx = thread->rand.Uniform(static_cast<int>(num_keys));
|
||||
const std::string lazy_key = std::to_string(lazy_idx) + key_suffix;
|
||||
const WideColumns& eager_ref_columns =
|
||||
FLAGS_use_attribute_group
|
||||
? attribute_group_results[lazy_idx].front().columns()
|
||||
: column_results[lazy_idx].columns();
|
||||
const WideColumns* const eager_ref =
|
||||
eager_ref_columns.empty() ? nullptr : &eager_ref_columns;
|
||||
MaybeTestGetEntityLazy(thread, read_opts_copy, cfh, lazy_key, eager_ref);
|
||||
}
|
||||
|
||||
void TestMultiGetEntity(ThreadState* thread, const ReadOptions& read_opts,
|
||||
@@ -485,6 +498,16 @@ class BatchedOpsStressTest : public StressTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<EagerEntityRef> eager_refs(num_prefixes);
|
||||
for (size_t j = 0; j < num_prefixes; ++j) {
|
||||
eager_refs[j].status = results[j][0].status();
|
||||
if (eager_refs[j].status.ok()) {
|
||||
eager_refs[j].columns = &results[j][0].columns();
|
||||
}
|
||||
}
|
||||
MaybeTestMultiGetEntityLazy(thread, read_opts_copy, cfh, num_prefixes,
|
||||
key_slices.data(), &eager_refs);
|
||||
} else {
|
||||
// Non-AttributeGroup MultiGetEntity verification
|
||||
|
||||
@@ -546,6 +569,16 @@ class BatchedOpsStressTest : public StressTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<EagerEntityRef> eager_refs(num_prefixes);
|
||||
for (size_t j = 0; j < num_prefixes; ++j) {
|
||||
eager_refs[j].status = statuses[j];
|
||||
if (statuses[j].ok()) {
|
||||
eager_refs[j].columns = &results[j].columns();
|
||||
}
|
||||
}
|
||||
MaybeTestMultiGetEntityLazy(thread, read_opts_copy, cfh, num_prefixes,
|
||||
key_slices.data(), &eager_refs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -574,6 +607,8 @@ class BatchedOpsStressTest : public StressTest {
|
||||
std::array<ReadOptions, num_prefixes> ro_copies;
|
||||
std::array<std::string, num_prefixes> upper_bounds;
|
||||
std::array<Slice, num_prefixes> ub_slices;
|
||||
std::array<std::function<bool(const TableProperties&)>, num_prefixes>
|
||||
table_filters;
|
||||
std::array<std::unique_ptr<Iterator>, num_prefixes> iters;
|
||||
|
||||
const Snapshot* const snapshot = db_->GetSnapshot();
|
||||
@@ -597,9 +632,9 @@ class BatchedOpsStressTest : public StressTest {
|
||||
ub_slices[i] = upper_bounds[i];
|
||||
ro_copies[i].iterate_upper_bound = &(ub_slices[i]);
|
||||
if (FLAGS_use_sqfc_for_range_queries) {
|
||||
ro_copies[i].table_filter =
|
||||
sqfc_factory_->GetTableFilterForRangeQuery(prefix_slices[i],
|
||||
ub_slices[i]);
|
||||
table_filters[i] = sqfc_factory_->GetTableFilterForRangeQuery(
|
||||
prefix_slices[i], ub_slices[i]);
|
||||
ro_copies[i].table_filter = &table_filters[i];
|
||||
}
|
||||
} else {
|
||||
// Otherwise, bound by prefix
|
||||
@@ -685,7 +720,7 @@ class BatchedOpsStressTest : public StressTest {
|
||||
// if the first iterator finished, they should have all finished
|
||||
assert(!iters[i]->Valid() ||
|
||||
!iters[i]->key().starts_with(prefix_slices[i]));
|
||||
assert(iters[i]->status().ok());
|
||||
DB_STRESS_ASSERT_OK(iters[i]->status());
|
||||
}
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
|
||||
@@ -382,6 +382,10 @@ class CfConsistencyStressTest : public StressTest {
|
||||
is_consistent = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Random single-CF read: no pinned snapshot to share with a reference, so
|
||||
// run a self-contained lazy read (it pins its own snapshot).
|
||||
MaybeTestGetEntityLazy(thread, read_opts, cfh, key);
|
||||
} else {
|
||||
// With a 1/2 chance, compare one key across all CFs
|
||||
ManagedSnapshot snapshot_guard(db_);
|
||||
@@ -422,6 +426,15 @@ class CfConsistencyStressTest : public StressTest {
|
||||
}
|
||||
}
|
||||
|
||||
// Reuse the CF[0] entity we just read (under read_opts_copy's snapshot,
|
||||
// and verified above) as the lazy read's reference for that CF.
|
||||
if (cmp_found && is_consistent) {
|
||||
const WideColumns& eager_ref = cmp_result.columns();
|
||||
MaybeTestGetEntityLazy(thread, read_opts_copy,
|
||||
column_families_[rand_column_families[0]], key,
|
||||
&eager_ref);
|
||||
}
|
||||
|
||||
if (is_consistent) {
|
||||
if (FLAGS_use_attribute_group) {
|
||||
PinnableAttributeGroups result;
|
||||
@@ -680,7 +693,7 @@ class CfConsistencyStressTest : public StressTest {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert(s.ok());
|
||||
DB_STRESS_ASSERT_OK(s);
|
||||
if (cmp_s.IsNotFound()) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity (AttributeGroup) returns different results "
|
||||
@@ -736,6 +749,16 @@ class CfConsistencyStressTest : public StressTest {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<EagerEntityRef> eager_refs(num_keys);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
eager_refs[i].status = results[i][0].status();
|
||||
if (eager_refs[i].status.ok()) {
|
||||
eager_refs[i].columns = &results[i][0].columns();
|
||||
}
|
||||
}
|
||||
MaybeTestMultiGetEntityLazy(thread, read_opts_copy, cfhs[0], num_keys,
|
||||
key_slices.data(), &eager_refs);
|
||||
|
||||
} else {
|
||||
// Non-AttributeGroup MultiGetEntity verification
|
||||
|
||||
@@ -793,7 +816,7 @@ class CfConsistencyStressTest : public StressTest {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert(s.ok());
|
||||
DB_STRESS_ASSERT_OK(s);
|
||||
if (cmp_s.IsNotFound()) {
|
||||
fprintf(
|
||||
stderr,
|
||||
@@ -860,6 +883,26 @@ class CfConsistencyStressTest : public StressTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-CF note: the lazy read API has no cross-column-family (`CF**`)
|
||||
// overload yet -- MultiGetEntityLazy is single-CF -- so we exercise it on
|
||||
// just cfhs[0] (a cross-CF lazy overload is a planned follow-up; see the
|
||||
// lazy blob resolution plan). The attribute-group branch above reuses its
|
||||
// already-read cfhs[0] columns as the reference; the non-attribute branch
|
||||
// reads each key across CFs one at a time and so holds no batch of cfhs[0]
|
||||
// results to reuse -- fall back to a self-contained lazy read there.
|
||||
if (!FLAGS_use_attribute_group && LazyEntityReadEnabled()) {
|
||||
std::vector<std::string> lazy_key_strs;
|
||||
std::vector<Slice> lazy_key_slices;
|
||||
lazy_key_strs.reserve(num_keys);
|
||||
lazy_key_slices.reserve(num_keys);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
lazy_key_strs.emplace_back(Key(rand_keys[i]));
|
||||
lazy_key_slices.emplace_back(lazy_key_strs.back());
|
||||
}
|
||||
MaybeTestMultiGetEntityLazy(thread, read_opts_copy, cfhs[0], num_keys,
|
||||
lazy_key_slices.data());
|
||||
}
|
||||
}
|
||||
|
||||
Status TestPrefixScan(ThreadState* thread, const ReadOptions& readoptions,
|
||||
@@ -877,6 +920,7 @@ class CfConsistencyStressTest : public StressTest {
|
||||
|
||||
std::string upper_bound;
|
||||
Slice ub_slice;
|
||||
std::function<bool(const TableProperties&)> table_filter;
|
||||
|
||||
ReadOptions ro_copy = readoptions;
|
||||
std::unique_ptr<ManagedSnapshot> snapshot = nullptr;
|
||||
@@ -891,8 +935,9 @@ class CfConsistencyStressTest : public StressTest {
|
||||
ub_slice = Slice(upper_bound);
|
||||
ro_copy.iterate_upper_bound = &ub_slice;
|
||||
if (FLAGS_use_sqfc_for_range_queries) {
|
||||
ro_copy.table_filter =
|
||||
table_filter =
|
||||
sqfc_factory_->GetTableFilterForRangeQuery(prefix, ub_slice);
|
||||
ro_copy.table_filter = &table_filter;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,11 @@
|
||||
#ifdef GFLAGS
|
||||
#include "db_stress_tool/db_stress_common.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
|
||||
#include "db_stress_tool/db_stress_test_base.h"
|
||||
@@ -170,6 +173,245 @@ int64_t GetOneHotKeyID(double rand_seed, int64_t max_key) {
|
||||
return rand_local.Next() % max_key;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint64_t kLongActiveOperationTimeoutMultiplier = 4;
|
||||
|
||||
int LivenessSleepMicros() {
|
||||
const std::chrono::microseconds max_sleep_micros(
|
||||
std::numeric_limits<int>::max());
|
||||
const uint64_t max_sleep_seconds =
|
||||
std::chrono::duration_cast<std::chrono::seconds>(max_sleep_micros)
|
||||
.count();
|
||||
const std::chrono::seconds sleep_seconds(
|
||||
std::min(FLAGS_liveness_check_interval_sec, max_sleep_seconds));
|
||||
return static_cast<int>(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(sleep_seconds)
|
||||
.count());
|
||||
}
|
||||
|
||||
bool StopBgThreadIfNeeded(SharedState* shared) {
|
||||
port::Mutex* mu = shared->GetMutex();
|
||||
// The liveness watchdog must not block on the shared harness mutex; a stuck
|
||||
// holder is one of the failure modes it needs to report.
|
||||
if (!mu->TryLock()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!shared->ShouldStopBgThread()) {
|
||||
mu->Unlock();
|
||||
return false;
|
||||
}
|
||||
|
||||
shared->IncBgThreadsFinished();
|
||||
if (shared->BgThreadsFinished()) {
|
||||
shared->GetCondVar()->SignalAll();
|
||||
}
|
||||
mu->Unlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t ElapsedMicros(uint64_t now, uint64_t start) {
|
||||
return now >= start ? now - start : 0;
|
||||
}
|
||||
|
||||
uint64_t ElapsedSeconds(uint64_t now, uint64_t start) {
|
||||
constexpr uint64_t kMicrosPerSecond = 1000 * 1000;
|
||||
return ElapsedMicros(now, start) / kMicrosPerSecond;
|
||||
}
|
||||
|
||||
uint64_t ActiveOperationTimeoutSeconds(StressOperationType type,
|
||||
uint64_t no_progress_timeout_sec) {
|
||||
switch (type) {
|
||||
case StressOperationType::kNone:
|
||||
case StressOperationType::kCount:
|
||||
return 0;
|
||||
case StressOperationType::kCompactFiles:
|
||||
case StressOperationType::kCompactRange:
|
||||
case StressOperationType::kFlush:
|
||||
case StressOperationType::kVerifyDb:
|
||||
case StressOperationType::kVerifyChecksum:
|
||||
case StressOperationType::kVerifyFileChecksums:
|
||||
case StressOperationType::kIngestExternalFile:
|
||||
case StressOperationType::kBackup:
|
||||
case StressOperationType::kCheckpoint:
|
||||
case StressOperationType::kApproximateSize:
|
||||
case StressOperationType::kPauseBackground:
|
||||
case StressOperationType::kGetLiveFiles:
|
||||
return no_progress_timeout_sec * kLongActiveOperationTimeoutMultiplier;
|
||||
case StressOperationType::kReopen:
|
||||
case StressOperationType::kSetOptions:
|
||||
case StressOperationType::kManualWalFlush:
|
||||
case StressOperationType::kLockWal:
|
||||
case StressOperationType::kSyncWal:
|
||||
case StressOperationType::kMetadata:
|
||||
case StressOperationType::kDisableFileDeletions:
|
||||
case StressOperationType::kDisableManualCompaction:
|
||||
case StressOperationType::kAbortResumeCompactions:
|
||||
case StressOperationType::kGetProperty:
|
||||
case StressOperationType::kTableProperties:
|
||||
case StressOperationType::kSnapshot:
|
||||
case StressOperationType::kKeyMayExist:
|
||||
case StressOperationType::kRead:
|
||||
case StressOperationType::kPrefixScan:
|
||||
case StressOperationType::kWrite:
|
||||
case StressOperationType::kDelete:
|
||||
case StressOperationType::kDeleteRange:
|
||||
case StressOperationType::kIterate:
|
||||
case StressOperationType::kCustom:
|
||||
return no_progress_timeout_sec;
|
||||
}
|
||||
return no_progress_timeout_sec;
|
||||
}
|
||||
|
||||
void PrintLivenessState(SharedState* shared, uint64_t now) {
|
||||
fprintf(stderr, "Finished db_stress operations=%" PRIu64 "\n",
|
||||
shared->GetFinishedOps());
|
||||
fprintf(stderr, "Successful compactions=%" PRIu64 "\n",
|
||||
shared->GetSuccessfulCompactions());
|
||||
fprintf(stderr, "Completed db_stress operation scopes by type:");
|
||||
bool printed_completed_op_count = false;
|
||||
for (size_t i = 1; i < kStressOperationTypeCount; ++i) {
|
||||
const auto type = static_cast<StressOperationType>(i);
|
||||
const uint64_t count = shared->GetCompletedOpsForDiagnostics(type);
|
||||
if (count > 0) {
|
||||
fprintf(stderr, " %s=%" PRIu64, StressOperationTypeName(type), count);
|
||||
printed_completed_op_count = true;
|
||||
}
|
||||
}
|
||||
if (!printed_completed_op_count) {
|
||||
fprintf(stderr, " none");
|
||||
}
|
||||
fprintf(stderr, "\n");
|
||||
|
||||
for (uint32_t tid = 0; tid < shared->GetNumThreads(); ++tid) {
|
||||
const ThreadOperationSnapshot snapshot =
|
||||
shared->GetThreadOperationSnapshot(tid);
|
||||
if (snapshot.type == StressOperationType::kNone ||
|
||||
snapshot.started_micros == 0) {
|
||||
fprintf(stderr, "thread %" PRIu32 " active_op=none\n", tid);
|
||||
continue;
|
||||
}
|
||||
|
||||
fprintf(stderr,
|
||||
"thread %" PRIu32 " active_op=%s active_op_started_micros=%" PRIu64
|
||||
" active_op_elapsed_micros=%" PRIu64 "\n",
|
||||
tid, StressOperationTypeName(snapshot.type),
|
||||
snapshot.started_micros,
|
||||
ElapsedMicros(now, snapshot.started_micros));
|
||||
}
|
||||
}
|
||||
|
||||
bool HasStuckWriteOperation(SharedState* shared, uint64_t now,
|
||||
uint64_t timeout_sec) {
|
||||
for (uint32_t tid = 0; tid < shared->GetNumThreads(); ++tid) {
|
||||
const ThreadOperationSnapshot snapshot =
|
||||
shared->GetThreadOperationSnapshot(tid);
|
||||
const uint64_t elapsed_micros = ElapsedMicros(now, snapshot.started_micros);
|
||||
const uint64_t elapsed_seconds =
|
||||
ElapsedSeconds(now, snapshot.started_micros);
|
||||
if (snapshot.type == StressOperationType::kWrite &&
|
||||
snapshot.started_micros != 0 && elapsed_seconds >= timeout_sec) {
|
||||
fprintf(stderr,
|
||||
"Liveness watchdog detected a stuck write operation on thread "
|
||||
"%" PRIu32 " for %" PRIu64
|
||||
" seconds. active_op_elapsed_micros=%" PRIu64
|
||||
" active_op_timeout_seconds=%" PRIu64 "\n",
|
||||
tid, elapsed_seconds, elapsed_micros, timeout_sec);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HasTimedOutNonWriteOperation(SharedState* shared, uint64_t now,
|
||||
uint64_t no_progress_timeout_sec) {
|
||||
for (uint32_t tid = 0; tid < shared->GetNumThreads(); ++tid) {
|
||||
const ThreadOperationSnapshot snapshot =
|
||||
shared->GetThreadOperationSnapshot(tid);
|
||||
if (snapshot.type == StressOperationType::kWrite ||
|
||||
snapshot.started_micros == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint64_t timeout_sec =
|
||||
ActiveOperationTimeoutSeconds(snapshot.type, no_progress_timeout_sec);
|
||||
if (timeout_sec != 0 &&
|
||||
ElapsedSeconds(now, snapshot.started_micros) >= timeout_sec) {
|
||||
fprintf(stderr,
|
||||
"Liveness watchdog detected a timed out active operation on "
|
||||
"thread %" PRIu32
|
||||
". active_op=%s active_op_elapsed_micros=%" PRIu64
|
||||
" active_op_timeout_seconds=%" PRIu64 "\n",
|
||||
tid, StressOperationTypeName(snapshot.type),
|
||||
ElapsedMicros(now, snapshot.started_micros), timeout_sec);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void LivenessWatchdogThread(void* v) {
|
||||
assert(FLAGS_liveness_check_interval_sec > 0);
|
||||
assert(FLAGS_liveness_no_progress_timeout_sec > 0);
|
||||
|
||||
auto* thread = static_cast<ThreadState*>(v);
|
||||
SharedState* shared = thread->shared;
|
||||
|
||||
uint64_t last_finished_ops = shared->GetFinishedOps();
|
||||
uint64_t last_progress_time_micros = raw_env->NowMicros();
|
||||
const uint64_t no_progress_timeout_sec =
|
||||
FLAGS_liveness_no_progress_timeout_sec;
|
||||
|
||||
while (true) {
|
||||
if (StopBgThreadIfNeeded(shared)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool started = shared->OperationStarted();
|
||||
const bool all_operated = shared->OperationFinished();
|
||||
|
||||
const uint64_t now = raw_env->NowMicros();
|
||||
if (!started || all_operated) {
|
||||
last_finished_ops = shared->GetFinishedOps();
|
||||
last_progress_time_micros = now;
|
||||
raw_env->SleepForMicroseconds(LivenessSleepMicros());
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint64_t finished_ops = shared->GetFinishedOps();
|
||||
if (HasStuckWriteOperation(shared, now, no_progress_timeout_sec)) {
|
||||
PrintLivenessState(shared, now);
|
||||
fflush(stderr);
|
||||
shared->TerminateWithoutMutex();
|
||||
} else if (HasTimedOutNonWriteOperation(shared, now,
|
||||
no_progress_timeout_sec)) {
|
||||
PrintLivenessState(shared, now);
|
||||
fflush(stderr);
|
||||
shared->TerminateWithoutMutex();
|
||||
} else if (finished_ops != last_finished_ops) {
|
||||
last_finished_ops = finished_ops;
|
||||
last_progress_time_micros = now;
|
||||
} else if (now >= last_progress_time_micros &&
|
||||
ElapsedSeconds(now, last_progress_time_micros) >=
|
||||
no_progress_timeout_sec) {
|
||||
fprintf(stderr,
|
||||
"Liveness watchdog detected no completed db_stress operations "
|
||||
"for %" PRIu64 " seconds. finished_ops=%" PRIu64 "\n",
|
||||
FLAGS_liveness_no_progress_timeout_sec, finished_ops);
|
||||
PrintLivenessState(shared, now);
|
||||
fflush(stderr);
|
||||
shared->TerminateWithoutMutex();
|
||||
} else if (now < last_progress_time_micros) {
|
||||
last_progress_time_micros = now;
|
||||
}
|
||||
|
||||
raw_env->SleepForMicroseconds(LivenessSleepMicros());
|
||||
}
|
||||
}
|
||||
|
||||
void PoolSizeChangeThread(void* v) {
|
||||
assert(FLAGS_compaction_thread_pool_adjust_interval > 0);
|
||||
ThreadState* thread = static_cast<ThreadState*>(v);
|
||||
@@ -754,6 +996,19 @@ std::string GetNowNanos() {
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string GetReadTimestamp() {
|
||||
uint64_t read_timestamp = raw_env->NowNanos();
|
||||
if (!FLAGS_persist_user_defined_timestamps) {
|
||||
// Add 10 seconds of headroom because a concurrent flush can advance
|
||||
// full_history_ts_low making the read timestamp invalid.
|
||||
read_timestamp += 10'000'000'000ULL;
|
||||
}
|
||||
|
||||
std::string ret;
|
||||
PutFixed64(&ret, read_timestamp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint64_t GetWriteUnixTime(ThreadState* thread) {
|
||||
static uint64_t kPreserveSeconds =
|
||||
std::max(FLAGS_preserve_internal_time_seconds,
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "db_stress_tool/db_stress_env_wrapper.h"
|
||||
#include "db_stress_tool/db_stress_listener.h"
|
||||
#include "db_stress_tool/db_stress_shared_state.h"
|
||||
#include "db_stress_tool/db_stress_status.h"
|
||||
#include "db_stress_tool/db_stress_test_base.h"
|
||||
#include "logging/logging.h"
|
||||
#include "monitoring/histogram.h"
|
||||
@@ -228,6 +229,10 @@ DECLARE_uint64(sst_file_manager_bytes_per_truncate);
|
||||
DECLARE_int32(backup_one_in);
|
||||
DECLARE_uint64(backup_max_size);
|
||||
DECLARE_int32(checkpoint_one_in);
|
||||
DECLARE_int32(checkpoint_engine_max_background_operations);
|
||||
DECLARE_bool(checkpoint_engine_use_link_file_when_available);
|
||||
DECLARE_int32(parallel_checkpoint_one_in);
|
||||
DECLARE_int32(subset_cf_checkpoint_one_in);
|
||||
DECLARE_int32(ingest_external_file_one_in);
|
||||
DECLARE_int32(ingest_external_file_width);
|
||||
DECLARE_int32(ingest_external_file_prepare_commit_one_in);
|
||||
@@ -244,6 +249,7 @@ DECLARE_int32(pause_background_one_in);
|
||||
DECLARE_int32(disable_file_deletions_one_in);
|
||||
DECLARE_int32(disable_manual_compaction_one_in);
|
||||
DECLARE_int32(abort_and_resume_compactions_one_in);
|
||||
DECLARE_int32(abort_and_resume_cf_compactions_one_in);
|
||||
DECLARE_int32(compact_range_width);
|
||||
DECLARE_int32(acquire_snapshot_one_in);
|
||||
DECLARE_bool(compare_full_db_state_snapshot);
|
||||
@@ -253,6 +259,7 @@ DECLARE_bool(use_multiget);
|
||||
DECLARE_bool(use_async_db_api);
|
||||
DECLARE_bool(use_get_entity);
|
||||
DECLARE_bool(use_multi_get_entity);
|
||||
DECLARE_int32(lazy_entity_read_one_in);
|
||||
DECLARE_int32(readpercent);
|
||||
DECLARE_int32(prefixpercent);
|
||||
DECLARE_int32(writepercent);
|
||||
@@ -268,6 +275,8 @@ DECLARE_int32(compression_max_dict_bytes);
|
||||
DECLARE_int32(compression_zstd_max_train_bytes);
|
||||
DECLARE_int32(compression_parallel_threads);
|
||||
DECLARE_uint64(compression_max_dict_buffer_bytes);
|
||||
DECLARE_bool(compression_auto_skip);
|
||||
DECLARE_int32(compression_auto_skip_min_sample_every);
|
||||
DECLARE_bool(compression_use_zstd_dict_trainer);
|
||||
DECLARE_bool(compression_checksum);
|
||||
DECLARE_string(checksum_type);
|
||||
@@ -340,6 +349,7 @@ DECLARE_bool(enable_blob_direct_write);
|
||||
DECLARE_uint64(blob_direct_write_partitions);
|
||||
DECLARE_uint64(min_blob_size);
|
||||
DECLARE_uint64(blob_file_size);
|
||||
DECLARE_uint64(blob_file_writable_file_max_buffer_size);
|
||||
DECLARE_string(blob_compression_type);
|
||||
DECLARE_bool(enable_blob_garbage_collection);
|
||||
DECLARE_double(blob_garbage_collection_age_cutoff);
|
||||
@@ -818,6 +828,8 @@ void PoolSizeChangeThread(void* v);
|
||||
|
||||
void DbVerificationThread(void* v);
|
||||
|
||||
void LivenessWatchdogThread(void* v);
|
||||
|
||||
void RemoteCompactionWorkerThread(void* v);
|
||||
|
||||
void CompressedCacheSetCapacityThread(void* v);
|
||||
@@ -880,6 +892,8 @@ int64_t GetOneHotKeyID(double rand_seed, int64_t max_key);
|
||||
|
||||
std::string GetNowNanos();
|
||||
|
||||
std::string GetReadTimestamp();
|
||||
|
||||
uint64_t GetWriteUnixTime(ThreadState* thread);
|
||||
|
||||
std::shared_ptr<FileChecksumGenFactory> GetFileChecksumImpl(
|
||||
|
||||
@@ -78,11 +78,8 @@ bool RunStressTestImpl(SharedState* shared) {
|
||||
if (s.ok() && !ev_dir.empty()) {
|
||||
s = InitUnverifiedSubdir(ev_dir);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "%sFailed to setup unverified state dir: %s\n",
|
||||
db_label.c_str(), s.ToString().c_str());
|
||||
exit(1);
|
||||
}
|
||||
DB_STRESS_ASSERT_OK_MSG(s, "%sFailed to setup unverified state dir",
|
||||
db_label.c_str());
|
||||
}
|
||||
|
||||
stress->InitDb(shared);
|
||||
@@ -95,6 +92,13 @@ bool RunStressTestImpl(SharedState* shared) {
|
||||
|
||||
shared->SetThreads(n);
|
||||
|
||||
const bool liveness_watchdog_enabled =
|
||||
FLAGS_liveness_check_interval_sec > 0 &&
|
||||
FLAGS_liveness_no_progress_timeout_sec > 0;
|
||||
if (liveness_watchdog_enabled) {
|
||||
shared->IncBgThreads();
|
||||
}
|
||||
|
||||
if (FLAGS_continuous_verification_interval > 0) {
|
||||
shared->IncBgThreads();
|
||||
}
|
||||
@@ -131,6 +135,11 @@ bool RunStressTestImpl(SharedState* shared) {
|
||||
raw_env->StartThread(DbVerificationThread, &continuous_verification_thread);
|
||||
}
|
||||
|
||||
ThreadState liveness_watchdog_thread(0, shared);
|
||||
if (liveness_watchdog_enabled) {
|
||||
raw_env->StartThread(LivenessWatchdogThread, &liveness_watchdog_thread);
|
||||
}
|
||||
|
||||
// Spawn at most one CompressedCacheSetCapacityThread globally. The cache
|
||||
// is shared across all DBs, and the thread's SetCapacity(0)/assert(==0)
|
||||
// sequence would race if multiple DBs each spawned their own copy.
|
||||
@@ -177,11 +186,8 @@ bool RunStressTestImpl(SharedState* shared) {
|
||||
if (s.ok() && !ev_dir.empty()) {
|
||||
s = DestroyUnverifiedSubdir(ev_dir);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "%sFailed to cleanup unverified state dir: %s\n",
|
||||
db_label.c_str(), s.ToString().c_str());
|
||||
exit(1);
|
||||
}
|
||||
DB_STRESS_ASSERT_OK_MSG(s, "%sFailed to cleanup unverified state dir",
|
||||
db_label.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +209,7 @@ bool RunStressTestImpl(SharedState* shared) {
|
||||
}
|
||||
if (ShouldDisableAutoCompactionsBeforeVerifyDb()) {
|
||||
Status s = stress->EnableAutoCompaction();
|
||||
assert(s.ok());
|
||||
DB_STRESS_ASSERT_OK(s);
|
||||
}
|
||||
fprintf(stdout, "%s %sStarting database operations\n",
|
||||
clock->TimeToString(now / 1000000).c_str(), db_label.c_str());
|
||||
@@ -268,7 +274,7 @@ bool RunStressTestImpl(SharedState* shared) {
|
||||
}
|
||||
|
||||
if (FLAGS_compaction_thread_pool_adjust_interval > 0 ||
|
||||
FLAGS_continuous_verification_interval > 0 ||
|
||||
FLAGS_continuous_verification_interval > 0 || liveness_watchdog_enabled ||
|
||||
FLAGS_compressed_secondary_cache_size > 0 ||
|
||||
FLAGS_compressed_secondary_cache_ratio > 0.0 ||
|
||||
remote_compaction_worker_thread_count > 0) {
|
||||
|
||||
@@ -92,11 +92,11 @@ class DbStressRandomAccessFileWrapper : public FSRandomAccessFileOwnerWrapper {
|
||||
dbg);
|
||||
}
|
||||
|
||||
void SubmitReadAsync(FSReadRequest& req, const IOOptions& options,
|
||||
bool SubmitReadAsync(FSReadRequest& req, const IOOptions& options,
|
||||
std::function<void(FSReadRequest&)> cb,
|
||||
IODebugContext* dbg) override {
|
||||
CheckIOActivity(options);
|
||||
target()->SubmitReadAsync(req, options, std::move(cb), dbg);
|
||||
return target()->SubmitReadAsync(req, options, std::move(cb), dbg);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include "db_stress_tool/db_stress_status.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
#ifdef GFLAGS
|
||||
@@ -82,7 +84,7 @@ SstQueryFilterConfigsManager& DbStressSqfcManager() {
|
||||
static std::shared_ptr<SstQueryFilterConfigsManager> mgr;
|
||||
std::call_once(flag, []() {
|
||||
Status s = SstQueryFilterConfigsManager::MakeShared(data, &mgr);
|
||||
assert(s.ok());
|
||||
DB_STRESS_ASSERT_OK(s);
|
||||
assert(mgr);
|
||||
});
|
||||
return *mgr;
|
||||
|
||||
@@ -156,6 +156,16 @@ DEFINE_bool(verbose, false, "Verbose");
|
||||
DEFINE_bool(progress_reports, true,
|
||||
"If true, db_stress will report number of finished operations");
|
||||
|
||||
DEFINE_uint64(
|
||||
liveness_check_interval_sec, 0,
|
||||
"If non-zero, check periodically whether db_stress operations are making "
|
||||
"progress. Requires --liveness_no_progress_timeout_sec to be non-zero.");
|
||||
|
||||
DEFINE_uint64(
|
||||
liveness_no_progress_timeout_sec, 0,
|
||||
"If non-zero with --liveness_check_interval_sec, terminate db_stress when "
|
||||
"no operation completes for this many seconds during the operation phase.");
|
||||
|
||||
DEFINE_uint64(db_write_buffer_size,
|
||||
ROCKSDB_NAMESPACE::Options().db_write_buffer_size,
|
||||
"Number of bytes to buffer in all memtables before compacting");
|
||||
@@ -527,6 +537,13 @@ DEFINE_uint64(blob_file_size,
|
||||
ROCKSDB_NAMESPACE::AdvancedColumnFamilyOptions().blob_file_size,
|
||||
"[Integrated BlobDB] The size limit for blob files.");
|
||||
|
||||
DEFINE_uint64(
|
||||
blob_file_writable_file_max_buffer_size,
|
||||
ROCKSDB_NAMESPACE::AdvancedColumnFamilyOptions()
|
||||
.blob_file_writable_file_max_buffer_size,
|
||||
"[Integrated BlobDB] Max WritableFileWriter buffer size for blob files. "
|
||||
"0 means inherit writable_file_max_buffer_size.");
|
||||
|
||||
DEFINE_string(blob_compression_type, "none",
|
||||
"[Integrated BlobDB] The compression algorithm to use for large "
|
||||
"values stored in blob files.");
|
||||
@@ -861,6 +878,25 @@ DEFINE_int32(checkpoint_one_in, 0,
|
||||
"every N operations on average. 0 indicates CreateCheckpoint() "
|
||||
"is disabled.");
|
||||
|
||||
DEFINE_int32(checkpoint_engine_max_background_operations, 1,
|
||||
"Background-thread parallelism of the shared CheckpointEngine "
|
||||
"(always opened). Values <= 0 are treated as 1.");
|
||||
|
||||
DEFINE_bool(checkpoint_engine_use_link_file_when_available, true,
|
||||
"Whether the parallel CheckpointEngine may hard-link data files "
|
||||
"instead of copying them. False forces the parallel copy path.");
|
||||
|
||||
DEFINE_int32(parallel_checkpoint_one_in, 0,
|
||||
"Each checkpoint uses the parallel CheckpointEngine with "
|
||||
"probability 1/N, else the legacy serial API (0 = always serial, "
|
||||
"1 = always parallel).");
|
||||
|
||||
DEFINE_int32(subset_cf_checkpoint_one_in, 0,
|
||||
"Each checkpoint covers only a random proper subset of the column "
|
||||
"families with probability 1/N (0 = always all column families, "
|
||||
"1 = always a subset). Ignored when there is only the default "
|
||||
"column family.");
|
||||
|
||||
DEFINE_int32(ingest_external_file_one_in, 0,
|
||||
"If non-zero, then IngestExternalFile() will be called once for "
|
||||
"every N operations on average. 0 indicates IngestExternalFile() "
|
||||
@@ -937,6 +973,11 @@ DEFINE_int32(abort_and_resume_compactions_one_in, 0,
|
||||
"If non-zero, then AbortAllCompactions()+Resume will be called "
|
||||
"once for every N ops on average. 0 disables.");
|
||||
|
||||
DEFINE_int32(abort_and_resume_cf_compactions_one_in, 0,
|
||||
"If non-zero, then AbortCompactions(cf)+ResumeCompactions(cf) "
|
||||
"will be called on a random column family once for every N ops "
|
||||
"on average. 0 disables.");
|
||||
|
||||
DEFINE_int32(compact_range_width, 10000,
|
||||
"The width of the ranges passed to CompactRange().");
|
||||
|
||||
@@ -968,6 +1009,18 @@ DEFINE_bool(use_get_entity, false, "If set, use the GetEntity API for reads.");
|
||||
DEFINE_bool(use_multi_get_entity, false,
|
||||
"If set, use the MultiGetEntity API for reads.");
|
||||
|
||||
DEFINE_int32(
|
||||
lazy_entity_read_one_in, 0,
|
||||
"If greater than 0, then on roughly 1/N of GetEntity / MultiGetEntity "
|
||||
"operations, additionally read the same key(s) via the lazy wide-column "
|
||||
"API "
|
||||
"(GetEntityLazy / MultiGetEntityLazy) under a pinned snapshot and verify "
|
||||
"the "
|
||||
"lazily-resolved columns (a random subset, possibly none) match the eager "
|
||||
"GetEntity result. Only takes effect when open_files == -1 (required by "
|
||||
"the "
|
||||
"lazy API), user_timestamp_size == 0, and not using transactions.");
|
||||
|
||||
DEFINE_int32(test_ingest_standalone_range_deletion_one_in, 0,
|
||||
"If non-zero, file ingestion flow will test standalone range "
|
||||
"deletion file once every N file ingestion operations.");
|
||||
@@ -1079,6 +1132,15 @@ DEFINE_uint64(compression_max_dict_buffer_bytes, 0,
|
||||
"Buffering limit for SST file data to sample for dictionary "
|
||||
"compression.");
|
||||
|
||||
DEFINE_bool(compression_auto_skip, false,
|
||||
"Enable AutoSkip compression: stop attempting compression on data "
|
||||
"blocks once it is not paying off (reuses max_compressed_bytes_per_"
|
||||
"kb as the bar).");
|
||||
|
||||
DEFINE_int32(compression_auto_skip_min_sample_every, 0,
|
||||
"AutoSkip nominal sampling interval (skipped data blocks between "
|
||||
"forced compression samples). 0 selects an internal default.");
|
||||
|
||||
DEFINE_bool(
|
||||
compression_use_zstd_dict_trainer, true,
|
||||
"Use zstd's trainer to generate dictionary. If the options is false, "
|
||||
|
||||
Loaded 100 of 231 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user