mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-07 03:22:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6ac302081 | ||
|
|
108d3bf062 | ||
|
|
d6cd1fc884 | ||
|
|
dfd356fa10 | ||
|
|
60d6f470e8 | ||
|
|
ea16e73a71 | ||
|
|
4c5486efec | ||
|
|
d699ffbd5c | ||
|
|
e4a7136d3e | ||
|
|
f7b791b213 | ||
|
|
6dbb6f30e6 | ||
|
|
bd16531142 | ||
|
|
e6a2ee0bd2 | ||
|
|
ad81b654a4 | ||
|
|
8f492b2f47 | ||
|
|
e8554f1b11 | ||
|
|
330ded5129 | ||
|
|
4b41ccd2a7 | ||
|
|
2846163d9e | ||
|
|
03b8d1afa0 | ||
|
|
42fca1bb39 | ||
|
|
d3a82f28ce | ||
|
|
d8536d3274 | ||
|
|
136d0d1fa1 | ||
|
|
49faa51e11 | ||
|
|
7d18ffe316 | ||
|
|
db1adf47bb | ||
|
|
bbdcd2825f | ||
|
|
495c21dc5c | ||
|
|
575b6df1b8 | ||
|
|
f0c1e3d7d9 | ||
|
|
b616b203b8 | ||
|
|
92a6308300 | ||
|
|
021374c62c | ||
|
|
9475798205 | ||
|
|
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 | ||
|
|
7247edbf0a | ||
|
|
f5c0c3e274 | ||
|
|
2fc58a678b | ||
|
|
a4645e864d | ||
|
|
039c45075e | ||
|
|
fbd446cebb | ||
|
|
6f5587ae2d | ||
|
|
c73841572a | ||
|
|
14a7646fb5 | ||
|
|
03c3c090fb | ||
|
|
71d3bd1773 | ||
|
|
bb77d17072 | ||
|
|
1eb4a6e3e1 | ||
|
|
19482988e3 | ||
|
|
0c0bfbe8be | ||
|
|
fac47c3072 | ||
|
|
f56112abb7 | ||
|
|
744f120b12 | ||
|
|
0347b4df61 | ||
|
|
755c104bca | ||
|
|
0a2ba2a164 | ||
|
|
dcb5c2bf53 | ||
|
|
7c385f4441 | ||
|
|
2c77c32ac9 | ||
|
|
2b628ccfe5 | ||
|
|
0745290ca6 | ||
|
|
ffa95117b9 | ||
|
|
30ba4908ba | ||
|
|
144deb248a | ||
|
|
990827e916 | ||
|
|
7c4bf11044 | ||
|
|
81ea677072 | ||
|
|
04cad97e7e | ||
|
|
865568398a | ||
|
|
2809ce9d0f | ||
|
|
223b5ee13d | ||
|
|
6d8370ec56 |
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",
|
||||
@@ -95,6 +96,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/merge_operator.cc",
|
||||
"db/multi_scan.cc",
|
||||
"db/output_validator.cc",
|
||||
"db/periodic_compaction_phaser.cc",
|
||||
"db/periodic_task_scheduler.cc",
|
||||
"db/range_del_aggregator.cc",
|
||||
"db/range_tombstone_fragmenter.cc",
|
||||
@@ -103,7 +105,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 +112,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",
|
||||
@@ -268,6 +271,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"util/compression.cc",
|
||||
"util/compression_context_cache.cc",
|
||||
"util/concurrent_task_limiter_impl.cc",
|
||||
"util/coro_stats_util.cc",
|
||||
"util/crc32c.cc",
|
||||
"util/crc32c_arm64.cc",
|
||||
"util/data_structure.cc",
|
||||
@@ -306,6 +310,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"utilities/compaction_filters.cc",
|
||||
"utilities/compaction_filters/remove_emptyvalue_compactionfilter.cc",
|
||||
"utilities/convenience/info_log_finder.cc",
|
||||
"utilities/copy_engine/copy_engine.cc",
|
||||
"utilities/counted_fs.cc",
|
||||
"utilities/debug.cc",
|
||||
"utilities/env_mirror.cc",
|
||||
@@ -379,11 +384,21 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"utilities/write_batch_with_index/write_batch_with_index_internal.cc",
|
||||
], deps=[
|
||||
"//folly/container:f14_hash",
|
||||
"//folly/coro:baton",
|
||||
"//folly/coro:blocking_wait",
|
||||
"//folly/coro:collect",
|
||||
"//folly/coro:coroutine",
|
||||
"//folly/coro:nothrow",
|
||||
"//folly/coro:task",
|
||||
"//folly/executors/thread_factory:named_thread_factory",
|
||||
"//folly/executors:io_executor",
|
||||
"//folly/executors:io_thread_pool_executor",
|
||||
"//folly/io/async:async_base",
|
||||
"//folly/io/async:event_base_manager",
|
||||
"//folly/io/async:io_uring_backend",
|
||||
"//folly/io/async:request_context",
|
||||
"//folly/synchronization:distributed_mutex",
|
||||
"//folly:executor",
|
||||
], headers=glob(["**/*.h"]), link_whole=False, extra_test_libs=False)
|
||||
|
||||
cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[], deps=[":rocksdb_lib"], headers=[], link_whole=True, extra_test_libs=False)
|
||||
@@ -4743,6 +4758,12 @@ cpp_unittest_wrapper(name="configurable_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="copy_engine_test",
|
||||
srcs=["utilities/copy_engine/copy_engine_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="corruption_test",
|
||||
srcs=["db/corruption_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -4929,6 +4950,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"],
|
||||
|
||||
@@ -14,6 +14,12 @@ This document provides guidance for generating and reviewing code in the RocksDB
|
||||
|
||||
**Error Handling:** Ensure robust error handling throughout the codebase. Use RocksDB's `Status` type consistently, propagate errors appropriately, and avoid silently ignoring failures. Reviewers pay close attention to edge cases and failure modes.
|
||||
|
||||
**Refactoring traps:** At least in production code, avoid constructs that could allow existing code to unexpectedly, quietly change meaning.
|
||||
Specifically:
|
||||
* Avoid new defaulted parameters. This is the #1 trap on refactoring!
|
||||
* Avoid static_cast, reinterpret_cast, and C-style casts; static_cast_with_check, up_cast, and lossless_cast from cast_util.h are preferred.
|
||||
* Avoid declared type `auto` (`auto&`, `auto*` OK).
|
||||
|
||||
### Testing Philosophy
|
||||
|
||||
**Comprehensive Coverage:** Every change should include appropriate test coverage. This includes unit tests for isolated functionality, integration tests for component interactions, and stress tests for concurrency and performance validation. Reviewers will ask for additional tests if coverage is insufficient.
|
||||
@@ -32,7 +38,7 @@ This document provides guidance for generating and reviewing code in the RocksDB
|
||||
|
||||
**Memory Copy:** Avoid unnecessary memory copies. Use move semantics, `std::string_view`, `Slice`, and pass-by-reference where appropriate. Be aware of implicit copies in STL containers and function returns. Prefer in-place operations over copy-and-modify patterns.
|
||||
|
||||
**CPU Cache Efficiency:** Design data structures and access patterns to be cache-friendly. Keep frequently accessed data together (data locality). Prefer sequential memory access over random access. Be mindful of cache line sizes (typically 64 bytes) and avoid false sharing in concurrent code. Consider struct packing and field ordering to improve cache utilization.
|
||||
**CPU Cache Efficiency:** Design data structures and access patterns to be cache-friendly. Keep frequently accessed data together (data locality). Prefer sequential memory access over random access. Be mindful of cache line sizes (CACHE_LINE_SIZE, typically 64 bytes) and avoid false sharing in concurrent code (see CacheAlignedWrapper). Especially when adding new data members, be mindful of ordering within structs and classes to reduce unnecessary padding (special care with bool members) and avoid larger-than-necessary types (e.g. prefer OptSlice to std::optional<Slice>).
|
||||
|
||||
**Loop Optimization:** Look for opportunities to collapse nested loops, reduce loop overhead, and minimize branch mispredictions. Hoist invariant computations out of loops. Consider loop unrolling for tight inner loops. Batch operations when possible to amortize per-operation overhead.
|
||||
|
||||
@@ -47,7 +53,7 @@ This document provides guidance for generating and reviewing code in the RocksDB
|
||||
- **Cold path** (executed rarely, e.g., DB open, configuration parsing, error handling): Maintainability and clarity are more important. Prefer readable code over micro-optimizations. Complex optimizations here add maintenance burden with negligible performance benefit.
|
||||
- **Warm path** (moderate frequency): Balance both concerns. Use profiling data to guide optimization decisions.
|
||||
|
||||
**Avoid Premature Optimization:** While performance is critical, focus on correctness first, then optimize based on profiling data. However, be performance-aware from the start—choosing the right algorithm and data structure upfront is not premature optimization. Use the hot path analysis above to decide how much optimization effort is warranted.
|
||||
**Avoid Premature Optimization:** Performance is known critical in places like inner loops of compaction, read path and write path. Elsewhere, try to find a "holy trinity" solution of *correct, simple, and reasonably efficient*. More complex solutions are only appropriate when profiling/benchmarking data shows a measurable payoff.
|
||||
|
||||
### API Design and Compatibility
|
||||
|
||||
@@ -55,7 +61,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.
|
||||
|
||||
---
|
||||
|
||||
@@ -73,7 +79,7 @@ The database core handles write-ahead logging (WAL), memtables, compaction, and
|
||||
|
||||
**Testing:** Database core changes require extensive testing, including unit tests, integration tests, and stress tests. Test with various configurations, compaction styles, and concurrent workloads.
|
||||
|
||||
### Public Headers (`include`)
|
||||
### Public Headers / "Public API" (`include/rocksdb/`)
|
||||
|
||||
Public headers define RocksDB's API surface. Changes here have the highest compatibility impact.
|
||||
|
||||
@@ -259,6 +265,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 +308,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 +323,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
|
||||
@@ -339,6 +356,9 @@ rather than relying on libstdc++ transitive includes.
|
||||
* For CI-style flaky tests that do not reproduce with `gtest_parallel.py`,
|
||||
`--gtest_repeat`, or normal coerce-mode runs, inspect
|
||||
`tools/gtest_parallel_repro.py --help`.
|
||||
* Each unit test file has overheads, so avoid creating new unit test files
|
||||
for random minor features. Consider adding to slice_test, db_etc3_test, or
|
||||
others.
|
||||
|
||||
### Unit test dedup guidelines
|
||||
* Extract helper functions for repeated patterns such as object
|
||||
@@ -386,6 +406,13 @@ 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.
|
||||
|
||||
### Blog posts (docs/_posts)
|
||||
* Blog post authors must be defined in `docs/_data/authors.yml` to be displayed
|
||||
@@ -397,7 +424,9 @@ rather than relying on libstdc++ transitive includes.
|
||||
multiple minutes.
|
||||
* Run `AUTO_CLEAN=1 ASSERT_STATUS_CHECKED=1 make check` to verify all Status
|
||||
objects are properly checked. This catches missing error handling that can
|
||||
lead to silent data corruption.
|
||||
lead to silent data corruption. Merely building with ASSERT_STATUS_CHECKED=1
|
||||
accomplishes nothing; it enables a runtime check. New unit tests missing
|
||||
Status checks is a common failure point, even from agents.
|
||||
|
||||
### Monitoring make check progress
|
||||
* Use `make check-progress` to get machine-parseable JSON progress while
|
||||
|
||||
+23
-2
@@ -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
|
||||
@@ -837,6 +838,7 @@ set(SOURCES
|
||||
db/merge_operator.cc
|
||||
db/multi_scan.cc
|
||||
db/output_validator.cc
|
||||
db/periodic_compaction_phaser.cc
|
||||
db/periodic_task_scheduler.cc
|
||||
db/range_del_aggregator.cc
|
||||
db/range_tombstone_fragmenter.cc
|
||||
@@ -845,7 +847,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 +854,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
|
||||
@@ -1002,6 +1005,7 @@ set(SOURCES
|
||||
util/simple_mixed_compressor.cc
|
||||
util/compression_context_cache.cc
|
||||
util/concurrent_task_limiter_impl.cc
|
||||
util/coro_stats_util.cc
|
||||
util/crc32c.cc
|
||||
util/data_structure.cc
|
||||
util/dynamic_bloom.cc
|
||||
@@ -1023,6 +1027,7 @@ set(SOURCES
|
||||
util/xxhash.cc
|
||||
utilities/agg_merge/agg_merge.cc
|
||||
utilities/backup/backup_engine.cc
|
||||
utilities/copy_engine/copy_engine.cc
|
||||
utilities/blob_db/blob_compaction_filter.cc
|
||||
utilities/blob_db/blob_db.cc
|
||||
utilities/blob_db/blob_db_impl.cc
|
||||
@@ -1558,6 +1563,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
|
||||
@@ -1640,6 +1646,7 @@ if(WITH_TESTS)
|
||||
utilities/cassandra/cassandra_row_merge_test.cc
|
||||
utilities/cassandra/cassandra_serialize_test.cc
|
||||
utilities/checkpoint/checkpoint_test.cc
|
||||
utilities/copy_engine/copy_engine_test.cc
|
||||
utilities/sorted_run_builder/sorted_run_builder_test.cc
|
||||
utilities/env_timed_test.cc
|
||||
utilities/fault_injection_fs_test.cc
|
||||
@@ -1707,7 +1714,21 @@ if(WITH_TESTS)
|
||||
)
|
||||
target_link_libraries(${exename}${ARTIFACT_SUFFIX} testutillib${ARTIFACT_SUFFIX} testharness gtest ${THIRDPARTY_LIBS} ${ROCKSDB_LIB})
|
||||
if(NOT "${exename}" MATCHES "db_sanity_test")
|
||||
gtest_discover_tests(${exename} DISCOVERY_TIMEOUT 120)
|
||||
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.18)
|
||||
# Discover tests at ctest time instead of during the parallel build.
|
||||
# POST_BUILD discovery (the default) runs each freshly-built test
|
||||
# binary while the -j build is still compiling; on resource-constrained
|
||||
# CI (notably the arm64 macOS runners) that discovery run can yield
|
||||
# empty output, and CMake's JSON parser then aborts the whole build
|
||||
# with "Syntax error: value, object or array expected". PRE_TEST moves
|
||||
# discovery out of the build phase so a slow/empty enumeration no
|
||||
# longer fails the build.
|
||||
gtest_discover_tests(${exename}
|
||||
DISCOVERY_TIMEOUT 120
|
||||
DISCOVERY_MODE PRE_TEST)
|
||||
else()
|
||||
gtest_discover_tests(${exename} DISCOVERY_TIMEOUT 120)
|
||||
endif()
|
||||
add_dependencies(rocksdb_check ${exename}${ARTIFACT_SUFFIX})
|
||||
endif()
|
||||
endforeach(sourcefile ${TESTS})
|
||||
|
||||
+71
@@ -1,6 +1,77 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 11.10.0 (08/28/2026)
|
||||
### New Features
|
||||
* Added `BlockBasedTableOptions::optimize_key_common_prefix` as part of new `format_version=8`. When enabled, each data block and index block stores the common user-key prefix of its keys once instead of at every restart point, reducing block size and block-cache footprint for prefix-heavy data and speeding up Seek for bytewise-ordered keys.
|
||||
* A new experimental feature spreads out periodic (time-based) compaction trigger times across a fleet of similarly-configured DBs to avoid recurring "thundering herd" triggers, especially with remote compaction. See the new experimental DB option `periodic_compaction_phase_recovery_percent`.
|
||||
|
||||
### Public API Changes
|
||||
* `IngestExternalFileArg::atomic_replace_range` now supports a range that only partially overlaps existing files, instead of failing. Requires universal compaction and a table factory that supports range deletion; other configurations still fail on partial overlap. The range is also now specified as half-open (`start` inclusive, `limit` exclusive), resolving a previously documented ambiguity in the upper bound.
|
||||
* Added an immutable `external_table_config` option that is persisted in OPTIONS files and passed to external table factories during bootstrap.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a liveness bug where RocksDB could remain indefinitely in recovery from a soft background error when concurrent writes continuously generated normal flush requests.
|
||||
* Fixed a race in online error recovery where a concurrent higher-severity I/O error could leave a stale retry-flush plan active, prevent the required WAL switch, and create duplicate internal WAL history detected on DB reopen.
|
||||
* Fix "bottommost files" infinite compaction loops (`kBottommostFiles`). This happens with the unsupported combination of leveled compaction and `preserve_internal_time_seconds` / `preclude_last_level_data_seconds`, and in an edge case with user-defined timestamps when `full_history_ts_low` is unset. The underlying logic is now more unified to reduce risk of a similar divergence in logic in the future.
|
||||
* Fixed `reuse_manifest_on_open` to create a fresh MANIFEST instead of appending after recovery sees an incomplete atomic group at the MANIFEST tail.
|
||||
* Starting remote compaction (`CompactionService` / `DB::OpenAndCompact`) is now more efficient and more reliable, by avoiding unnecessary file operations (latency) while using more accurate consistency / precondition checks, both avoiding some spurious failures and rejecting more legitimate inconsistencies. New DBOption `remote_compaction_manifest_floor` can be disabled in case of unexpected problems with the change.
|
||||
* Fixed unbounded retention of recovered transaction state by a long-lived secondary opened with `allow_2pc`.
|
||||
* Fixed stale reads on secondary instances, where entries replayed from a WAL that the primary had already flushed could shadow the newer values in the flushed files indefinitely. Also fixed a case where a secondary could stop returning such a key altogether while the primary's flushed file holding it was unreadable.
|
||||
* Fixed subset checkpoints to discard tolerated MANIFEST tail corruption before appending column-family drop records.
|
||||
|
||||
### Performance Improvements
|
||||
* Improved compaction CPU efficiency for column families using an `SstPartitioner` (such as the built-in `SstPartitionerFixedPrefix`): when the partitioner can express its next boundary as a key prefix via the new `SstPartitioner::ShouldPartitionByPrefix()` API, compaction now checks that cached prefix inline instead of copying every user key and making a virtual `ShouldPartition()` call for each key-value written to an SST.
|
||||
|
||||
## 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.
|
||||
* Added `MultiScanArgs::reverse` to support reverse MultiScan reads over bounded scan ranges.
|
||||
|
||||
### Behavior Changes
|
||||
* When using `LockWAL()`, live-file capture APIs such as checkpoint and backup now flush WAL-disabled unpersisted data; they can block until `UnlockWAL()` or return `Aborted` when called from the same thread that holds `LockWAL()` to avoid deadlock. `UnlockWAL()` must now also be called from the same thread that called `LockWAL()`.
|
||||
|
||||
### Performance Improvements
|
||||
* Fixed slow DB open with `best_efforts_recovery`, and slow catch-up of secondary and follower (read-only) instances, in which time and memory grew quadratically with the number of files in the DB (so the slowdown was worst for large DBs with long history).
|
||||
|
||||
## 11.6.0 (07/02/2026)
|
||||
### New Features
|
||||
* Added EXPERIMENTAL embedded blob SST support through `SstFileWriter::OpenWithEmbeddedBlobs()`, storing eligible large values as same-file blob records in block-based SST files and resolving them transparently for reads. This niche feature currently supports uncompressed embedded blobs only; compression options are placeholders and compression support is deferred to follow-up work.
|
||||
|
||||
@@ -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:
|
||||
@@ -1174,30 +1229,43 @@ check-c-api-gen:
|
||||
echo "Skipping C API backward-compatibility check ($(API_COMPAT_REF) not found; set API_COMPAT_REF)"; \
|
||||
fi
|
||||
# Staleness: regenerate and confirm the checked-in output is current. Needs a
|
||||
# clang++ (the generator parses C++ ASTs); detect one the way the generator
|
||||
# does (a clang in $(CXX) -- which may be ccache-prefixed/versioned -- else a
|
||||
# bare/versioned clang++ on PATH) rather than testing $(CXX) verbatim.
|
||||
# clang++ (the generator parses C++ ASTs). Use CLANG_CXX, which
|
||||
# build_tools/build_detect_platform resolves once (the fbcode clang++ in
|
||||
# fbcode builds, else a versioned clang++ on PATH) and passes explicitly, so
|
||||
# the check and the generator agree on the same binary without heuristics.
|
||||
@strict=""; case "$(CHECK_C_API_GEN_STRICT)" in ""|0|no|NO|false|FALSE) ;; *) strict=1 ;; esac; \
|
||||
cf_arg=""; \
|
||||
if [ -n "$(CLANG_FORMAT_BINARY)" ]; then cf_arg="--clang-format $(CLANG_FORMAT_BINARY)"; fi; \
|
||||
have_clang=""; \
|
||||
for c in $$(printf '%s\n' $(CXX) | grep -i clang) clang++ clang++-21 clang++-20 clang++-19 clang++-18 clang++-17 clang++-16 clang++-15 clang++-14 clang++-13; do \
|
||||
if command -v "$$c" >/dev/null 2>&1; then have_clang=1; break; fi; \
|
||||
done; \
|
||||
if [ -n "$$have_clang" ]; then \
|
||||
$(PYTHON) tools/c_api_gen/verify_generated_up_to_date.py $$cf_arg; \
|
||||
if [ -n "$(CLANG_CXX)" ] && command -v "$(CLANG_CXX)" >/dev/null 2>&1; then \
|
||||
$(PYTHON) tools/c_api_gen/verify_generated_up_to_date.py --clang "$(CLANG_CXX)" $$cf_arg; \
|
||||
elif [ -n "$$strict" ]; then \
|
||||
echo "ERROR: no clang++ found and CHECK_C_API_GEN_STRICT is set; cannot run the C API staleness check" >&2; exit 1; \
|
||||
echo "ERROR: no clang++ found (CLANG_CXX unset) and CHECK_C_API_GEN_STRICT is set; cannot run the C API staleness check" >&2; exit 1; \
|
||||
else \
|
||||
echo "Skipping C API codegen staleness check (no clang++ found; install clang++ or set CXX to a clang to enable)"; \
|
||||
echo "Skipping C API codegen staleness check (no clang++ found; CLANG_CXX unset, install clang++ to enable)"; \
|
||||
fi
|
||||
|
||||
# Regenerate the checked-in C API generated files (fragments under c_api_gen/,
|
||||
# the inlined include/rocksdb/c.h and db/c.cc, and the round-trip tests). Uses
|
||||
# CLANG_CXX (resolved by build_tools/build_detect_platform) so it works in a gcc
|
||||
# build too. Aborts up front if no usable clang++ is available, so a bad
|
||||
# toolchain never leaves the tree in a partially regenerated state. Pin the
|
||||
# formatter to match CI with CLANG_FORMAT_BINARY, e.g.:
|
||||
# make regen-c-api CLANG_FORMAT_BINARY=clang-format-21
|
||||
.PHONY: regen-c-api
|
||||
regen-c-api:
|
||||
@if [ -z "$(CLANG_CXX)" ] || ! command -v "$(CLANG_CXX)" >/dev/null 2>&1; then \
|
||||
echo "ERROR: no clang++ found (CLANG_CXX unset); build on a host with clang++ or the fbcode toolchain to regenerate the C API" >&2; exit 1; \
|
||||
fi
|
||||
@cf_arg=""; \
|
||||
if [ -n "$(CLANG_FORMAT_BINARY)" ]; then cf_arg="--clang-format $(CLANG_FORMAT_BINARY)"; fi; \
|
||||
$(PYTHON) tools/c_api_gen/regen_all.py --clang "$(CLANG_CXX)" $$cf_arg
|
||||
|
||||
# Quick local validation for C API generation plus the focused C API test.
|
||||
# This verifies the checked-in generated fragments as well as the inlined
|
||||
# include/rocksdb/c.h and db/c.cc outputs, then runs c_test in an isolated
|
||||
# TEST_TMPDIR to avoid stale-state failures.
|
||||
check-c-api-c_test:
|
||||
$(PYTHON) tools/c_api_gen/verify_generated_up_to_date.py
|
||||
$(PYTHON) tools/c_api_gen/verify_generated_up_to_date.py --clang "$(CLANG_CXX)"
|
||||
$(MAKE) c_test
|
||||
@tmpdir=$$(mktemp -d); \
|
||||
trap 'rm -rf "$$tmpdir"' EXIT; \
|
||||
@@ -1222,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
|
||||
@@ -1657,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)
|
||||
|
||||
@@ -1816,6 +1895,9 @@ backup_engine_test: $(OBJ_DIR)/utilities/backup/backup_engine_test.o $(TEST_LIBR
|
||||
checkpoint_test: $(OBJ_DIR)/utilities/checkpoint/checkpoint_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
copy_engine_test: $(OBJ_DIR)/utilities/copy_engine/copy_engine_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
sorted_run_builder_test: $(OBJ_DIR)/utilities/sorted_run_builder/sorted_run_builder_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -2358,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)))
|
||||
@@ -2452,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
|
||||
@@ -2514,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\"; \
|
||||
@@ -2532,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\"; \
|
||||
@@ -2546,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\"; \
|
||||
@@ -2561,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\"; \
|
||||
@@ -2575,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\"; \
|
||||
|
||||
@@ -145,11 +145,21 @@ def generate_buck(repo_path, deps_map):
|
||||
# always add range_tree, it's only excluded on ppc64, which we don't use internally
|
||||
src_mk["RANGE_TREE_SOURCES"] + src_mk["TOOL_LIB_SOURCES"],
|
||||
deps=[
|
||||
"//folly:executor",
|
||||
"//folly/container:f14_hash",
|
||||
"//folly/coro:baton",
|
||||
"//folly/coro:blocking_wait",
|
||||
"//folly/coro:collect",
|
||||
"//folly/coro:coroutine",
|
||||
"//folly/coro:nothrow",
|
||||
"//folly/coro:task",
|
||||
"//folly/executors:io_executor",
|
||||
"//folly/executors:io_thread_pool_executor",
|
||||
"//folly/executors/thread_factory:named_thread_factory",
|
||||
"//folly/io/async:async_base",
|
||||
"//folly/io/async:event_base_manager",
|
||||
"//folly/io/async:io_uring_backend",
|
||||
"//folly/io/async:request_context",
|
||||
"//folly/synchronization:distributed_mutex",
|
||||
],
|
||||
headers=LiteralValue("glob([\"**/*.h\"])")
|
||||
|
||||
@@ -89,13 +89,36 @@ if test -z "$CC"; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# CLANG_CXX is the single clang++ detection point for the whole script: it is
|
||||
# used for CXX when building with clang, for CLANG_ANALYZER (scan-build), and by
|
||||
# tooling that needs clang even in a gcc build (e.g. the C API codegen, which
|
||||
# parses C++ ASTs). The fbcode config above already sets CLANG_CXX; otherwise
|
||||
# resolve it here.
|
||||
if test -z "$CLANG_CXX"; then
|
||||
if [ -n "$CXX" ] && echo | $CXX -dM -E -x c++ - 2>/dev/null | grep -q '\b__clang__\b'; then
|
||||
# Honor an already-configured clang compiler (e.g. CI sets
|
||||
# CXX=clang++-21) so codegen parses headers with the same clang the
|
||||
# build uses, not an unrelated clang++ that merely happens to come first
|
||||
# on PATH (an image may ship both a bare clang++ and clang++-NN).
|
||||
CLANG_CXX="$CXX"
|
||||
else
|
||||
# Otherwise take the first clang++ on PATH. Candidates cover a bare
|
||||
# clang++, versioned names, and clang++.par (a Meta-internal wrapper
|
||||
# often present on dev machines).
|
||||
CLANG_CXX=$(command -v clang++ || command -v clang++-21 || command -v clang++-20 \
|
||||
|| command -v clang++-19 || command -v clang++-18 || command -v clang++-17 \
|
||||
|| command -v clang++-16 || command -v clang++-15 || command -v clang++-14 \
|
||||
|| command -v clang++-13 || command -v clang++.par || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
if test -z "$CXX"; then
|
||||
if [ "$USE_CLANG" -a -x "$(command -v clang++)" ]; then
|
||||
CXX=clang++
|
||||
if [ "$USE_CLANG" ] && [ -n "$CLANG_CXX" ]; then
|
||||
CXX="$CLANG_CXX"
|
||||
elif [ -x "$(command -v g++)" ]; then
|
||||
CXX=g++
|
||||
elif [ -x "$(command -v clang++)" ]; then
|
||||
CXX=clang++
|
||||
elif [ -n "$CLANG_CXX" ]; then
|
||||
CXX="$CLANG_CXX"
|
||||
else
|
||||
CXX=g++
|
||||
fi
|
||||
@@ -113,6 +136,17 @@ if test -z "$AR"; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# If the selected C++ compiler is actually clang (e.g. clang was the only
|
||||
# compiler available), reflect that as USE_CLANG so clang-specific logic in the
|
||||
# Makefile applies consistently. Detect it via the compiler's own __clang__
|
||||
# predefined macro rather than matching the binary path, which can false-positive
|
||||
# (e.g. a home directory containing "clang").
|
||||
if test -z "$USE_CLANG"; then
|
||||
if echo | $CXX -dM -E -x c++ - 2>/dev/null | grep -q '\b__clang__\b'; then
|
||||
USE_CLANG=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Detect OS
|
||||
if test -z "$TARGET_OS"; then
|
||||
TARGET_OS=`uname -s`
|
||||
@@ -127,7 +161,7 @@ if test -z "$CLANG_SCAN_BUILD"; then
|
||||
fi
|
||||
|
||||
if test -z "$CLANG_ANALYZER"; then
|
||||
CLANG_ANALYZER=$(command -v clang++ 2> /dev/null)
|
||||
CLANG_ANALYZER="$CLANG_CXX"
|
||||
fi
|
||||
|
||||
if test -z "$FIND"; then
|
||||
@@ -818,6 +852,10 @@ echo "ROCKSDB_MINOR=$ROCKSDB_MINOR" >> "$OUTPUT"
|
||||
echo "ROCKSDB_PATCH=$ROCKSDB_PATCH" >> "$OUTPUT"
|
||||
echo "CLANG_SCAN_BUILD=$CLANG_SCAN_BUILD" >> "$OUTPUT"
|
||||
echo "CLANG_ANALYZER=$CLANG_ANALYZER" >> "$OUTPUT"
|
||||
echo "CLANG_CXX=$CLANG_CXX" >> "$OUTPUT"
|
||||
if test -n "$USE_CLANG"; then
|
||||
echo "USE_CLANG=$USE_CLANG" >> "$OUTPUT"
|
||||
fi
|
||||
echo "PROFILING_FLAGS=$PROFILING_FLAGS" >> "$OUTPUT"
|
||||
echo "FIND=$FIND" >> "$OUTPUT"
|
||||
echo "WATCH=$WATCH" >> "$OUTPUT"
|
||||
|
||||
@@ -104,6 +104,10 @@ CLANG_BIN="$CLANG_BASE/bin"
|
||||
CLANG_LIB="$CLANG_BASE/lib"
|
||||
CLANG_SRC="$CLANG_BASE/../../src"
|
||||
|
||||
# A clang++ is always available in the fbcode toolchain regardless of whether
|
||||
# the build itself uses gcc or clang. CLANG_CXX exposes it for tooling (e.g. the
|
||||
# C API codegen, which parses C++ ASTs) that needs clang even in a gcc build.
|
||||
CLANG_CXX="$CLANG_BIN/clang++"
|
||||
CLANG_ANALYZER="$CLANG_BIN/clang++"
|
||||
CLANG_SCAN_BUILD="$CLANG_BIN/scan-build"
|
||||
|
||||
@@ -167,4 +171,4 @@ EXEC_LDFLAGS_SHARED="$SNAPPY_LIBS $ZLIB_LIBS $BZIP_LIBS $LZ4_LIBS $ZSTD_LIBS $GF
|
||||
|
||||
VALGRIND_VER="$VALGRIND_BASE/bin/"
|
||||
|
||||
export CC CXX AR AS CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD
|
||||
export CC CXX AR AS CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_CXX CLANG_ANALYZER CLANG_SCAN_BUILD
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
|
||||
@@ -478,6 +482,17 @@ uint64_t rocksdb_compaction_file_info_oldest_blob_file_number(
|
||||
|
||||
/* BlobFileAdditionInfo */
|
||||
|
||||
const char* rocksdb_blob_file_addition_info_blob_file_path(
|
||||
const rocksdb_blob_file_addition_info_t* info, size_t* size) {
|
||||
*size = info->rep.blob_file_path.size();
|
||||
return info->rep.blob_file_path.data();
|
||||
}
|
||||
|
||||
uint64_t rocksdb_blob_file_addition_info_blob_file_number(
|
||||
const rocksdb_blob_file_addition_info_t* info) {
|
||||
return info->rep.blob_file_number;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_blob_file_addition_info_total_blob_count(
|
||||
const rocksdb_blob_file_addition_info_t* info) {
|
||||
return info->rep.total_blob_count;
|
||||
@@ -490,6 +505,17 @@ uint64_t rocksdb_blob_file_addition_info_total_blob_bytes(
|
||||
|
||||
/* BlobFileGarbageInfo */
|
||||
|
||||
const char* rocksdb_blob_file_garbage_info_blob_file_path(
|
||||
const rocksdb_blob_file_garbage_info_t* info, size_t* size) {
|
||||
*size = info->rep.blob_file_path.size();
|
||||
return info->rep.blob_file_path.data();
|
||||
}
|
||||
|
||||
uint64_t rocksdb_blob_file_garbage_info_blob_file_number(
|
||||
const rocksdb_blob_file_garbage_info_t* info) {
|
||||
return info->rep.blob_file_number;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_blob_file_garbage_info_garbage_blob_count(
|
||||
const rocksdb_blob_file_garbage_info_t* info) {
|
||||
return info->rep.garbage_blob_count;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
|
||||
@@ -342,6 +346,14 @@ rocksdb_compaction_file_info_oldest_blob_file_number(
|
||||
|
||||
/* BlobFileAdditionInfo */
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_blob_file_addition_info_blob_file_path(
|
||||
const rocksdb_blob_file_addition_info_t* info, size_t* size);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_blob_file_addition_info_blob_file_number(
|
||||
const rocksdb_blob_file_addition_info_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_blob_file_addition_info_total_blob_count(
|
||||
const rocksdb_blob_file_addition_info_t* info);
|
||||
@@ -352,6 +364,14 @@ rocksdb_blob_file_addition_info_total_blob_bytes(
|
||||
|
||||
/* BlobFileGarbageInfo */
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_blob_file_garbage_info_blob_file_path(
|
||||
const rocksdb_blob_file_garbage_info_t* info, size_t* size);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_blob_file_garbage_info_blob_file_number(
|
||||
const rocksdb_blob_file_garbage_info_t* info);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_blob_file_garbage_info_garbage_blob_count(
|
||||
const rocksdb_blob_file_garbage_info_t* info);
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
|
||||
@@ -137,6 +141,15 @@ int rocksdb_options_get_max_file_opening_threads(rocksdb_options_t* opt) {
|
||||
return opt->rep.max_file_opening_threads;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_read_io_executor_threads(rocksdb_options_t* opt,
|
||||
int v) {
|
||||
opt->rep.read_io_executor_threads = v;
|
||||
}
|
||||
|
||||
int rocksdb_options_get_read_io_executor_threads(rocksdb_options_t* opt) {
|
||||
return opt->rep.read_io_executor_threads;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_open_files_async(rocksdb_options_t* opt,
|
||||
unsigned char v) {
|
||||
opt->rep.open_files_async = v;
|
||||
@@ -803,6 +816,16 @@ const char* rocksdb_options_get_db_host_id(rocksdb_options_t* opt,
|
||||
return opt->rep.db_host_id.data();
|
||||
}
|
||||
|
||||
void rocksdb_options_set_remote_compaction_manifest_floor(
|
||||
rocksdb_options_t* opt, unsigned char v) {
|
||||
opt->rep.remote_compaction_manifest_floor = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_remote_compaction_manifest_floor(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.remote_compaction_manifest_floor;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_lowest_used_cache_tier(rocksdb_options_t* opt, int v) {
|
||||
opt->rep.lowest_used_cache_tier =
|
||||
static_cast<decltype(opt->rep.lowest_used_cache_tier)>(v);
|
||||
@@ -843,6 +866,16 @@ uint64_t rocksdb_options_get_max_compaction_trigger_wakeup_seconds(
|
||||
return opt->rep.max_compaction_trigger_wakeup_seconds;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt, int v) {
|
||||
opt->rep.periodic_compaction_phase_recovery_percent = v;
|
||||
}
|
||||
|
||||
int rocksdb_options_get_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.periodic_compaction_phase_recovery_percent;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_follower_refresh_catchup_period_ms(
|
||||
rocksdb_options_t* opt, uint64_t v) {
|
||||
opt->rep.follower_refresh_catchup_period_ms = v;
|
||||
@@ -1292,6 +1325,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);
|
||||
@@ -1674,6 +1717,16 @@ unsigned char rocksdb_flushoptions_get_force_atomic_flush(
|
||||
return opt->rep.force_atomic_flush;
|
||||
}
|
||||
|
||||
void rocksdb_flushoptions_set_listener_wait(rocksdb_flushoptions_t* opt,
|
||||
unsigned char v) {
|
||||
opt->rep.listener_wait = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_flushoptions_get_listener_wait(
|
||||
rocksdb_flushoptions_t* opt) {
|
||||
return opt->rep.listener_wait;
|
||||
}
|
||||
|
||||
/* FlushWALOptions */
|
||||
|
||||
void rocksdb_flushwaloptions_set_sync(rocksdb_flushwaloptions_t* opt,
|
||||
@@ -2481,6 +2534,17 @@ double rocksdb_block_based_options_get_uniform_cv_threshold(
|
||||
return opt->rep.uniform_cv_threshold;
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt, int v) {
|
||||
opt->rep.optimize_key_common_prefix =
|
||||
static_cast<decltype(opt->rep.optimize_key_common_prefix)>(v);
|
||||
}
|
||||
|
||||
int rocksdb_block_based_options_get_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt) {
|
||||
return static_cast<int>(opt->rep.optimize_key_common_prefix);
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_enable_index_compression(
|
||||
rocksdb_block_based_table_options_t* opt, unsigned char v) {
|
||||
opt->rep.enable_index_compression = v;
|
||||
@@ -2703,6 +2767,36 @@ uint64_t rocksdb_fifo_compaction_options_get_trivial_copy_buffer_size(
|
||||
|
||||
/* BackupEngineOptions */
|
||||
|
||||
void rocksdb_backup_engine_options_set_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.io_buffer_size = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_backup_engine_options_get_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.io_buffer_size;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.backup_rate_limit = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_backup_engine_options_get_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.backup_rate_limit;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt, int v) {
|
||||
opt->rep.max_background_operations = v;
|
||||
}
|
||||
|
||||
int rocksdb_backup_engine_options_get_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.max_background_operations;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_backup_dir(
|
||||
rocksdb_backup_engine_options_t* opt, const char* v) {
|
||||
opt->rep.backup_dir = v;
|
||||
@@ -2754,26 +2848,6 @@ unsigned char rocksdb_backup_engine_options_get_backup_log_files(
|
||||
return opt->rep.backup_log_files;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.io_buffer_size = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_backup_engine_options_get_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.io_buffer_size;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.backup_rate_limit = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_backup_engine_options_get_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.backup_rate_limit;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_restore_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.restore_rate_limit = v;
|
||||
@@ -2794,16 +2868,6 @@ unsigned char rocksdb_backup_engine_options_get_share_files_with_checksum(
|
||||
return opt->rep.share_files_with_checksum;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt, int v) {
|
||||
opt->rep.max_background_operations = v;
|
||||
}
|
||||
|
||||
int rocksdb_backup_engine_options_get_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.max_background_operations;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_callback_trigger_interval_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.callback_trigger_interval_size = v;
|
||||
@@ -2858,16 +2922,6 @@ rocksdb_backup_engine_options_get_current_temperatures_override_manifest(
|
||||
|
||||
/* CreateBackupOptions */
|
||||
|
||||
void rocksdb_create_backup_options_set_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v) {
|
||||
opt->rep.flush_before_backup = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_create_backup_options_get_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt) {
|
||||
return opt->rep.flush_before_backup;
|
||||
}
|
||||
|
||||
void rocksdb_create_backup_options_set_decrease_background_thread_cpu_priority(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v) {
|
||||
opt->rep.decrease_background_thread_cpu_priority = v;
|
||||
@@ -2890,6 +2944,16 @@ int rocksdb_create_backup_options_get_background_thread_cpu_priority(
|
||||
return static_cast<int>(opt->rep.background_thread_cpu_priority);
|
||||
}
|
||||
|
||||
void rocksdb_create_backup_options_set_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v) {
|
||||
opt->rep.flush_before_backup = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_create_backup_options_get_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt) {
|
||||
return opt->rep.flush_before_backup;
|
||||
}
|
||||
|
||||
void rocksdb_create_backup_options_set_atomic_flush(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v) {
|
||||
opt->rep.atomic_flush = v;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
|
||||
@@ -51,6 +55,12 @@ rocksdb_options_set_verify_sst_unique_id_in_manifest(rocksdb_options_t* opt,
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_options_get_verify_sst_unique_id_in_manifest(rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_read_io_executor_threads(
|
||||
rocksdb_options_t* opt, int v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int rocksdb_options_get_read_io_executor_threads(
|
||||
rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char* rocksdb_options_get_db_log_dir(
|
||||
rocksdb_options_t* opt, size_t* size);
|
||||
|
||||
@@ -237,6 +247,13 @@ extern ROCKSDB_LIBRARY_API void rocksdb_options_set_db_host_id(
|
||||
extern ROCKSDB_LIBRARY_API const char* rocksdb_options_get_db_host_id(
|
||||
rocksdb_options_t* opt, size_t* size);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_options_set_remote_compaction_manifest_floor(rocksdb_options_t* opt,
|
||||
unsigned char v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_options_get_remote_compaction_manifest_floor(rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_lowest_used_cache_tier(
|
||||
rocksdb_options_t* opt, int v);
|
||||
|
||||
@@ -257,6 +274,14 @@ extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_options_get_daily_offpeak_time_utc(rocksdb_options_t* opt,
|
||||
size_t* size);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_options_set_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt, int v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_options_get_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_options_set_follower_refresh_catchup_period_ms(rocksdb_options_t* opt,
|
||||
uint64_t v);
|
||||
@@ -377,6 +402,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);
|
||||
|
||||
@@ -541,6 +574,12 @@ extern ROCKSDB_LIBRARY_API void rocksdb_flushoptions_set_force_atomic_flush(
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_flushoptions_get_force_atomic_flush(rocksdb_flushoptions_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_flushoptions_set_listener_wait(
|
||||
rocksdb_flushoptions_t* opt, unsigned char v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char rocksdb_flushoptions_get_listener_wait(
|
||||
rocksdb_flushoptions_t* opt);
|
||||
|
||||
/* FlushWALOptions */
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_flushwaloptions_set_sync(
|
||||
@@ -1146,6 +1185,14 @@ extern ROCKSDB_LIBRARY_API double
|
||||
rocksdb_block_based_options_get_uniform_cv_threshold(
|
||||
rocksdb_block_based_table_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_block_based_options_set_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt, int v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_block_based_options_get_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_block_based_options_set_enable_index_compression(
|
||||
rocksdb_block_based_table_options_t* opt, unsigned char v);
|
||||
@@ -1313,6 +1360,30 @@ rocksdb_fifo_compaction_options_get_trivial_copy_buffer_size(
|
||||
|
||||
/* BackupEngineOptions */
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_backup_engine_options_set_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_backup_engine_options_get_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_backup_engine_options_set_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_backup_engine_options_get_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_backup_engine_options_set_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt, int v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_backup_engine_options_get_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_backup_engine_options_set_backup_dir(
|
||||
rocksdb_backup_engine_options_t* opt, const char* v);
|
||||
|
||||
@@ -1350,22 +1421,6 @@ extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_backup_engine_options_get_backup_log_files(
|
||||
rocksdb_backup_engine_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_backup_engine_options_set_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_backup_engine_options_get_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_backup_engine_options_set_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_backup_engine_options_get_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_backup_engine_options_set_restore_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v);
|
||||
@@ -1382,14 +1437,6 @@ extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_backup_engine_options_get_share_files_with_checksum(
|
||||
rocksdb_backup_engine_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_backup_engine_options_set_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt, int v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_backup_engine_options_get_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_backup_engine_options_set_callback_trigger_interval_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v);
|
||||
@@ -1431,14 +1478,6 @@ rocksdb_backup_engine_options_get_current_temperatures_override_manifest(
|
||||
|
||||
/* CreateBackupOptions */
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_create_backup_options_set_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_create_backup_options_get_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_create_backup_options_set_decrease_background_thread_cpu_priority(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v);
|
||||
@@ -1455,6 +1494,14 @@ extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_create_backup_options_get_background_thread_cpu_priority(
|
||||
rocksdb_create_backup_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_create_backup_options_set_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_create_backup_options_get_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_create_backup_options_set_atomic_flush(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v);
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/gen_roundtrip_tests.py.
|
||||
// DO NOT EDIT THIS FILE DIRECTLY.
|
||||
// Round-trip (set -> get -> assert) coverage for generated option wrappers.
|
||||
// Rerun: python3 tools/c_api_gen/regen_all.py
|
||||
// Rerun `make regen-c-api`
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
static void rocksdb_roundtrip_test_rocksdb_block_based_options(void) {
|
||||
@@ -147,6 +151,12 @@ static void rocksdb_roundtrip_test_rocksdb_block_based_options(void) {
|
||||
rocksdb_block_based_options_set_optimize_filters_for_memory(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_block_based_options_get_optimize_filters_for_memory(obj) == 0);
|
||||
rocksdb_block_based_options_set_optimize_key_common_prefix(obj, 1);
|
||||
CheckCondition(
|
||||
rocksdb_block_based_options_get_optimize_key_common_prefix(obj) == 1);
|
||||
rocksdb_block_based_options_set_optimize_key_common_prefix(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_block_based_options_get_optimize_key_common_prefix(obj) == 0);
|
||||
rocksdb_block_based_options_set_partition_filters(obj, 1);
|
||||
CheckCondition(rocksdb_block_based_options_get_partition_filters(obj) == 1);
|
||||
rocksdb_block_based_options_set_partition_filters(obj, 0);
|
||||
@@ -459,6 +469,10 @@ static void rocksdb_roundtrip_test_rocksdb_flushoptions(void) {
|
||||
CheckCondition(rocksdb_flushoptions_get_force_atomic_flush(obj) == 1);
|
||||
rocksdb_flushoptions_set_force_atomic_flush(obj, 0);
|
||||
CheckCondition(rocksdb_flushoptions_get_force_atomic_flush(obj) == 0);
|
||||
rocksdb_flushoptions_set_listener_wait(obj, 1);
|
||||
CheckCondition(rocksdb_flushoptions_get_listener_wait(obj) == 1);
|
||||
rocksdb_flushoptions_set_listener_wait(obj, 0);
|
||||
CheckCondition(rocksdb_flushoptions_get_listener_wait(obj) == 0);
|
||||
rocksdb_flushoptions_set_wait(obj, 1);
|
||||
CheckCondition(rocksdb_flushoptions_get_wait(obj) == 1);
|
||||
rocksdb_flushoptions_set_wait(obj, 0);
|
||||
@@ -639,6 +653,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);
|
||||
@@ -819,6 +839,12 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
|
||||
CheckCondition(rocksdb_options_get_paranoid_memory_checks(obj) == 1);
|
||||
rocksdb_options_set_paranoid_memory_checks(obj, 0);
|
||||
CheckCondition(rocksdb_options_get_paranoid_memory_checks(obj) == 0);
|
||||
rocksdb_options_set_periodic_compaction_phase_recovery_percent(obj, 1);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_periodic_compaction_phase_recovery_percent(obj) == 1);
|
||||
rocksdb_options_set_periodic_compaction_phase_recovery_percent(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_periodic_compaction_phase_recovery_percent(obj) == 0);
|
||||
rocksdb_options_set_persist_stats_to_disk(obj, 1);
|
||||
CheckCondition(rocksdb_options_get_persist_stats_to_disk(obj) == 1);
|
||||
rocksdb_options_set_persist_stats_to_disk(obj, 0);
|
||||
@@ -841,6 +867,16 @@ 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_remote_compaction_manifest_floor(obj, 1);
|
||||
CheckCondition(rocksdb_options_get_remote_compaction_manifest_floor(obj) ==
|
||||
1);
|
||||
rocksdb_options_set_remote_compaction_manifest_floor(obj, 0);
|
||||
CheckCondition(rocksdb_options_get_remote_compaction_manifest_floor(obj) ==
|
||||
0);
|
||||
rocksdb_options_set_reuse_manifest_on_open(obj, 1);
|
||||
CheckCondition(rocksdb_options_get_reuse_manifest_on_open(obj) == 1);
|
||||
rocksdb_options_set_reuse_manifest_on_open(obj, 0);
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
// @generated
|
||||
// -----------------------------------------------------------------------------
|
||||
// Auto-generated by tools/c_api_gen/generate_c_api.py.
|
||||
|
||||
@@ -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.
|
||||
@@ -25,6 +25,7 @@ CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --
|
||||
crash_test_with_multiops_wup_txn \
|
||||
crash_test_with_optimistic_txn \
|
||||
crash_test_with_tiered_storage \
|
||||
liveness_crash_test \
|
||||
blackbox_crash_test blackbox_crash_test_with_atomic_flush \
|
||||
blackbox_crash_test_with_wc_txn blackbox_crash_test_with_wp_txn \
|
||||
blackbox_crash_test_with_wup_txn \
|
||||
@@ -94,6 +95,9 @@ crash_test_with_multiops_wp_txn: $(DB_STRESS_CMD)
|
||||
crash_test_with_multiops_wup_txn: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_MAKE) blackbox_crash_test_with_multiops_wup_txn
|
||||
|
||||
liveness_crash_test: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_PY) liveness $(CRASH_TEST_EXT_ARGS)
|
||||
|
||||
blackbox_crash_test: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_PY) --simple blackbox $(CRASH_TEST_EXT_ARGS)
|
||||
$(CRASHTEST_PY) blackbox $(CRASH_TEST_EXT_ARGS)
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
|
||||
@@ -31,4 +33,43 @@ constexpr uint64_t kInvalidBlobFileNumber = 0;
|
||||
constexpr uint64_t kCurrentFileBlobIndexFileNumber = kInvalidBlobFileNumber;
|
||||
static_assert(kCurrentFileBlobIndexFileNumber == kInvalidBlobFileNumber);
|
||||
|
||||
// Sentinel range length meaning "the whole blob value" for the unified
|
||||
// blob-read primitives, which take a range descriptor {range_offset,
|
||||
// range_length}; range_length == kWholeBlobLength selects the whole value and
|
||||
// any other value selects the strict sub-range
|
||||
// [range_offset, range_offset + range_length).
|
||||
//
|
||||
// This is the internal counterpart of the public kLazyWholeColumn
|
||||
// (include/rocksdb/lazy_wide_columns.h); both are the maximum size_t. The lazy
|
||||
// resolver does not forward the public value as a range descriptor -- it
|
||||
// normalizes a whole-column request to kWholeBlobLength -- so the two are not
|
||||
// required to be equal, but both rely on the sentinel exceeding any real
|
||||
// blob/column size so a "whole" request is never mistaken for a strict
|
||||
// sub-range. Keep them in sync.
|
||||
inline constexpr size_t kWholeBlobLength = std::numeric_limits<size_t>::max();
|
||||
|
||||
// How aggressively a blob read verifies the record's checksum. Derived once at
|
||||
// the lazy read-path boundary (ReadPathBlobResolver) from
|
||||
// (ReadOptions::verify_checksums, LazyColumnReadRequest::force_verify) and
|
||||
// threaded down through the blob-read primitives, replacing the earlier mix of
|
||||
// a verify_checksums bool and a separate force-verify method. (Not literally a
|
||||
// constant, but kept here as a small shared blob-read enum rather than its own
|
||||
// header.)
|
||||
//
|
||||
// - kVerifyIfPresent: verify the whole-record checksum whenever the format
|
||||
// provides one, even if ReadOptions::verify_checksums is off. A byte-range
|
||||
// request under this policy is escalated to a whole-record read (a strict
|
||||
// sub-range cannot cover the checksum). This is the public force_verify.
|
||||
// - kVerifyIfNoAmplification: verify when the checksummable unit is already
|
||||
// being read -- a whole-value read verifies (the check is "free"), while a
|
||||
// strict sub-range read skips verification rather than amplify the read to
|
||||
// the whole record. Corresponds to verify_checksums on, force_verify off.
|
||||
// - kSkip: never verify. Corresponds to verify_checksums off, force_verify
|
||||
// off.
|
||||
enum class BlobVerifyPolicy : uint8_t {
|
||||
kVerifyIfPresent,
|
||||
kVerifyIfNoAmplification,
|
||||
kSkip,
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+78
-7
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "db/blob/blob_file_partition_manager.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/blob/same_file_blob_reader.h"
|
||||
#include "db/version_set.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -25,21 +26,91 @@ Status BlobFetcher::FetchBlob(const Slice& user_key,
|
||||
return status;
|
||||
}
|
||||
|
||||
Status BlobFetcher::FetchBlob(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
Status VersionBlobFetcherBase::FetchBlob(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
const ReadOptions& read_options = this->read_options();
|
||||
if (!allow_write_path_fallback_) {
|
||||
assert(version_);
|
||||
|
||||
return version_->GetBlob(read_options_, user_key, blob_index,
|
||||
return version_->GetBlob(read_options, user_key, blob_index,
|
||||
prefetch_buffer, blob_value, bytes_read);
|
||||
}
|
||||
|
||||
return BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
|
||||
read_options_, user_key, blob_index, version_, blob_file_cache_,
|
||||
read_options, user_key, blob_index, version_, blob_file_cache_,
|
||||
prefetch_buffer, blob_value, bytes_read);
|
||||
}
|
||||
|
||||
Status VersionBlobFetcherBase::FetchBlobRange(
|
||||
const Slice& user_key, const BlobIndex& blob_index, uint64_t range_offset,
|
||||
size_t range_length, BlobVerifyPolicy verify_policy,
|
||||
PinnableSlice* blob_value, uint64_t* bytes_read) const {
|
||||
if (range_length == kWholeBlobLength) {
|
||||
// Whole-value read. Delegate to FetchBlob (which handles the direct-write
|
||||
// fallback). For kVerifyIfPresent, force whole-record checksum verification
|
||||
// even when ReadOptions::verify_checksums is off, via a transient fetcher
|
||||
// over a verify-enabled copy of the read options (both locals outlive the
|
||||
// borrowed-options VersionBlobFetcher).
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
if (verify_policy == BlobVerifyPolicy::kVerifyIfPresent &&
|
||||
!read_options().verify_checksums) {
|
||||
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);
|
||||
}
|
||||
return FetchBlob(user_key, blob_index, prefetch_buffer, blob_value,
|
||||
bytes_read);
|
||||
}
|
||||
|
||||
// Strict sub-range read: value bytes only, no whole-record checksum, no cache
|
||||
// fill. Only ever chosen when SupportsRangeRead() holds (a Version to read
|
||||
// through, no direct-write fallback) and verification is not forced (a strict
|
||||
// sub-range cannot cover the whole-record checksum).
|
||||
assert(SupportsRangeRead());
|
||||
assert(version_);
|
||||
assert(verify_policy != BlobVerifyPolicy::kVerifyIfPresent);
|
||||
return version_->GetBlobRange(read_options(), user_key, blob_index,
|
||||
range_offset, range_length, blob_value,
|
||||
bytes_read);
|
||||
}
|
||||
|
||||
Status EmbeddedAwareBlobFetcher::FetchBlob(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
// Only an enabled decorator is ever routed to (see EffectiveFetcher()).
|
||||
assert(same_file_reader_ != nullptr);
|
||||
if (blob_index.IsSameFile()) {
|
||||
// Same-file ("embedded") blob records live in the current SST and are read
|
||||
// by the SameFileBlobReader (the BlockBasedTable). A separate blob-file
|
||||
// prefetch buffer and bytes_read counter do not apply here (embedded reads
|
||||
// account their own BLOB_DB_* stats via BlobSource). This is a whole-value
|
||||
// read; the verify policy follows read_options().verify_checksums (the
|
||||
// decorator carries no separate force-verify signal).
|
||||
const BlobVerifyPolicy verify_policy =
|
||||
read_options().verify_checksums
|
||||
? BlobVerifyPolicy::kVerifyIfNoAmplification
|
||||
: BlobVerifyPolicy::kSkip;
|
||||
return same_file_reader_->GetSameFileBlob(
|
||||
read_options(), blob_index,
|
||||
/*range_offset=*/0, kWholeBlobLength, verify_policy, blob_value);
|
||||
}
|
||||
|
||||
if (base_ == nullptr) {
|
||||
return Status::Corruption(
|
||||
"Cannot resolve non-same-file blob reference without a base blob "
|
||||
"fetcher");
|
||||
}
|
||||
return base_->FetchBlob(user_key, blob_index, prefetch_buffer, blob_value,
|
||||
bytes_read);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+185
-13
@@ -5,6 +5,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "db/blob/blob_constants.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
@@ -16,32 +19,201 @@ class Slice;
|
||||
class FilePrefetchBuffer;
|
||||
class PinnableSlice;
|
||||
class BlobIndex;
|
||||
class SameFileBlobReader;
|
||||
|
||||
// A thin wrapper around blob retrieval. By default it reads through Version,
|
||||
// and it can optionally fall back to direct-write blob files that are not yet
|
||||
// manifest-visible.
|
||||
// Abstract interface for blob retrieval on the read path. An implementation
|
||||
// resolves a BlobIndex to its value, pinning it into a PinnableSlice. Callers
|
||||
// hold a plain const BlobFetcher* so resolution can be intercepted by a
|
||||
// decorator (e.g. EmbeddedAwareBlobFetcher) without their knowledge.
|
||||
class BlobFetcher {
|
||||
public:
|
||||
BlobFetcher(const Version* version, const ReadOptions& read_options,
|
||||
BlobFileCache* blob_file_cache = nullptr,
|
||||
bool allow_write_path_fallback = false)
|
||||
: version_(version),
|
||||
read_options_(read_options),
|
||||
blob_file_cache_(blob_file_cache),
|
||||
allow_write_path_fallback_(allow_write_path_fallback) {}
|
||||
virtual ~BlobFetcher() = default;
|
||||
|
||||
// Convenience overload: decodes `blob_index_slice` into a BlobIndex and
|
||||
// dispatches to the virtual overload below. Non-virtual; shared by all
|
||||
// implementations.
|
||||
Status FetchBlob(const Slice& user_key, const Slice& blob_index_slice,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value, uint64_t* bytes_read) const;
|
||||
|
||||
virtual Status FetchBlob(const Slice& user_key, const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const = 0;
|
||||
|
||||
// The read options this fetcher operates under. Used (via an
|
||||
// EmbeddedAwareBlobFetcher decorator) to resolve same-file blob references,
|
||||
// which need the current read_tier.
|
||||
virtual const ReadOptions& read_options() const = 0;
|
||||
};
|
||||
|
||||
// Shared implementation of the two Version-backed BlobFetchers below. Reads
|
||||
// through a Version, optionally falling back to direct-write blob files that
|
||||
// are not yet manifest-visible. Everything except the ReadOptions is stored
|
||||
// here; the concrete subclass decides whether it references a caller-owned
|
||||
// ReadOptions (VersionBlobFetcher) or owns its own copy
|
||||
// (OwningVersionBlobFetcher). FetchBlob reads the options via the virtual
|
||||
// read_options() so both storage strategies share one implementation.
|
||||
class VersionBlobFetcherBase : public BlobFetcher {
|
||||
public:
|
||||
// Un-hide the base's Slice-based convenience overload.
|
||||
using BlobFetcher::FetchBlob;
|
||||
|
||||
Status FetchBlob(const Slice& user_key, const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value, uint64_t* bytes_read) const;
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const override;
|
||||
|
||||
// Fetches the value of a separate-file blob reference into *blob_value,
|
||||
// applying `verify_policy` (see BlobVerifyPolicy). range_length ==
|
||||
// kWholeBlobLength reads the whole value (delegating to FetchBlob, which also
|
||||
// handles the direct-write fallback, and forcing whole-record checksum
|
||||
// verification for kVerifyIfPresent even when ReadOptions::verify_checksums
|
||||
// is off). Any other length reads only the sub-range [range_offset,
|
||||
// range_offset + range_length) directly from the blob file, reading just
|
||||
// those bytes on a cache miss -- no record-header/key read, no whole-record
|
||||
// checksum, no cache fill (see Version::GetBlobRange /
|
||||
// BlobSource::GetBlobRange). A strict sub-range is only valid when
|
||||
// SupportsRangeRead() is true and verify_policy != kVerifyIfPresent
|
||||
// (verifying a strict sub-range would have to amplify the read to the whole
|
||||
// record); callers check that first and take the whole-value path otherwise.
|
||||
// Not routed through the same-file (embedded) decorator -- same-file
|
||||
// references are resolved by the caller through a SameFileBlobReader.
|
||||
Status FetchBlobRange(const Slice& user_key, const BlobIndex& blob_index,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
BlobVerifyPolicy verify_policy,
|
||||
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
|
||||
// null Version (e.g. DBIter) can check this to surface a clean Corruption on
|
||||
// an unexpected blob index instead of dereferencing null in FetchBlob.
|
||||
bool CanResolve() const {
|
||||
return version_ != nullptr ||
|
||||
(allow_write_path_fallback_ && blob_file_cache_ != nullptr);
|
||||
}
|
||||
|
||||
// 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)
|
||||
: version_(version),
|
||||
blob_file_cache_(blob_file_cache),
|
||||
allow_write_path_fallback_(allow_write_path_fallback) {}
|
||||
|
||||
private:
|
||||
const Version* version_;
|
||||
ReadOptions read_options_;
|
||||
BlobFileCache* blob_file_cache_;
|
||||
bool allow_write_path_fallback_;
|
||||
};
|
||||
|
||||
// The default BlobFetcher for transient/stack use: holds a reference to a
|
||||
// caller-owned ReadOptions rather than a copy, avoiding a per-fetcher copy of
|
||||
// the (growing) ReadOptions on the hot path. The referenced ReadOptions must
|
||||
// outlive the fetcher, so this is only appropriate for fetchers whose lifetime
|
||||
// is bounded by the caller's (e.g. a stack local per Get / per resolution).
|
||||
// Use OwningVersionBlobFetcher for lifetime-independent fetchers.
|
||||
class VersionBlobFetcher : public VersionBlobFetcherBase {
|
||||
public:
|
||||
VersionBlobFetcher(const Version* version, const ReadOptions& read_options,
|
||||
BlobFileCache* blob_file_cache = nullptr,
|
||||
bool allow_write_path_fallback = false)
|
||||
: VersionBlobFetcherBase(version, blob_file_cache,
|
||||
allow_write_path_fallback),
|
||||
read_options_(read_options) {}
|
||||
|
||||
// Reject binding to a temporary ReadOptions, which would leave a dangling
|
||||
// reference. Callers that cannot guarantee the ReadOptions outlives the
|
||||
// fetcher must use OwningVersionBlobFetcher instead.
|
||||
VersionBlobFetcher(const Version* version, ReadOptions&& read_options,
|
||||
BlobFileCache* blob_file_cache = nullptr,
|
||||
bool allow_write_path_fallback = false) = delete;
|
||||
|
||||
const ReadOptions& read_options() const override { return read_options_; }
|
||||
|
||||
private:
|
||||
const ReadOptions& read_options_;
|
||||
};
|
||||
|
||||
// A BlobFetcher that owns its ReadOptions, for fetchers whose lifetime is
|
||||
// independent of the caller that created them (the lazy read-path resolver and
|
||||
// compaction). Takes ownership of the ReadOptions by rvalue so ownership
|
||||
// transfer is explicit and no hidden copy is made on the way in; later reads
|
||||
// never touch caller state. Callers holding a borrowed ReadOptions they cannot
|
||||
// move must make the copy explicit (e.g. ReadOptions(read_options)).
|
||||
class OwningVersionBlobFetcher : public VersionBlobFetcherBase {
|
||||
public:
|
||||
OwningVersionBlobFetcher(const Version* version, ReadOptions&& read_options,
|
||||
BlobFileCache* blob_file_cache = nullptr,
|
||||
bool allow_write_path_fallback = false)
|
||||
: VersionBlobFetcherBase(version, blob_file_cache,
|
||||
allow_write_path_fallback),
|
||||
read_options_(std::move(read_options)) {}
|
||||
|
||||
const ReadOptions& read_options() const override { return read_options_; }
|
||||
|
||||
private:
|
||||
ReadOptions read_options_;
|
||||
};
|
||||
|
||||
// A BlobFetcher decorator that resolves same-file ("embedded") blob references
|
||||
// against a SameFileBlobReader (the current SST) and delegates all other
|
||||
// references to a base BlobFetcher. It is cheap to construct as a stack local
|
||||
// per resolution: the same-file target is per-SST while the base fetcher is one
|
||||
// per-Get object shared across SSTs, so an immutable per-SST decorator avoids
|
||||
// shared-mutable-state hazards under async MultiGet.
|
||||
//
|
||||
// A decorator is "enabled" when it has a same-file reader and "disabled"
|
||||
// (unusable) when constructed with a null `same_file_reader` (e.g. via the
|
||||
// default constructor). A disabled decorator must not be routed through
|
||||
// directly; callers construct one unconditionally and use EffectiveFetcher(),
|
||||
// which returns this decorator when enabled or the undecorated base fetcher
|
||||
// otherwise. This avoids std::optional / branching at call sites while adding
|
||||
// no indirection on the common (non-embedded) path.
|
||||
//
|
||||
// The base fetcher may be null for an entity whose blob columns are all
|
||||
// same-file (no separate-file blob support wired, e.g. SstFileReader); a
|
||||
// non-same-file reference then surfaces as a Corruption rather than silently.
|
||||
class EmbeddedAwareBlobFetcher : public BlobFetcher {
|
||||
public:
|
||||
explicit EmbeddedAwareBlobFetcher(
|
||||
const BlobFetcher* base = nullptr,
|
||||
const SameFileBlobReader* same_file_reader = nullptr)
|
||||
: base_(base), same_file_reader_(same_file_reader) {}
|
||||
|
||||
// The fetcher to route resolution through: this decorator when it has a
|
||||
// same-file reader, otherwise the (undecorated) base fetcher. Only an enabled
|
||||
// decorator's FetchBlob is ever invoked.
|
||||
const BlobFetcher* EffectiveFetcher() const {
|
||||
return same_file_reader_ != nullptr ? this : base_;
|
||||
}
|
||||
|
||||
// Un-hide the base's Slice-based convenience overload.
|
||||
using BlobFetcher::FetchBlob;
|
||||
|
||||
Status FetchBlob(const Slice& user_key, const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const override;
|
||||
|
||||
const ReadOptions& read_options() const override {
|
||||
// Same-file resolution uses the base fetcher's read options; when there is
|
||||
// no base (e.g. SstFileReader) a default (kReadAllTier) suffices.
|
||||
static const ReadOptions kDefaultReadOptions;
|
||||
return base_ != nullptr ? base_->read_options() : kDefaultReadOptions;
|
||||
}
|
||||
|
||||
private:
|
||||
const BlobFetcher* base_;
|
||||
// Null iff this decorator is disabled (see class comment); otherwise the
|
||||
// same-file blob reader for the current SST.
|
||||
const SameFileBlobReader* same_file_reader_;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -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
|
||||
|
||||
+287
-49
@@ -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,93 @@ 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) {
|
||||
s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
return s;
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -348,7 +502,8 @@ Status BlobSource::GetSimpleGen2Blob(
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
return s;
|
||||
}
|
||||
|
||||
// Cache miss (or no cache configured). Read the record into a buffer
|
||||
@@ -379,6 +534,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 +558,84 @@ 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) {
|
||||
s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
return s;
|
||||
}
|
||||
|
||||
// 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"
|
||||
|
||||
@@ -215,11 +215,10 @@ Status BlobWriteBatchTransformer::PutEntityCF(uint32_t column_family_id,
|
||||
output_batch_, column_family_id, key, entity);
|
||||
}
|
||||
|
||||
Slice entity_ref = entity;
|
||||
std::vector<WideColumn> columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> existing_blob_columns;
|
||||
Status status = WideColumnSerialization::DeserializeV2(entity_ref, columns,
|
||||
existing_blob_columns);
|
||||
Status status = WideColumnSerialization::Deserialize(entity, columns,
|
||||
&existing_blob_columns);
|
||||
if (status.ok()) {
|
||||
// Reject pre-serialized entities that already contain blob references.
|
||||
// Passing them through would preserve references to blob files outside this
|
||||
|
||||
@@ -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
|
||||
|
||||
+358
-14
@@ -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,19 +139,32 @@ 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);
|
||||
}
|
||||
|
||||
bool MaybeResolveMemtableBlobValueForTest(const Slice& key,
|
||||
const BlobFetcher* blob_fetcher,
|
||||
PinnableSlice* value,
|
||||
PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index,
|
||||
bool* value_found = nullptr) {
|
||||
return DBImpl::MaybeResolveMemtableBlobValue(
|
||||
key, blob_fetcher, value, columns, s, is_blob_index, value_found);
|
||||
Status MaybeResolveMemtableBlobValueForTest(
|
||||
const Slice& key, const BlobFetcher* blob_fetcher, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, bool* did_resolve, bool* is_blob_index,
|
||||
bool* value_found = nullptr) {
|
||||
return DBImpl::MaybeResolveMemtableBlobValue(key, blob_fetcher, value,
|
||||
columns, did_resolve,
|
||||
is_blob_index, value_found);
|
||||
}
|
||||
|
||||
Status PostprocessMemtableValueReadForTest(
|
||||
const Slice& key, bool resolve_blob_backed_memtable_value,
|
||||
const BlobFetcher* blob_fetcher, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, Status memtable_read_status,
|
||||
bool* is_blob_index, bool* value_found = nullptr) {
|
||||
return DBImpl::PostprocessMemtableValueRead(
|
||||
key, /*timestamp=*/nullptr, resolve_blob_backed_memtable_value,
|
||||
blob_fetcher, value, columns, std::move(memtable_read_status),
|
||||
is_blob_index, value_found);
|
||||
}
|
||||
|
||||
Options GetTestOptions() {
|
||||
@@ -309,17 +323,65 @@ TEST_F(DBBlobIndexTest,
|
||||
value.GetSelf()->assign(blob_index.data(), blob_index.size());
|
||||
value.PinSelf();
|
||||
|
||||
Status s = Status::OK();
|
||||
bool did_resolve = false;
|
||||
bool is_blob_index = true;
|
||||
ASSERT_TRUE(MaybeResolveMemtableBlobValueForTest(
|
||||
Slice("key"), /*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr, &s,
|
||||
&is_blob_index));
|
||||
Status s = MaybeResolveMemtableBlobValueForTest(
|
||||
Slice("key"), /*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr,
|
||||
&did_resolve, &is_blob_index);
|
||||
|
||||
ASSERT_TRUE(s.IsNotSupported()) << s.ToString();
|
||||
ASSERT_TRUE(did_resolve);
|
||||
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
|
||||
ASSERT_TRUE(value.empty());
|
||||
ASSERT_FALSE(is_blob_index);
|
||||
}
|
||||
|
||||
TEST_F(DBBlobIndexTest, PostprocessMemtableValueReadClearsOutputsOnError) {
|
||||
// Regression test for the "swallowed status" FIXME: on any error,
|
||||
// PostprocessMemtableValueRead must clear the outputs and return the status,
|
||||
// never leaving a half-populated result behind a non-OK status.
|
||||
|
||||
// Case 1: the incoming memtable read status is already non-OK. The resolve
|
||||
// path is skipped; the finalization block clears outputs and returns it.
|
||||
{
|
||||
PinnableSlice value;
|
||||
value.GetSelf()->assign("stale", 5);
|
||||
value.PinSelf();
|
||||
|
||||
bool is_blob_index = false;
|
||||
Status s = PostprocessMemtableValueReadForTest(
|
||||
Slice("key"), /*resolve_blob_backed_memtable_value=*/true,
|
||||
/*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr,
|
||||
Status::Corruption("read failed"), &is_blob_index);
|
||||
|
||||
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
|
||||
ASSERT_TRUE(value.empty());
|
||||
}
|
||||
|
||||
// Case 2: the incoming status is OK but the blob-resolve path itself fails (a
|
||||
// blob-backed value with no fetcher). This is the actual scenario the FIXME
|
||||
// referred to -- previously the non-OK status from the resolve path was
|
||||
// dropped. The failure must propagate and the raw blob-index bytes must be
|
||||
// cleared rather than handed back as if they were the value.
|
||||
{
|
||||
std::string blob_index;
|
||||
BlobIndex::EncodeBlob(&blob_index, /*file_number=*/123, /*offset=*/456,
|
||||
/*size=*/789, kNoCompression);
|
||||
|
||||
PinnableSlice value;
|
||||
value.GetSelf()->assign(blob_index.data(), blob_index.size());
|
||||
value.PinSelf();
|
||||
|
||||
bool is_blob_index = true;
|
||||
Status s = PostprocessMemtableValueReadForTest(
|
||||
Slice("key"), /*resolve_blob_backed_memtable_value=*/true,
|
||||
/*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr, Status::OK(),
|
||||
&is_blob_index);
|
||||
|
||||
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
|
||||
ASSERT_TRUE(value.empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBBlobIndexTest, ReadOnlyGetImplReturnsBlobIndexWhenRequested) {
|
||||
// Goal: cover the internal read-only GetImpl contract when the caller
|
||||
// explicitly asks for raw blob-index bytes via `is_blob_index`. Recovery
|
||||
@@ -775,6 +837,288 @@ TEST_F(DBBlobIndexTest, EmbeddedBlobSharedBlockAndBlobCache) {
|
||||
ASSERT_OK(iter->status());
|
||||
}
|
||||
|
||||
// Point GetEntity / MultiGetEntity of a wide-column entity whose non-default
|
||||
// columns are embedded (same-file) blobs must return the correct columns and
|
||||
// resolve them zero-copy: each resolved blob column value points straight at
|
||||
// its pinned blob-cache buffer (identical across reads, and in a separate
|
||||
// buffer per column), rather than being copied into one re-serialized entity.
|
||||
TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnGetEntityZeroCopy) {
|
||||
Options options = GetTestOptions();
|
||||
options.create_if_missing = true;
|
||||
options.statistics = CreateDBStatistics();
|
||||
LRUCacheOptions co;
|
||||
co.capacity = 8 << 20;
|
||||
options.blob_cache = NewLRUCache(co);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string sst_path = dbname_ + "/embedded_wc_zerocopy.sst";
|
||||
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
|
||||
embedded_blob_options.min_blob_size = 8;
|
||||
|
||||
const std::string small_default = "d"; // inline default column
|
||||
const std::string big1(1024, 'a'); // same-file blob column
|
||||
const std::string big2(2048, 'b'); // same-file blob column
|
||||
const WideColumns columns{
|
||||
{kDefaultWideColumnName, small_default}, {"big1", big1}, {"big2", big2}};
|
||||
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
|
||||
ASSERT_OK(writer.PutEntity("k", columns));
|
||||
ASSERT_OK(writer.Finish());
|
||||
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
|
||||
|
||||
const char* big1_ptr = nullptr;
|
||||
const char* big2_ptr = nullptr;
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k",
|
||||
&result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
|
||||
// columns() is sorted by name -> [default(""), big1, big2].
|
||||
big1_ptr = result.columns()[1].value().data();
|
||||
big2_ptr = result.columns()[2].value().data();
|
||||
// Multi-buffer: each blob column lives in its own backing buffer, so the
|
||||
// two values are not laid out contiguously (as they would be in a single
|
||||
// re-serialized V1 entity).
|
||||
ASSERT_NE(big1_ptr + big1.size(), big2_ptr);
|
||||
|
||||
// Pointer stability across move (address-stable backing nodes).
|
||||
PinnableWideColumns moved(std::move(result));
|
||||
ASSERT_EQ(moved.columns(), columns);
|
||||
ASSERT_EQ(moved.columns()[1].value().data(), big1_ptr);
|
||||
ASSERT_EQ(moved.columns()[2].value().data(), big2_ptr);
|
||||
}
|
||||
|
||||
// A second read serves the same pinned blob-cache buffers (zero-copy): the
|
||||
// resolved column values point at the very same addresses, which a per-read
|
||||
// re-serialization would not.
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k",
|
||||
&result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
ASSERT_EQ(result.columns()[1].value().data(), big1_ptr);
|
||||
ASSERT_EQ(result.columns()[2].value().data(), big2_ptr);
|
||||
}
|
||||
|
||||
// MultiGetEntity: same correctness and zero-copy (cache-hit) guarantee.
|
||||
{
|
||||
const std::array<Slice, 1> keys{Slice("k")};
|
||||
std::array<PinnableWideColumns, 1> results;
|
||||
std::array<Status, 1> statuses;
|
||||
db_->MultiGetEntity(ReadOptions(), db_->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), results.data(), statuses.data());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(results[0].columns(), columns);
|
||||
ASSERT_EQ(results[0].columns()[1].value().data(), big1_ptr);
|
||||
ASSERT_EQ(results[0].columns()[2].value().data(), big2_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// A wide-column entity whose default column is itself an embedded (same-file)
|
||||
// blob must resolve correctly on the plain Get path (default-column
|
||||
// extraction) as well as GetEntity, matching the values written.
|
||||
TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnGetDefaultColumn) {
|
||||
Options options = GetTestOptions();
|
||||
options.create_if_missing = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string sst_path = dbname_ + "/embedded_wc_default.sst";
|
||||
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
|
||||
embedded_blob_options.min_blob_size = 8;
|
||||
|
||||
const std::string big_default(1024, 'x'); // same-file blob default column
|
||||
const std::string small_attr = "s"; // inline non-default column
|
||||
const WideColumns columns{{kDefaultWideColumnName, big_default},
|
||||
{"attr", small_attr}};
|
||||
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
|
||||
ASSERT_OK(writer.PutEntity("k", columns));
|
||||
ASSERT_OK(writer.Finish());
|
||||
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
|
||||
|
||||
// Plain Get returns the (blob-backed) default column value.
|
||||
PinnableSlice value;
|
||||
ASSERT_OK(db_->Get(ReadOptions(), db_->DefaultColumnFamily(), "k", &value));
|
||||
ASSERT_EQ(value, big_default);
|
||||
|
||||
// GetEntity returns all columns.
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(
|
||||
db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k", &result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
}
|
||||
|
||||
// Merge over an embedded-blob wide-column base entity must resolve the base
|
||||
// entity's same-file default column before merging, and GetMergeOperands must
|
||||
// return the resolved default column as the base operand.
|
||||
TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnMergeBase) {
|
||||
Options options = GetTestOptions(); // merge_operator = StringAppend(',')
|
||||
options.create_if_missing = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string sst_path = dbname_ + "/embedded_wc_merge.sst";
|
||||
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
|
||||
embedded_blob_options.min_blob_size = 8;
|
||||
|
||||
const std::string big_default(1024, 'D'); // same-file blob default column
|
||||
const WideColumns columns{{kDefaultWideColumnName, big_default},
|
||||
{"attr", "s"}};
|
||||
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
|
||||
ASSERT_OK(writer.PutEntity("k", columns));
|
||||
ASSERT_OK(writer.Finish());
|
||||
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
|
||||
|
||||
// Newer merge operand lives in the memtable; the embedded entity base lives
|
||||
// in the ingested SST, so the merge resolves the base via the same-file path.
|
||||
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), "k", "op1"));
|
||||
|
||||
PinnableSlice value;
|
||||
ASSERT_OK(db_->Get(ReadOptions(), db_->DefaultColumnFamily(), "k", &value));
|
||||
ASSERT_EQ(value, big_default + "," + "op1");
|
||||
|
||||
// GetMergeOperands returns the resolved base default column plus the operand.
|
||||
std::array<PinnableSlice, 8> operands;
|
||||
GetMergeOperandsOptions get_merge_opts;
|
||||
get_merge_opts.expected_max_number_of_operands =
|
||||
static_cast<int>(operands.size());
|
||||
int num_operands = 0;
|
||||
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
"k", operands.data(), &get_merge_opts,
|
||||
&num_operands));
|
||||
ASSERT_EQ(num_operands, 2);
|
||||
ASSERT_EQ(operands[0], big_default);
|
||||
ASSERT_EQ(operands[1], "op1");
|
||||
}
|
||||
|
||||
// With read_tier == kBlockCacheTier, an unresolved same-file blob column of a
|
||||
// wide-column entity must surface Incomplete (never a raw same-file BlobIndex).
|
||||
TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnGetEntityBlockCacheTier) {
|
||||
Options options = GetTestOptions();
|
||||
options.create_if_missing = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string sst_path = dbname_ + "/embedded_wc_blockcache.sst";
|
||||
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
|
||||
embedded_blob_options.min_blob_size = 8;
|
||||
|
||||
const std::string big1(1024, 'a');
|
||||
const WideColumns columns{{kDefaultWideColumnName, "d"}, {"big", big1}};
|
||||
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
|
||||
ASSERT_OK(writer.PutEntity("k", columns));
|
||||
ASSERT_OK(writer.Finish());
|
||||
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
|
||||
|
||||
// Warm the block cache so the data block read succeeds under kBlockCacheTier
|
||||
// and the lookup actually reaches same-file blob column resolution.
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k",
|
||||
&result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
}
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.read_tier = kBlockCacheTier;
|
||||
PinnableWideColumns result;
|
||||
Status s =
|
||||
db_->GetEntity(read_options, db_->DefaultColumnFamily(), "k", &result);
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
}
|
||||
|
||||
// Companion to the test above (genuine miss -> Incomplete): with a blob cache
|
||||
// configured, a warm read populates the embedded record into the blob cache, so
|
||||
// a subsequent kBlockCacheTier GetEntity serves it from cache instead of
|
||||
// returning Incomplete. This exercises the non-lazy Get/GetEntity embedded path
|
||||
// (get_context -> EmbeddedAwareBlobFetcher -> GetSameFileBlob whole ->
|
||||
// ResolveEmbeddedBlobCached), which shares the block-cache-tier behavior with
|
||||
// the lazy resolver (a same-file column already in the blob cache is served).
|
||||
TEST_F(DBBlobIndexTest,
|
||||
EmbeddedBlobWideColumnGetEntityBlockCacheTierServesCached) {
|
||||
Options options = GetTestOptions();
|
||||
options.create_if_missing = true;
|
||||
LRUCacheOptions co;
|
||||
co.capacity = 8 << 20;
|
||||
options.blob_cache = NewLRUCache(co);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string sst_path = dbname_ + "/embedded_wc_blockcache_hit.sst";
|
||||
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
|
||||
embedded_blob_options.min_blob_size = 8;
|
||||
|
||||
const std::string big1(1024, 'a');
|
||||
const WideColumns columns{{kDefaultWideColumnName, "d"}, {"big", big1}};
|
||||
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
|
||||
ASSERT_OK(writer.PutEntity("k", columns));
|
||||
ASSERT_OK(writer.Finish());
|
||||
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
|
||||
|
||||
// Warm both the block cache (data block) and the blob cache (embedded record)
|
||||
// with a full read.
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k",
|
||||
&result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
}
|
||||
|
||||
// The embedded record is now cached, so a block-cache-only read serves it.
|
||||
ReadOptions read_options;
|
||||
read_options.read_tier = kBlockCacheTier;
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(
|
||||
db_->GetEntity(read_options, db_->DefaultColumnFamily(), "k", &result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
}
|
||||
|
||||
// A corrupt embedded blob record backing a wide-column column must surface an
|
||||
// error on GetEntity, not expose the raw same-file BlobIndex.
|
||||
// A failure resolving an embedded blob record backing a wide-column column
|
||||
// (e.g. a bad record or blob-region read fault) must surface on GetEntity, not
|
||||
// expose the raw same-file BlobIndex. The failure is injected via a sync point
|
||||
// so the test is independent of the on-disk byte layout (e.g. encrypted env).
|
||||
TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnGetEntityCorruptionSurfaced) {
|
||||
Options options = GetTestOptions();
|
||||
options.create_if_missing = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string sst_path = dbname_ + "/embedded_wc_corrupt.sst";
|
||||
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
|
||||
embedded_blob_options.min_blob_size = 8;
|
||||
|
||||
const std::string big_col(1024, 'Z');
|
||||
const WideColumns columns{{kDefaultWideColumnName, "d"}, {"big", big_col}};
|
||||
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
|
||||
ASSERT_OK(writer.PutEntity("k", columns));
|
||||
ASSERT_OK(writer.Finish());
|
||||
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
|
||||
|
||||
const Status kInjected = Status::Corruption("injected embedded blob error");
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlockBasedTable::MaybeResolveEmbeddedValue:InjectError",
|
||||
[&](void* arg) { *static_cast<Status*>(arg) = kInjected; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
PinnableWideColumns result;
|
||||
const Status s =
|
||||
db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k", &result);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
|
||||
}
|
||||
|
||||
class PlainBlobValueFilterV3 : public CompactionFilter {
|
||||
public:
|
||||
PlainBlobValueFilterV3(std::atomic<int>* filter_call_count,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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 <cstdint>
|
||||
|
||||
#include "db/blob/blob_constants.h"
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class BlobIndex;
|
||||
class PinnableSlice;
|
||||
struct ReadOptions;
|
||||
|
||||
// Narrow interface for resolving a same-file ("embedded") blob reference
|
||||
// against the physical file that currently holds the table entry. Implemented
|
||||
// by BlockBasedTable, which owns the reader context (file, footer, blob cache)
|
||||
// needed to read an embedded blob record.
|
||||
//
|
||||
// Kept in its own low-level header, free of any table-layer dependency, so that
|
||||
// db/blob (EmbeddedAwareBlobFetcher) and table/get_context can depend on just
|
||||
// this interface without pulling in BlockBasedTable. ReadPathBlobResolver (the
|
||||
// lazy-resolution read path) can likewise hold a const SameFileBlobReader*.
|
||||
class SameFileBlobReader {
|
||||
public:
|
||||
virtual ~SameFileBlobReader() = default;
|
||||
|
||||
// Resolves a same-file ("embedded") blob reference (one for which
|
||||
// blob_index.IsSameFile() is true) into `value`, pinning the payload
|
||||
// zero-copy (from the blob cache or an owned buffer).
|
||||
//
|
||||
// range_length == kWholeBlobLength resolves the whole value. Any other length
|
||||
// resolves only the sub-range [range_offset, range_offset + range_length),
|
||||
// reading just those bytes on a cache miss -- skipping whole-record checksum
|
||||
// verification (a strict sub-range cannot cover it) and never populating the
|
||||
// blob cache; valid only for an uncompressed record and a verify_policy other
|
||||
// than kVerifyIfPresent, with range_offset + range_length <=
|
||||
// blob_index.size().
|
||||
//
|
||||
// verify_policy controls whole-record checksum verification (see
|
||||
// BlobVerifyPolicy); a whole read under kVerifyIfPresent verifies even when
|
||||
// ReadOptions::verify_checksums is off. read_tier == kBlockCacheTier without
|
||||
// a cached record yields Status::Incomplete; a bad record yields
|
||||
// Status::Corruption.
|
||||
virtual Status GetSameFileBlob(const ReadOptions& read_options,
|
||||
const BlobIndex& blob_index,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
BlobVerifyPolicy verify_policy,
|
||||
PinnableSlice* value) const = 0;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -7,13 +7,14 @@
|
||||
// Auto-generated by tools/c_api_gen/regen_all.py.
|
||||
// DO NOT EDIT THIS FILE DIRECTLY.
|
||||
// Edit tools/c_api_gen/c_base.cc or the inputs under tools/c_api_gen/,
|
||||
// then rerun: python3 tools/c_api_gen/regen_all.py
|
||||
// then rerun `make regen-c-api`
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
#include "rocksdb/c.h"
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -5289,6 +5390,17 @@ uint64_t rocksdb_compaction_file_info_oldest_blob_file_number(
|
||||
|
||||
/* BlobFileAdditionInfo */
|
||||
|
||||
const char* rocksdb_blob_file_addition_info_blob_file_path(
|
||||
const rocksdb_blob_file_addition_info_t* info, size_t* size) {
|
||||
*size = info->rep.blob_file_path.size();
|
||||
return info->rep.blob_file_path.data();
|
||||
}
|
||||
|
||||
uint64_t rocksdb_blob_file_addition_info_blob_file_number(
|
||||
const rocksdb_blob_file_addition_info_t* info) {
|
||||
return info->rep.blob_file_number;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_blob_file_addition_info_total_blob_count(
|
||||
const rocksdb_blob_file_addition_info_t* info) {
|
||||
return info->rep.total_blob_count;
|
||||
@@ -5301,6 +5413,17 @@ uint64_t rocksdb_blob_file_addition_info_total_blob_bytes(
|
||||
|
||||
/* BlobFileGarbageInfo */
|
||||
|
||||
const char* rocksdb_blob_file_garbage_info_blob_file_path(
|
||||
const rocksdb_blob_file_garbage_info_t* info, size_t* size) {
|
||||
*size = info->rep.blob_file_path.size();
|
||||
return info->rep.blob_file_path.data();
|
||||
}
|
||||
|
||||
uint64_t rocksdb_blob_file_garbage_info_blob_file_number(
|
||||
const rocksdb_blob_file_garbage_info_t* info) {
|
||||
return info->rep.blob_file_number;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_blob_file_garbage_info_garbage_blob_count(
|
||||
const rocksdb_blob_file_garbage_info_t* info) {
|
||||
return info->rep.garbage_blob_count;
|
||||
@@ -9085,6 +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;
|
||||
@@ -9751,6 +9883,16 @@ const char* rocksdb_options_get_db_host_id(rocksdb_options_t* opt,
|
||||
return opt->rep.db_host_id.data();
|
||||
}
|
||||
|
||||
void rocksdb_options_set_remote_compaction_manifest_floor(
|
||||
rocksdb_options_t* opt, unsigned char v) {
|
||||
opt->rep.remote_compaction_manifest_floor = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_remote_compaction_manifest_floor(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.remote_compaction_manifest_floor;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_lowest_used_cache_tier(rocksdb_options_t* opt, int v) {
|
||||
opt->rep.lowest_used_cache_tier =
|
||||
static_cast<decltype(opt->rep.lowest_used_cache_tier)>(v);
|
||||
@@ -9791,6 +9933,16 @@ uint64_t rocksdb_options_get_max_compaction_trigger_wakeup_seconds(
|
||||
return opt->rep.max_compaction_trigger_wakeup_seconds;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt, int v) {
|
||||
opt->rep.periodic_compaction_phase_recovery_percent = v;
|
||||
}
|
||||
|
||||
int rocksdb_options_get_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.periodic_compaction_phase_recovery_percent;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_follower_refresh_catchup_period_ms(
|
||||
rocksdb_options_t* opt, uint64_t v) {
|
||||
opt->rep.follower_refresh_catchup_period_ms = v;
|
||||
@@ -10240,6 +10392,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);
|
||||
@@ -10622,6 +10784,16 @@ unsigned char rocksdb_flushoptions_get_force_atomic_flush(
|
||||
return opt->rep.force_atomic_flush;
|
||||
}
|
||||
|
||||
void rocksdb_flushoptions_set_listener_wait(rocksdb_flushoptions_t* opt,
|
||||
unsigned char v) {
|
||||
opt->rep.listener_wait = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_flushoptions_get_listener_wait(
|
||||
rocksdb_flushoptions_t* opt) {
|
||||
return opt->rep.listener_wait;
|
||||
}
|
||||
|
||||
/* FlushWALOptions */
|
||||
|
||||
void rocksdb_flushwaloptions_set_sync(rocksdb_flushwaloptions_t* opt,
|
||||
@@ -11429,6 +11601,17 @@ double rocksdb_block_based_options_get_uniform_cv_threshold(
|
||||
return opt->rep.uniform_cv_threshold;
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt, int v) {
|
||||
opt->rep.optimize_key_common_prefix =
|
||||
static_cast<decltype(opt->rep.optimize_key_common_prefix)>(v);
|
||||
}
|
||||
|
||||
int rocksdb_block_based_options_get_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt) {
|
||||
return static_cast<int>(opt->rep.optimize_key_common_prefix);
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_enable_index_compression(
|
||||
rocksdb_block_based_table_options_t* opt, unsigned char v) {
|
||||
opt->rep.enable_index_compression = v;
|
||||
@@ -11651,6 +11834,36 @@ uint64_t rocksdb_fifo_compaction_options_get_trivial_copy_buffer_size(
|
||||
|
||||
/* BackupEngineOptions */
|
||||
|
||||
void rocksdb_backup_engine_options_set_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.io_buffer_size = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_backup_engine_options_get_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.io_buffer_size;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.backup_rate_limit = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_backup_engine_options_get_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.backup_rate_limit;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt, int v) {
|
||||
opt->rep.max_background_operations = v;
|
||||
}
|
||||
|
||||
int rocksdb_backup_engine_options_get_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.max_background_operations;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_backup_dir(
|
||||
rocksdb_backup_engine_options_t* opt, const char* v) {
|
||||
opt->rep.backup_dir = v;
|
||||
@@ -11702,26 +11915,6 @@ unsigned char rocksdb_backup_engine_options_get_backup_log_files(
|
||||
return opt->rep.backup_log_files;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.io_buffer_size = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_backup_engine_options_get_io_buffer_size(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.io_buffer_size;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.backup_rate_limit = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_backup_engine_options_get_backup_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.backup_rate_limit;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_restore_rate_limit(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.restore_rate_limit = v;
|
||||
@@ -11742,16 +11935,6 @@ unsigned char rocksdb_backup_engine_options_get_share_files_with_checksum(
|
||||
return opt->rep.share_files_with_checksum;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt, int v) {
|
||||
opt->rep.max_background_operations = v;
|
||||
}
|
||||
|
||||
int rocksdb_backup_engine_options_get_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* opt) {
|
||||
return opt->rep.max_background_operations;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_callback_trigger_interval_size(
|
||||
rocksdb_backup_engine_options_t* opt, uint64_t v) {
|
||||
opt->rep.callback_trigger_interval_size = v;
|
||||
@@ -11806,16 +11989,6 @@ rocksdb_backup_engine_options_get_current_temperatures_override_manifest(
|
||||
|
||||
/* CreateBackupOptions */
|
||||
|
||||
void rocksdb_create_backup_options_set_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v) {
|
||||
opt->rep.flush_before_backup = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_create_backup_options_get_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt) {
|
||||
return opt->rep.flush_before_backup;
|
||||
}
|
||||
|
||||
void rocksdb_create_backup_options_set_decrease_background_thread_cpu_priority(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v) {
|
||||
opt->rep.decrease_background_thread_cpu_priority = v;
|
||||
@@ -11838,6 +12011,16 @@ int rocksdb_create_backup_options_get_background_thread_cpu_priority(
|
||||
return static_cast<int>(opt->rep.background_thread_cpu_priority);
|
||||
}
|
||||
|
||||
void rocksdb_create_backup_options_set_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v) {
|
||||
opt->rep.flush_before_backup = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_create_backup_options_get_flush_before_backup(
|
||||
rocksdb_create_backup_options_t* opt) {
|
||||
return opt->rep.flush_before_backup;
|
||||
}
|
||||
|
||||
void rocksdb_create_backup_options_set_atomic_flush(
|
||||
rocksdb_create_backup_options_t* opt, unsigned char v) {
|
||||
opt->rep.atomic_flush = v;
|
||||
|
||||
+183
@@ -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;
|
||||
|
||||
@@ -4531,6 +4709,11 @@ int main(int argc, char** argv) {
|
||||
CheckNoError(err);
|
||||
|
||||
rocksdb_flushoptions_set_wait(flush_options, 1);
|
||||
// Ensure the OnFlushCompleted callbacks (which update listener_state) have
|
||||
// finished by the time rocksdb_flush() returns, so the flush_count /
|
||||
// saw_flush_entries_* assertions below are not racy.
|
||||
rocksdb_flushoptions_set_listener_wait(flush_options, 1);
|
||||
CheckCondition(1 == rocksdb_flushoptions_get_listener_wait(flush_options));
|
||||
|
||||
rocksdb_put(local_db, local_woptions, "a", 1, "1", 1, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
+11
-8
@@ -1281,15 +1281,15 @@ Compaction* ColumnFamilyData::PickCompaction(
|
||||
}
|
||||
|
||||
bool ColumnFamilyData::RangeOverlapWithCompaction(
|
||||
const Slice& smallest_user_key, const Slice& largest_user_key,
|
||||
int level) const {
|
||||
const Slice& smallest_user_key, const Slice& largest_user_key, int level,
|
||||
bool range_limit_exclusive) const {
|
||||
return compaction_picker_->RangeOverlapWithCompaction(
|
||||
smallest_user_key, largest_user_key, level);
|
||||
smallest_user_key, largest_user_key, level, range_limit_exclusive);
|
||||
}
|
||||
|
||||
Status ColumnFamilyData::RangesOverlapWithMemtables(
|
||||
const autovector<UserKeyRange>& ranges, SuperVersion* super_version,
|
||||
bool allow_data_in_errors, bool* overlap) {
|
||||
bool allow_data_in_errors, bool* overlap, bool range_limit_exclusive) {
|
||||
assert(overlap != nullptr);
|
||||
*overlap = false;
|
||||
// Create an InternalIterator over all unflushed memtables
|
||||
@@ -1334,12 +1334,15 @@ Status ColumnFamilyData::RangesOverlapWithMemtables(
|
||||
}
|
||||
|
||||
if (status.ok()) {
|
||||
const int limit_cmp = memtable_iter->Valid()
|
||||
? ucmp->CompareWithoutTimestamp(
|
||||
seek_result.user_key, ranges[i].limit)
|
||||
: 1;
|
||||
if (memtable_iter->Valid() &&
|
||||
ucmp->CompareWithoutTimestamp(seek_result.user_key,
|
||||
ranges[i].limit) <= 0) {
|
||||
(range_limit_exclusive ? limit_cmp < 0 : limit_cmp <= 0)) {
|
||||
*overlap = true;
|
||||
} else if (range_del_agg.IsRangeOverlapped(ranges[i].start,
|
||||
ranges[i].limit)) {
|
||||
} else if (range_del_agg.IsRangeOverlapped(
|
||||
ranges[i].start, ranges[i].limit, range_limit_exclusive)) {
|
||||
*overlap = true;
|
||||
}
|
||||
}
|
||||
|
||||
+49
-5
@@ -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 {
|
||||
@@ -444,8 +445,8 @@ class ColumnFamilyData {
|
||||
// Check if the passed range overlap with any running compactions.
|
||||
// REQUIRES: DB mutex held
|
||||
bool RangeOverlapWithCompaction(const Slice& smallest_user_key,
|
||||
const Slice& largest_user_key,
|
||||
int level) const;
|
||||
const Slice& largest_user_key, int level,
|
||||
bool range_limit_exclusive) const;
|
||||
|
||||
// Check if the passed ranges overlap with any unflushed memtables
|
||||
// (immutable or mutable).
|
||||
@@ -456,7 +457,8 @@ class ColumnFamilyData {
|
||||
// Thread-safe
|
||||
Status RangesOverlapWithMemtables(const autovector<UserKeyRange>& ranges,
|
||||
SuperVersion* super_version,
|
||||
bool allow_data_in_errors, bool* overlap);
|
||||
bool allow_data_in_errors, bool* overlap,
|
||||
bool range_limit_exclusive);
|
||||
|
||||
// A flag to tell a manual compaction is to compact all levels together
|
||||
// instead of a specific level.
|
||||
@@ -520,6 +522,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 +739,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
|
||||
|
||||
@@ -491,7 +491,8 @@ int Compaction::GetProximalLevel() const { return proximal_level_; }
|
||||
// smallest_key and largest_key include timestamps if user-defined timestamp is
|
||||
// enabled.
|
||||
bool Compaction::OverlapProximalLevelOutputRange(
|
||||
const Slice& smallest_key, const Slice& largest_key) const {
|
||||
const Slice& smallest_key, const Slice& largest_key,
|
||||
bool range_limit_exclusive) const {
|
||||
if (!SupportsPerKeyPlacement()) {
|
||||
return false;
|
||||
}
|
||||
@@ -506,10 +507,11 @@ bool Compaction::OverlapProximalLevelOutputRange(
|
||||
const Comparator* ucmp =
|
||||
input_vstorage_->InternalComparator()->user_comparator();
|
||||
|
||||
const int largest_cmp = ucmp->CompareWithoutTimestamp(
|
||||
largest_key, proximal_level_smallest_.user_key());
|
||||
return ucmp->CompareWithoutTimestamp(
|
||||
smallest_key, proximal_level_largest_.user_key()) <= 0 &&
|
||||
ucmp->CompareWithoutTimestamp(
|
||||
largest_key, proximal_level_smallest_.user_key()) >= 0;
|
||||
(range_limit_exclusive ? largest_cmp > 0 : largest_cmp >= 0);
|
||||
}
|
||||
|
||||
// key includes timestamp if user-defined timestamp is enabled.
|
||||
|
||||
@@ -397,7 +397,8 @@ class Compaction {
|
||||
// Both smallest_key and largest_key include timestamps if user-defined
|
||||
// timestamp is enabled.
|
||||
bool OverlapProximalLevelOutputRange(const Slice& smallest_key,
|
||||
const Slice& largest_key) const;
|
||||
const Slice& largest_key,
|
||||
bool range_limit_exclusive) const;
|
||||
|
||||
// For testing purposes, check that a key is within proximal level
|
||||
// output range for per_key_placement feature, which is safe to place the key
|
||||
|
||||
@@ -251,8 +251,9 @@ void CompactionIterator::SetBlobFetcher(const Version* version,
|
||||
read_options.io_activity = io_activity;
|
||||
read_options.fill_cache = false;
|
||||
|
||||
blob_fetcher_ = std::make_unique<BlobFetcher>(
|
||||
version, read_options, blob_file_cache, allow_write_path_fallback);
|
||||
blob_fetcher_ = std::make_unique<OwningVersionBlobFetcher>(
|
||||
version, std::move(read_options), blob_file_cache,
|
||||
allow_write_path_fallback);
|
||||
blob_resolver_.Init(blob_fetcher_.get(), prefetch_buffers_.get(),
|
||||
&iter_stats_);
|
||||
}
|
||||
@@ -467,14 +468,14 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
}
|
||||
}
|
||||
if (UNLIKELY(has_blob_columns)) {
|
||||
// Entity has blob columns. DeserializeV2() populates
|
||||
// Entity has blob columns. Deserialize() populates
|
||||
// entity_columns_ with the full column set; blob-backed entries still
|
||||
// carry the serialized BlobIndex bytes in value(). The companion
|
||||
// entity_blob_columns_ side list identifies which column indexes are
|
||||
// blob references and provides the decoded BlobIndex objects.
|
||||
Slice input_copy = value_;
|
||||
Status s = WideColumnSerialization::DeserializeV2(
|
||||
input_copy, entity_columns_, entity_blob_columns_);
|
||||
Status s = WideColumnSerialization::Deserialize(
|
||||
input_copy, entity_columns_, &entity_blob_columns_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
@@ -541,7 +542,7 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
} else {
|
||||
// No blob columns, use fast path
|
||||
Slice value_copy = value_;
|
||||
const Status s = WideColumnSerialization::Deserialize(
|
||||
const Status s = WideColumnSerialization::DeserializeSimple(
|
||||
value_copy, filter_existing_columns_);
|
||||
|
||||
if (!s.ok()) {
|
||||
@@ -1550,8 +1551,8 @@ void CompactionIterator::ExtractLargeColumnValuesIfNeeded() {
|
||||
Slice entity_slice = value_;
|
||||
entity_columns_.clear();
|
||||
entity_blob_columns_.clear();
|
||||
Status s = WideColumnSerialization::DeserializeV2(
|
||||
entity_slice, entity_columns_, entity_blob_columns_);
|
||||
Status s = WideColumnSerialization::Deserialize(
|
||||
entity_slice, entity_columns_, &entity_blob_columns_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
@@ -1669,7 +1670,7 @@ bool CompactionIterator::FetchBlobsNeedingGC(
|
||||
Status s = blob_fetcher_->FetchBlob(user_key(), blob_index, prefetch_buffer,
|
||||
&blob_value, &bytes_read);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
status_ = std::move(s);
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
@@ -1817,8 +1818,8 @@ void CompactionIterator::GarbageCollectEntityBlobsIfNeeded() {
|
||||
Slice entity_slice = value_;
|
||||
entity_columns_.clear();
|
||||
entity_blob_columns_.clear();
|
||||
Status s = WideColumnSerialization::DeserializeV2(
|
||||
entity_slice, entity_columns_, entity_blob_columns_);
|
||||
Status s = WideColumnSerialization::Deserialize(
|
||||
entity_slice, entity_columns_, &entity_blob_columns_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
@@ -1869,13 +1870,13 @@ void CompactionIterator::PrepareOutput() {
|
||||
"CompactionIterator::PrepareOutput:DeserializeEntity");
|
||||
// Deserialize entity once into member variables, then decide between
|
||||
// blob GC and extraction based on whether blob columns exist.
|
||||
// This avoids the double parse of HasBlobColumns() + DeserializeV2().
|
||||
// This avoids the double parse of HasBlobColumns() + Deserialize().
|
||||
entity_columns_.clear();
|
||||
entity_blob_columns_.clear();
|
||||
Slice entity_slice = value_;
|
||||
{
|
||||
Status s_deser = WideColumnSerialization::DeserializeV2(
|
||||
entity_slice, entity_columns_, entity_blob_columns_);
|
||||
Status s_deser = WideColumnSerialization::Deserialize(
|
||||
entity_slice, entity_columns_, &entity_blob_columns_);
|
||||
if (!s_deser.ok()) {
|
||||
status_ = s_deser;
|
||||
validity_info_.Invalidate();
|
||||
@@ -1908,8 +1909,7 @@ void CompactionIterator::PrepareOutput() {
|
||||
// KeyNotExistsBeyondOutputLevel() return true?
|
||||
if (Valid() && bottommost_level_ &&
|
||||
DefinitelyInSnapshot(ikey_.sequence, earliest_snapshot_) &&
|
||||
ikey_.type != kTypeMerge && current_key_committed_ &&
|
||||
ikey_.sequence <= preserve_seqno_after_ && !is_range_del_) {
|
||||
ikey_.type != kTypeMerge && current_key_committed_ && !is_range_del_) {
|
||||
assert(compaction_ != nullptr && !compaction_->allow_ingest_behind());
|
||||
if (ikey_.type == kTypeDeletion ||
|
||||
(ikey_.type == kTypeSingleDeletion && timestamp_size_ == 0)) {
|
||||
@@ -1931,19 +1931,29 @@ void CompactionIterator::PrepareOutput() {
|
||||
}
|
||||
|
||||
bool zeroed_seqno = false;
|
||||
if (!timestamp_size_) {
|
||||
current_key_.UpdateInternalKey(0, ikey_.type);
|
||||
zeroed_seqno = true;
|
||||
} else if (full_history_ts_low_ && cmp_with_history_ts_low_ < 0) {
|
||||
// For UDT, the seqno and timestamp could only be zeroed out after the
|
||||
// key is below history_ts_low_.
|
||||
// For the same user key (excluding timestamp), the timestamp-based
|
||||
// history can be collapsed to save some space if the timestamp is
|
||||
// older than *full_history_ts_low_.
|
||||
const std::string kTsMin(timestamp_size_, static_cast<char>(0));
|
||||
const Slice ts_slice = kTsMin;
|
||||
ikey_.SetTimestamp(ts_slice);
|
||||
current_key_.UpdateInternalKey(0, ikey_.type, &ts_slice);
|
||||
// Whether the sequence number can actually be zeroed out here is decided
|
||||
// by the shared BottommostSeqnoCanBeZeroed() predicate, which the
|
||||
// bottommost-file marking logic
|
||||
// (VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction) also
|
||||
// consults. Keeping both on one predicate ensures a file is only marked
|
||||
// for a compaction that can make progress; otherwise the file would be
|
||||
// re-marked forever (infinite compaction loop).
|
||||
if (BottommostSeqnoCanBeZeroed(
|
||||
ikey_.sequence, preserve_seqno_after_, timestamp_size_,
|
||||
full_history_ts_low_ != nullptr, cmp_with_history_ts_low_ < 0)) {
|
||||
if (!timestamp_size_) {
|
||||
current_key_.UpdateInternalKey(0, ikey_.type);
|
||||
} else {
|
||||
// For UDT, the seqno and timestamp could only be zeroed out after the
|
||||
// key is below history_ts_low_.
|
||||
// For the same user key (excluding timestamp), the timestamp-based
|
||||
// history can be collapsed to save some space if the timestamp is
|
||||
// older than *full_history_ts_low_.
|
||||
const std::string kTsMin(timestamp_size_, static_cast<char>(0));
|
||||
const Slice ts_slice = kTsMin;
|
||||
ikey_.SetTimestamp(ts_slice);
|
||||
current_key_.UpdateInternalKey(0, ikey_.type, &ts_slice);
|
||||
}
|
||||
zeroed_seqno = true;
|
||||
}
|
||||
|
||||
@@ -2064,8 +2074,9 @@ std::unique_ptr<BlobFetcher> CompactionIterator::CreateBlobFetcherIfNeeded(
|
||||
}
|
||||
}
|
||||
|
||||
return std::unique_ptr<BlobFetcher>(new BlobFetcher(
|
||||
version, read_options, blob_file_cache, allow_write_path_fallback));
|
||||
return std::unique_ptr<BlobFetcher>(
|
||||
new OwningVersionBlobFetcher(version, std::move(read_options),
|
||||
blob_file_cache, allow_write_path_fallback));
|
||||
}
|
||||
|
||||
std::unique_ptr<PrefetchBufferCollection>
|
||||
|
||||
@@ -1896,8 +1896,8 @@ TEST_F(WideColumnEntityBlobExtractionTest,
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns));
|
||||
|
||||
// The large column should now be a blob reference
|
||||
ASSERT_EQ(1, blob_columns.size());
|
||||
@@ -1941,8 +1941,8 @@ TEST_F(WideColumnEntityBlobExtractionTest,
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns));
|
||||
|
||||
// All three columns should be blob references
|
||||
ASSERT_EQ(3, blob_columns.size());
|
||||
@@ -2014,8 +2014,8 @@ TEST_F(WideColumnEntityBlobExtractionTest, WideColumnEntityMixedSizes) {
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns));
|
||||
|
||||
// Only the large column should be a blob reference
|
||||
ASSERT_EQ(1, blob_columns.size());
|
||||
@@ -2058,8 +2058,8 @@ TEST_F(WideColumnEntityBlobExtractionTest,
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns));
|
||||
|
||||
// The default column should be a blob reference
|
||||
ASSERT_EQ(1, blob_columns.size());
|
||||
@@ -2078,11 +2078,14 @@ TEST_F(WideColumnEntityBlobExtractionTest,
|
||||
// Mock BlobFetcher for testing blob GC without a real Version
|
||||
class MockBlobFetcher : public BlobFetcher {
|
||||
public:
|
||||
MockBlobFetcher() : BlobFetcher(nullptr /* version */, ReadOptions()) {}
|
||||
MockBlobFetcher() = default;
|
||||
|
||||
using BlobFetcher::FetchBlob;
|
||||
|
||||
Status FetchBlob(const Slice& /* user_key */, const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* /* prefetch_buffer */,
|
||||
PinnableSlice* blob_value, uint64_t* bytes_read) const {
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const override {
|
||||
auto it = blob_data_.find(
|
||||
std::make_pair(blob_index.file_number(), blob_index.offset()));
|
||||
if (it == blob_data_.end()) {
|
||||
@@ -2096,6 +2099,8 @@ class MockBlobFetcher : public BlobFetcher {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
const ReadOptions& read_options() const override { return read_options_; }
|
||||
|
||||
void AddBlob(uint64_t file_number, uint64_t offset,
|
||||
const std::string& value) {
|
||||
blob_data_[std::make_pair(file_number, offset)] = value;
|
||||
@@ -2103,6 +2108,7 @@ class MockBlobFetcher : public BlobFetcher {
|
||||
|
||||
private:
|
||||
std::map<std::pair<uint64_t, uint64_t>, std::string> blob_data_;
|
||||
ReadOptions read_options_;
|
||||
};
|
||||
|
||||
// FakeCompaction that supports blob garbage collection
|
||||
@@ -2372,8 +2378,8 @@ TEST_F(WideColumnEntityBlobGCTest, WideColumnEntityBlobGCRelocate) {
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns_out;
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns_out));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns_out));
|
||||
|
||||
// Entity structure should be preserved
|
||||
ASSERT_EQ(2, output_columns.size());
|
||||
@@ -2431,8 +2437,8 @@ TEST_F(WideColumnEntityBlobGCTest, WideColumnEntityBlobGCNoRelocateNewerFile) {
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns_out;
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns_out));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns_out));
|
||||
|
||||
// Blob column should still exist and reference the same file
|
||||
ASSERT_EQ(1, blob_columns_out.size());
|
||||
@@ -2491,8 +2497,8 @@ TEST_F(WideColumnEntityBlobGCTest, WideColumnEntityBlobGCPartialRelocation) {
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns_out;
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns_out));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns_out));
|
||||
|
||||
// All three columns should exist
|
||||
ASSERT_EQ(3, output_columns.size());
|
||||
@@ -2563,8 +2569,8 @@ TEST_F(WideColumnEntityBlobGCTest, WideColumnEntityBlobGCToInline) {
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns_out;
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
blob_columns_out));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&blob_columns_out));
|
||||
|
||||
// Both columns should exist
|
||||
ASSERT_EQ(2, output_columns.size());
|
||||
@@ -2788,8 +2794,8 @@ TEST_F(WideColumnEntityBlobGCTest,
|
||||
Slice value_slice(output_values[0]);
|
||||
std::vector<WideColumn> output_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> output_blob_columns;
|
||||
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
|
||||
output_blob_columns));
|
||||
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
|
||||
&output_blob_columns));
|
||||
|
||||
ASSERT_EQ(2, output_columns.size());
|
||||
ASSERT_EQ("alpha", output_columns[0].name().ToString());
|
||||
|
||||
@@ -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);
|
||||
@@ -393,6 +395,24 @@ void CompactionJob::Prepare(
|
||||
std::min(preclude_last_level_min_seqno, preserve_time_min_seqno);
|
||||
#endif
|
||||
|
||||
// For a bottommost-file compaction, which exists only because the marker
|
||||
// (VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction) decided the
|
||||
// file's largest sequence number could be zeroed, honor that same
|
||||
// version-carried preserve boundary here. The marker reads DBImpl's live,
|
||||
// DB-wide seqno->time mapping, whereas the value computed above is derived
|
||||
// only from the selected file's persisted mapping, which can be sparse or
|
||||
// empty (e.g. a file written before preserve/preclude was enabled). Taking
|
||||
// the max ensures this compaction can zero at least the sequence numbers the
|
||||
// marker considered zeroable, so a marked file makes progress instead of
|
||||
// being re-marked forever (infinite compaction loop). Other compaction
|
||||
// reasons keep the per-input-file boundary so their seqno-zeroing behavior is
|
||||
// unchanged. When preserve/preclude is inactive the version value is
|
||||
// kMaxSequenceNumber, making this a no-op.
|
||||
if (c->compaction_reason() == CompactionReason::kBottommostFiles) {
|
||||
preserve_time_min_seqno = std::max(preserve_time_min_seqno,
|
||||
storage_info->GetPreserveTimeMinSeqno());
|
||||
}
|
||||
|
||||
// Preserve sequence numbers for preserved write times and snapshots, though
|
||||
// the specific sequence number of the earliest snapshot can be zeroed.
|
||||
preserve_seqno_after_ =
|
||||
@@ -421,6 +441,17 @@ void CompactionJob::Prepare(
|
||||
c->GetKeepInLastLevelThroughSeqno());
|
||||
|
||||
options_file_number_ = versions_->options_file_number();
|
||||
|
||||
// For remote compaction hardening: capture the current MANIFEST position as a
|
||||
// floor for the worker's recovery. manifest_file_size() is a physical byte
|
||||
// offset that VersionSet::ProcessManifestWrites records under the DB mutex
|
||||
// only after the whole version-edit batch is written and sync'd, so it is a
|
||||
// durable, complete-batch offset -- never mid-record or mid-atomic-group --
|
||||
// at or past the compaction's (already committed) input version.
|
||||
if (mutable_db_options_copy_.remote_compaction_manifest_floor) {
|
||||
min_manifest_file_number_ = versions_->manifest_file_number();
|
||||
min_manifest_file_size_ = versions_->manifest_file_size();
|
||||
}
|
||||
}
|
||||
|
||||
void CompactionJob::MaybeAssignCompactionProgressAndWriter(
|
||||
@@ -1668,8 +1699,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 +1726,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 +2649,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_;
|
||||
@@ -517,12 +524,23 @@ class CompactionJob {
|
||||
// Setting this requires DBMutex.
|
||||
uint64_t options_file_number_ = 0;
|
||||
|
||||
// MANIFEST position (file number and size, in bytes) captured in Prepare()
|
||||
// (mutex held) to send to the remote worker as a floor. Zero when
|
||||
// DBOptions::remote_compaction_manifest_floor is off (or this is not a remote
|
||||
// compaction), meaning no floor is sent. Setting these requires DBMutex.
|
||||
uint64_t min_manifest_file_number_ = 0;
|
||||
uint64_t min_manifest_file_size_ = 0;
|
||||
|
||||
// Writer for persisting compaction progress during compaction
|
||||
log::Writer* compaction_progress_writer_ = nullptr;
|
||||
|
||||
// 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.
|
||||
@@ -592,6 +610,18 @@ struct CompactionServiceInput {
|
||||
|
||||
uint64_t options_file_number = 0;
|
||||
|
||||
// MANIFEST position (file number and size, in bytes) the primary scheduled
|
||||
// this compaction from, used as a floor by the remote worker: it refuses to
|
||||
// reconstruct the compaction against an older MANIFEST view (an older file
|
||||
// number, or the same file number read to a smaller size) and falls back to
|
||||
// local compaction. A non-zero min_manifest_file_number also signals the
|
||||
// worker to use "trust the MANIFEST" recovery. Zero means the primary did not
|
||||
// provide a floor (kill switch off, or an older primary); the worker then
|
||||
// uses the original recovery with no floor check.
|
||||
// See DBOptions::remote_compaction_manifest_floor.
|
||||
uint64_t min_manifest_file_number = 0;
|
||||
uint64_t min_manifest_file_size = 0;
|
||||
|
||||
// serialization interface to read and write the object
|
||||
static Status Read(const std::string& data_str, CompactionServiceInput* obj);
|
||||
Status Write(std::string* output);
|
||||
@@ -730,6 +760,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_;
|
||||
|
||||
|
||||
@@ -267,11 +267,22 @@ bool CompactionOutputs::ShouldStopBefore(const CompactionIterator& c_iter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If there's user defined partitioner, check that first
|
||||
if (partitioner_ && partitioner_->ShouldPartition(PartitionerRequest(
|
||||
last_key_for_partitioner_, c_iter.user_key(),
|
||||
current_output_file_size_)) == kRequired) {
|
||||
return true;
|
||||
if (partitioner_) {
|
||||
if (partitioner_prefix_valid_) {
|
||||
// Fast path: no per-key copy or virtual call. A partition is requested at
|
||||
// the first key that no longer shares the established prefix. An empty
|
||||
// prefix means no further partitions (every key "starts with" it).
|
||||
if (UNLIKELY(!c_iter.user_key().starts_with(partitioner_prefix_))) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Slow path: per-key callback (and copy elsewhere)
|
||||
if (UNLIKELY(partitioner_->ShouldPartition(PartitionerRequest(
|
||||
last_key_for_partitioner_, c_iter.user_key(),
|
||||
current_output_file_size_)) == kRequired)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// files output to Level 0 won't be split
|
||||
@@ -401,11 +412,27 @@ Status CompactionOutputs::AddToOutput(
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
// A new output file starts a fresh partition-tracking window. Establish the
|
||||
// partitioner fast path from this file's first key: if the partitioner can
|
||||
// express its next boundary as "first key not sharing this prefix", cache
|
||||
// that prefix; otherwise fall back to per-key ShouldPartition(). This is
|
||||
// the only place the prefix needs to be (re)derived, since a partition
|
||||
// boundary always forces a file cut, and re-deriving on non-partition cuts
|
||||
// just recomputes the same still-valid prefix.
|
||||
if (partitioner_) {
|
||||
OptSlice prefix =
|
||||
partitioner_->ShouldPartitionByPrefix(c_iter.user_key());
|
||||
partitioner_prefix_valid_ = prefix.has_value();
|
||||
if (partitioner_prefix_valid_) {
|
||||
partitioner_prefix_.assign(prefix->data(), prefix->size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// c_iter may emit range deletion keys, so update `last_key_for_partitioner_`
|
||||
// here before returning below when `is_range_del` is true
|
||||
if (partitioner_) {
|
||||
// here before returning below when `is_range_del` is true. Only needed in the
|
||||
// fallback mode; the prefix fast path does not use it.
|
||||
if (partitioner_ && !partitioner_prefix_valid_) {
|
||||
last_key_for_partitioner_.assign(c_iter.user_key().data_,
|
||||
c_iter.user_key().size_);
|
||||
}
|
||||
|
||||
@@ -349,8 +349,19 @@ class CompactionOutputs {
|
||||
const bool is_proximal_level_;
|
||||
|
||||
// partitioner information
|
||||
std::string last_key_for_partitioner_;
|
||||
std::unique_ptr<SstPartitioner> partitioner_;
|
||||
// Previous user key, only maintained (copied per key) in the fallback mode
|
||||
// (see below).
|
||||
std::string last_key_for_partitioner_;
|
||||
// Fast path for partitioners that support ShouldPartitionByPrefix(): when
|
||||
// partitioner_prefix_valid_ is true, all keys in the current output file
|
||||
// share partitioner_prefix_, and a partition boundary is requested at the
|
||||
// first future key that does not start with that prefix. This is (re)derived
|
||||
// once per output file from its first key, avoiding a per-key copy and
|
||||
// virtual ShouldPartition() call. When false, we fall back to comparing
|
||||
// against last_key_for_partitioner_ via ShouldPartition() on every key.
|
||||
std::string partitioner_prefix_;
|
||||
bool partitioner_prefix_valid_ = false;
|
||||
|
||||
// A flag determines if this subcompaction has been split by the cursor
|
||||
// for RoundRobin compaction
|
||||
|
||||
@@ -319,21 +319,22 @@ bool CompactionPicker::ExpandInputsToCleanCut(const std::string& /*cf_name*/,
|
||||
}
|
||||
|
||||
bool CompactionPicker::RangeOverlapWithCompaction(
|
||||
const Slice& smallest_user_key, const Slice& largest_user_key,
|
||||
int level) const {
|
||||
const Slice& smallest_user_key, const Slice& largest_user_key, int level,
|
||||
bool range_limit_exclusive) const {
|
||||
const Comparator* ucmp = icmp_->user_comparator();
|
||||
for (Compaction* c : compactions_in_progress_) {
|
||||
const int largest_cmp = ucmp->CompareWithoutTimestamp(
|
||||
largest_user_key, c->GetSmallestUserKey());
|
||||
if (c->output_level() == level &&
|
||||
ucmp->CompareWithoutTimestamp(smallest_user_key,
|
||||
c->GetLargestUserKey()) <= 0 &&
|
||||
ucmp->CompareWithoutTimestamp(largest_user_key,
|
||||
c->GetSmallestUserKey()) >= 0) {
|
||||
(range_limit_exclusive ? largest_cmp > 0 : largest_cmp >= 0)) {
|
||||
// Overlap
|
||||
return true;
|
||||
}
|
||||
if (c->SupportsPerKeyPlacement()) {
|
||||
if (c->OverlapProximalLevelOutputRange(smallest_user_key,
|
||||
largest_user_key)) {
|
||||
if (c->OverlapProximalLevelOutputRange(
|
||||
smallest_user_key, largest_user_key, range_limit_exclusive)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -364,7 +365,8 @@ bool CompactionPicker::FilesRangeOverlapWithCompaction(
|
||||
if (proximal_level != Compaction::kInvalidLevel) {
|
||||
if (ioptions_.compaction_style == kCompactionStyleUniversal) {
|
||||
if (RangeOverlapWithCompaction(smallest.user_key(), largest.user_key(),
|
||||
proximal_level)) {
|
||||
proximal_level,
|
||||
/*range_limit_exclusive=*/false)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
@@ -372,14 +374,16 @@ bool CompactionPicker::FilesRangeOverlapWithCompaction(
|
||||
GetRange(inputs, &proximal_smallest, &proximal_largest, level);
|
||||
if (RangeOverlapWithCompaction(proximal_smallest.user_key(),
|
||||
proximal_largest.user_key(),
|
||||
proximal_level)) {
|
||||
proximal_level,
|
||||
/*range_limit_exclusive=*/false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RangeOverlapWithCompaction(smallest.user_key(), largest.user_key(),
|
||||
level);
|
||||
level,
|
||||
/*range_limit_exclusive=*/false);
|
||||
}
|
||||
|
||||
// Returns true if any one of specified files are being compacted
|
||||
|
||||
@@ -154,8 +154,8 @@ class CompactionPicker {
|
||||
// Return true if the passed key range overlap with a compaction output
|
||||
// that is currently running.
|
||||
bool RangeOverlapWithCompaction(const Slice& smallest_user_key,
|
||||
const Slice& largest_user_key,
|
||||
int level) const;
|
||||
const Slice& largest_user_key, int level,
|
||||
bool range_limit_exclusive) const;
|
||||
|
||||
// Stores the minimal range that covers all entries in inputs in
|
||||
// *smallest, *largest.
|
||||
|
||||
@@ -90,7 +90,8 @@ class CompactionPickerTestBase : public testing::Test {
|
||||
&icmp_, ucmp_, options_.num_levels, style, nullptr, false,
|
||||
EpochNumberRequirement::kMustPresent, ioptions_.clock,
|
||||
options_.bottommost_file_compaction_delay,
|
||||
OffpeakTimeOption(mutable_db_options_.daily_offpeak_time_utc)));
|
||||
OffpeakTimeOption(mutable_db_options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{}));
|
||||
vstorage_->PrepareForVersionAppend(ioptions_, mutable_cf_options_);
|
||||
}
|
||||
|
||||
@@ -101,7 +102,8 @@ class CompactionPickerTestBase : public testing::Test {
|
||||
&icmp_, ucmp_, options_.num_levels, ioptions_.compaction_style,
|
||||
vstorage_.get(), false, EpochNumberRequirement::kMustPresent,
|
||||
ioptions_.clock, options_.bottommost_file_compaction_delay,
|
||||
OffpeakTimeOption(mutable_db_options_.daily_offpeak_time_utc)));
|
||||
OffpeakTimeOption(mutable_db_options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{}));
|
||||
}
|
||||
|
||||
void DeleteVersionStorage() {
|
||||
@@ -4162,6 +4164,18 @@ TEST_F(CompactionPickerU64TsTest, Overlap) {
|
||||
CompactionOptions(), input_files, level, vstorage_.get(),
|
||||
mutable_cf_options_, mutable_db_options_, /*output_path_id=*/0));
|
||||
|
||||
{
|
||||
std::string range_start("400");
|
||||
PutFixed64(&range_start, ts_of_smallest);
|
||||
std::string range_limit(smallest);
|
||||
PutFixed64(&range_limit, ts_of_smallest);
|
||||
ASSERT_FALSE(level_compaction_picker.RangeOverlapWithCompaction(
|
||||
range_start, range_limit, level, /*range_limit_exclusive=*/true));
|
||||
ASSERT_TRUE(level_compaction_picker.RangeOverlapWithCompaction(
|
||||
range_start, range_limit, level,
|
||||
/*range_limit_exclusive=*/false));
|
||||
}
|
||||
|
||||
{
|
||||
// [600, ts=50000] to [600, ts=50000] is the range to check.
|
||||
// ucmp->Compare(smallest_user_key, c->GetLargestUserKey()) > 0, but
|
||||
@@ -4173,7 +4187,8 @@ TEST_F(CompactionPickerU64TsTest, Overlap) {
|
||||
std::string user_key_with_ts2(largest);
|
||||
PutFixed64(&user_key_with_ts2, ts_of_largest - 1);
|
||||
ASSERT_TRUE(level_compaction_picker.RangeOverlapWithCompaction(
|
||||
user_key_with_ts1, user_key_with_ts2, level));
|
||||
user_key_with_ts1, user_key_with_ts2, level,
|
||||
/*range_limit_exclusive=*/false));
|
||||
}
|
||||
{
|
||||
// [500, ts=60000] to [500, ts=60000] is the range to check.
|
||||
@@ -4186,7 +4201,8 @@ TEST_F(CompactionPickerU64TsTest, Overlap) {
|
||||
std::string user_key_with_ts2(smallest);
|
||||
PutFixed64(&user_key_with_ts2, ts_of_smallest + 1);
|
||||
ASSERT_TRUE(level_compaction_picker.RangeOverlapWithCompaction(
|
||||
user_key_with_ts1, user_key_with_ts2, level));
|
||||
user_key_with_ts1, user_key_with_ts2, level,
|
||||
/*range_limit_exclusive=*/false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5058,6 +5074,41 @@ TEST_F(CompactionPickerTest, StandaloneRangeDeletionOnlyPicksOlderFiles) {
|
||||
ASSERT_EQ(10U, compaction->input(1, 0)->fd.GetNumber());
|
||||
}
|
||||
|
||||
// Tests for the seqno->time preserve window controlling bottommost file
|
||||
// marking. A bottommost file whose largest seqno is still within the preserve
|
||||
// window cannot have its seqno zeroed, so it must not be marked (otherwise the
|
||||
// rewrite makes no progress and the file is re-marked forever -- an infinite
|
||||
// loop).
|
||||
TEST_F(CompactionPickerTest, BottommostFileNotMarkedWithinPreserveWindow) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
Add(5, 1U, "100", "200", /*file_size=*/1000, /*path_id=*/0,
|
||||
/*smallest_seq=*/90, /*largest_seq=*/100);
|
||||
// preserve_time_min_seqno = 100 => preserve_seqno_after = 99; largest_seqno
|
||||
// 100 is within the window (> 99) and cannot be zeroed.
|
||||
vstorage_->SetPreserveTimeMinSeqno(100);
|
||||
UpdateVersionStorageInfo();
|
||||
vstorage_->UpdateOldestSnapshot(/*oldest_snapshot_seqnum=*/200,
|
||||
/*allow_ingest_behind=*/false, /*ucmp=*/ucmp_,
|
||||
/*full_history_ts_low=*/"");
|
||||
ASSERT_EQ(0U, vstorage_->BottommostFilesMarkedForCompaction().size());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, BottommostFileMarkedBelowPreserveWindow) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
Add(5, 1U, "100", "200", /*file_size=*/1000, /*path_id=*/0,
|
||||
/*smallest_seq=*/90, /*largest_seq=*/100);
|
||||
// preserve_time_min_seqno = 150 => preserve_seqno_after = 149; largest_seqno
|
||||
// 100 <= 149 so it can be zeroed and the file is marked as usual.
|
||||
vstorage_->SetPreserveTimeMinSeqno(150);
|
||||
UpdateVersionStorageInfo();
|
||||
vstorage_->UpdateOldestSnapshot(/*oldest_snapshot_seqnum=*/200,
|
||||
/*allow_ingest_behind=*/false, /*ucmp=*/ucmp_,
|
||||
/*full_history_ts_low=*/"");
|
||||
ASSERT_EQ(1U, vstorage_->BottommostFilesMarkedForCompaction().size());
|
||||
ASSERT_EQ(1U, vstorage_->BottommostFilesMarkedForCompaction()[0]
|
||||
.second->fd.GetNumber());
|
||||
}
|
||||
|
||||
// Tests for full_history_ts_low parameter in compaction picker.
|
||||
// The full_history_ts_low parameter is used to control bottommost file marking
|
||||
// for compaction when user-defined timestamps (UDT) are enabled.
|
||||
@@ -5137,6 +5188,90 @@ TEST_F(CompactionPickerU64TsTest,
|
||||
ASSERT_EQ(0U, vstorage_->BottommostFilesMarkedForCompaction().size());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerU64TsTest,
|
||||
BottommostNotMarkedWithEmptyFullHistoryTsLowAndUnknownMaxTs) {
|
||||
// Regression test for an infinite bottommost compaction loop. A bottommost
|
||||
// UDT file whose max_timestamp was not recorded in its FileMetaData (e.g. a
|
||||
// file written by an older version, or one whose timestamp property is
|
||||
// otherwise absent) must NOT be marked for compaction when
|
||||
// full_history_ts_low is unset. Under UDT the only progress a bottommost
|
||||
// compaction can make here is zeroing sequence numbers, which
|
||||
// CompactionIterator does only when full_history_ts_low is set. With it unset
|
||||
// the compaction cannot advance the file, so the rewritten output
|
||||
// re-qualifies forever.
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
|
||||
std::string ts_small = MakeU64Timestamp(50);
|
||||
std::string ts_large = MakeU64Timestamp(100);
|
||||
|
||||
// Add a bottommost file with seqno < oldest_snapshot.
|
||||
Add(5, 1U, "100", "200", /*file_size=*/1000, /*path_id=*/0,
|
||||
/*smallest_seq=*/10, /*largest_seq=*/40,
|
||||
/*compensated_file_size=*/1000,
|
||||
/*marked_for_compact=*/false, Temperature::kUnknown,
|
||||
kUnknownOldestAncesterTime, kUnknownNewestKeyTime, ts_small, ts_large);
|
||||
|
||||
// Simulate a file whose min/max timestamp was never recorded.
|
||||
ASSERT_EQ(1U, vstorage_->LevelFiles(5).size());
|
||||
FileMetaData* f = vstorage_->LevelFiles(5)[0];
|
||||
f->min_timestamp.clear();
|
||||
f->max_timestamp.clear();
|
||||
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
vstorage_->UpdateOldestSnapshot(
|
||||
/*oldest_snapshot_seqnum=*/50,
|
||||
/*allow_ingest_behind=*/false,
|
||||
/*ucmp=*/ucmp_,
|
||||
/*full_history_ts_low=*/"");
|
||||
|
||||
// No bottommost file can make progress with empty full_history_ts_low, so
|
||||
// nothing is marked even though max_timestamp is unknown. Before the fix this
|
||||
// file was marked, producing an infinite compaction loop.
|
||||
ASSERT_EQ(0U, vstorage_->BottommostFilesMarkedForCompaction().size());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerU64TsTest,
|
||||
BottommostMarkedWithSetFullHistoryTsLowAndUnknownMaxTs) {
|
||||
// A bottommost UDT file whose max_timestamp was not recorded (e.g. written by
|
||||
// an older version) must still be marked for a one-time backfill compaction
|
||||
// when full_history_ts_low IS set: that compaction can collapse timestamp
|
||||
// history / zero seqnos and record max_timestamp, so it makes progress
|
||||
// (bounded, not a loop). This complements
|
||||
// BottommostNotMarkedWithEmptyFullHistoryTsLowAndUnknownMaxTs, which covers
|
||||
// the unset case where no progress is possible.
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
|
||||
std::string ts_small = MakeU64Timestamp(50);
|
||||
std::string ts_large = MakeU64Timestamp(100);
|
||||
|
||||
Add(5, 1U, "100", "200", /*file_size=*/1000, /*path_id=*/0,
|
||||
/*smallest_seq=*/10, /*largest_seq=*/40,
|
||||
/*compensated_file_size=*/1000,
|
||||
/*marked_for_compact=*/false, Temperature::kUnknown,
|
||||
kUnknownOldestAncesterTime, kUnknownNewestKeyTime, ts_small, ts_large);
|
||||
|
||||
// Simulate a file whose min/max timestamp was never recorded.
|
||||
ASSERT_EQ(1U, vstorage_->LevelFiles(5).size());
|
||||
FileMetaData* f = vstorage_->LevelFiles(5)[0];
|
||||
f->min_timestamp.clear();
|
||||
f->max_timestamp.clear();
|
||||
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
std::string full_history_ts_low = MakeU64Timestamp(500);
|
||||
vstorage_->UpdateOldestSnapshot(
|
||||
/*oldest_snapshot_seqnum=*/50,
|
||||
/*allow_ingest_behind=*/false,
|
||||
/*ucmp=*/ucmp_, full_history_ts_low);
|
||||
|
||||
// With full_history_ts_low set, the unknown-max_timestamp file is marked so a
|
||||
// bounded compaction can backfill its metadata and reclaim obsolete data.
|
||||
ASSERT_EQ(1U, vstorage_->BottommostFilesMarkedForCompaction().size());
|
||||
ASSERT_EQ(1U, vstorage_->BottommostFilesMarkedForCompaction()[0]
|
||||
.second->fd.GetNumber());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerU64TsTest, LevelPickCompactionWithFullHistoryTsLow) {
|
||||
// Test that level compaction correctly passes full_history_ts_low
|
||||
// and picks compaction appropriately
|
||||
|
||||
@@ -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;
|
||||
@@ -49,6 +50,8 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
|
||||
compaction_input.end =
|
||||
compaction_input.has_end ? sub_compact->end->ToString() : "";
|
||||
compaction_input.options_file_number = options_file_number_;
|
||||
compaction_input.min_manifest_file_number = min_manifest_file_number_;
|
||||
compaction_input.min_manifest_file_size = min_manifest_file_size_;
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
|
||||
@@ -114,9 +117,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 +129,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());
|
||||
}
|
||||
|
||||
@@ -555,6 +566,14 @@ static std::unordered_map<std::string, OptionTypeInfo> cs_input_type_info = {
|
||||
{offsetof(struct CompactionServiceInput, options_file_number),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"min_manifest_file_number",
|
||||
{offsetof(struct CompactionServiceInput, min_manifest_file_number),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"min_manifest_file_size",
|
||||
{offsetof(struct CompactionServiceInput, min_manifest_file_size),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo>
|
||||
|
||||
@@ -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"
|
||||
@@ -159,6 +163,14 @@ class MyTestCompactionService : public CompactionService {
|
||||
compaction_num_.fetch_add(1);
|
||||
if (s.ok()) {
|
||||
return CompactionServiceJobStatus::kSuccess;
|
||||
} else if (s.IsIncomplete() && !s.IsManualCompactionPaused()) {
|
||||
// The remote worker declined the job (e.g. the MANIFEST floor check
|
||||
// rejected an older/stale filesystem view). Mapping that decline to
|
||||
// kUseLocal (retry locally) vs. kFailure (surface it) is the integrator's
|
||||
// choice; this example picks kUseLocal to exercise the fallback path,
|
||||
// while DbStressCompactionService deliberately picks kFailure. Real
|
||||
// cancellation (kManualCompactionPaused) is handled as a failure.
|
||||
return CompactionServiceJobStatus::kUseLocal;
|
||||
} else {
|
||||
return CompactionServiceJobStatus::kFailure;
|
||||
}
|
||||
@@ -360,6 +372,7 @@ class CompactionServiceTest : public DBTestBase {
|
||||
|
||||
TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
Options options = CurrentOptions();
|
||||
options.max_open_files = -1;
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
Statistics* primary_statistics = GetPrimaryStatistics();
|
||||
@@ -394,8 +407,12 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
ASSERT_EQ(primary_statistics->getTickerCount(REMOTE_COMPACT_WRITE_BYTES),
|
||||
compactor_statistics->getTickerCount(COMPACT_WRITE_BYTES));
|
||||
ASSERT_GE(primary_statistics->getTickerCount(REMOTE_COMPACT_READ_BYTES), 1);
|
||||
ASSERT_GT(primary_statistics->getTickerCount(COMPACT_READ_BYTES),
|
||||
primary_statistics->getTickerCount(REMOTE_COMPACT_READ_BYTES));
|
||||
// The remote worker reads the full compaction inputs on demand, while the
|
||||
// primary only re-reads the (smaller) output files for verify_table(), so
|
||||
// remote read bytes dominate. (The worker no longer eagerly opens every file
|
||||
// during secondary recovery, so its input reads are attributed accurately.)
|
||||
ASSERT_GT(primary_statistics->getTickerCount(REMOTE_COMPACT_READ_BYTES),
|
||||
primary_statistics->getTickerCount(COMPACT_READ_BYTES));
|
||||
// compactor is already the remote side, which doesn't have remote
|
||||
ASSERT_EQ(compactor_statistics->getTickerCount(REMOTE_COMPACT_READ_BYTES), 0);
|
||||
ASSERT_EQ(compactor_statistics->getTickerCount(REMOTE_COMPACT_WRITE_BYTES),
|
||||
@@ -480,6 +497,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 +1885,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 +2006,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();
|
||||
@@ -1953,6 +2181,286 @@ TEST_F(CompactionServiceTest, ConcurrentCompaction) {
|
||||
ASSERT_EQ(FilesPerLevel(), "0,0,10");
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RemoteCompactionMissingLowerFileTrustsManifest) {
|
||||
// A remote compaction reconstructs its view of the LSM (which files sit below
|
||||
// the output level) from the secondary's recovered MANIFEST version. That
|
||||
// view drives correctness-critical decisions like whether a deletion
|
||||
// tombstone can be dropped (Compaction::KeyNotExistsBeyondOutputLevel /
|
||||
// IsBottommostLevel, which read input_version_'s VersionStorageInfo).
|
||||
//
|
||||
// If a non-input file that is still referenced by the MANIFEST is momentarily
|
||||
// missing (as on a live primary), the OLD point-in-time recovery rolls back
|
||||
// to an earlier version. Here that rollback drops the (later, whole-DB)
|
||||
// manifest record entirely, so the remote worker can no longer find the
|
||||
// compaction input and the job fails ("cannot find matched SST files") --
|
||||
// i.e. it fails safe rather than mis-compacting. The fix makes recovery TRUST
|
||||
// the manifest: it keeps the current version, the job completes, and the
|
||||
// deleted key correctly stays deleted (the below-output tombstone is
|
||||
// retained).
|
||||
Options options = CurrentOptions();
|
||||
options.num_levels = 7;
|
||||
// -1 keeps the primary's descriptor for the (unlinked) lower file valid, so
|
||||
// the final read below is served from the primary regardless of the deletion.
|
||||
options.max_open_files = -1;
|
||||
options.level0_file_num_compaction_trigger = 100; // no auto compaction
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
// Old value for "k", pushed down to L2 (below the compaction output level).
|
||||
ASSERT_OK(Put("k", "OLD"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(2); // L2 file contains k=OLD
|
||||
|
||||
// Newer tombstone for "k" at L0 (the compaction input).
|
||||
ASSERT_OK(Delete("k"));
|
||||
ASSERT_OK(Flush()); // L0 file contains Delete(k)
|
||||
|
||||
// Reopen so the MANIFEST is rewritten as current state before we perturb it.
|
||||
ReopenWithColumnFamilies({kDefaultColumnFamilyName, "cf_1", "cf_2", "cf_3"},
|
||||
options);
|
||||
// Closing on reopen cancels pending remote jobs; re-arm the service.
|
||||
GetCompactionService()->SetCanceled(false);
|
||||
|
||||
ColumnFamilyMetaData meta;
|
||||
db_->GetColumnFamilyMetaData(&meta); // default CF
|
||||
ASSERT_EQ(meta.levels[0].files.size(), 1);
|
||||
ASSERT_EQ(meta.levels[2].files.size(), 1);
|
||||
const auto& f_del = meta.levels[0].files[0];
|
||||
const auto& f_old = meta.levels[2].files[0];
|
||||
|
||||
// Make the lower (L2) file momentarily unavailable to a fresh open, as a live
|
||||
// primary would if it had compacted it away. It is NOT a compaction input.
|
||||
ASSERT_OK(env_->DeleteFile(f_old.db_path + "/" + f_old.name));
|
||||
|
||||
// Remote-compact the L0 tombstone into L1. L2 (with k=OLD) is below the
|
||||
// output level, so a correct compaction must retain the tombstone. Without
|
||||
// the fix, recovery rolls back and this job fails; with the fix it completes.
|
||||
std::string fname = f_del.db_path + "/" + f_del.name;
|
||||
ASSERT_OK(db_->CompactFiles(CompactionOptions(), {fname}, 1));
|
||||
|
||||
// Deleted key stays deleted (tombstone retained above the still-present L2
|
||||
// value, which the primary can still read via its open descriptor).
|
||||
ASSERT_EQ("NOT_FOUND", Get("k"));
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorSendsPosition) {
|
||||
// By default (remote_compaction_manifest_floor=true), the primary attaches a
|
||||
// non-zero MANIFEST floor to every remote compaction request, and the worker
|
||||
// runs its floor check. Here the worker recovers the same-or-later MANIFEST
|
||||
// position the primary scheduled from, so the check accepts (the
|
||||
// later-manifest positive case).
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
ReopenWithCompactionService(&options);
|
||||
ASSERT_TRUE(db_->GetDBOptions().remote_compaction_manifest_floor);
|
||||
GenerateTestData();
|
||||
|
||||
uint64_t floor_number = 0;
|
||||
uint64_t floor_size = 0;
|
||||
int floor_check_count = 0;
|
||||
bool floor_check_rejected = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
|
||||
[&](void* arg) {
|
||||
auto* input = static_cast<CompactionServiceInput*>(arg);
|
||||
floor_number = input->min_manifest_file_number;
|
||||
floor_size = input->min_manifest_file_size;
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck", [&](void* arg) {
|
||||
floor_check_count++;
|
||||
if (*static_cast<bool*>(arg)) {
|
||||
floor_check_rejected = true;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
std::string start_str = Key(15);
|
||||
std::string end_str = Key(45);
|
||||
Slice start(start_str);
|
||||
Slice end(end_str);
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &start, &end));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// A floor was populated and the worker accepted its (later-or-equal) view.
|
||||
ASSERT_GT(floor_number, 0U);
|
||||
ASSERT_GT(floor_size, 0U);
|
||||
ASSERT_GE(floor_check_count, 1);
|
||||
ASSERT_FALSE(floor_check_rejected);
|
||||
VerifyTestData();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorRejectsStaleView) {
|
||||
// If the worker recovers an OLDER MANIFEST view than the primary scheduled
|
||||
// from (simulated here by inflating the floor above the real position), the
|
||||
// floor check rejects the job and the primary falls back to a correct local
|
||||
// compaction rather than compacting against a stale/older LSM shape.
|
||||
Options options = CurrentOptions();
|
||||
options.num_levels = 7;
|
||||
options.level0_file_num_compaction_trigger = 100; // no auto compaction
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
// Old value for "k", pushed down to L2 (below the compaction output level).
|
||||
ASSERT_OK(Put("k", "OLD"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(2); // L2: k=OLD
|
||||
|
||||
// Newer tombstone for "k" at L0 (the compaction input).
|
||||
ASSERT_OK(Delete("k"));
|
||||
ASSERT_OK(Flush()); // L0: Delete(k)
|
||||
|
||||
int floor_check_rejected = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
|
||||
[&](void* arg) {
|
||||
auto* input = static_cast<CompactionServiceInput*>(arg);
|
||||
// Simulate the worker seeing an older MANIFEST than the primary
|
||||
// scheduled from by raising the floor above the real position.
|
||||
input->min_manifest_file_number += 1000000;
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck", [&](void* arg) {
|
||||
if (*static_cast<bool*>(arg)) {
|
||||
floor_check_rejected++;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ColumnFamilyMetaData meta;
|
||||
db_->GetColumnFamilyMetaData(&meta); // default CF
|
||||
ASSERT_EQ(meta.levels[0].files.size(), 1);
|
||||
const auto& f_del = meta.levels[0].files[0];
|
||||
std::string fname = f_del.db_path + "/" + f_del.name;
|
||||
|
||||
// The worker rejects the stale view -> local fallback -> the compaction still
|
||||
// completes correctly (CompactFiles returns OK).
|
||||
ASSERT_OK(db_->CompactFiles(CompactionOptions(), {fname}, 1));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_GE(floor_check_rejected, 1);
|
||||
// Deleted key stays deleted: the local fallback retained the below-output
|
||||
// tombstone. A silent mis-compaction against the older view would resurrect
|
||||
// "OLD".
|
||||
ASSERT_EQ("NOT_FOUND", Get("k"));
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorKillSwitchOff) {
|
||||
// With the kill switch off, the primary sends no floor, so the worker uses
|
||||
// the original point-in-time recovery (no trust-manifest, no floor check). A
|
||||
// transiently-missing non-input file then rolls recovery back and fails the
|
||||
// job -- confirming the escape hatch reverts to the pre-fix behavior.
|
||||
Options options = CurrentOptions();
|
||||
options.num_levels = 7;
|
||||
options.max_open_files = -1;
|
||||
options.level0_file_num_compaction_trigger = 100; // no auto compaction
|
||||
options.remote_compaction_manifest_floor = false; // kill switch OFF
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
ASSERT_OK(Put("k", "OLD"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(2); // L2: k=OLD
|
||||
|
||||
ASSERT_OK(Delete("k"));
|
||||
ASSERT_OK(Flush()); // L0: Delete(k)
|
||||
|
||||
// Reopen so the MANIFEST is rewritten as current state before we perturb it.
|
||||
ReopenWithColumnFamilies({kDefaultColumnFamilyName, "cf_1", "cf_2", "cf_3"},
|
||||
options);
|
||||
GetCompactionService()->SetCanceled(false);
|
||||
|
||||
bool floor_provided_seen = true;
|
||||
int floor_check_count = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
|
||||
[&](void* arg) {
|
||||
auto* input = static_cast<CompactionServiceInput*>(arg);
|
||||
floor_provided_seen = (input->min_manifest_file_number != 0);
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck",
|
||||
[&](void* /*arg*/) { floor_check_count++; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ColumnFamilyMetaData meta;
|
||||
db_->GetColumnFamilyMetaData(&meta);
|
||||
ASSERT_EQ(meta.levels[0].files.size(), 1);
|
||||
ASSERT_EQ(meta.levels[2].files.size(), 1);
|
||||
const auto& f_del = meta.levels[0].files[0];
|
||||
const auto& f_old = meta.levels[2].files[0];
|
||||
|
||||
// Make the lower (L2) non-input file momentarily unavailable, as a live
|
||||
// primary would if it had compacted it away.
|
||||
ASSERT_OK(env_->DeleteFile(f_old.db_path + "/" + f_old.name));
|
||||
|
||||
std::string fname = f_del.db_path + "/" + f_del.name;
|
||||
Status s = db_->CompactFiles(CompactionOptions(), {fname}, 1);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// No floor was sent and the worker skipped the floor check; the original
|
||||
// recovery rolled back and failed the job (reverted behavior).
|
||||
ASSERT_FALSE(floor_provided_seen);
|
||||
ASSERT_EQ(0, floor_check_count);
|
||||
ASSERT_NOK(s);
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorMutable) {
|
||||
// remote_compaction_manifest_floor is a mutable DB option: flipping it via
|
||||
// SetDBOptions changes whether the primary attaches a floor to subsequent
|
||||
// remote compaction requests (and thus whether the worker runs the floor
|
||||
// check).
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
ReopenWithCompactionService(&options);
|
||||
GenerateTestData();
|
||||
|
||||
bool floor_provided = false;
|
||||
int floor_check_count = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
|
||||
[&](void* arg) {
|
||||
auto* input = static_cast<CompactionServiceInput*>(arg);
|
||||
floor_provided = (input->min_manifest_file_number != 0);
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck",
|
||||
[&](void* /*arg*/) { floor_check_count++; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
std::string start_str = Key(15);
|
||||
std::string end_str = Key(45);
|
||||
Slice start(start_str);
|
||||
Slice end(end_str);
|
||||
|
||||
// Default on: floor attached, floor check runs.
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &start, &end));
|
||||
ASSERT_TRUE(floor_provided);
|
||||
ASSERT_GE(floor_check_count, 1);
|
||||
|
||||
// Turn the kill switch off dynamically.
|
||||
ASSERT_OK(db_->SetDBOptions({{"remote_compaction_manifest_floor", "false"}}));
|
||||
ASSERT_FALSE(db_->GetDBOptions().remote_compaction_manifest_floor);
|
||||
|
||||
floor_provided = true;
|
||||
floor_check_count = 0;
|
||||
|
||||
// Create a fresh compaction target overlapping the range and re-compact.
|
||||
ASSERT_OK(Put(Key(20), "v"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &start, &end));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// Now no floor is attached and the worker skips the floor check.
|
||||
ASSERT_FALSE(floor_provided);
|
||||
ASSERT_EQ(0, floor_check_count);
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, CompactionInfo) {
|
||||
Options options = CurrentOptions();
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
@@ -25,6 +25,18 @@ SstPartitionerFixedPrefixFactory::SstPartitionerFixedPrefixFactory(size_t len)
|
||||
RegisterOptions("Length", &len_, &sst_fixed_prefix_type_info);
|
||||
}
|
||||
|
||||
OptSlice SstPartitionerFixedPrefix::ShouldPartitionByPrefix(
|
||||
const Slice& user_key) {
|
||||
if (user_key.size() >= len_) {
|
||||
return {user_key.data(), len_};
|
||||
} else {
|
||||
// ShouldPartition() cuts a partition for EVERY UNIQUE KEY outside the
|
||||
// prefix length domain, for better or worse. We can't directly represent
|
||||
// that so we defer to ShouldPartition().
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
PartitionerResult SstPartitionerFixedPrefix::ShouldPartition(
|
||||
const PartitionerRequest& request) {
|
||||
Slice last_key_fixed(*request.prev_user_key);
|
||||
|
||||
@@ -1339,9 +1339,13 @@ TEST_F(TieredCompactionTest, SequenceBasedTieredStorageLevel) {
|
||||
// This test was essentially for a hacked-up version on future functionality.
|
||||
// It can be resurrected if/when a form of range-based tiering is properly
|
||||
// implemented.
|
||||
// FIXME: aside from that, this test reproduces a near-endless compaction
|
||||
// cycle that needs to be reproduced independently and fixed before
|
||||
// leveled compaction can be used with the preclude feature in production.
|
||||
// The near-endless compaction cycle this used to reproduce (a bottommost file
|
||||
// whose seqno could not be zeroed within the preserve window being re-marked
|
||||
// forever) is now prevented by sharing the seqno-zeroability decision between
|
||||
// the compaction output path and bottommost-file marking (see
|
||||
// BottommostSeqnoCanBeZeroed and CompactionPickerTest.BottommostFile*
|
||||
// PreserveWindow). This test remains disabled only because range-based tiering
|
||||
// is not implemented.
|
||||
TEST_F(TieredCompactionTest, DISABLED_RangeBasedTieredStorageLevel) {
|
||||
const int kNumTrigger = 4;
|
||||
const int kNumLevels = 7;
|
||||
@@ -1558,6 +1562,188 @@ class PrecludeLastLevelTest : public PrecludeLastLevelTestBase,
|
||||
}
|
||||
};
|
||||
|
||||
// Regression test for an infinite kBottommostFiles compaction loop on a
|
||||
// leveled column family that has the seqno->time preserve feature enabled. A
|
||||
// bottommost file whose largest sequence number is still within the preserve
|
||||
// window cannot have its seqno zeroed by compaction, so it must not be marked
|
||||
// for bottommost compaction; otherwise each rewrite makes no progress and the
|
||||
// file is re-marked forever. This exercises the DBImpl plumbing that feeds
|
||||
// VersionStorageInfo::preserve_time_min_seqno_ (DBImpl::
|
||||
// MaybeUpdatePreserveTimeMinSeqno) end to end, complementing the isolated
|
||||
// marking-predicate coverage in compaction_picker_test.cc.
|
||||
class PrecludeBottommostLoopTest : public PrecludeLastLevelTestBase {};
|
||||
|
||||
TEST_F(PrecludeBottommostLoopTest, LeveledPreserveTimeNoBottommostLoop) {
|
||||
const int kNumLevels = 7;
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.preserve_internal_time_seconds = 10000;
|
||||
options.env = mock_env_.get();
|
||||
options.num_levels = kNumLevels;
|
||||
// Prevent the extra L0 file below from auto-triggering an L0 compaction.
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Populate the seqno->time mapping with recent samples so the preserve window
|
||||
// maps the file's (recent) largest seqno as non-zeroable. Total elapsed mock
|
||||
// time stays far below preserve_internal_time_seconds, so nothing ages out.
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ASSERT_OK(Put("infinite", "v" + std::to_string(i)));
|
||||
dbfull()->TEST_WaitForPeriodicTaskRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(10); });
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
// Push the file with the hot key down to the bottommost level.
|
||||
MoveFilesToLevel(kNumLevels - 1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(kNumLevels - 1));
|
||||
|
||||
// Confirm the bottommost file's largest seqno was not zeroed (it is within
|
||||
// the preserve window), so it is a genuine marking candidate that only the
|
||||
// preserve-window guard should protect -- this keeps the test from passing
|
||||
// vacuously.
|
||||
{
|
||||
std::vector<LiveFileMetaData> files;
|
||||
db_->GetLiveFilesMetaData(&files);
|
||||
bool found_bottommost = false;
|
||||
for (const auto& f : files) {
|
||||
if (f.level == kNumLevels - 1) {
|
||||
ASSERT_GT(f.largest_seqno, 0U);
|
||||
found_bottommost = true;
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(found_bottommost);
|
||||
}
|
||||
|
||||
// Bump the sequence number above the bottommost file so that releasing a
|
||||
// snapshot advances oldest_snapshot_seqnum_ past the file's largest seqno,
|
||||
// which makes the file a candidate for bottommost marking (the snapshot gate
|
||||
// passes). Only the preserve-window guard should keep it from being marked.
|
||||
ASSERT_OK(Put("bumpseqnum", ""));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::atomic<int> bottommost_compactions{0};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"LevelCompactionPicker::PickCompaction:Return", [&](void* arg) {
|
||||
auto* c = static_cast<Compaction*>(arg);
|
||||
if (c != nullptr &&
|
||||
c->compaction_reason() == CompactionReason::kBottommostFiles) {
|
||||
bottommost_compactions.fetch_add(1);
|
||||
}
|
||||
});
|
||||
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
|
||||
// The bottommost file's largest seqno is within the preserve window, so it is
|
||||
// not marked and the DB quiesces. Before the fix the file was marked and the
|
||||
// rewrite re-qualified it forever (this wait would hang).
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
ASSERT_EQ(0, bottommost_compactions.load());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
// Close explicitly because mock_env_ is destroyed before the DB otherwise.
|
||||
Close();
|
||||
}
|
||||
|
||||
// Regression test for the empty-mapping variant of the same loop: a bottommost
|
||||
// file that predates a preserve/preclude rollout has an EMPTY persisted
|
||||
// seqno->time mapping. Once the live, DB-wide mapping ages the file out of the
|
||||
// preserve window, the marker (which reads the live mapping) marks it. The
|
||||
// compaction that rewrites it must use the same version-carried preserve
|
||||
// boundary rather than the file's own empty mapping; otherwise it computes
|
||||
// preserve_seqno_after_ = 0, zeroes nothing, and re-marks the file forever.
|
||||
// With the fix the file's seqno is zeroed in one bounded compaction.
|
||||
TEST_F(PrecludeBottommostLoopTest, EmptyMappingAgedOutNoBottommostLoop) {
|
||||
const int kNumLevels = 7;
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.env = mock_env_.get();
|
||||
options.num_levels = kNumLevels;
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
// Start with preserve/preclude disabled so the bottommost file below is
|
||||
// written with an EMPTY persisted seqno->time mapping, mimicking a file that
|
||||
// predates a preserve/preclude rollout.
|
||||
options.preserve_internal_time_seconds = 0;
|
||||
options.preclude_last_level_data_seconds = 0;
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Hold a snapshot below the "aged" key so moving it to the bottommost level
|
||||
// does not zero its seqno (there is no preserve window yet to protect it).
|
||||
ASSERT_OK(Put("keeplow", "v"));
|
||||
const Snapshot* protect = db_->GetSnapshot();
|
||||
ASSERT_OK(Put("aged", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(kNumLevels - 1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(kNumLevels - 1));
|
||||
|
||||
SequenceNumber aged_seqno = 0;
|
||||
{
|
||||
std::vector<LiveFileMetaData> files;
|
||||
db_->GetLiveFilesMetaData(&files);
|
||||
for (const auto& f : files) {
|
||||
if (f.level == kNumLevels - 1) {
|
||||
aged_seqno = f.largest_seqno;
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_GT(aged_seqno, 0U);
|
||||
|
||||
// Roll out the preserve feature dynamically and let the live, DB-wide
|
||||
// seqno->time mapping accumulate samples while mock time advances well past
|
||||
// preserve_internal_time_seconds, so the pre-existing file's seqno ages out
|
||||
// of the preserve window per the live mapping (even though the file's own
|
||||
// persisted mapping is empty). The protecting snapshot keeps the file from
|
||||
// being marked during this loop (oldest_snapshot_seqnum_ stays below it).
|
||||
ASSERT_OK(db_->SetOptions({{"preserve_internal_time_seconds", "10000"}}));
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ASSERT_OK(Put("hot", "v" + std::to_string(i)));
|
||||
dbfull()->TEST_WaitForPeriodicTaskRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(200); });
|
||||
}
|
||||
|
||||
std::atomic<int> bottommost_compactions{0};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"LevelCompactionPicker::PickCompaction:Return", [&](void* arg) {
|
||||
auto* c = static_cast<Compaction*>(arg);
|
||||
if (c != nullptr &&
|
||||
c->compaction_reason() == CompactionReason::kBottommostFiles) {
|
||||
bottommost_compactions.fetch_add(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Release the protecting snapshot so oldest_snapshot_seqnum_ advances past
|
||||
// the file's largest seqno, making it a bottommost marking candidate. The
|
||||
// marker (using the live mapping) marks it; the compaction that rewrites it
|
||||
// must use the same version-carried preserve boundary (not the file's empty
|
||||
// mapping) to actually zero the seqno, otherwise it makes no progress and
|
||||
// loops.
|
||||
db_->ReleaseSnapshot(protect);
|
||||
|
||||
// With the fix the file's seqno is zeroed in a bounded number of bottommost
|
||||
// compactions and the DB quiesces. Before the fix this wait would hang.
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
{
|
||||
std::vector<LiveFileMetaData> files;
|
||||
db_->GetLiveFilesMetaData(&files);
|
||||
for (const auto& f : files) {
|
||||
if (f.level == kNumLevels - 1) {
|
||||
ASSERT_EQ(0U, f.largest_seqno);
|
||||
}
|
||||
}
|
||||
}
|
||||
// The file was genuinely marked and rewritten (not vacuously skipped), and
|
||||
// the rewrite made progress instead of looping.
|
||||
ASSERT_GE(bottommost_compactions.load(), 1);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(PrecludeLastLevelTest, MigrationFromPreserveTimeManualCompaction) {
|
||||
const int kNumTrigger = 4;
|
||||
const int kNumLevels = 7;
|
||||
|
||||
+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
|
||||
+382
-282
@@ -10,6 +10,10 @@
|
||||
#include <cstring>
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/log_writer.h"
|
||||
#include "db/manifest_ops.h"
|
||||
#include "db/version_edit.h"
|
||||
#include "file/writable_file_writer.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/filter_policy.h"
|
||||
@@ -17,10 +21,13 @@
|
||||
#include "rocksdb/merge_operator.h"
|
||||
#include "rocksdb/perf_context.h"
|
||||
#include "rocksdb/table.h"
|
||||
#include "rocksdb/utilities/checkpoint.h"
|
||||
#include "rocksdb/utilities/debug.h"
|
||||
#include "table/block_based/block_based_table_reader.h"
|
||||
#include "table/block_based/block_builder.h"
|
||||
#include "table/format.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/defer.h"
|
||||
#include "util/file_checksum_helper.h"
|
||||
#include "util/random.h"
|
||||
#include "utilities/counted_fs.h"
|
||||
@@ -78,6 +85,7 @@ class MyFlushBlockPolicyFactory : public FlushBlockPolicyFactory {
|
||||
private:
|
||||
const int num_keys_in_block_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
static bool enable_io_uring = true;
|
||||
@@ -88,6 +96,15 @@ class DBBasicTest : public DBTestBase {
|
||||
DBBasicTest() : DBTestBase("db_basic_test", /*env_do_fsync=*/false) {}
|
||||
};
|
||||
|
||||
class DBBasicGetWithParam : public DBBasicTest,
|
||||
public testing::WithParamInterface<bool> {
|
||||
public:
|
||||
DBBasicGetWithParam() : DBBasicTest(), use_coroutine_(GetParam()) {}
|
||||
|
||||
protected:
|
||||
const bool use_coroutine_;
|
||||
};
|
||||
|
||||
TEST_F(DBBasicTest, OpenWhenOpen) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
@@ -128,6 +145,7 @@ struct RecoveryOptimizationCounters {
|
||||
sp->ClearAllCallBacks();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// optimize_manifest_for_recovery=true: a clean reopen of a flushed DB must
|
||||
@@ -338,6 +356,45 @@ TEST_F(DBBasicTest,
|
||||
synthetic_number);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest,
|
||||
OptimizeManifestForRecoveryKeepsCurrentOptionsFileWithStaleOptions) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.avoid_unnecessary_blocking_io = false;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
const uint64_t next_before_close =
|
||||
dbfull()->GetVersionSet()->current_next_file_number();
|
||||
Close();
|
||||
|
||||
const uint64_t stale_options_number = next_before_close + 100;
|
||||
ASSERT_OK(WriteStringToFile(
|
||||
env_, "" /*data*/, dbname_ + "/" + OptionsFileName(stale_options_number),
|
||||
/*should_sync=*/true));
|
||||
ASSERT_OK(WriteStringToFile(
|
||||
env_, "" /*data*/,
|
||||
dbname_ + "/" + OptionsFileName(stale_options_number + 1),
|
||||
/*should_sync=*/true));
|
||||
|
||||
Reopen(options);
|
||||
const uint64_t current_options_number =
|
||||
dbfull()->GetVersionSet()->options_file_number();
|
||||
ASSERT_GT(current_options_number, 0u);
|
||||
ASSERT_OK(env_->FileExists(OptionsFileName(dbname_, current_options_number)));
|
||||
|
||||
const std::string checkpoint_dir = dbname_ + "_checkpoint";
|
||||
ASSERT_OK(DestroyDir(env_, checkpoint_dir));
|
||||
Checkpoint* checkpoint = nullptr;
|
||||
ASSERT_OK(Checkpoint::Create(db_.get(), &checkpoint));
|
||||
std::unique_ptr<Checkpoint> checkpoint_guard(checkpoint);
|
||||
ASSERT_OK(checkpoint_guard->CreateCheckpoint(checkpoint_dir));
|
||||
ASSERT_OK(DestroyDir(env_, checkpoint_dir));
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// optimize_manifest_for_recovery=true: after a clean Put + Flush + Close, the
|
||||
// next Open's min_log_number_to_keep equals (max_wal_before_close + 1),
|
||||
// proving the close-time MANIFEST write took effect.
|
||||
@@ -965,27 +1022,19 @@ TEST_F(DBBasicTest, WritableFileWriterInitialFileSizeAdoptsExistingSize) {
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
options.write_dbid_to_manifest = false;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
// Find the MANIFEST file and append garbage to it.
|
||||
std::string manifest_path;
|
||||
{
|
||||
std::vector<std::string> files;
|
||||
ASSERT_OK(env_->GetChildren(dbname_, &files));
|
||||
for (const auto& f : files) {
|
||||
uint64_t number;
|
||||
FileType type;
|
||||
if (ParseFileName(f, &number, &type) && type == kDescriptorFile) {
|
||||
manifest_path = dbname_ + "/" + f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_FALSE(manifest_path.empty());
|
||||
uint64_t manifest_file_number = 0;
|
||||
ASSERT_OK(GetCurrentManifestPath(dbname_, env_->GetFileSystem().get(),
|
||||
/*is_retry=*/false, &manifest_path,
|
||||
&manifest_file_number));
|
||||
{
|
||||
std::string contents;
|
||||
ASSERT_OK(ReadFileToString(env_, manifest_path, &contents));
|
||||
@@ -994,9 +1043,13 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
|
||||
}
|
||||
|
||||
std::atomic<int> reopened{0};
|
||||
std::atomic<int> new_manifest{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ReopenManifestForAppend:Reopened",
|
||||
[&](void* /*arg*/) { reopened.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ProcessManifestWrites:BeforeNewManifest",
|
||||
[&](void* /*arg*/) { new_manifest.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Reopen(options);
|
||||
@@ -1005,9 +1058,111 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_EQ(0, reopened.load());
|
||||
ASSERT_EQ(1, new_manifest.load());
|
||||
std::string manifest_path_after;
|
||||
ASSERT_OK(GetCurrentManifestPath(dbname_, env_->GetFileSystem().get(),
|
||||
/*is_retry=*/false, &manifest_path_after,
|
||||
&manifest_file_number));
|
||||
ASSERT_NE(manifest_path, manifest_path_after);
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// Regression test for corrupted atomic group when reuse_manifest_on_open
|
||||
// appends after an incomplete atomic group at the MANIFEST tail.
|
||||
// Before the fix, last_valid_record_end_ included records from an incomplete
|
||||
// trailing atomic group, causing ReopenManifestForAppend to append new records
|
||||
// after them. On subsequent recovery the incomplete group followed by new
|
||||
// records triggers "corrupted atomic group".
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenIncompleteAtomicGroupAtTail) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
options.write_dbid_to_manifest = false;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
std::string manifest_path;
|
||||
uint64_t manifest_file_number = 0;
|
||||
ASSERT_OK(GetCurrentManifestPath(dbname_, env_->GetFileSystem().get(),
|
||||
/*is_retry=*/false, &manifest_path,
|
||||
&manifest_file_number));
|
||||
|
||||
// Append an incomplete atomic group (2 records of a 3-member group) to the
|
||||
// MANIFEST. These are valid log records with correct CRCs but the atomic
|
||||
// group is not complete (missing the third record with remaining_entries=0).
|
||||
{
|
||||
uint64_t file_size;
|
||||
ASSERT_OK(env_->GetFileSize(manifest_path, &file_size));
|
||||
const auto& fs = env_->GetFileSystem();
|
||||
std::unique_ptr<FSWritableFile> fs_file;
|
||||
ASSERT_OK(fs->ReopenWritableFile(manifest_path, FileOptions(), &fs_file,
|
||||
nullptr));
|
||||
std::unique_ptr<WritableFileWriter> file_writer(new WritableFileWriter(
|
||||
std::move(fs_file), manifest_path, FileOptions()));
|
||||
log::Writer log_writer(std::move(file_writer), 0, false, false,
|
||||
kNoCompression, false, file_size % log::kBlockSize);
|
||||
|
||||
// Write 2 records of a 3-member atomic group
|
||||
VersionEdit edit1;
|
||||
edit1.SetLogNumber(0);
|
||||
edit1.SetNextFile(100);
|
||||
edit1.SetLastSequence(100);
|
||||
edit1.MarkAtomicGroup(2); // remaining_entries=2, expects 3 total
|
||||
std::string record1;
|
||||
ASSERT_TRUE(edit1.EncodeTo(&record1, 0));
|
||||
ASSERT_OK(log_writer.AddRecord(WriteOptions(), record1));
|
||||
|
||||
VersionEdit edit2;
|
||||
edit2.SetLogNumber(0);
|
||||
edit2.SetNextFile(100);
|
||||
edit2.SetLastSequence(100);
|
||||
edit2.MarkAtomicGroup(1); // remaining_entries=1
|
||||
std::string record2;
|
||||
ASSERT_TRUE(edit2.EncodeTo(&record2, 0));
|
||||
ASSERT_OK(log_writer.AddRecord(WriteOptions(), record2));
|
||||
// Deliberately NOT writing the third record (remaining_entries=0)
|
||||
}
|
||||
|
||||
std::atomic<int> reopened{0};
|
||||
std::atomic<int> new_manifest{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ReopenManifestForAppend:Reopened",
|
||||
[&](void* /*arg*/) { reopened.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ProcessManifestWrites:BeforeNewManifest",
|
||||
[&](void* /*arg*/) { new_manifest.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Reopen: recovery should succeed, but ReopenManifestForAppend must not
|
||||
// reuse a MANIFEST ending with an incomplete atomic group.
|
||||
Reopen(options);
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_EQ(0, reopened.load());
|
||||
ASSERT_EQ(1, new_manifest.load());
|
||||
std::string manifest_path_after;
|
||||
ASSERT_OK(GetCurrentManifestPath(dbname_, env_->GetFileSystem().get(),
|
||||
/*is_retry=*/false, &manifest_path_after,
|
||||
&manifest_file_number));
|
||||
ASSERT_NE(manifest_path, manifest_path_after);
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
|
||||
// Write new data to create new MANIFEST records.
|
||||
ASSERT_OK(Put("k2", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
// Final reopen: must NOT fail with "corrupted atomic group".
|
||||
Reopen(options);
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
ASSERT_EQ("v2", Get("k2"));
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, EnableDirectIOWithZeroBuf) {
|
||||
if (!IsDirectIOSupported()) {
|
||||
ROCKSDB_GTEST_BYPASS("Direct IO not supported");
|
||||
@@ -1370,27 +1525,36 @@ TEST_F(DBBasicTest, LevelLimitReopen) {
|
||||
ASSERT_OK(TryReopenWithColumnFamilies({"default", "pikachu"}, options));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, PutDeleteGet) {
|
||||
TEST_P(DBBasicGetWithParam, PutDeleteGet) {
|
||||
do {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
ASSERT_OK(Put(1, "foo", "v1"));
|
||||
ASSERT_EQ("v1", Get(1, "foo"));
|
||||
ASSERT_EQ("v1", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_OK(Put(1, "foo", "v2"));
|
||||
ASSERT_EQ("v2", Get(1, "foo"));
|
||||
ASSERT_EQ("v2", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_OK(Delete(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "foo", nullptr, use_coroutine_));
|
||||
|
||||
ASSERT_OK(Put("default_foo", "default_v1"));
|
||||
ASSERT_EQ("default_v1",
|
||||
Get("default_foo", static_cast<const Snapshot*>(nullptr),
|
||||
use_coroutine_));
|
||||
|
||||
PinnableSlice default_pinnable_value;
|
||||
ASSERT_OK(Get("default_foo", &default_pinnable_value, use_coroutine_));
|
||||
ASSERT_EQ("default_v1", default_pinnable_value.ToString());
|
||||
} while (ChangeOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, PutSingleDeleteGet) {
|
||||
TEST_P(DBBasicGetWithParam, PutSingleDeleteGet) {
|
||||
do {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
ASSERT_OK(Put(1, "foo", "v1"));
|
||||
ASSERT_EQ("v1", Get(1, "foo"));
|
||||
ASSERT_EQ("v1", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_OK(Put(1, "foo2", "v2"));
|
||||
ASSERT_EQ("v2", Get(1, "foo2"));
|
||||
ASSERT_EQ("v2", Get(1, "foo2", nullptr, use_coroutine_));
|
||||
ASSERT_OK(SingleDelete(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "foo", nullptr, use_coroutine_));
|
||||
// Ski FIFO and universal compaction because they do not apply to the test
|
||||
// case. Skip MergePut because single delete does not get removed when it
|
||||
// encounters a merge.
|
||||
@@ -1398,40 +1562,40 @@ TEST_F(DBBasicTest, PutSingleDeleteGet) {
|
||||
kSkipMergePut));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, TimedPutBasic) {
|
||||
TEST_P(DBBasicGetWithParam, TimedPutBasic) {
|
||||
do {
|
||||
Options options = CurrentOptions();
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
ASSERT_OK(TimedPut(1, "foo", "v1", /*write_unix_time=*/0));
|
||||
// Read from memtable
|
||||
ASSERT_EQ("v1", Get(1, "foo"));
|
||||
ASSERT_EQ("v1", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_OK(TimedPut(1, "foo", "v2.1", /*write_unix_time=*/3));
|
||||
ASSERT_EQ("v2.1", Get(1, "foo"));
|
||||
ASSERT_EQ("v2.1", Get(1, "foo", nullptr, use_coroutine_));
|
||||
|
||||
// Read from sst file
|
||||
ASSERT_OK(db_->Flush(FlushOptions(), handles_[1]));
|
||||
ASSERT_OK(Merge(1, "foo", "v2.2"));
|
||||
ASSERT_EQ("v2.1,v2.2", Get(1, "foo"));
|
||||
ASSERT_EQ("v2.1,v2.2", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_OK(Delete(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "foo", nullptr, use_coroutine_));
|
||||
|
||||
ASSERT_OK(TimedPut(1, "bar", "bv1", /*write_unix_time=*/0));
|
||||
ASSERT_EQ("bv1", Get(1, "bar"));
|
||||
ASSERT_EQ("bv1", Get(1, "bar", nullptr, use_coroutine_));
|
||||
ASSERT_OK(TimedPut(1, "baz", "bzv1", /*write_unix_time=*/0));
|
||||
ASSERT_EQ("bzv1", Get(1, "baz"));
|
||||
ASSERT_EQ("bzv1", Get(1, "baz", nullptr, use_coroutine_));
|
||||
if (option_config_ != kRowCache) {
|
||||
std::string range_del_begin = "b";
|
||||
std::string range_del_end = "baz";
|
||||
Slice begin_rdel = range_del_begin, end_rdel = range_del_end;
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), handles_[1], begin_rdel, end_rdel));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "bar"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "bar", nullptr, use_coroutine_));
|
||||
}
|
||||
|
||||
ASSERT_EQ("bzv1", Get(1, "baz"));
|
||||
ASSERT_EQ("bzv1", Get(1, "baz", nullptr, use_coroutine_));
|
||||
ASSERT_OK(SingleDelete(1, "baz"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "baz"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "baz", nullptr, use_coroutine_));
|
||||
} while (ChangeOptions(kSkipPlainTable));
|
||||
}
|
||||
|
||||
@@ -1457,17 +1621,17 @@ TEST_F(DBBasicTest, EmptyFlush) {
|
||||
kSkipMergePut));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, GetFromVersions) {
|
||||
TEST_P(DBBasicGetWithParam, GetFromVersions) {
|
||||
do {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
ASSERT_OK(Put(1, "foo", "v1"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_EQ("v1", Get(1, "foo"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(0, "foo"));
|
||||
ASSERT_EQ("v1", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("NOT_FOUND", Get(0, "foo", nullptr, use_coroutine_));
|
||||
} while (ChangeOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, GetSnapshot) {
|
||||
TEST_P(DBBasicGetWithParam, GetSnapshot) {
|
||||
anon::OptionsOverride options_override;
|
||||
options_override.skip_policy = kSkipNoSnapshot;
|
||||
do {
|
||||
@@ -1478,11 +1642,11 @@ TEST_F(DBBasicTest, GetSnapshot) {
|
||||
ASSERT_OK(Put(1, key, "v1"));
|
||||
const Snapshot* s1 = db_->GetSnapshot();
|
||||
ASSERT_OK(Put(1, key, "v2"));
|
||||
ASSERT_EQ("v2", Get(1, key));
|
||||
ASSERT_EQ("v1", Get(1, key, s1));
|
||||
ASSERT_EQ("v2", Get(1, key, nullptr, use_coroutine_));
|
||||
ASSERT_EQ("v1", Get(1, key, s1, use_coroutine_));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_EQ("v2", Get(1, key));
|
||||
ASSERT_EQ("v1", Get(1, key, s1));
|
||||
ASSERT_EQ("v2", Get(1, key, nullptr, use_coroutine_));
|
||||
ASSERT_EQ("v1", Get(1, key, s1, use_coroutine_));
|
||||
db_->ReleaseSnapshot(s1);
|
||||
}
|
||||
} while (ChangeOptions());
|
||||
@@ -1739,7 +1903,7 @@ TEST_F(DBBasicTest, LockFileRecovery) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, Snapshot) {
|
||||
TEST_P(DBBasicGetWithParam, Snapshot) {
|
||||
env_->SetMockSleep();
|
||||
anon::OptionsOverride options_override;
|
||||
options_override.skip_policy = kSkipNoSnapshot;
|
||||
@@ -1773,31 +1937,31 @@ TEST_F(DBBasicTest, Snapshot) {
|
||||
|
||||
ASSERT_OK(Put(0, "foo", "0v4"));
|
||||
ASSERT_OK(Put(1, "foo", "1v4"));
|
||||
ASSERT_EQ("0v1", Get(0, "foo", s1));
|
||||
ASSERT_EQ("1v1", Get(1, "foo", s1));
|
||||
ASSERT_EQ("0v2", Get(0, "foo", s2));
|
||||
ASSERT_EQ("1v2", Get(1, "foo", s2));
|
||||
ASSERT_EQ("0v3", Get(0, "foo", s3.snapshot()));
|
||||
ASSERT_EQ("1v3", Get(1, "foo", s3.snapshot()));
|
||||
ASSERT_EQ("0v4", Get(0, "foo"));
|
||||
ASSERT_EQ("1v4", Get(1, "foo"));
|
||||
ASSERT_EQ("0v1", Get(0, "foo", s1, use_coroutine_));
|
||||
ASSERT_EQ("1v1", Get(1, "foo", s1, use_coroutine_));
|
||||
ASSERT_EQ("0v2", Get(0, "foo", s2, use_coroutine_));
|
||||
ASSERT_EQ("1v2", Get(1, "foo", s2, use_coroutine_));
|
||||
ASSERT_EQ("0v3", Get(0, "foo", s3.snapshot(), use_coroutine_));
|
||||
ASSERT_EQ("1v3", Get(1, "foo", s3.snapshot(), use_coroutine_));
|
||||
ASSERT_EQ("0v4", Get(0, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("1v4", Get(1, "foo", nullptr, use_coroutine_));
|
||||
}
|
||||
|
||||
ASSERT_EQ(2U, GetNumSnapshots());
|
||||
ASSERT_EQ(time_snap1, GetTimeOldestSnapshots());
|
||||
ASSERT_EQ(GetSequenceOldestSnapshots(), s1->GetSequenceNumber());
|
||||
ASSERT_EQ("0v1", Get(0, "foo", s1));
|
||||
ASSERT_EQ("1v1", Get(1, "foo", s1));
|
||||
ASSERT_EQ("0v2", Get(0, "foo", s2));
|
||||
ASSERT_EQ("1v2", Get(1, "foo", s2));
|
||||
ASSERT_EQ("0v4", Get(0, "foo"));
|
||||
ASSERT_EQ("1v4", Get(1, "foo"));
|
||||
ASSERT_EQ("0v1", Get(0, "foo", s1, use_coroutine_));
|
||||
ASSERT_EQ("1v1", Get(1, "foo", s1, use_coroutine_));
|
||||
ASSERT_EQ("0v2", Get(0, "foo", s2, use_coroutine_));
|
||||
ASSERT_EQ("1v2", Get(1, "foo", s2, use_coroutine_));
|
||||
ASSERT_EQ("0v4", Get(0, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("1v4", Get(1, "foo", nullptr, use_coroutine_));
|
||||
|
||||
db_->ReleaseSnapshot(s1);
|
||||
ASSERT_EQ("0v2", Get(0, "foo", s2));
|
||||
ASSERT_EQ("1v2", Get(1, "foo", s2));
|
||||
ASSERT_EQ("0v4", Get(0, "foo"));
|
||||
ASSERT_EQ("1v4", Get(1, "foo"));
|
||||
ASSERT_EQ("0v2", Get(0, "foo", s2, use_coroutine_));
|
||||
ASSERT_EQ("1v2", Get(1, "foo", s2, use_coroutine_));
|
||||
ASSERT_EQ("0v4", Get(0, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("1v4", Get(1, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ(1U, GetNumSnapshots());
|
||||
ASSERT_LT(time_snap1, GetTimeOldestSnapshots());
|
||||
ASSERT_EQ(GetSequenceOldestSnapshots(), s2->GetSequenceNumber());
|
||||
@@ -1805,11 +1969,19 @@ TEST_F(DBBasicTest, Snapshot) {
|
||||
db_->ReleaseSnapshot(s2);
|
||||
ASSERT_EQ(0U, GetNumSnapshots());
|
||||
ASSERT_EQ(GetSequenceOldestSnapshots(), 0);
|
||||
ASSERT_EQ("0v4", Get(0, "foo"));
|
||||
ASSERT_EQ("1v4", Get(1, "foo"));
|
||||
ASSERT_EQ("0v4", Get(0, "foo", nullptr, use_coroutine_));
|
||||
ASSERT_EQ("1v4", Get(1, "foo", nullptr, use_coroutine_));
|
||||
} while (ChangeOptions());
|
||||
}
|
||||
|
||||
#if USE_COROUTINES
|
||||
INSTANTIATE_TEST_CASE_P(DBBasicGetWithParam, DBBasicGetWithParam,
|
||||
testing::Bool());
|
||||
#else
|
||||
INSTANTIATE_TEST_CASE_P(DBBasicGetWithParam, DBBasicGetWithParam,
|
||||
testing::Values(false));
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
class DBBasicMultiConfigs : public DBBasicTest,
|
||||
public ::testing::WithParamInterface<int> {
|
||||
public:
|
||||
@@ -2111,6 +2283,9 @@ INSTANTIATE_TEST_CASE_P(FormatVersions, DBBlockChecksumTest,
|
||||
TEST_P(DBBlockChecksumTest, BlockChecksumTest) {
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.format_version = GetParam();
|
||||
// kFooterFormatVersionsToTest includes the unpublished draft format_version
|
||||
// 8; writing it requires this opt-in.
|
||||
SaveAndRestore<bool> allow_draft(&TEST_AllowUnsupportedFormatVersion(), true);
|
||||
Options options = CurrentOptions();
|
||||
const int kNumPerFile = 2;
|
||||
|
||||
@@ -3758,11 +3933,15 @@ INSTANTIATE_TEST_CASE_P(DBMultiGetTestWithParam, DBMultiGetTestWithParam,
|
||||
testing::Combine(testing::Bool(), testing::Bool()));
|
||||
|
||||
#if USE_COROUTINES
|
||||
class DBMultiGetAsyncIOTest : public DBBasicTest,
|
||||
public ::testing::WithParamInterface<bool> {
|
||||
class DBMultiGetAsyncIOTest
|
||||
: public DBBasicTest,
|
||||
public ::testing::WithParamInterface<std::tuple<bool, bool>> {
|
||||
public:
|
||||
DBMultiGetAsyncIOTest()
|
||||
: DBBasicTest(), statistics_(ROCKSDB_NAMESPACE::CreateDBStatistics()) {
|
||||
: DBBasicTest(),
|
||||
optimize_multiget_for_io_(std::get<0>(GetParam())),
|
||||
use_coroutine_(std::get<1>(GetParam())),
|
||||
statistics_(ROCKSDB_NAMESPACE::CreateDBStatistics()) {
|
||||
BlockBasedTableOptions bbto;
|
||||
bbto.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
options_ = CurrentOptions();
|
||||
@@ -3836,6 +4015,20 @@ class DBMultiGetAsyncIOTest : public DBBasicTest,
|
||||
const std::shared_ptr<Statistics>& statistics() { return statistics_; }
|
||||
|
||||
protected:
|
||||
void AssertMultiGetIOBatchSize(uint64_t expected_count,
|
||||
uint64_t expected_max) {
|
||||
HistogramData multiget_io_batch_size;
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE,
|
||||
&multiget_io_batch_size);
|
||||
ASSERT_EQ(multiget_io_batch_size.count, expected_count);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, expected_max);
|
||||
}
|
||||
|
||||
bool UseCoroutineRead() const {
|
||||
return use_coroutine_ &&
|
||||
options_.env->GetFileSystem()->GetReadExecutor() != nullptr;
|
||||
}
|
||||
|
||||
void PrepareDBForTest() {
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
Reopen(options_);
|
||||
@@ -3851,6 +4044,9 @@ class DBMultiGetAsyncIOTest : public DBBasicTest,
|
||||
|
||||
void ReopenDB() { Reopen(options_); }
|
||||
|
||||
const bool optimize_multiget_for_io_;
|
||||
const bool use_coroutine_;
|
||||
|
||||
private:
|
||||
std::shared_ptr<Statistics> statistics_;
|
||||
Options options_;
|
||||
@@ -3859,104 +4055,89 @@ class DBMultiGetAsyncIOTest : public DBBasicTest,
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL0) {
|
||||
// All 3 keys in L0. The L0 files should be read serially.
|
||||
std::vector<std::string> key_strs{Key(0), Key(40), Key(80)};
|
||||
std::vector<Slice> keys{key_strs[0], key_strs[1], key_strs[2]};
|
||||
std::vector<PinnableSlice> values(key_strs.size());
|
||||
std::vector<Status> statuses(key_strs.size());
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ASSERT_EQ(values.size(), 3);
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_OK(statuses[2]);
|
||||
ASSERT_EQ(values[0], "val_l0_" + std::to_string(0));
|
||||
ASSERT_EQ(values[1], "val_l0_" + std::to_string(40));
|
||||
ASSERT_EQ(values[2], "val_l0_" + std::to_string(80));
|
||||
|
||||
HistogramData multiget_io_batch_size;
|
||||
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
|
||||
|
||||
// With async IO, lookups will happen in parallel for each key
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
if (GetParam()) {
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 1);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 3);
|
||||
if (UseCoroutineRead()) {
|
||||
AssertMultiGetIOBatchSize(3, 1);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
|
||||
} else if (optimize_multiget_for_io_) {
|
||||
AssertMultiGetIOBatchSize(1, 3);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
|
||||
} else {
|
||||
// Without Async IO, MultiGet will call MultiRead 3 times, once for each
|
||||
// L0 file
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 3);
|
||||
AssertMultiGetIOBatchSize(3, 1);
|
||||
}
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 3 : 0);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL1) {
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
key_strs.push_back(Key(33));
|
||||
key_strs.push_back(Key(54));
|
||||
key_strs.push_back(Key(102));
|
||||
keys.emplace_back(key_strs[0]);
|
||||
keys.emplace_back(key_strs[1]);
|
||||
keys.emplace_back(key_strs[2]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
std::vector<std::string> key_strs{Key(33), Key(54), Key(102)};
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ASSERT_EQ(values.size(), 3);
|
||||
ASSERT_EQ(statuses[0], Status::OK());
|
||||
ASSERT_EQ(statuses[1], Status::OK());
|
||||
ASSERT_EQ(statuses[2], Status::OK());
|
||||
ASSERT_EQ(values[0], "val_l1_" + std::to_string(33));
|
||||
ASSERT_EQ(values[1], "val_l1_" + std::to_string(54));
|
||||
ASSERT_EQ(values[2], "val_l1_" + std::to_string(102));
|
||||
|
||||
HistogramData multiget_io_batch_size;
|
||||
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
// A batch of 3 async IOs is expected, one for each overlapping file in L1
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 1);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 3);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
|
||||
if (UseCoroutineRead()) {
|
||||
AssertMultiGetIOBatchSize(3, 1);
|
||||
} else {
|
||||
// A batch of 3 async IOs is expected, one for each overlapping file in L1.
|
||||
AssertMultiGetIOBatchSize(1, 3);
|
||||
}
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 6 : 3);
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 6 : 0);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, SingleGetFromL1UsesCoroutineRead) {
|
||||
int coroutine_read_count = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"RandomAccessFileReader::ReadCoroutine:SubmitReadAsync",
|
||||
[&](void*) { ++coroutine_read_count; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ASSERT_EQ(
|
||||
"val_l1_" + std::to_string(33),
|
||||
Get(Key(33), static_cast<const Snapshot*>(nullptr), use_coroutine_));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
if (UseCoroutineRead()) {
|
||||
ASSERT_GT(coroutine_read_count, 0);
|
||||
} else {
|
||||
ASSERT_EQ(coroutine_read_count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL1Error) {
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
key_strs.push_back(Key(33));
|
||||
key_strs.push_back(Key(54));
|
||||
key_strs.push_back(Key(102));
|
||||
keys.emplace_back(key_strs[0]);
|
||||
keys.emplace_back(key_strs[1]);
|
||||
keys.emplace_back(key_strs[2]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
std::vector<std::string> key_strs{Key(33), Key(54), Key(102)};
|
||||
|
||||
int count = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
@@ -3985,210 +4166,137 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1Error) {
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
ASSERT_EQ(values.size(), 3);
|
||||
ASSERT_EQ(statuses[0], Status::OK());
|
||||
ASSERT_EQ(statuses[1], Status::OK());
|
||||
ASSERT_EQ(statuses[2], Status::IOError());
|
||||
ASSERT_EQ(values[0], "val_l1_" + std::to_string(33));
|
||||
ASSERT_EQ(values[1], "val_l1_" + std::to_string(54));
|
||||
ASSERT_EQ(values[2], Status::IOError().ToString());
|
||||
|
||||
HistogramData multiget_io_batch_size;
|
||||
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
|
||||
|
||||
// A batch of 3 async IOs is expected, one for each overlapping file in L1
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 1);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 2);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 2);
|
||||
if (UseCoroutineRead()) {
|
||||
AssertMultiGetIOBatchSize(2, 1);
|
||||
} else {
|
||||
// A batch of 3 async IOs is expected, one for each overlapping file in L1.
|
||||
AssertMultiGetIOBatchSize(1, 2);
|
||||
}
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 5 : 2);
|
||||
}
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, LastKeyInFile) {
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
// 21 is the last key in the first L1 file
|
||||
key_strs.push_back(Key(21));
|
||||
key_strs.push_back(Key(54));
|
||||
key_strs.push_back(Key(102));
|
||||
keys.emplace_back(key_strs[0]);
|
||||
keys.emplace_back(key_strs[1]);
|
||||
keys.emplace_back(key_strs[2]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
std::vector<std::string> key_strs{Key(21), Key(54), Key(102)};
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ASSERT_EQ(values.size(), 3);
|
||||
ASSERT_EQ(statuses[0], Status::OK());
|
||||
ASSERT_EQ(statuses[1], Status::OK());
|
||||
ASSERT_EQ(statuses[2], Status::OK());
|
||||
ASSERT_EQ(values[0], "val_l1_" + std::to_string(21));
|
||||
ASSERT_EQ(values[1], "val_l1_" + std::to_string(54));
|
||||
ASSERT_EQ(values[2], "val_l1_" + std::to_string(102));
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
HistogramData multiget_io_batch_size;
|
||||
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
|
||||
|
||||
// Since the first MultiGet key is the last key in a file, the MultiGet is
|
||||
// expected to lookup in that file first, before moving on to other files.
|
||||
// So the first file lookup will issue one async read, and the next lookup
|
||||
// will lookup 2 files in parallel and issue 2 async reads
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 2);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 2);
|
||||
if (UseCoroutineRead()) {
|
||||
AssertMultiGetIOBatchSize(3, 1);
|
||||
} else {
|
||||
AssertMultiGetIOBatchSize(2, 2);
|
||||
}
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2) {
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
// 33 and 102 are in L1, and 56 is in L2
|
||||
key_strs.push_back(Key(33));
|
||||
key_strs.push_back(Key(56));
|
||||
key_strs.push_back(Key(102));
|
||||
keys.emplace_back(key_strs[0]);
|
||||
keys.emplace_back(key_strs[1]);
|
||||
keys.emplace_back(key_strs[2]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
std::vector<std::string> key_strs{Key(33), Key(56), Key(102)};
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ASSERT_EQ(values.size(), 3);
|
||||
ASSERT_EQ(statuses[0], Status::OK());
|
||||
ASSERT_EQ(statuses[1], Status::OK());
|
||||
ASSERT_EQ(statuses[2], Status::OK());
|
||||
ASSERT_EQ(values[0], "val_l1_" + std::to_string(33));
|
||||
ASSERT_EQ(values[1], "val_l2_" + std::to_string(56));
|
||||
ASSERT_EQ(values[2], "val_l1_" + std::to_string(102));
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
HistogramData multiget_io_batch_size;
|
||||
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
|
||||
|
||||
// There are 2 keys in L1 in twp separate files, and 1 in L2. With
|
||||
// optimize_multiget_for_io, all three lookups will happen in parallel.
|
||||
// Otherwise, the L2 lookup will happen after L1.
|
||||
ASSERT_EQ(multiget_io_batch_size.count, GetParam() ? 1 : 2);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, GetParam() ? 3 : 2);
|
||||
if (UseCoroutineRead()) {
|
||||
AssertMultiGetIOBatchSize(3, 1);
|
||||
} else {
|
||||
AssertMultiGetIOBatchSize(optimize_multiget_for_io_ ? 1 : 2,
|
||||
optimize_multiget_for_io_ ? 3 : 2);
|
||||
}
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL2WithRangeOverlapL0L1) {
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
// 19 and 26 are in L2, but overlap with L0 and L1 file ranges
|
||||
key_strs.push_back(Key(19));
|
||||
key_strs.push_back(Key(26));
|
||||
keys.emplace_back(key_strs[0]);
|
||||
keys.emplace_back(key_strs[1]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
std::vector<std::string> key_strs{Key(19), Key(26)};
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ASSERT_EQ(values.size(), 2);
|
||||
ASSERT_EQ(statuses[0], Status::OK());
|
||||
ASSERT_EQ(statuses[1], Status::OK());
|
||||
ASSERT_EQ(values[0], "val_l2_" + std::to_string(19));
|
||||
ASSERT_EQ(values[1], "val_l2_" + std::to_string(26));
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
// Bloom filters in L0/L1 will avoid the coroutine calls in those levels
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 2);
|
||||
// Bloom filters in L0/L1 avoid the coroutine calls in those levels. The
|
||||
// coroutine path counts every per-file MultiGetFromSST coroutine (including
|
||||
// serial reads), whereas the async_io path counts only the parallel overlap
|
||||
// batch, so the coroutine path observes one more.
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 3 : 2);
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 3 : 0);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL2WithRangeDelInL1) {
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
// 139 and 163 are in L2, but overlap with a range deletes in L1
|
||||
key_strs.push_back(Key(139));
|
||||
key_strs.push_back(Key(163));
|
||||
keys.emplace_back(key_strs[0]);
|
||||
keys.emplace_back(key_strs[1]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
std::vector<std::string> key_strs{Key(139), Key(163)};
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ASSERT_EQ(values.size(), 2);
|
||||
ASSERT_EQ(statuses[0], Status::NotFound());
|
||||
ASSERT_EQ(statuses[1], Status::NotFound());
|
||||
ASSERT_EQ(values[0], "NOT_FOUND");
|
||||
ASSERT_EQ(values[1], "NOT_FOUND");
|
||||
|
||||
// Bloom filters in L0/L1 will avoid the coroutine calls in those levels
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 2);
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2WithRangeDelInL1) {
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
// 139 and 163 are in L2, but overlap with a range deletes in L1
|
||||
key_strs.push_back(Key(139));
|
||||
key_strs.push_back(Key(144));
|
||||
key_strs.push_back(Key(163));
|
||||
keys.emplace_back(key_strs[0]);
|
||||
keys.emplace_back(key_strs[1]);
|
||||
keys.emplace_back(key_strs[2]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
std::vector<std::string> key_strs{Key(139), Key(144), Key(163)};
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
ASSERT_EQ(values.size(), keys.size());
|
||||
ASSERT_EQ(statuses[0], Status::NotFound());
|
||||
ASSERT_EQ(statuses[1], Status::OK());
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ASSERT_EQ(values.size(), 3);
|
||||
ASSERT_EQ(values[0], "NOT_FOUND");
|
||||
ASSERT_EQ(values[1], "val_l1_" + std::to_string(144));
|
||||
ASSERT_EQ(statuses[2], Status::NotFound());
|
||||
ASSERT_EQ(values[2], "NOT_FOUND");
|
||||
|
||||
// Bloom filters in L0/L1 will avoid the coroutine calls in those levels
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
|
||||
@@ -4196,32 +4304,18 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2WithRangeDelInL1) {
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetNoIOUring) {
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
key_strs.push_back(Key(33));
|
||||
key_strs.push_back(Key(54));
|
||||
key_strs.push_back(Key(102));
|
||||
keys.emplace_back(key_strs[0]);
|
||||
keys.emplace_back(key_strs[1]);
|
||||
keys.emplace_back(key_strs[2]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
std::vector<std::string> key_strs{Key(33), Key(54), Key(102)};
|
||||
|
||||
enable_io_uring = false;
|
||||
ReopenDB();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
std::vector<std::string> values =
|
||||
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
|
||||
optimize_multiget_for_io_, use_coroutine_);
|
||||
ASSERT_EQ(values.size(), 3);
|
||||
ASSERT_EQ(statuses[0], Status::OK());
|
||||
ASSERT_EQ(statuses[1], Status::OK());
|
||||
ASSERT_EQ(statuses[2], Status::OK());
|
||||
ASSERT_EQ(values[0], "val_l1_" + std::to_string(33));
|
||||
ASSERT_EQ(values[1], "val_l1_" + std::to_string(54));
|
||||
ASSERT_EQ(values[2], "val_l1_" + std::to_string(102));
|
||||
|
||||
HistogramData async_read_bytes;
|
||||
|
||||
@@ -4229,11 +4323,17 @@ TEST_P(DBMultiGetAsyncIOTest, GetNoIOUring) {
|
||||
|
||||
// A batch of 3 async IOs is expected, one for each overlapping file in L1
|
||||
ASSERT_EQ(async_read_bytes.count, 0);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
const uint64_t coroutine_count =
|
||||
statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT);
|
||||
if (UseCoroutineRead()) {
|
||||
ASSERT_GT(coroutine_count, 0);
|
||||
} else {
|
||||
ASSERT_EQ(coroutine_count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(DBMultiGetAsyncIOTest, DBMultiGetAsyncIOTest,
|
||||
testing::Bool());
|
||||
testing::Combine(testing::Bool(), testing::Bool()));
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetStats) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+325
-2
@@ -14,6 +14,7 @@
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/dbformat.h"
|
||||
#include "db/periodic_compaction_phaser.h"
|
||||
#include "db/table_cache.h"
|
||||
#include "env/mock_env.h"
|
||||
#include "file/filename.h"
|
||||
@@ -29,6 +30,8 @@
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testutil.h"
|
||||
#include "util/concurrent_task_limiter_impl.h"
|
||||
#include "util/fastrange.h"
|
||||
#include "util/hash.h"
|
||||
#include "util/random.h"
|
||||
#include "utilities/fault_injection_env.h"
|
||||
#include "utilities/fault_injection_fs.h"
|
||||
@@ -813,6 +816,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 +858,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
|
||||
@@ -1316,6 +1337,118 @@ TEST_F(DBCompactionTest, CompactionSstPartitionerNonTrivial) {
|
||||
ASSERT_EQ("B", Get("bbbb1"));
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, CompactionSstPartitionerPrefixFastPath) {
|
||||
// Exercises the ShouldPartitionByPrefix() fast path in CompactionOutputs:
|
||||
// multiple keys sharing a fixed prefix must stay together in one output file
|
||||
// (no spurious cuts), with boundaries landing exactly at prefix changes.
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.level0_file_num_compaction_trigger = 3;
|
||||
options.sst_partitioner_factory = NewSstPartitionerFixedPrefixFactory(4);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Three prefixes with multiple keys each, spread across two flushed files so
|
||||
// that a non-trivial compaction merges them.
|
||||
ASSERT_OK(Put("aaaa1", "v"));
|
||||
ASSERT_OK(Put("aaaa3", "v"));
|
||||
ASSERT_OK(Put("bbbb1", "v"));
|
||||
ASSERT_OK(Put("cccc2", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
ASSERT_OK(Put("aaaa2", "v"));
|
||||
ASSERT_OK(Put("bbbb2", "v"));
|
||||
ASSERT_OK(Put("cccc1", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
// Exactly one output file per distinct 4-byte prefix.
|
||||
std::vector<LiveFileMetaData> files;
|
||||
dbfull()->GetLiveFilesMetaData(&files);
|
||||
ASSERT_EQ(3, files.size());
|
||||
// Each file holds a whole prefix group (multiple keys => the fast path
|
||||
// correctly did NOT cut within a shared prefix).
|
||||
for (const auto& f : files) {
|
||||
ASSERT_EQ(f.smallestkey.substr(0, 4), f.largestkey.substr(0, 4));
|
||||
}
|
||||
for (const std::string k :
|
||||
{"aaaa1", "aaaa2", "aaaa3", "bbbb1", "bbbb2", "cccc1", "cccc2"}) {
|
||||
ASSERT_EQ("v", Get(k));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, CompactionSstPartitionerPrefixShortKeyFallback) {
|
||||
// Keys shorter than the fixed prefix length cannot be represented as a
|
||||
// prefix, so ShouldPartitionByPrefix() returns no value and CompactionOutputs
|
||||
// falls back to per-key ShouldPartition(). This also exercises short->long
|
||||
// and long->short mode transitions across output file boundaries.
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.level0_file_num_compaction_trigger = 3;
|
||||
options.sst_partitioner_factory = NewSstPartitionerFixedPrefixFactory(4);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// "a" and "bb" are shorter than len 4 (fallback, each in its own file);
|
||||
// "cccc1"/"cccc2" share prefix "cccc" (fast path, one file); "dd" is short
|
||||
// again (fallback, own file).
|
||||
ASSERT_OK(Put("a", "v"));
|
||||
ASSERT_OK(Put("bb", "v"));
|
||||
ASSERT_OK(Put("cccc1", "v"));
|
||||
ASSERT_OK(Put("dd", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
// Second file overlaps to force a non-trivial (rewriting) compaction.
|
||||
ASSERT_OK(Put("cccc2", "v"));
|
||||
ASSERT_OK(Put("a", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
// "a" | "bb" | {"cccc1","cccc2"} | "dd" => 4 files.
|
||||
std::vector<LiveFileMetaData> files;
|
||||
dbfull()->GetLiveFilesMetaData(&files);
|
||||
ASSERT_EQ(4, files.size());
|
||||
ASSERT_EQ("v2", Get("a"));
|
||||
for (const std::string k : {"bb", "cccc1", "cccc2", "dd"}) {
|
||||
ASSERT_EQ("v", Get(k));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, CompactionSstPartitionerZeroLenNoSplit) {
|
||||
// A zero-length fixed prefix yields an empty (but present) prefix from
|
||||
// ShouldPartitionByPrefix(), which means "no further partitions": every key
|
||||
// "starts with" the empty prefix, so the fast path never cuts.
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.level0_file_num_compaction_trigger = 3;
|
||||
options.sst_partitioner_factory = NewSstPartitionerFixedPrefixFactory(0);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("aaaa1", "v1"));
|
||||
ASSERT_OK(Put("bbbb1", "v1"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
// Overlapping key forces a non-trivial compaction (not a trivial move).
|
||||
ASSERT_OK(Put("aaaa1", "v2"));
|
||||
ASSERT_OK(Put("cccc1", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
std::vector<LiveFileMetaData> files;
|
||||
dbfull()->GetLiveFilesMetaData(&files);
|
||||
ASSERT_EQ(1, files.size());
|
||||
ASSERT_EQ("v2", Get("aaaa1"));
|
||||
ASSERT_EQ("v1", Get("bbbb1"));
|
||||
ASSERT_EQ("v2", Get("cccc1"));
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, ZeroSeqIdCompaction) {
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
@@ -5353,6 +5486,7 @@ TEST_F(DBCompactionTest, LevelPeriodicCompaction) {
|
||||
for (bool if_open_all_files : {false, true}) {
|
||||
Options options = CurrentOptions();
|
||||
options.periodic_compaction_seconds = 48 * 60 * 60; // 2 days
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
if (if_open_all_files) {
|
||||
options.max_open_files = -1; // needed for ttl compaction
|
||||
} else {
|
||||
@@ -5448,6 +5582,169 @@ TEST_F(DBCompactionTest, LevelPeriodicCompaction) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, PeriodicCompactionPhaseTriggerTime) {
|
||||
// Unit-tests PeriodicCompactionPhaser::TriggerTime, the core of
|
||||
// preferred-phase periodic compaction (see
|
||||
// DBOptions::periodic_compaction_phase_recovery_percent).
|
||||
using Params = PeriodicCompactionPhaseParams;
|
||||
const uint64_t kN = 1000; // periodic_compaction_seconds
|
||||
|
||||
// The trigger-time helper treats seed_hash as an opaque value; use a fixed
|
||||
// constant and derive the resulting preferred-phase target the same way
|
||||
// production does (FastRange64 of the seed over the interval).
|
||||
const uint64_t kSeed = 0x9e3779b97f4a7c15ULL;
|
||||
const uint64_t target = FastRange64(kSeed, kN);
|
||||
ASSERT_LT(target, kN);
|
||||
|
||||
// recovery_percent == 0 => classic behavior: trigger at file_mod + N.
|
||||
{
|
||||
Params p;
|
||||
p.seed_hash = kSeed;
|
||||
p.recovery_percent = 0;
|
||||
for (uint64_t fm : {uint64_t{1}, uint64_t{7}, uint64_t{12345}}) {
|
||||
ASSERT_EQ(PeriodicCompactionPhaser::TriggerTime(fm, kN, p), fm + kN);
|
||||
}
|
||||
}
|
||||
|
||||
// Steady state (anchor in the distant past): trigger is pulled earlier by
|
||||
// recovery_percent% of the phase offset, and never later than the deadline.
|
||||
{
|
||||
Params p;
|
||||
p.seed_hash = kSeed;
|
||||
p.recovery_percent = 50;
|
||||
p.anchor_time = 0;
|
||||
const uint64_t fm = 500;
|
||||
const uint64_t natural = fm + kN;
|
||||
const uint64_t offset = (fm % kN + kN - target) % kN;
|
||||
const uint64_t expected = natural - offset * 50 / 100;
|
||||
ASSERT_EQ(PeriodicCompactionPhaser::TriggerTime(fm, kN, p), expected);
|
||||
ASSERT_LE(expected, natural);
|
||||
}
|
||||
|
||||
// Geometric convergence: repeatedly re-stamp the file at its trigger time and
|
||||
// confirm the phase error is non-increasing and converges to (near) zero.
|
||||
for (int recovery_percent : {50, 100}) {
|
||||
Params p;
|
||||
p.seed_hash = kSeed;
|
||||
p.recovery_percent = recovery_percent;
|
||||
p.anchor_time = 0;
|
||||
uint64_t fm = 123;
|
||||
uint64_t prev_error = kN;
|
||||
for (int cycle = 0; cycle < 64; ++cycle) {
|
||||
const uint64_t trig = PeriodicCompactionPhaser::TriggerTime(fm, kN, p);
|
||||
ASSERT_LE(trig, fm + kN); // never later than the deadline
|
||||
const uint64_t error = (trig % kN + kN - target) % kN;
|
||||
ASSERT_LE(error, prev_error);
|
||||
prev_error = error;
|
||||
fm = trig;
|
||||
}
|
||||
// rp=100 pulls the whole gap so it reaches the phase exactly; smaller rates
|
||||
// decay geometrically and leave a tiny integer-rounding residue.
|
||||
if (recovery_percent == 100) {
|
||||
ASSERT_EQ(prev_error, uint64_t{0});
|
||||
} else {
|
||||
ASSERT_LE(prev_error, uint64_t{1});
|
||||
}
|
||||
}
|
||||
|
||||
// Already past the hard deadline when phasing took effect (e.g. the DB was
|
||||
// down, or periodic_compaction_seconds was turned down): instead of firing
|
||||
// the whole cohort immediately, spread it over the first quarter-period after
|
||||
// the anchor on an absolute (epoch-aligned) grid of period N/4.
|
||||
{
|
||||
Params p;
|
||||
p.seed_hash = kSeed;
|
||||
p.recovery_percent = 50;
|
||||
p.anchor_time = 200;
|
||||
const uint64_t n = 100;
|
||||
const uint64_t grid = n / 4; // 25
|
||||
const uint64_t phase = FastRange64(kSeed, grid);
|
||||
// natural deadline (10 + 100 = 110) is before the anchor (200).
|
||||
const uint64_t expected = 200 + (phase + grid - 200 % grid) % grid;
|
||||
const uint64_t trig = PeriodicCompactionPhaser::TriggerTime(10, n, p);
|
||||
ASSERT_EQ(trig, expected);
|
||||
ASSERT_GE(trig, uint64_t{200}); // never before the anchor
|
||||
ASSERT_LT(trig, 200 + grid); // within N/4 of the anchor
|
||||
}
|
||||
|
||||
// Past due against the recovery target but not yet at the deadline => spread
|
||||
// across [anchor, deadline] using the phase; never later than the deadline.
|
||||
{
|
||||
const uint64_t n = 100;
|
||||
const uint64_t small_target = FastRange64(kSeed, n);
|
||||
const uint64_t fm = small_target + 50 + 3 * n; // phase offset == 50
|
||||
const uint64_t natural = fm + n;
|
||||
const uint64_t anchor =
|
||||
natural - 25; // recovery target (natural-50) < anchor
|
||||
Params p;
|
||||
p.seed_hash = kSeed;
|
||||
p.recovery_percent = 100;
|
||||
p.anchor_time = anchor;
|
||||
ASSERT_EQ((fm % n + n - small_target) % n, uint64_t{50});
|
||||
const uint64_t expected = anchor + FastRange64(kSeed, natural - anchor);
|
||||
const uint64_t trig = PeriodicCompactionPhaser::TriggerTime(fm, n, p);
|
||||
ASSERT_EQ(trig, expected);
|
||||
ASSERT_GE(trig, anchor);
|
||||
ASSERT_LT(trig, natural);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, PeriodicCompactionPhaseCfSpread) {
|
||||
// Unit-tests PeriodicCompactionPhaser::CfPhaseSeedHash: a DB's column
|
||||
// families are spread quasi-uniformly around the DB base phase via the
|
||||
// golden-ratio recurrence, for any base and any number of CFs.
|
||||
using Phaser = PeriodicCompactionPhaser;
|
||||
|
||||
// cf_id 0 leaves the base phase unchanged; distinct cf_ids -> distinct
|
||||
// phases.
|
||||
for (uint64_t base : {uint64_t{0}, uint64_t{0x9e3779b97f4a7c15ULL},
|
||||
uint64_t{12345678901234567ULL}, ~uint64_t{0}}) {
|
||||
ASSERT_EQ(Phaser::CfPhaseSeedHash(base, 0), base);
|
||||
std::vector<uint64_t> hs;
|
||||
for (uint32_t id = 0; id < 64; ++id) {
|
||||
hs.push_back(Phaser::CfPhaseSeedHash(base, id));
|
||||
}
|
||||
std::sort(hs.begin(), hs.end());
|
||||
for (size_t i = 1; i < hs.size(); ++i) {
|
||||
ASSERT_NE(hs[i], hs[i - 1]) << "duplicate CF phase seed at base=" << base;
|
||||
}
|
||||
}
|
||||
|
||||
// Low-discrepancy: for N CFs the phases over an interval are well spread --
|
||||
// no gap larger than ~2x the average (a hash would allow chance clustering).
|
||||
for (uint64_t base : {uint64_t{0}, uint64_t{0xdeadbeefcafef00dULL}}) {
|
||||
for (uint32_t N : {uint32_t{3}, uint32_t{8}, uint32_t{16}, uint32_t{40}}) {
|
||||
const uint64_t n = 100000;
|
||||
std::vector<uint64_t> phases;
|
||||
for (uint32_t id = 0; id < N; ++id) {
|
||||
phases.push_back(FastRange64(Phaser::CfPhaseSeedHash(base, id), n));
|
||||
}
|
||||
std::sort(phases.begin(), phases.end());
|
||||
uint64_t max_gap = n - phases.back() + phases.front(); // wrap-around gap
|
||||
for (size_t i = 1; i < phases.size(); ++i) {
|
||||
max_gap = std::max(max_gap, phases[i] - phases[i - 1]);
|
||||
}
|
||||
ASSERT_LE(max_gap, 2 * (n / N))
|
||||
<< "base=" << base << " N=" << N << " max_gap=" << max_gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, PeriodicCompactionPhaseOptions) {
|
||||
// Verifies periodic_compaction_phase_recovery_percent is plumbed and
|
||||
// dynamically settable.
|
||||
Options options = CurrentOptions();
|
||||
options.periodic_compaction_phase_recovery_percent = 25;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_EQ(dbfull()->GetDBOptions().periodic_compaction_phase_recovery_percent,
|
||||
25);
|
||||
|
||||
ASSERT_OK(dbfull()->SetDBOptions(
|
||||
{{"periodic_compaction_phase_recovery_percent", "0"}}));
|
||||
ASSERT_EQ(dbfull()->GetDBOptions().periodic_compaction_phase_recovery_percent,
|
||||
0);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, LevelPeriodicCompactionOffpeak) {
|
||||
// This test simply checks if offpeak adjustment works in Leveled
|
||||
// Compactions. For testing offpeak periodic compactions in various
|
||||
@@ -5465,6 +5762,7 @@ TEST_F(DBCompactionTest, LevelPeriodicCompactionOffpeak) {
|
||||
Options options = CurrentOptions();
|
||||
options.ttl = 0;
|
||||
options.periodic_compaction_seconds = 5 * kSecondsPerDay; // 5 days
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
// In the case where all files are opened and doing DB restart
|
||||
// forcing the file creation time in manifest file to be 0 to
|
||||
// simulate the case of reading from an old version.
|
||||
@@ -5612,6 +5910,7 @@ TEST_F(DBCompactionTest, LevelPeriodicCompactionWithOldDB) {
|
||||
// Forward the clock by 2 days.
|
||||
env_->MockSleepForSeconds(2 * 24 * 60 * 60);
|
||||
options.periodic_compaction_seconds = 1 * 24 * 60 * 60; // 1 day
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
|
||||
Reopen(options);
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
@@ -5630,6 +5929,7 @@ TEST_F(DBCompactionTest, LevelPeriodicAndTtlCompaction) {
|
||||
Options options = CurrentOptions();
|
||||
options.ttl = 10 * 60 * 60; // 10 hours
|
||||
options.periodic_compaction_seconds = 48 * 60 * 60; // 2 days
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
options.max_open_files = -1; // needed for both periodic and ttl compactions
|
||||
env_->SetMockSleep();
|
||||
options.env = env_;
|
||||
@@ -5712,6 +6012,7 @@ TEST_F(DBCompactionTest, LevelTtlBooster) {
|
||||
Options options = CurrentOptions();
|
||||
options.ttl = 10 * 60 * 60; // 10 hours
|
||||
options.periodic_compaction_seconds = 480 * 60 * 60; // very long
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
options.level0_file_num_compaction_trigger = 2;
|
||||
options.max_bytes_for_level_base = 5 * uint64_t{kNumKeysPerFile * kValueSize};
|
||||
options.max_open_files = -1; // needed for both periodic and ttl compactions
|
||||
@@ -6156,6 +6457,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 +6465,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 +6480,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 +6515,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();
|
||||
@@ -12240,6 +12562,7 @@ TEST_F(DBCompactionTest, PeriodicTask) {
|
||||
options.statistics = CreateDBStatistics();
|
||||
int kPeriodicCompactionSeconds = 7 * 24 * 60 * 60; // 1 week
|
||||
options.periodic_compaction_seconds = kPeriodicCompactionSeconds;
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
options.num_levels = 50;
|
||||
auto listener = std::make_shared<PeriodicCompactionListener>();
|
||||
options.listeners.push_back(listener);
|
||||
|
||||
+124
-1
@@ -28,6 +28,7 @@
|
||||
#include "rocksdb/wal_filter.h"
|
||||
#include "test_util/testutil.h"
|
||||
#include "util/defer.h"
|
||||
#include "util/mutexlock.h"
|
||||
#include "util/random.h"
|
||||
#include "utilities/fault_injection_env.h"
|
||||
|
||||
@@ -7902,7 +7903,10 @@ class FastOpenTestFS : public FileSystemWrapper {
|
||||
IODebugContext* dbg) override {
|
||||
if (file_opts.file_metadata != nullptr) {
|
||||
metadata_passed_on_open_.fetch_add(1, std::memory_order_relaxed);
|
||||
last_metadata_received_ = *file_opts.file_metadata;
|
||||
{
|
||||
MutexLock l(&last_metadata_mu_);
|
||||
last_metadata_received_ = *file_opts.file_metadata;
|
||||
}
|
||||
}
|
||||
IOStatus s = target()->NewRandomAccessFile(fname, file_opts, result, dbg);
|
||||
if (s.ok()) {
|
||||
@@ -7921,18 +7925,21 @@ class FastOpenTestFS : public FileSystemWrapper {
|
||||
}
|
||||
|
||||
std::string GetLastMetadataReceived() const {
|
||||
MutexLock l(&last_metadata_mu_);
|
||||
return last_metadata_received_;
|
||||
}
|
||||
|
||||
void ResetCounters() {
|
||||
metadata_retrieved_count_.store(0, std::memory_order_relaxed);
|
||||
metadata_passed_on_open_.store(0, std::memory_order_relaxed);
|
||||
MutexLock l(&last_metadata_mu_);
|
||||
last_metadata_received_.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<int> metadata_retrieved_count_{0};
|
||||
std::atomic<int> metadata_passed_on_open_{0};
|
||||
mutable port::Mutex last_metadata_mu_;
|
||||
std::string last_metadata_received_;
|
||||
};
|
||||
|
||||
@@ -8274,6 +8281,122 @@ TEST_F(DBTest2, FastSstOpenDisableAfterMetadataPersisted) {
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, FastSstOpenMetadataPreservedAcrossManifestRotation) {
|
||||
// Regression test: FileMetaData::file_open_metadata for unchanged live
|
||||
// SSTs must survive a MANIFEST rotation. WriteCurrentStateToManifest
|
||||
// snapshots live SSTs into a new MANIFEST; if it drops file_open_metadata
|
||||
// for those unchanged SSTs, the fast_sst_open optimization is silently
|
||||
// disabled for them until they are rewritten by flush/compaction/ingest.
|
||||
auto test_fs = std::make_shared<FastOpenTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> test_env(new CompositeEnvWrapper(env_, test_fs));
|
||||
|
||||
Options options;
|
||||
options.env = test_env.get();
|
||||
options.fast_sst_open = true;
|
||||
options.create_if_missing = true;
|
||||
// Keep the pre-rotation SST from being rewritten by compaction.
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
std::string test_dbname = test::PerThreadDBPath("fast_open_manifest_rotate");
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
|
||||
// Flush an SST; metadata is retrieved and persisted in the MANIFEST.
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key1", "value1"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
|
||||
|
||||
// Shrink max_manifest_file_size so the next MANIFEST write rotates and
|
||||
// exercises WriteCurrentStateToManifest.
|
||||
ASSERT_OK(db->SetDBOptions(
|
||||
{{"max_manifest_file_size", "1"}, {"max_manifest_space_amp_pct", "0"}}));
|
||||
|
||||
uint64_t manifest_before =
|
||||
static_cast<DBImpl*>(db.get())->TEST_Current_Manifest_FileNo();
|
||||
|
||||
// Flush a second SST to trigger the rotation. SST #1 is unchanged, so
|
||||
// the rotated MANIFEST snapshot must carry SST #1's file_open_metadata.
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key2", "value2"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
|
||||
uint64_t manifest_after =
|
||||
static_cast<DBImpl*>(db.get())->TEST_Current_Manifest_FileNo();
|
||||
ASSERT_GT(manifest_after, manifest_before);
|
||||
|
||||
db.reset();
|
||||
test_fs->ResetCounters();
|
||||
|
||||
// Reopen. Every live SST should be opened with file_open_metadata.
|
||||
// Pre-fix: only SST #2 (added by the rotating edit) has metadata; SST #1
|
||||
// was stripped by the rotation snapshot, so the count would be 1.
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
|
||||
std::string value;
|
||||
ASSERT_OK(db->Get(ReadOptions(), "key1", &value));
|
||||
ASSERT_EQ("value1", value);
|
||||
ASSERT_OK(db->Get(ReadOptions(), "key2", &value));
|
||||
ASSERT_EQ("value2", value);
|
||||
|
||||
ASSERT_EQ(2, test_fs->GetMetadataPassedOnOpenCount());
|
||||
|
||||
db.reset();
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, FastSstOpenMetadataPreservedAcrossTrivialMove) {
|
||||
// Regression test: FileMetaData::file_open_metadata must survive a trivial
|
||||
// move. Trivial move re-records an SST at a new level via DeleteFile +
|
||||
// AddFile without physically rewriting the file. If AddFile drops
|
||||
// file_open_metadata, the file loses its fast_sst_open advantage until
|
||||
// it is next rewritten by real compaction/flush/ingest.
|
||||
auto test_fs = std::make_shared<FastOpenTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> test_env(new CompositeEnvWrapper(env_, test_fs));
|
||||
|
||||
Options options;
|
||||
options.env = test_env.get();
|
||||
options.fast_sst_open = true;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
std::string test_dbname = test::PerThreadDBPath("fast_open_trivial_move");
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
|
||||
// Flush a single SST at L0; metadata is retrieved once and persisted.
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key1", "value1"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
|
||||
|
||||
// Manual compaction. With a single L0 file and empty lower levels, the
|
||||
// compaction picker converts this into a trivial move (no physical
|
||||
// rewrite), exercising PrepareTrivialMoveEdit.
|
||||
ASSERT_OK(db->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
// If the SST had been rewritten, metadata retrieval would have re-run on
|
||||
// the new file. Unchanged count confirms this was a trivial move.
|
||||
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
|
||||
|
||||
db.reset();
|
||||
test_fs->ResetCounters();
|
||||
|
||||
// Reopen. The trivially-moved SST should still carry its
|
||||
// file_open_metadata and be opened via the fast_sst_open fast path.
|
||||
// Pre-fix, the trivial move stripped it and this count would be 0.
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
|
||||
std::string value;
|
||||
ASSERT_OK(db->Get(ReadOptions(), "key1", &value));
|
||||
ASSERT_EQ("value1", value);
|
||||
|
||||
ASSERT_EQ(1, test_fs->GetMetadataPassedOnOpenCount());
|
||||
|
||||
db.reset();
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+79
-2
@@ -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,9 +402,27 @@ 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();
|
||||
uint64_t manifest_size = versions_->manifest_file_size();
|
||||
if (cf_subset && !excluded_cf_ids->empty()) {
|
||||
Status manifest_s = versions_->GetManifestAppendBoundary(&manifest_size);
|
||||
if (!manifest_s.ok()) {
|
||||
mutex_.Unlock();
|
||||
return manifest_s;
|
||||
}
|
||||
}
|
||||
const uint64_t options_number = versions_->options_file_number();
|
||||
const uint64_t options_size = versions_->options_file_size_;
|
||||
const uint64_t min_log_num = MinLogNumberToKeep();
|
||||
|
||||
+348
-5
@@ -8,7 +8,9 @@
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/db_test_util.h"
|
||||
@@ -18,9 +20,11 @@
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/utilities/transaction_db.h"
|
||||
#include "table/block_based/block_based_table_builder.h"
|
||||
#include "table/format.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testutil.h"
|
||||
#include "util/cast_util.h"
|
||||
#include "util/defer.h"
|
||||
#include "util/mutexlock.h"
|
||||
#include "utilities/fault_injection_env.h"
|
||||
#include "utilities/fault_injection_fs.h"
|
||||
@@ -2219,8 +2223,12 @@ TEST_F(DBFlushTest, FireOnFlushCompletedAfterCommittedResult) {
|
||||
listener->seq1 = db_->GetLatestSequenceNumber();
|
||||
// t1 will wait for the second flush complete before committing flush result.
|
||||
auto t1 = port::Thread([&]() {
|
||||
// flush_opts.wait = true
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
// flush_opts.wait = true, and listener_wait = true so that Flush() does not
|
||||
// return until the OnFlushCompleted callbacks (which set completed1 and
|
||||
// completed2) have finished running.
|
||||
FlushOptions flush_opts;
|
||||
flush_opts.listener_wait = true;
|
||||
ASSERT_OK(db_->Flush(flush_opts));
|
||||
});
|
||||
// Wait for first flush started.
|
||||
TEST_SYNC_POINT(
|
||||
@@ -2233,15 +2241,142 @@ TEST_F(DBFlushTest, FireOnFlushCompletedAfterCommittedResult) {
|
||||
flush_opts.wait = false;
|
||||
ASSERT_OK(db_->Flush(flush_opts));
|
||||
t1.join();
|
||||
// Ensure background work is fully finished including listener callbacks
|
||||
// before accessing listener state.
|
||||
ASSERT_OK(dbfull()->TEST_WaitForBackgroundWork());
|
||||
// listener_wait on t1's flush above guarantees both OnFlushCompleted
|
||||
// callbacks have finished by the time Flush() returned; no need to
|
||||
// additionally wait for background work here.
|
||||
ASSERT_TRUE(listener->completed1);
|
||||
ASSERT_TRUE(listener->completed2);
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
namespace {
|
||||
// EventListener whose OnFlushCompleted blocks until explicitly released, used
|
||||
// to verify FlushOptions::listener_wait. Optionally only blocks on the first
|
||||
// callback (useful for atomic flush, which fires one callback per CF).
|
||||
class BlockingFlushListener : public EventListener {
|
||||
public:
|
||||
explicit BlockingFlushListener(bool block_only_first)
|
||||
: block_only_first_(block_only_first) {}
|
||||
|
||||
void OnFlushCompleted(DB* /*db*/, const FlushJobInfo& /*info*/) override {
|
||||
if (block_only_first_ && num_completed_.fetch_add(1) != 0) {
|
||||
return;
|
||||
}
|
||||
std::unique_lock<std::mutex> lk(mu_);
|
||||
entered_ = true;
|
||||
cv_.notify_all();
|
||||
cv_.wait(lk, [this] { return release_; });
|
||||
returned_ = true;
|
||||
}
|
||||
|
||||
void WaitUntilEntered() {
|
||||
std::unique_lock<std::mutex> lk(mu_);
|
||||
cv_.wait(lk, [this] { return entered_; });
|
||||
}
|
||||
|
||||
void Release() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
release_ = true;
|
||||
}
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
bool CallbackReturned() {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
return returned_;
|
||||
}
|
||||
|
||||
private:
|
||||
const bool block_only_first_;
|
||||
std::atomic<int> num_completed_{0};
|
||||
std::mutex mu_;
|
||||
std::condition_variable cv_;
|
||||
bool entered_ = false;
|
||||
bool release_ = false;
|
||||
bool returned_ = false;
|
||||
};
|
||||
} // anonymous namespace
|
||||
|
||||
TEST_F(DBFlushTest, ListenerWaitWaitsForOnFlushCompleted) {
|
||||
// With FlushOptions::listener_wait, Flush(wait=true) must not return until
|
||||
// the OnFlushCompleted callback has finished, even if the flush result is
|
||||
// already committed and bg_cv_ is signalled by unrelated background work.
|
||||
auto listener = std::make_shared<BlockingFlushListener>(
|
||||
/*block_only_first=*/false);
|
||||
Options options = CurrentOptions();
|
||||
options.listeners.push_back(listener);
|
||||
Reopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
|
||||
std::atomic<bool> flush_returned{false};
|
||||
port::Thread flush_thread([&] {
|
||||
// Exercises the TEST_ wrapper, which flushes with wait=true and
|
||||
// listener_wait=true.
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTableWithListenerWait());
|
||||
flush_returned.store(true);
|
||||
});
|
||||
|
||||
// Wait until we are inside OnFlushCompleted. At this point the memtable has
|
||||
// already been committed/removed, which is what unblocks a plain
|
||||
// Flush(wait=true).
|
||||
listener->WaitUntilEntered();
|
||||
|
||||
// Pump spurious bg_cv_ wakeups. A waiter that ignored listener_wait would
|
||||
// wake, observe the memtable already gone, and return here.
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
db_->DisableManualCompaction();
|
||||
db_->EnableManualCompaction();
|
||||
}
|
||||
|
||||
// The OnFlushCompleted callback has not returned, so neither must Flush().
|
||||
ASSERT_FALSE(listener->CallbackReturned());
|
||||
ASSERT_FALSE(flush_returned.load());
|
||||
|
||||
listener->Release();
|
||||
flush_thread.join();
|
||||
ASSERT_TRUE(flush_returned.load());
|
||||
ASSERT_TRUE(listener->CallbackReturned());
|
||||
}
|
||||
|
||||
TEST_F(DBFlushTest, ListenerWaitAtomicFlushWaitsForAllOnFlushCompleted) {
|
||||
// Same guarantee for atomic flush across multiple column families. The atomic
|
||||
// flush removes all CFs' memtables together and then fires OnFlushCompleted
|
||||
// per CF, releasing the DB mutex between CFs; listener_wait must still block
|
||||
// Flush() until the (first, still-running) callback has finished.
|
||||
auto listener = std::make_shared<BlockingFlushListener>(
|
||||
/*block_only_first=*/true);
|
||||
Options options = CurrentOptions();
|
||||
options.atomic_flush = true;
|
||||
options.listeners.push_back(listener);
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
ASSERT_OK(Put(0, "k0", "v0"));
|
||||
ASSERT_OK(Put(1, "k1", "v1"));
|
||||
|
||||
std::atomic<bool> flush_returned{false};
|
||||
port::Thread flush_thread([&] {
|
||||
FlushOptions fo;
|
||||
fo.wait = true;
|
||||
fo.listener_wait = true;
|
||||
ASSERT_OK(db_->Flush(fo, {handles_[0], handles_[1]}));
|
||||
flush_returned.store(true);
|
||||
});
|
||||
|
||||
listener->WaitUntilEntered();
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
db_->DisableManualCompaction();
|
||||
db_->EnableManualCompaction();
|
||||
}
|
||||
ASSERT_FALSE(listener->CallbackReturned());
|
||||
ASSERT_FALSE(flush_returned.load());
|
||||
|
||||
listener->Release();
|
||||
flush_thread.join();
|
||||
ASSERT_TRUE(flush_returned.load());
|
||||
ASSERT_TRUE(listener->CallbackReturned());
|
||||
}
|
||||
|
||||
TEST_F(DBFlushTest, FlushWithBlob) {
|
||||
constexpr uint64_t min_blob_size = 10;
|
||||
|
||||
@@ -3590,6 +3725,108 @@ TEST_F(DBFlushTest, NonAtomicNormalFlushAbortWhenBGError) {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
TEST_F(DBFlushTest, NonRecoveryFlushDoesNotStarveErrorRecovery) {
|
||||
constexpr uint64_t kRecoveryTimeoutMicros = 15 * 1000 * 1000;
|
||||
constexpr int kLateWriteCount = 32;
|
||||
|
||||
Options opts = CurrentOptions();
|
||||
opts.atomic_flush = true;
|
||||
opts.memtable_factory.reset(test::NewSpecialSkipListFactory(1));
|
||||
opts.max_write_buffer_number = 64;
|
||||
opts.max_background_flushes = 1;
|
||||
DestroyAndReopen(opts);
|
||||
env_->SetBackgroundThreads(1, Env::HIGH);
|
||||
|
||||
std::atomic_int flush_write_table_count{0};
|
||||
port::Mutex wait_mutex;
|
||||
port::CondVar wait_cv(&wait_mutex);
|
||||
bool recovery_wait_started = false;
|
||||
bool recovery_succeeded = false;
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"FlushJob::WriteLevel0Table:s", [&](void* s_ptr) {
|
||||
if (flush_write_table_count.fetch_add(1) == 0) {
|
||||
Status* s = static_cast<Status*>(s_ptr);
|
||||
IOStatus io_error = IOStatus::IOError("injected foobar");
|
||||
io_error.SetRetryable(true);
|
||||
*s = io_error;
|
||||
TEST_SYNC_POINT(
|
||||
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
|
||||
"FirstFlushFailed");
|
||||
TEST_SYNC_POINT(
|
||||
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
|
||||
"ContinueFirstFlush");
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::ResumeImpl:BeforeWaitForBackgroundWork", [&](void*) {
|
||||
MutexLock l(&wait_mutex);
|
||||
recovery_wait_started = true;
|
||||
wait_cv.SignalAll();
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"RecoverFromRetryableBGIOError:RecoverSuccess", [&](void*) {
|
||||
MutexLock l(&wait_mutex);
|
||||
recovery_succeeded = true;
|
||||
wait_cv.SignalAll();
|
||||
});
|
||||
SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
|
||||
"FirstFlushFailed",
|
||||
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
|
||||
"FailureObserved"},
|
||||
{"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
|
||||
"ReleaseFirstFlush",
|
||||
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
|
||||
"ContinueFirstFlush"}});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
auto wait_until = [&](const auto& predicate) {
|
||||
const uint64_t abs_time =
|
||||
SystemClock::Default()->NowMicros() + kRecoveryTimeoutMicros;
|
||||
MutexLock l(&wait_mutex);
|
||||
while (!predicate()) {
|
||||
if (wait_cv.TimedWait(abs_time)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
ASSERT_OK(Put(Key(1), "val1"));
|
||||
ASSERT_OK(Put(Key(2), "val2"));
|
||||
TEST_SYNC_POINT(
|
||||
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
|
||||
"FailureObserved");
|
||||
TEST_SYNC_POINT(
|
||||
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
|
||||
"ReleaseFirstFlush");
|
||||
|
||||
const bool recovery_wait_reached =
|
||||
wait_until([&] { return recovery_wait_started; });
|
||||
Status late_write_status;
|
||||
if (recovery_wait_reached) {
|
||||
for (int key = 3; key < 3 + kLateWriteCount && late_write_status.ok();
|
||||
++key) {
|
||||
late_write_status = Put(Key(key), "val");
|
||||
}
|
||||
}
|
||||
const bool recovered = wait_until([&] { return recovery_succeeded; });
|
||||
|
||||
if (!recovered) {
|
||||
IOStatus cleanup_error = IOStatus::IOError("stop stuck error recovery");
|
||||
dbfull()->TEST_SetBGError(cleanup_error, BackgroundErrorReason::kFlush);
|
||||
dbfull()->TEST_SignalAllBgCv();
|
||||
}
|
||||
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
ASSERT_TRUE(recovery_wait_reached);
|
||||
ASSERT_OK(late_write_status);
|
||||
ASSERT_TRUE(recovered);
|
||||
}
|
||||
|
||||
TEST_F(DBFlushTest, DBStuckAfterAtomicFlushError) {
|
||||
// Test for a bug with atomic flush where DB can become stuck
|
||||
// after a flush error. A repro timeline:
|
||||
@@ -3874,6 +4111,112 @@ INSTANTIATE_TEST_CASE_P(
|
||||
// larger than the max allowed padding size
|
||||
testing::Values(4, kLowSpaceOverheadRatio)));
|
||||
|
||||
// super_block_alignment pads data blocks, giving the padded block's index entry
|
||||
// a non-contiguous handle. At format_version <= 7 the writer forces that index
|
||||
// entry to shared==0 (full key AND full value). format_version 8 instead uses
|
||||
// the index value-delta escape (see IndexValue::EncodeTo), keeping the key
|
||||
// delta-encoded and the restart cadence intact. This verifies that fv8 returns
|
||||
// byte-identical read results to fv7 for the same keys (also with
|
||||
// separate_key_value_in_data_block, the intersection that motivated the
|
||||
// escape), that padding -- and hence the escape -- is actually exercised, and
|
||||
// that fv8 index values are delta encoded.
|
||||
TEST_F(DBFlushTest, SuperBlockAlignmentValueDeltaEscape) {
|
||||
// Writing the unpublished draft format_version 8 requires this opt-in.
|
||||
SaveAndRestore<bool> allow_draft(&TEST_AllowUnsupportedFormatVersion(), true);
|
||||
|
||||
constexpr int kKeyCount = 5000;
|
||||
auto format_key = [](int i) {
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%010d", i);
|
||||
return std::string(buf);
|
||||
};
|
||||
|
||||
// Deterministic values so fv7 and fv8 see identical input.
|
||||
Random rnd(test::RandomSeed());
|
||||
std::vector<std::string> values;
|
||||
values.reserve(kKeyCount);
|
||||
for (int i = 0; i < kKeyCount; ++i) {
|
||||
values.push_back(rnd.RandomString(static_cast<int>(rnd.Uniform(1000))));
|
||||
}
|
||||
|
||||
auto build_and_read = [&](uint32_t format_version, bool separate_kv,
|
||||
std::vector<std::string>* got_get,
|
||||
std::vector<std::string>* got_iter, int* pad_count,
|
||||
bool* index_delta_encoded) {
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
BlockBasedTableOptions bbto;
|
||||
bbto.format_version = format_version;
|
||||
bbto.index_block_restart_interval = 3; // > 1 so value delta encoding is on
|
||||
bbto.super_block_alignment_size = 16 * 1024;
|
||||
bbto.super_block_alignment_space_overhead_ratio = 8;
|
||||
bbto.separate_key_value_in_data_block = separate_kv;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(bbto));
|
||||
DestroyAndReopen(options);
|
||||
|
||||
int local_pad = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlockBasedTableBuilder::WriteMaybeCompressedBlock:SuperBlockAlignment",
|
||||
[&local_pad](void*) { local_pad++; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
for (int i = 0; i < kKeyCount; ++i) {
|
||||
ASSERT_OK(Put(format_key(i), values[i]));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
*pad_count = local_pad;
|
||||
|
||||
got_get->clear();
|
||||
for (int i = 0; i < kKeyCount; ++i) {
|
||||
PinnableSlice v;
|
||||
ASSERT_OK(Get(format_key(i), &v));
|
||||
got_get->push_back(v.ToString());
|
||||
}
|
||||
got_iter->clear();
|
||||
{
|
||||
std::unique_ptr<Iterator> it(db_->NewIterator(ReadOptions()));
|
||||
for (it->SeekToFirst(); it->Valid(); it->Next()) {
|
||||
ASSERT_OK(it->status());
|
||||
got_iter->push_back(it->value().ToString());
|
||||
}
|
||||
ASSERT_OK(it->status());
|
||||
}
|
||||
|
||||
// Confirm the index value encoding of the produced SST(s).
|
||||
TablePropertiesCollection props;
|
||||
ASSERT_OK(db_->GetPropertiesOfAllTables(&props));
|
||||
ASSERT_FALSE(props.empty());
|
||||
*index_delta_encoded = true;
|
||||
for (const auto& kv : props) {
|
||||
if (kv.second->index_value_is_delta_encoded == 0) {
|
||||
*index_delta_encoded = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (bool separate_kv : {false, true}) {
|
||||
SCOPED_TRACE("separate_kv=" + std::to_string(separate_kv));
|
||||
std::vector<std::string> get7, iter7, get8, iter8;
|
||||
int pad7 = 0, pad8 = 0;
|
||||
bool delta7 = false, delta8 = false;
|
||||
build_and_read(7, separate_kv, &get7, &iter7, &pad7, &delta7);
|
||||
build_and_read(/*format_version=*/8, separate_kv, &get8, &iter8, &pad8,
|
||||
&delta8);
|
||||
|
||||
// Reads must be identical across versions.
|
||||
ASSERT_EQ(get7, get8);
|
||||
ASSERT_EQ(iter7, iter8);
|
||||
ASSERT_EQ(static_cast<int>(iter8.size()), kKeyCount);
|
||||
// Padding (the non-contiguity that drives the escape) actually happened.
|
||||
EXPECT_GT(pad8, 0);
|
||||
// fv8 index values are delta encoded, so the escape path was exercised.
|
||||
EXPECT_TRUE(delta8);
|
||||
}
|
||||
}
|
||||
|
||||
// Test that when the table builder's io_status becomes bad during flush
|
||||
// (simulating write fault injection), BuildTable properly propagates the
|
||||
// builder's IO error instead of producing a misleading Corruption from the
|
||||
|
||||
+13
-182
@@ -34,188 +34,6 @@ size_t CompactedDBImpl::FindFile(const Slice& key) {
|
||||
files_.files);
|
||||
}
|
||||
|
||||
Status CompactedDBImpl::Get(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle*, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGet) {
|
||||
return Status::InvalidArgument(
|
||||
"Can only call Get with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGet`");
|
||||
}
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kGet;
|
||||
}
|
||||
|
||||
assert(user_comparator_);
|
||||
if (read_options.timestamp) {
|
||||
Status s =
|
||||
FailIfTsMismatchCf(DefaultColumnFamily(), *(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd_, cfd_->GetSuperVersion(),
|
||||
*(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const Status s = FailIfCfHasTs(DefaultColumnFamily());
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (timestamp) {
|
||||
timestamp->clear();
|
||||
}
|
||||
|
||||
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
|
||||
BlobFetcher blob_fetcher(version_, read_options);
|
||||
std::string* ts =
|
||||
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr;
|
||||
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
|
||||
GetContext get_context(user_comparator_, nullptr, nullptr, nullptr,
|
||||
GetContext::kNotFound, lkey.user_key(), value,
|
||||
/*columns=*/nullptr, ts, nullptr, nullptr, true,
|
||||
nullptr, nullptr, nullptr, nullptr, &read_cb,
|
||||
nullptr /* is_blob_index */, 0 /* tracing_get_id */,
|
||||
&blob_fetcher);
|
||||
|
||||
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
|
||||
if (user_comparator_->CompareWithoutTimestamp(
|
||||
key, /*a_has_ts=*/false,
|
||||
ExtractUserKeyAndStripTimestamp(f.smallest_key,
|
||||
user_comparator_->timestamp_size()),
|
||||
/*b_has_ts=*/false) < 0) {
|
||||
return Status::NotFound();
|
||||
}
|
||||
TableReader* t = nullptr;
|
||||
TableCache::TypedHandle* handle = nullptr;
|
||||
Status s = cfd_->table_cache()->FindTable(
|
||||
read_options, cfd_->table_cache()->file_options(),
|
||||
cfd_->internal_comparator(), *f.file_metadata, &handle,
|
||||
version_->GetMutableCFOptions(), &t, false /* no_io */,
|
||||
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
|
||||
true /* prefetch_index_and_filter_in_cache */,
|
||||
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
|
||||
true /* pin_table_handle */);
|
||||
if (s.ok()) {
|
||||
assert(handle == nullptr);
|
||||
// Use TableReader directly to avoid extra table_cache->Get() overheads
|
||||
s = t->Get(read_options, lkey.internal_key(), &get_context, nullptr);
|
||||
}
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
return s;
|
||||
}
|
||||
if (get_context.State() == GetContext::kFound) {
|
||||
return Status::OK();
|
||||
}
|
||||
return Status::NotFound();
|
||||
}
|
||||
|
||||
void CompactedDBImpl::MultiGet(const ReadOptions& _read_options,
|
||||
size_t num_keys,
|
||||
ColumnFamilyHandle** /*column_families*/,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
std::string* timestamps, Status* statuses,
|
||||
const bool /*sorted_input*/) {
|
||||
assert(user_comparator_);
|
||||
Status s;
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kMultiGet) {
|
||||
s = Status::InvalidArgument(
|
||||
"Can only call MultiGet with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kMultiGet`");
|
||||
}
|
||||
|
||||
ReadOptions read_options(_read_options);
|
||||
if (s.ok()) {
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kMultiGet;
|
||||
}
|
||||
|
||||
if (read_options.timestamp) {
|
||||
s = FailIfTsMismatchCf(DefaultColumnFamily(), *(read_options.timestamp));
|
||||
if (s.ok()) {
|
||||
if (read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd_, cfd_->GetSuperVersion(),
|
||||
*(read_options.timestamp));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s = FailIfCfHasTs(DefaultColumnFamily());
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] = s;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (timestamps) {
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
timestamps[i].clear();
|
||||
}
|
||||
}
|
||||
|
||||
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
|
||||
BlobFetcher blob_fetcher(version_, read_options);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
const Slice& key = keys[i];
|
||||
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
|
||||
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
|
||||
if (user_comparator_->CompareWithoutTimestamp(
|
||||
key, /*a_has_ts=*/false,
|
||||
ExtractUserKeyAndStripTimestamp(f.smallest_key,
|
||||
user_comparator_->timestamp_size()),
|
||||
/*b_has_ts=*/false) < 0) {
|
||||
statuses[i] = Status::NotFound();
|
||||
continue;
|
||||
}
|
||||
|
||||
PinnableSlice& pinnable_val = values[i];
|
||||
std::string* timestamp = timestamps ? ×tamps[i] : nullptr;
|
||||
GetContext get_context(
|
||||
user_comparator_, nullptr, nullptr, nullptr, GetContext::kNotFound,
|
||||
lkey.user_key(), &pinnable_val, /*columns=*/nullptr,
|
||||
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr, nullptr,
|
||||
nullptr, true, nullptr, nullptr, nullptr, nullptr, &read_cb,
|
||||
nullptr /* is_blob_index */, 0 /* tracing_get_id */, &blob_fetcher);
|
||||
TableReader* t = nullptr;
|
||||
TableCache::TypedHandle* handle = nullptr;
|
||||
Status status = cfd_->table_cache()->FindTable(
|
||||
read_options, cfd_->table_cache()->file_options(),
|
||||
cfd_->internal_comparator(), *f.file_metadata, &handle,
|
||||
version_->GetMutableCFOptions(), &t, false /* no_io */,
|
||||
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
|
||||
true /* prefetch_index_and_filter_in_cache */,
|
||||
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
|
||||
true /* pin_table_handle */);
|
||||
if (status.ok()) {
|
||||
assert(handle == nullptr);
|
||||
// Use TableReader directly to avoid extra table_cache->Get() overheads
|
||||
status = t->Get(read_options, lkey.internal_key(), &get_context, nullptr);
|
||||
}
|
||||
if (!status.ok() && !status.IsNotFound()) {
|
||||
statuses[i] = status;
|
||||
} else if (get_context.State() == GetContext::kFound) {
|
||||
statuses[i] = Status::OK();
|
||||
} else {
|
||||
statuses[i] = Status::NotFound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Status CompactedDBImpl::Init(const Options& options) {
|
||||
SuperVersionContext sv_context(/* create_superversion */ true);
|
||||
mutex_.Lock();
|
||||
@@ -301,3 +119,16 @@ Status CompactedDBImpl::Open(const Options& options, const std::string& dbname,
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
// Generate the regular and coroutine versions of the functions defined in
|
||||
// compacted_db_impl_sync_and_async.h by including it twice. The macros expand
|
||||
// differently based on whether WITH_COROUTINES or WITHOUT_COROUTINES is
|
||||
// defined.
|
||||
// clang-format off
|
||||
#define WITHOUT_COROUTINES
|
||||
#include "db/db_impl/compacted_db_impl_sync_and_async.h"
|
||||
#undef WITHOUT_COROUTINES
|
||||
#define WITH_COROUTINES
|
||||
#include "db/db_impl/compacted_db_impl_sync_and_async.h"
|
||||
#undef WITH_COROUTINES
|
||||
// clang-format on
|
||||
@@ -26,18 +26,21 @@ class CompactedDBImpl : public DBImpl {
|
||||
|
||||
// Implementations of the DB interface
|
||||
using DB::Get;
|
||||
Status Get(const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp) override;
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, Get, const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp);
|
||||
|
||||
using DB::MultiGet;
|
||||
// Note that CompactedDBImpl::MultiGet is not the optimized version of
|
||||
// MultiGet to use.
|
||||
// TODO: optimize CompactedDBImpl::MultiGet, see DBImpl::MultiGet for details.
|
||||
void MultiGet(const ReadOptions& options, size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps,
|
||||
Status* statuses, const bool sorted_input) override;
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(void, MultiGet, const ReadOptions& options,
|
||||
size_t num_keys,
|
||||
ColumnFamilyHandle** column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input);
|
||||
|
||||
using DBImpl::Put;
|
||||
Status Put(const WriteOptions& /*options*/,
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
//
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "util/coro_utils.h"
|
||||
|
||||
#if defined(USE_COROUTINES) && defined(WITH_COROUTINES)
|
||||
#include "util/coro_stats_util.h"
|
||||
#endif // USE_COROUTINES && WITH_COROUTINES
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, CompactedDBImpl::Get)
|
||||
(const ReadOptions& _read_options, ColumnFamilyHandle*, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp) {
|
||||
#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(
|
||||
"Can only call Get with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGet`");
|
||||
}
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kGet;
|
||||
}
|
||||
|
||||
assert(user_comparator_);
|
||||
if (read_options.timestamp) {
|
||||
Status s =
|
||||
FailIfTsMismatchCf(DefaultColumnFamily(), *(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
if (read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd_, cfd_->GetSuperVersion(),
|
||||
*(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const Status s = FailIfCfHasTs(DefaultColumnFamily());
|
||||
if (!s.ok()) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (timestamp) {
|
||||
timestamp->clear();
|
||||
}
|
||||
|
||||
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
|
||||
VersionBlobFetcher blob_fetcher(version_, read_options);
|
||||
std::string* ts =
|
||||
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr;
|
||||
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
|
||||
GetContext get_context(user_comparator_, nullptr, nullptr, nullptr,
|
||||
GetContext::kNotFound, lkey.user_key(), value,
|
||||
/*columns=*/nullptr, ts, nullptr, nullptr, true,
|
||||
nullptr, nullptr, nullptr, nullptr, &read_cb,
|
||||
nullptr /* is_blob_index */, 0 /* tracing_get_id */,
|
||||
&blob_fetcher);
|
||||
|
||||
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
|
||||
if (user_comparator_->CompareWithoutTimestamp(
|
||||
key, /*a_has_ts=*/false,
|
||||
ExtractUserKeyAndStripTimestamp(f.smallest_key,
|
||||
user_comparator_->timestamp_size()),
|
||||
/*b_has_ts=*/false) < 0) {
|
||||
CO_RETURN Status::NotFound();
|
||||
}
|
||||
TableReader* t = nullptr;
|
||||
TableCache::TypedHandle* handle = nullptr;
|
||||
Status s = cfd_->table_cache()->FindTable(
|
||||
read_options, cfd_->table_cache()->file_options(),
|
||||
cfd_->internal_comparator(), *f.file_metadata, &handle,
|
||||
version_->GetMutableCFOptions(), &t, false /* no_io */,
|
||||
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
|
||||
true /* prefetch_index_and_filter_in_cache */,
|
||||
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
|
||||
true /* pin_table_handle */);
|
||||
if (s.ok()) {
|
||||
assert(handle == nullptr);
|
||||
// Use TableReader directly to avoid extra table_cache->Get() overheads
|
||||
s = CO_AWAIT(t->Get, read_options, lkey.internal_key(), &get_context,
|
||||
nullptr);
|
||||
}
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
if (get_context.State() == GetContext::kFound) {
|
||||
CO_RETURN Status::OK();
|
||||
}
|
||||
CO_RETURN Status::NotFound();
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(void, CompactedDBImpl::MultiGet)
|
||||
(const ReadOptions& _read_options, size_t num_keys,
|
||||
ColumnFamilyHandle** /*column_families*/, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps, Status* statuses,
|
||||
const bool /*sorted_input*/) {
|
||||
#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 &&
|
||||
_read_options.io_activity != Env::IOActivity::kMultiGet) {
|
||||
s = Status::InvalidArgument(
|
||||
"Can only call MultiGet with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kMultiGet`");
|
||||
}
|
||||
|
||||
ReadOptions read_options(_read_options);
|
||||
if (s.ok()) {
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kMultiGet;
|
||||
}
|
||||
|
||||
if (read_options.timestamp) {
|
||||
s = FailIfTsMismatchCf(DefaultColumnFamily(), *(read_options.timestamp));
|
||||
if (s.ok()) {
|
||||
if (read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd_, cfd_->GetSuperVersion(),
|
||||
*(read_options.timestamp));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s = FailIfCfHasTs(DefaultColumnFamily());
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] = s;
|
||||
}
|
||||
CO_RETURN;
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (timestamps) {
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
timestamps[i].clear();
|
||||
}
|
||||
}
|
||||
|
||||
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
|
||||
VersionBlobFetcher blob_fetcher(version_, read_options);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
const Slice& key = keys[i];
|
||||
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
|
||||
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
|
||||
if (user_comparator_->CompareWithoutTimestamp(
|
||||
key, /*a_has_ts=*/false,
|
||||
ExtractUserKeyAndStripTimestamp(f.smallest_key,
|
||||
user_comparator_->timestamp_size()),
|
||||
/*b_has_ts=*/false) < 0) {
|
||||
statuses[i] = Status::NotFound();
|
||||
continue;
|
||||
}
|
||||
|
||||
PinnableSlice& pinnable_val = values[i];
|
||||
std::string* timestamp = timestamps ? ×tamps[i] : nullptr;
|
||||
GetContext get_context(
|
||||
user_comparator_, nullptr, nullptr, nullptr, GetContext::kNotFound,
|
||||
lkey.user_key(), &pinnable_val, /*columns=*/nullptr,
|
||||
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr, nullptr,
|
||||
nullptr, true, nullptr, nullptr, nullptr, nullptr, &read_cb,
|
||||
nullptr /* is_blob_index */, 0 /* tracing_get_id */, &blob_fetcher);
|
||||
TableReader* t = nullptr;
|
||||
TableCache::TypedHandle* handle = nullptr;
|
||||
Status status = cfd_->table_cache()->FindTable(
|
||||
read_options, cfd_->table_cache()->file_options(),
|
||||
cfd_->internal_comparator(), *f.file_metadata, &handle,
|
||||
version_->GetMutableCFOptions(), &t, false /* no_io */,
|
||||
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
|
||||
true /* prefetch_index_and_filter_in_cache */,
|
||||
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
|
||||
true /* pin_table_handle */);
|
||||
if (status.ok()) {
|
||||
assert(handle == nullptr);
|
||||
// Use TableReader directly to avoid extra table_cache->Get() overheads
|
||||
status = CO_AWAIT(t->Get, read_options, lkey.internal_key(), &get_context,
|
||||
nullptr);
|
||||
}
|
||||
if (!status.ok() && !status.IsNotFound()) {
|
||||
statuses[i] = status;
|
||||
} else if (get_context.State() == GetContext::kFound) {
|
||||
statuses[i] = Status::OK();
|
||||
} else {
|
||||
statuses[i] = Status::NotFound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
#endif
|
||||
+707
-889
File diff suppressed because it is too large.
Load diff
+278
-77
@@ -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,23 +62,30 @@
|
||||
#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"
|
||||
#include "util/coro_utils.h"
|
||||
#include "util/hash.h"
|
||||
#include "util/repeatable_thread.h"
|
||||
#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;
|
||||
@@ -195,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();
|
||||
|
||||
@@ -268,9 +285,10 @@ class DBImpl : public DB {
|
||||
bool HasAnyBlobDirectWriteColumnFamily();
|
||||
|
||||
using DB::Get;
|
||||
Status Get(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp) override;
|
||||
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,
|
||||
@@ -279,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,
|
||||
@@ -302,10 +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
|
||||
void MultiGet(const ReadOptions& _read_options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps,
|
||||
Status* statuses, const bool sorted_input = false) override;
|
||||
DECLARE_SYNC_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,
|
||||
@@ -327,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,
|
||||
@@ -464,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(
|
||||
@@ -560,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);
|
||||
@@ -576,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;
|
||||
@@ -716,15 +777,33 @@ 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;
|
||||
};
|
||||
|
||||
Status GetImpl(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value);
|
||||
DECLARE_SYNC_AND_ASYNC(Status, GetImpl, const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value);
|
||||
|
||||
Status GetImpl(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp);
|
||||
DECLARE_SYNC_AND_ASYNC(Status, GetImpl, const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp);
|
||||
|
||||
// Function that Get and KeyMayExist call with no_io true or false
|
||||
// Note: 'value_found' from KeyMayExist propagates here
|
||||
@@ -733,8 +812,20 @@ class DBImpl : public DB {
|
||||
// get_impl_options.key via get_impl_options.value
|
||||
// If get_impl_options.get_value = false get merge operands associated with
|
||||
// get_impl_options.key via get_impl_options.merge_operands
|
||||
virtual Status GetImpl(const ReadOptions& options, const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
DECLARE_SYNC_AND_ASYNC_VIRTUAL(Status, GetImpl, const ReadOptions& options,
|
||||
const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
|
||||
// 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,
|
||||
@@ -887,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_;
|
||||
}
|
||||
@@ -1058,10 +1156,12 @@ class DBImpl : public DB {
|
||||
}
|
||||
};
|
||||
|
||||
using RecoveredTransactionMap =
|
||||
std::unordered_map<std::string, RecoveredTransaction*>;
|
||||
|
||||
bool allow_2pc() const { return immutable_db_options_.allow_2pc; }
|
||||
|
||||
std::unordered_map<std::string, RecoveredTransaction*>
|
||||
recovered_transactions() {
|
||||
RecoveredTransactionMap recovered_transactions() {
|
||||
return recovered_transactions_;
|
||||
}
|
||||
|
||||
@@ -1092,16 +1192,24 @@ class DBImpl : public DB {
|
||||
logs_with_prep_tracker_.MarkLogAsContainingPrepSection(log);
|
||||
}
|
||||
|
||||
void DeleteRecoveredTransaction(const std::string& name) {
|
||||
auto it = recovered_transactions_.find(name);
|
||||
// Deletes the recovered transaction `it` points to and returns the iterator
|
||||
// following it, like std::unordered_map::erase().
|
||||
RecoveredTransactionMap::iterator DeleteRecoveredTransaction(
|
||||
RecoveredTransactionMap::iterator it) {
|
||||
assert(it != recovered_transactions_.end());
|
||||
auto* trx = it->second;
|
||||
recovered_transactions_.erase(it);
|
||||
RecoveredTransactionMap::iterator next = recovered_transactions_.erase(it);
|
||||
for (const auto& info : trx->batches_) {
|
||||
logs_with_prep_tracker_.MarkLogAsHavingPrepSectionFlushed(
|
||||
info.second.log_number_);
|
||||
}
|
||||
delete trx;
|
||||
return next;
|
||||
}
|
||||
|
||||
void DeleteRecoveredTransaction(const std::string& name) {
|
||||
RecoveredTransactionMap::iterator it = recovered_transactions_.find(name);
|
||||
DeleteRecoveredTransaction(it);
|
||||
}
|
||||
|
||||
void DeleteAllRecoveredTransactions() {
|
||||
@@ -1189,6 +1297,13 @@ class DBImpl : public DB {
|
||||
Status TEST_FlushMemTable(ColumnFamilyData* cfd,
|
||||
const FlushOptions& flush_opts);
|
||||
|
||||
// Like TEST_FlushMemTable(bool wait, ...), but also sets
|
||||
// FlushOptions::listener_wait so the flush does not return until the
|
||||
// registered EventListener::OnFlushCompleted callbacks for the flushed
|
||||
// memtables have finished running.
|
||||
Status TEST_FlushMemTableWithListenerWait(bool allow_write_stall = false,
|
||||
ColumnFamilyHandle* cfh = nullptr);
|
||||
|
||||
// Flush (multiple) ColumnFamilyData without using ColumnFamilyHandle. This
|
||||
// is because in certain cases, we can flush column families, wait for the
|
||||
// flush to complete, but delete the column family handle before the wait
|
||||
@@ -1213,6 +1328,8 @@ class DBImpl : public DB {
|
||||
// Get the background error status
|
||||
Status TEST_GetBGError();
|
||||
|
||||
void TEST_SetBGError(const IOStatus& error, BackgroundErrorReason reason);
|
||||
|
||||
bool TEST_IsRecoveryInProgress();
|
||||
|
||||
Status TEST_ResumeImpl(DBRecoverContext context);
|
||||
@@ -1336,6 +1453,20 @@ class DBImpl : public DB {
|
||||
// REQUIRES: DB mutex held or during open
|
||||
void EnsureSeqnoToTimeMapping(const MinAndMaxPreserveSeconds& preserve_secs);
|
||||
|
||||
// Computes the seqno->time preserve-window lower bound from
|
||||
// seqno_to_time_mapping_ and stores it on cfd's current version, so
|
||||
// bottommost file marking does not mark files whose largest seqno cannot be
|
||||
// zeroed yet (which would loop). No-op for column families without
|
||||
// preserve/preclude enabled. Returns true if the stored value changed, so
|
||||
// callers can recompute bottommost marking when the boundary moves.
|
||||
// Note: right after opening an existing DB, seqno_to_time_mapping_ may not be
|
||||
// fully reconstructed, so this bound can be imprecise until the first
|
||||
// periodic RecordSeqnoToTimeMapping. That is safe: CompactionJob folds this
|
||||
// same bound into kBottommostFiles compactions, so any marked file still
|
||||
// makes progress (never loops), and the periodic task self-corrects the
|
||||
// bound. REQUIRES: DB mutex held
|
||||
bool MaybeUpdatePreserveTimeMinSeqno(ColumnFamilyData* cfd);
|
||||
|
||||
// Only called during open
|
||||
void PrepopulateSeqnoToTimeMapping(
|
||||
const MinAndMaxPreserveSeconds& preserve_secs);
|
||||
@@ -1397,6 +1528,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_;
|
||||
@@ -1416,8 +1551,7 @@ class DBImpl : public DB {
|
||||
FileSystemPtr fs_;
|
||||
MutableDBOptions mutable_db_options_;
|
||||
Statistics* stats_;
|
||||
std::unordered_map<std::string, RecoveredTransaction*>
|
||||
recovered_transactions_;
|
||||
RecoveredTransactionMap recovered_transactions_;
|
||||
std::unique_ptr<Tracer> tracer_;
|
||||
InstrumentedMutex trace_mutex_;
|
||||
BlockCacheTracer block_cache_tracer_;
|
||||
@@ -1518,6 +1652,15 @@ class DBImpl : public DB {
|
||||
edit_lists_[i].emplace_back(new VersionEdit(edit));
|
||||
}
|
||||
|
||||
bool HasVersionEdits() const {
|
||||
for (const auto& edit_list : edit_lists_) {
|
||||
if (!edit_list.empty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unordered_map<uint32_t, uint32_t> map_; // cf_id to index;
|
||||
autovector<ColumnFamilyData*> cfds_;
|
||||
autovector<autovector<VersionEdit*>> edit_lists_;
|
||||
@@ -1541,7 +1684,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);
|
||||
@@ -1887,7 +2030,7 @@ class DBImpl : public DB {
|
||||
// LogAndApplyForRecovery should be called only once during recovery and it
|
||||
// should be called when RocksDB writes to a first new MANIFEST since this
|
||||
// recovery.
|
||||
Status LogAndApplyForRecovery(const RecoveryContext& recovery_ctx);
|
||||
Status LogAndApplyForRecovery(RecoveryContext& recovery_ctx);
|
||||
|
||||
// Schedule background work to open and validate SST files asynchronously.
|
||||
// Called when open_files_async is enabled.
|
||||
@@ -2219,7 +2362,10 @@ class DBImpl : public DB {
|
||||
// Required: DB mutex held
|
||||
Status PersistentStatsProcessFormatVersion();
|
||||
|
||||
Status ResumeImpl(DBRecoverContext context);
|
||||
// `io_activity` is used to construct the ReadOptions/WriteOptions for the
|
||||
// internal recovery work (e.g. the MANIFEST write in LogAndApply). Recovery
|
||||
// is driven by flush operations, so callers pass Env::IOActivity::kFlush.
|
||||
Status ResumeImpl(DBRecoverContext context, Env::IOActivity io_activity);
|
||||
|
||||
void MaybeIgnoreError(Status* s) const;
|
||||
|
||||
@@ -2501,18 +2647,24 @@ class DBImpl : public DB {
|
||||
// memtable pending flush.
|
||||
// resuming_from_bg_err indicates whether the caller is attempting to resume
|
||||
// from background error.
|
||||
// If wait_for_listener_notifications is true, also wait until the
|
||||
// EventListener::OnFlushCompleted callbacks for the flushed memtables have
|
||||
// finished running (see FlushOptions::listener_wait).
|
||||
Status WaitForFlushMemTable(
|
||||
ColumnFamilyData* cfd, const uint64_t* flush_memtable_id = nullptr,
|
||||
bool resuming_from_bg_err = false,
|
||||
std::optional<FlushReason> flush_reason = std::nullopt) {
|
||||
std::optional<FlushReason> flush_reason = std::nullopt,
|
||||
bool wait_for_listener_notifications = false) {
|
||||
return WaitForFlushMemTables({cfd}, {flush_memtable_id},
|
||||
resuming_from_bg_err, flush_reason);
|
||||
resuming_from_bg_err, flush_reason,
|
||||
wait_for_listener_notifications);
|
||||
}
|
||||
// Wait for memtables to be flushed for multiple column families.
|
||||
Status WaitForFlushMemTables(
|
||||
const autovector<ColumnFamilyData*>& cfds,
|
||||
const autovector<const uint64_t*>& flush_memtable_ids,
|
||||
bool resuming_from_bg_err, std::optional<FlushReason> flush_reason);
|
||||
bool resuming_from_bg_err, std::optional<FlushReason> flush_reason,
|
||||
bool wait_for_listener_notifications = false);
|
||||
|
||||
inline void WaitForPendingWrites() {
|
||||
mutex_.AssertHeld();
|
||||
@@ -2656,7 +2808,11 @@ class DBImpl : public DB {
|
||||
// equal to this per-column-family specified value, this flush request is
|
||||
// considered to have completed its work of flushing this column family.
|
||||
// After completing the work for all column families in this request, this
|
||||
// flush is considered complete.
|
||||
// flush is considered complete. EnqueuePendingFlush() acquires one
|
||||
// reference for each CFD when it successfully queues this request.
|
||||
// PopFirstFromFlushQueue() transfers responsibility for those references
|
||||
// to its caller, which must release each one with UnrefAndTryDelete() after
|
||||
// processing or discarding the request.
|
||||
std::unordered_map<ColumnFamilyData*, uint64_t>
|
||||
cfd_to_max_mem_id_to_persist;
|
||||
|
||||
@@ -2771,9 +2927,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,
|
||||
@@ -2954,11 +3125,12 @@ class DBImpl : public DB {
|
||||
PinnableWideColumns* columns, std::string* timestamps,
|
||||
Status* statuses, bool sorted_input);
|
||||
|
||||
void MultiGetCommon(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, PinnableWideColumns* columns,
|
||||
std::string* timestamps, Status* statuses,
|
||||
bool sorted_input);
|
||||
DECLARE_SYNC_AND_ASYNC(void, MultiGetCommon, const ReadOptions& options,
|
||||
const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
PinnableWideColumns* columns, std::string* timestamps,
|
||||
Status* statuses, bool sorted_input);
|
||||
|
||||
// A structure to hold the information required to process MultiGet of keys
|
||||
// belonging to one column family. For a multi column family MultiGet, there
|
||||
@@ -3033,8 +3205,9 @@ class DBImpl : public DB {
|
||||
// to have acquired the SuperVersion and pass in a snapshot sequence number
|
||||
// in order to construct the LookupKeys. The start_key and num_keys specify
|
||||
// the range of keys in the sorted_keys vector for a single column family.
|
||||
Status MultiGetImpl(
|
||||
const ReadOptions& read_options, size_t start_key, size_t num_keys,
|
||||
DECLARE_SYNC_AND_ASYNC(
|
||||
Status, MultiGetImpl, const ReadOptions& read_options, size_t start_key,
|
||||
size_t num_keys,
|
||||
autovector<KeyContext*, MultiGetContext::MAX_BATCH_SIZE>* sorted_keys,
|
||||
SuperVersion* sv, SequenceNumber snap_seqnum, ReadCallback* callback);
|
||||
|
||||
@@ -3052,50 +3225,56 @@ 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 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.
|
||||
static bool MaybeResolveMemtableBlobValue(const Slice& key,
|
||||
const BlobFetcher* blob_fetcher,
|
||||
PinnableSlice* value,
|
||||
PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index,
|
||||
bool* value_found = nullptr);
|
||||
// Completes read-only/secondary memtable Get()/GetEntity() hits by resolving
|
||||
// blob-backed payloads when `resolve_blob_backed_memtable_value` is true,
|
||||
// pinning plain values on success, and clearing outputs on error. When the
|
||||
// caller explicitly requested raw blob indices via
|
||||
// `GetImplOptions::is_blob_index`, this helper leaves that payload
|
||||
// untouched. `memtable_blob_fetcher` may be null when blob support is
|
||||
// disabled for the column family.
|
||||
static void PostprocessMemtableValueRead(
|
||||
// `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
|
||||
// finalizes the plain value). Only call with an OK incoming status.
|
||||
static Status MaybeResolveMemtableBlobValue(
|
||||
const Slice& key, const BlobFetcher* blob_fetcher, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, bool* did_resolve, bool* is_blob_index,
|
||||
bool* value_found = nullptr);
|
||||
// Completes read-only/secondary memtable Get()/GetEntity() hits. Given the
|
||||
// status from the memtable read, resolves blob-backed payloads when
|
||||
// `resolve_blob_backed_memtable_value` is true, self-pins a plain value on
|
||||
// success, and clears outputs on any error (never leaving a half-populated
|
||||
// result). When the caller explicitly requested raw blob indices via
|
||||
// `GetImplOptions::is_blob_index`, this helper leaves that payload untouched.
|
||||
// `memtable_blob_fetcher` may be null when blob support is disabled for the
|
||||
// column family. Returns the finalized status.
|
||||
static Status PostprocessMemtableValueRead(
|
||||
const Slice& key, const std::string* timestamp,
|
||||
bool resolve_blob_backed_memtable_value,
|
||||
const BlobFetcher* memtable_blob_fetcher, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
|
||||
bool* value_found = nullptr);
|
||||
PinnableWideColumns* columns, Status memtable_read_status,
|
||||
bool* is_blob_index, bool* value_found = nullptr);
|
||||
|
||||
template <typename IterType, typename ImplType,
|
||||
typename ErrorIteratorFuncType>
|
||||
@@ -3147,6 +3326,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
|
||||
@@ -3356,7 +3539,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
|
||||
@@ -3371,9 +3554,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_;
|
||||
@@ -3404,6 +3601,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;
|
||||
|
||||
@@ -3727,7 +3928,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>
|
||||
@@ -31,6 +32,7 @@
|
||||
#include "util/cast_util.h"
|
||||
#include "util/coding.h"
|
||||
#include "util/concurrent_task_limiter_impl.h"
|
||||
#include "util/defer.h"
|
||||
#include "util/udt_util.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -358,6 +360,34 @@ Status DBImpl::FlushMemTableToOutputFile(
|
||||
need_cancel = false;
|
||||
}
|
||||
|
||||
// Reserve a pending flush-completion notification before the DB mutex can be
|
||||
// released, so that a Flush(wait=true, listener_wait=true) caller cannot
|
||||
// observe this flush as finished until NotifyOnFlushCompleted() below has
|
||||
// run. flush_job.Run() removed the memtables from the immutable list (which
|
||||
// is what unblocks the waiter) and the DB mutex has been held continuously
|
||||
// since, so there is no window in which the waiter could see the flush
|
||||
// committed with a zero count. Reserved only when a notification will
|
||||
// actually fire (matches the NotifyOnFlushCompleted call below): flush
|
||||
// committed successfully, not a mempurge, listeners are registered, and this
|
||||
// job committed at least one flush result to report.
|
||||
const bool reserved_flush_notification =
|
||||
s.ok() && !switched_to_mempurge &&
|
||||
!immutable_db_options_.listeners.empty() &&
|
||||
!flush_job.GetCommittedFlushJobsInfo()->empty();
|
||||
if (reserved_flush_notification) {
|
||||
cfd->imm()->AddPendingFlushNotifications(1);
|
||||
}
|
||||
// Release the reservation on every exit path (RAII) so a future early return
|
||||
// cannot leak the count and hang listener_wait waiters. The DB mutex is held
|
||||
// throughout this function, including at scope exit, which
|
||||
// SubPendingFlushNotifications() requires. Runs after
|
||||
// NotifyOnFlushCompleted() below has returned.
|
||||
Defer release_flush_notification([&] {
|
||||
if (reserved_flush_notification) {
|
||||
cfd->imm()->SubPendingFlushNotifications(1);
|
||||
}
|
||||
});
|
||||
|
||||
if (cfd->blob_partition_manager() != nullptr &&
|
||||
prepared_blob_generations > 0) {
|
||||
auto unconsumed_additions = flush_job.TakeExternalBlobFileAdditions();
|
||||
@@ -900,6 +930,42 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
|
||||
directories_.GetDbDir(), log_buffer);
|
||||
}
|
||||
|
||||
// Reserve a pending flush-completion notification for every CF that the
|
||||
// NotifyOnFlushCompleted loop below will service, while the DB mutex is still
|
||||
// held continuously since InstallMemtableAtomicFlushResults removed the
|
||||
// memtables. This closes the window where a
|
||||
// Flush(wait=true, listener_wait=true) waiter could observe an atomic flush
|
||||
// as finished before a later CF's OnFlushCompleted has run, since the notify
|
||||
// loop releases and re-acquires the mutex between CFs. The reservations are
|
||||
// released by a matching pass after the notify loop.
|
||||
autovector<uint8_t> reserved_flush_notification;
|
||||
reserved_flush_notification.reserve(num_cfs);
|
||||
{
|
||||
const bool has_listeners = !immutable_db_options_.listeners.empty();
|
||||
for (int i = 0; i != num_cfs; ++i) {
|
||||
const bool reserve = s.ok() && has_listeners &&
|
||||
!switched_to_mempurge[i] && !cfds[i]->IsDropped() &&
|
||||
!jobs[i]->GetCommittedFlushJobsInfo()->empty();
|
||||
if (reserve) {
|
||||
cfds[i]->imm()->AddPendingFlushNotifications(1);
|
||||
}
|
||||
reserved_flush_notification.push_back(reserve ? 1 : 0);
|
||||
}
|
||||
}
|
||||
// Release the reservations on every exit path (RAII) so a future early return
|
||||
// cannot leak the count and hang listener_wait waiters. Done independently of
|
||||
// the notify loop's skips so the count stays balanced even if a CF was
|
||||
// dropped after it was reserved. The DB mutex is held throughout this
|
||||
// function, including at scope exit, which SubPendingFlushNotifications()
|
||||
// requires.
|
||||
Defer release_flush_notifications([&] {
|
||||
for (int i = 0; i != num_cfs; ++i) {
|
||||
if (reserved_flush_notification[i]) {
|
||||
cfds[i]->imm()->SubPendingFlushNotifications(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!s.ok()) {
|
||||
// If the atomic flush's combined MANIFEST write failed, the output files
|
||||
// are cached in the table cache (added by BuildTable during FlushJob::Run)
|
||||
@@ -1130,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);
|
||||
}
|
||||
|
||||
@@ -1269,7 +1339,7 @@ Status DBImpl::CompactRangeInternal(const CompactRangeOptions& options,
|
||||
SuperVersion* super_version = cfd->GetReferencedSuperVersion(this);
|
||||
s = cfd->RangesOverlapWithMemtables(
|
||||
{range}, super_version, immutable_db_options_.allow_data_in_errors,
|
||||
&flush_needed);
|
||||
&flush_needed, /*range_limit_exclusive=*/false);
|
||||
CleanupSuperVersion(super_version);
|
||||
}
|
||||
|
||||
@@ -1545,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,
|
||||
@@ -1623,16 +1708,16 @@ void DBImpl::PrepareTrivialMoveEdit(Compaction& c, LogBuffer* log_buffer,
|
||||
for (size_t i = 0; i < c.num_input_files(l); i++) {
|
||||
FileMetaData* f = c.input(l, i);
|
||||
c.edit()->DeleteFile(c.level(l), f->fd.GetNumber());
|
||||
c.edit()->AddFile(c.output_level(), f->fd.GetNumber(), f->fd.GetPathId(),
|
||||
f->fd.GetFileSize(), f->smallest, f->largest,
|
||||
f->fd.smallest_seqno, f->fd.largest_seqno,
|
||||
f->marked_for_compaction, f->temperature,
|
||||
f->oldest_blob_file_number, f->oldest_ancester_time,
|
||||
f->file_creation_time, f->epoch_number,
|
||||
f->file_checksum, f->file_checksum_func_name,
|
||||
f->unique_id, f->compensated_range_deletion_size,
|
||||
f->tail_size, f->user_defined_timestamps_persisted,
|
||||
f->min_timestamp, f->max_timestamp);
|
||||
c.edit()->AddFile(
|
||||
c.output_level(), f->fd.GetNumber(), f->fd.GetPathId(),
|
||||
f->fd.GetFileSize(), f->smallest, f->largest, f->fd.smallest_seqno,
|
||||
f->fd.largest_seqno, f->marked_for_compaction, f->temperature,
|
||||
f->oldest_blob_file_number, f->oldest_ancester_time,
|
||||
f->file_creation_time, f->epoch_number, f->file_checksum,
|
||||
f->file_checksum_func_name, f->unique_id,
|
||||
f->compensated_range_deletion_size, f->tail_size,
|
||||
f->user_defined_timestamps_persisted, f->min_timestamp,
|
||||
f->max_timestamp, f->file_open_metadata);
|
||||
moved_bytes += static_cast<size_t>(c.input(l, i)->fd.GetFileSize());
|
||||
ROCKS_LOG_BUFFER(
|
||||
log_buffer, "[%s] Moved #%" PRIu64 " to level-%d %" PRIu64 " bytes\n",
|
||||
@@ -1673,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);
|
||||
}
|
||||
|
||||
@@ -1801,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();
|
||||
@@ -1831,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
|
||||
@@ -1936,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();
|
||||
@@ -2140,8 +2224,8 @@ Status DBImpl::ReFitLevel(ColumnFamilyData* cfd, int level, int target_level) {
|
||||
"Levels between source and target are not empty for a move.");
|
||||
}
|
||||
if (cfd->RangeOverlapWithCompaction(refit_level_smallest.user_key(),
|
||||
refit_level_largest.user_key(),
|
||||
l)) {
|
||||
refit_level_largest.user_key(), l,
|
||||
/*range_limit_exclusive=*/false)) {
|
||||
refitting_level_ = false;
|
||||
return Status::NotSupported(
|
||||
"Levels between source and target "
|
||||
@@ -2164,8 +2248,8 @@ Status DBImpl::ReFitLevel(ColumnFamilyData* cfd, int level, int target_level) {
|
||||
"Levels between source and target are not empty for a move.");
|
||||
}
|
||||
if (cfd->RangeOverlapWithCompaction(refit_level_smallest.user_key(),
|
||||
refit_level_largest.user_key(),
|
||||
l)) {
|
||||
refit_level_largest.user_key(), l,
|
||||
/*range_limit_exclusive=*/false)) {
|
||||
refitting_level_ = false;
|
||||
return Status::NotSupported(
|
||||
"Levels between source and target "
|
||||
@@ -2209,7 +2293,7 @@ Status DBImpl::ReFitLevel(ColumnFamilyData* cfd, int level, int target_level) {
|
||||
f->file_checksum, f->file_checksum_func_name, f->unique_id,
|
||||
f->compensated_range_deletion_size, f->tail_size,
|
||||
f->user_defined_timestamps_persisted, f->min_timestamp,
|
||||
f->max_timestamp);
|
||||
f->max_timestamp, f->file_open_metadata);
|
||||
}
|
||||
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
|
||||
"[%s] Apply version edit:\n%s", cfd->GetName().c_str(),
|
||||
@@ -2406,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;
|
||||
@@ -2441,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;
|
||||
@@ -2477,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) ||
|
||||
@@ -2566,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;
|
||||
@@ -2794,7 +2881,7 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
|
||||
s = WaitForFlushMemTables(
|
||||
cfds, flush_memtable_ids,
|
||||
flush_reason == FlushReason::kErrorRecovery /* resuming_from_bg_err */,
|
||||
flush_reason);
|
||||
flush_reason, flush_options.listener_wait);
|
||||
InstrumentedMutexLock lock_guard(&mutex_);
|
||||
for (auto* tmp_cfd : cfds) {
|
||||
tmp_cfd->UnrefAndTryDelete();
|
||||
@@ -2949,7 +3036,7 @@ Status DBImpl::AtomicFlushMemTables(
|
||||
s = WaitForFlushMemTables(
|
||||
cfds, flush_memtable_ids,
|
||||
flush_reason == FlushReason::kErrorRecovery /* resuming_from_bg_err */,
|
||||
flush_reason);
|
||||
flush_reason, flush_options.listener_wait);
|
||||
InstrumentedMutexLock lock_guard(&mutex_);
|
||||
for (auto* cfd : cfds) {
|
||||
cfd->UnrefAndTryDelete();
|
||||
@@ -3118,7 +3205,8 @@ Status DBImpl::WaitUntilFlushWouldNotStallWrites(ColumnFamilyData* cfd,
|
||||
Status DBImpl::WaitForFlushMemTables(
|
||||
const autovector<ColumnFamilyData*>& cfds,
|
||||
const autovector<const uint64_t*>& flush_memtable_ids,
|
||||
bool resuming_from_bg_err, std::optional<FlushReason> flush_reason) {
|
||||
bool resuming_from_bg_err, std::optional<FlushReason> flush_reason,
|
||||
bool wait_for_listener_notifications) {
|
||||
int num = static_cast<int>(cfds.size());
|
||||
// Wait until the compaction completes
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
@@ -3163,7 +3251,13 @@ Status DBImpl::WaitForFlushMemTables(
|
||||
flush_reason.value() != FlushReason::kExternalFileIngestion ||
|
||||
cfds[i]->GetSuperVersion()->imm->GetID() ==
|
||||
cfds[i]->imm()->current()->GetID()) {
|
||||
++num_finished;
|
||||
// When the caller requested it, also wait for the flush-completion
|
||||
// listener callbacks for this CF to finish before counting it as
|
||||
// finished (see FlushOptions::listener_wait).
|
||||
if (!wait_for_listener_notifications ||
|
||||
cfds[i]->imm()->NumPendingFlushNotifications() == 0) {
|
||||
++num_finished;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3227,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_);
|
||||
|
||||
@@ -3253,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);
|
||||
@@ -3278,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();
|
||||
}
|
||||
}
|
||||
@@ -3294,14 +3512,9 @@ void DBImpl::MaybeScheduleFlushOrCompaction() {
|
||||
return;
|
||||
} else if (error_handler_.IsBGWorkStopped() &&
|
||||
!error_handler_.IsRecoveryInProgress()) {
|
||||
// There has been a hard error and this call is not part of the recovery
|
||||
// sequence. Bail out here so we don't get into an endless loop of
|
||||
// scheduling BG work which will again call this function
|
||||
//
|
||||
// Note that a non-recovery flush can still be scheduled if
|
||||
// error_handler_.IsRecoveryInProgress() returns true. We rely on
|
||||
// BackgroundCallFlush() to check flush reason and drop non-recovery
|
||||
// flushes.
|
||||
// Recovery must be able to schedule its flushes while background work is
|
||||
// stopped. New non-recovery requests are rejected by EnqueuePendingFlush,
|
||||
// and BackgroundFlush drops any request queued before the error.
|
||||
return;
|
||||
} else if (shutting_down_.load(std::memory_order_acquire)) {
|
||||
// DB is being deleted; no more background compactions
|
||||
@@ -3504,6 +3717,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();
|
||||
@@ -3531,22 +3765,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.
|
||||
@@ -3554,7 +3803,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) {
|
||||
@@ -3563,6 +3812,10 @@ bool DBImpl::EnqueuePendingFlush(const FlushRequest& flush_req) {
|
||||
if (reject_new_background_jobs_) {
|
||||
return enqueued;
|
||||
}
|
||||
if (error_handler_.IsBGWorkStopped() &&
|
||||
!IsRecoveryFlush(flush_req.flush_reason)) {
|
||||
return enqueued;
|
||||
}
|
||||
if (flush_req.cfd_to_max_mem_id_to_persist.empty()) {
|
||||
return enqueued;
|
||||
}
|
||||
@@ -3601,13 +3854,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3711,8 +3972,8 @@ Status DBImpl::BackgroundFlush(bool* made_progress, JobContext* job_context,
|
||||
|
||||
Status status;
|
||||
*reason = FlushReason::kOthers;
|
||||
// If BG work is stopped due to an error, but a recovery is in progress,
|
||||
// that means this flush is part of the recovery. So allow it to go through
|
||||
// During recovery, allow flush workers to inspect the queue. Non-recovery
|
||||
// requests are dropped below.
|
||||
if (!error_handler_.IsBGWorkStopped()) {
|
||||
if (shutting_down_.load(std::memory_order_acquire)) {
|
||||
status = Status::ShutdownInProgress();
|
||||
@@ -3734,12 +3995,9 @@ Status DBImpl::BackgroundFlush(bool* made_progress, JobContext* job_context,
|
||||
FlushRequest flush_req = PopFirstFromFlushQueue();
|
||||
FlushReason flush_reason = flush_req.flush_reason;
|
||||
if (!error_handler_.GetBGError().ok() && error_handler_.IsBGWorkStopped() &&
|
||||
flush_reason != FlushReason::kErrorRecovery &&
|
||||
flush_reason != FlushReason::kErrorRecoveryRetryFlush) {
|
||||
// Stop non-recovery flush when bg work is stopped
|
||||
// Note that we drop the flush request here.
|
||||
// Recovery thread should schedule further flushes after bg error
|
||||
// is cleared.
|
||||
!IsRecoveryFlush(flush_reason)) {
|
||||
// A request queued before the error can reach a worker before recovery
|
||||
// clears the queue. Drop it; recovery rebuilds flush work for all CFs.
|
||||
status = error_handler_.GetBGError();
|
||||
assert(!status.ok());
|
||||
ROCKS_LOG_BUFFER(log_buffer,
|
||||
@@ -4067,7 +4325,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) {
|
||||
@@ -4109,7 +4367,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 =
|
||||
@@ -4137,6 +4417,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);
|
||||
@@ -4241,13 +4524,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
|
||||
@@ -4288,6 +4576,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) {
|
||||
@@ -4768,7 +5060,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;
|
||||
@@ -5010,6 +5302,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);
|
||||
@@ -5221,6 +5521,11 @@ void DBImpl::InstallSuperVersionAndScheduleWork(
|
||||
cfd->InstallSuperVersion(sv_context, &mutex_,
|
||||
std::move(new_seqno_to_time_mapping));
|
||||
|
||||
// Refresh the seqno->time preserve-window bound used to gate bottommost file
|
||||
// marking, so a file whose largest seqno is still within the preserve window
|
||||
// is not marked for a compaction that cannot zero it out (infinite loop).
|
||||
MaybeUpdatePreserveTimeMinSeqno(cfd);
|
||||
|
||||
// There may be a small data race here. The snapshot tricking bottommost
|
||||
// compaction may already be released here. But assuming there will always be
|
||||
// newer snapshot created and released frequently, the compaction will be
|
||||
@@ -5360,6 +5665,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())) {
|
||||
|
||||
@@ -37,7 +37,7 @@ Status DBImpl::TEST_SwitchWAL() {
|
||||
|
||||
Status DBImpl::TEST_ResumeImpl(DBRecoverContext context) {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
return ResumeImpl(context);
|
||||
return ResumeImpl(context, Env::IOActivity::kFlush);
|
||||
}
|
||||
|
||||
uint64_t DBImpl::TEST_MaxNextLevelOverlappingBytes(
|
||||
@@ -164,6 +164,22 @@ Status DBImpl::TEST_FlushMemTable(ColumnFamilyData* cfd,
|
||||
return FlushMemTable(cfd, flush_opts, FlushReason::kTest);
|
||||
}
|
||||
|
||||
Status DBImpl::TEST_FlushMemTableWithListenerWait(bool allow_write_stall,
|
||||
ColumnFamilyHandle* cfh) {
|
||||
FlushOptions fo;
|
||||
fo.wait = true;
|
||||
fo.listener_wait = true;
|
||||
fo.allow_write_stall = allow_write_stall;
|
||||
ColumnFamilyData* cfd;
|
||||
if (cfh == nullptr) {
|
||||
cfd = default_cf_handle_->cfd();
|
||||
} else {
|
||||
auto cfhi = static_cast_with_check<ColumnFamilyHandleImpl>(cfh);
|
||||
cfd = cfhi->cfd();
|
||||
}
|
||||
return FlushMemTable(cfd, fo, FlushReason::kTest);
|
||||
}
|
||||
|
||||
Status DBImpl::TEST_AtomicFlushMemTables(
|
||||
const autovector<ColumnFamilyData*>& provided_candidate_cfds,
|
||||
const FlushOptions& flush_opts) {
|
||||
@@ -209,6 +225,12 @@ Status DBImpl::TEST_GetBGError() {
|
||||
return error_handler_.GetBGError();
|
||||
}
|
||||
|
||||
void DBImpl::TEST_SetBGError(const IOStatus& error,
|
||||
BackgroundErrorReason reason) {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
error_handler_.SetBGError(error, reason);
|
||||
}
|
||||
|
||||
bool DBImpl::TEST_IsRecoveryInProgress() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
return error_handler_.IsRecoveryInProgress();
|
||||
|
||||
@@ -145,7 +145,7 @@ Status DBImpl::PromoteL0(ColumnFamilyHandle* column_family, int target_level) {
|
||||
f->file_checksum_func_name, f->unique_id,
|
||||
f->compensated_range_deletion_size, f->tail_size,
|
||||
f->user_defined_timestamps_persisted, f->min_timestamp,
|
||||
f->max_timestamp);
|
||||
f->max_timestamp, f->file_open_metadata);
|
||||
}
|
||||
|
||||
status = versions_->LogAndApply(cfd, read_options, write_options, &edit,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -131,21 +131,21 @@ Status DBImplFollower::TryCatchUpWithLeader() {
|
||||
for (auto cfd : cfds_changed) {
|
||||
if (cfd->mem()->GetEarliestSequenceNumber() <
|
||||
versions_->LastSequence()) {
|
||||
// Construct a new memtable with earliest sequence number set to the
|
||||
// last sequence number in the VersionSet. This matters when
|
||||
// DBImpl::MultiCFSnapshot tries to get consistent references
|
||||
// to super versions in a lock free manner, it checks the earliest
|
||||
// sequence number to detect if there was a change in version in
|
||||
// the meantime.
|
||||
MemTable* new_mem = cfd->ConstructNewMemtable(
|
||||
cfd->GetLatestMutableCFOptions(), versions_->LastSequence());
|
||||
cfd->mem()->SetNextLogNumber(cfd->GetLogNumber());
|
||||
cfd->mem()->ConstructFragmentedRangeTombstones();
|
||||
cfd->imm()->Add(cfd->mem(), &job_context.memtables_to_free);
|
||||
new_mem->Ref();
|
||||
cfd->SetMemtable(new_mem);
|
||||
// Seal the active memtable so that its replacement gets its earliest
|
||||
// sequence number set to the last sequence number in the VersionSet.
|
||||
// This matters when DBImpl::MultiCFSnapshot tries to get consistent
|
||||
// references to super versions in a lock free manner, it checks the
|
||||
// earliest sequence number to detect if there was a change in
|
||||
// version in the meantime.
|
||||
SealActiveMemtable(cfd, cfd->GetLogNumber(),
|
||||
versions_->LastSequence(), &job_context);
|
||||
}
|
||||
|
||||
// `cfd->GetLogNumber()` is a safe watermark here, unlike in
|
||||
// DBImplSecondary::TryCatchUpWithPrimary(): this catch-up path never
|
||||
// replays WALs, so the memtables it collects hold nothing that only a
|
||||
// not-yet-installed Version could make readable.
|
||||
//
|
||||
// This will check if the old memtable is still referenced
|
||||
cfd->imm()->RemoveOldMemTables(cfd->GetLogNumber(),
|
||||
&job_context.memtables_to_free);
|
||||
|
||||
@@ -243,6 +243,11 @@ Status DBImpl::ValidateOptions(
|
||||
}
|
||||
|
||||
Status DBImpl::ValidateOptions(const DBOptions& db_options) {
|
||||
if (db_options.read_io_executor_threads <= 0) {
|
||||
return Status::InvalidArgument(
|
||||
"read_io_executor_threads must be greater than zero");
|
||||
}
|
||||
|
||||
if (db_options.db_paths.size() > 4) {
|
||||
return Status::NotSupported(
|
||||
"More than four DB paths are not supported yet. ");
|
||||
@@ -683,7 +688,8 @@ Status DBImpl::Recover(
|
||||
f->file_checksum, f->file_checksum_func_name,
|
||||
f->unique_id, f->compensated_range_deletion_size,
|
||||
f->tail_size, f->user_defined_timestamps_persisted,
|
||||
f->min_timestamp, f->max_timestamp);
|
||||
f->min_timestamp, f->max_timestamp,
|
||||
f->file_open_metadata);
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"[%s] Moving #%" PRIu64
|
||||
" from from_level-%d to from_level-%d %" PRIu64
|
||||
@@ -1028,7 +1034,7 @@ Status DBImpl::InitPersistStatsColumnFamily() {
|
||||
return s;
|
||||
}
|
||||
|
||||
Status DBImpl::LogAndApplyForRecovery(const RecoveryContext& recovery_ctx) {
|
||||
Status DBImpl::LogAndApplyForRecovery(RecoveryContext& recovery_ctx) {
|
||||
mutex_.AssertHeld();
|
||||
// descriptor_log_ is normally null after Recover, but when
|
||||
// reuse_manifest_on_open is set VersionSet::Recover may have already
|
||||
@@ -1038,9 +1044,21 @@ Status DBImpl::LogAndApplyForRecovery(const RecoveryContext& recovery_ctx) {
|
||||
const ReadOptions read_options(Env::IOActivity::kDBOpen);
|
||||
const WriteOptions write_options(Env::IOActivity::kDBOpen);
|
||||
|
||||
if (versions_->force_new_manifest_on_open_ &&
|
||||
!recovery_ctx.HasVersionEdits()) {
|
||||
VersionEdit edit;
|
||||
ColumnFamilyData* default_cfd =
|
||||
versions_->GetColumnFamilySet()->GetDefault();
|
||||
assert(default_cfd);
|
||||
recovery_ctx.UpdateVersionEdits(default_cfd, edit);
|
||||
}
|
||||
|
||||
Status s = versions_->LogAndApply(recovery_ctx.cfds_, read_options,
|
||||
write_options, recovery_ctx.edit_lists_,
|
||||
&mutex_, directories_.GetDbDir());
|
||||
if (s.ok()) {
|
||||
versions_->force_new_manifest_on_open_ = false;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -2170,7 +2188,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;
|
||||
@@ -2871,11 +2889,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();
|
||||
@@ -2965,6 +2988,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 {
|
||||
|
||||
Loaded 100 of 408 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user