mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-07 11:32:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a3d362471 | ||
|
|
987e62e576 | ||
|
|
77bd1f3815 | ||
|
|
b7b8326c89 | ||
|
|
ed73bb989e | ||
|
|
951647e596 | ||
|
|
8941ff459d | ||
|
|
1aaec657a5 | ||
|
|
323d915dbc | ||
|
|
2dc6bc51b4 | ||
|
|
7217bf34a7 | ||
|
|
58d835d951 | ||
|
|
276495843d | ||
|
|
ec5106b3b8 | ||
|
|
f9e4c5c51f | ||
|
|
fcf382c95c | ||
|
|
66d7035708 | ||
|
|
9381544cd9 | ||
|
|
f8c601b91c | ||
|
|
5dfde1c262 | ||
|
|
44e0294c1d | ||
|
|
3f4d840e1a | ||
|
|
3949208acf | ||
|
|
88752298ff | ||
|
|
1dfa729793 | ||
|
|
8e1569b351 | ||
|
|
5fe225357a | ||
|
|
e8bbe08760 | ||
|
|
fd7399004f | ||
|
|
d1fbdffdae | ||
|
|
dc4006480b | ||
|
|
af0eae4de3 | ||
|
|
dbf9328c71 | ||
|
|
751bde0f7f | ||
|
|
14ecf7f13c | ||
|
|
b1b50fe102 | ||
|
|
cda4a73a19 | ||
|
|
00e73b99a2 | ||
|
|
460aea1523 | ||
|
|
4b35e9966c | ||
|
|
b7b6d7c8c0 | ||
|
|
afc922bf5f | ||
|
|
6e73763b8a | ||
|
|
f9df618e4d | ||
|
|
026f63de6c | ||
|
|
e2ccb78cf2 | ||
|
|
c64f682946 | ||
|
|
359d57be96 | ||
|
|
ba81144d8d | ||
|
|
4b9070bccb |
No files matched your search
@@ -20,9 +20,15 @@ runs:
|
||||
echo "GTEST_COLOR=1" >> "$GITHUB_ENV"
|
||||
echo "CTEST_OUTPUT_ON_FAILURE=1" >> "$GITHUB_ENV"
|
||||
echo "CTEST_TEST_TIMEOUT=300" >> "$GITHUB_ENV"
|
||||
echo "ZLIB_DOWNLOAD_BASE=https://rocksdb-deps.s3.us-west-2.amazonaws.com/pkgs/zlib" >> "$GITHUB_ENV"
|
||||
echo "BZIP2_DOWNLOAD_BASE=https://rocksdb-deps.s3.us-west-2.amazonaws.com/pkgs/bzip2" >> "$GITHUB_ENV"
|
||||
echo "SNAPPY_DOWNLOAD_BASE=https://rocksdb-deps.s3.us-west-2.amazonaws.com/pkgs/snappy" >> "$GITHUB_ENV"
|
||||
echo "LZ4_DOWNLOAD_BASE=https://rocksdb-deps.s3.us-west-2.amazonaws.com/pkgs/lz4" >> "$GITHUB_ENV"
|
||||
echo "ZSTD_DOWNLOAD_BASE=https://rocksdb-deps.s3.us-west-2.amazonaws.com/pkgs/zstd" >> "$GITHUB_ENV"
|
||||
# These third-party sources are mirrored on the "third-party-deps" GitHub
|
||||
# release. To add one, upload the asset here (intentionally hidden):
|
||||
# https://github.com/facebook/rocksdb/releases/edit/third-party-deps
|
||||
# Note how file names are generated below (vs. from the original source)
|
||||
# before uploading. Removing or modifying files will break builds for
|
||||
# older releases/revisions (not found or checksum mismatch).
|
||||
echo "ZLIB_DOWNLOAD_PREFIX=https://github.com/facebook/rocksdb/releases/download/third-party-deps/zlib-" >> "$GITHUB_ENV"
|
||||
echo "BZIP2_DOWNLOAD_PREFIX=https://github.com/facebook/rocksdb/releases/download/third-party-deps/bzip2-" >> "$GITHUB_ENV"
|
||||
echo "SNAPPY_DOWNLOAD_PREFIX=https://github.com/facebook/rocksdb/releases/download/third-party-deps/snappy-" >> "$GITHUB_ENV"
|
||||
echo "LZ4_DOWNLOAD_PREFIX=https://github.com/facebook/rocksdb/releases/download/third-party-deps/lz4-" >> "$GITHUB_ENV"
|
||||
echo "ZSTD_DOWNLOAD_PREFIX=https://github.com/facebook/rocksdb/releases/download/third-party-deps/zstd-" >> "$GITHUB_ENV"
|
||||
shell: bash
|
||||
@@ -5,23 +5,40 @@
|
||||
// return build({ icon, headerTitle, triggerLine, responseBody, footerLines
|
||||
// });
|
||||
|
||||
// GitHub rejects issue comments longer than this many characters. The model
|
||||
// response (responseBody) is the only unbounded input, so we trim it to fit
|
||||
// while keeping the fixed scaffolding (header, trigger, footer) intact.
|
||||
const MAX_COMMENT_LENGTH = 65536;
|
||||
const TRUNCATION_NOTE =
|
||||
'\n\n*[Truncated — full output in execution log artifact]*';
|
||||
|
||||
module.exports = function buildAiReviewComment(
|
||||
{icon, headerTitle, triggerLine, responseBody, footerLines}) {
|
||||
return [
|
||||
`## ${icon} ${headerTitle}`,
|
||||
'',
|
||||
triggerLine,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
responseBody,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>ℹ️ About this response</summary>',
|
||||
'',
|
||||
...footerLines,
|
||||
'</details>',
|
||||
const assemble = (body) => [`## ${icon} ${headerTitle}`,
|
||||
'',
|
||||
triggerLine,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
body,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>ℹ️ About this response</summary>',
|
||||
'',
|
||||
...footerLines,
|
||||
'</details>',
|
||||
].join('\n');
|
||||
|
||||
const body = responseBody || '';
|
||||
const comment = assemble(body);
|
||||
if (comment.length <= MAX_COMMENT_LENGTH) {
|
||||
return comment;
|
||||
}
|
||||
// Drop only as much of the body as needed to fit, then re-assemble so the
|
||||
// footer/<details> block is preserved rather than cut off mid-comment.
|
||||
const overflow = comment.length - MAX_COMMENT_LENGTH;
|
||||
const keep = Math.max(0, body.length - overflow - TRUNCATION_NOTE.length);
|
||||
return assemble(body.substring(0, keep) + TRUNCATION_NOTE);
|
||||
};
|
||||
@@ -103,7 +103,7 @@ module.exports = function parseClaude({executionFile, conclusion, meta}) {
|
||||
responseBody,
|
||||
footerLines: [
|
||||
'Generated by [Claude Code](https://github.com/anthropics/claude-code-action).',
|
||||
'Review methodology: `claude_md/code_review.md`',
|
||||
'Review methodology: `claude_md/ci_review_prompt.md`',
|
||||
'',
|
||||
'**Limitations:**',
|
||||
'- Claude may miss context from files not in the diff',
|
||||
|
||||
@@ -83,7 +83,7 @@ module.exports = function parseCodex(
|
||||
responseBody,
|
||||
footerLines: [
|
||||
'Generated by Codex CLI.',
|
||||
'Review methodology: `claude_md/code_review.md`',
|
||||
'Review methodology: `claude_md/ci_review_prompt.md`',
|
||||
'',
|
||||
'**Limitations:**',
|
||||
'- Codex may miss context from files not in the diff',
|
||||
|
||||
@@ -140,7 +140,17 @@ jobs:
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: sudo apt-get update && sudo apt-get install -y build-essential libgflags-dev libsnappy-dev zlib1g-dev libbz2-dev liblz4-dev libzstd-dev
|
||||
- run: sudo mount -o remount,size=16G /dev/shm
|
||||
- run: sudo dd bs=1048576 count=4096 if=/dev/zero of=/swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
|
||||
- name: Ensure swap space
|
||||
run: |-
|
||||
if sudo swapon --show=NAME --noheadings --raw | grep -Fxq /swapfile; then
|
||||
echo "/swapfile is already active"
|
||||
else
|
||||
sudo dd bs=1048576 count=4096 if=/dev/zero of=/swapfile
|
||||
sudo chmod 600 /swapfile
|
||||
sudo mkswap /swapfile
|
||||
sudo swapon /swapfile
|
||||
fi
|
||||
sudo swapon --show
|
||||
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=1800 --max_key=2500000' blackbox_crash_test_with_atomic_flush
|
||||
- run: rm -rf /dev/shm/rocksdb.*
|
||||
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=1800 --max_key=2500000' blackbox_crash_test_with_multiops_wc_txn
|
||||
|
||||
@@ -65,7 +65,9 @@ jobs:
|
||||
pip install https://files.pythonhosted.org/packages/fb/ac/3c04772acc0257f5730e83adb542b2603c1a62d1315010ab593a980af404/clang_format-21.1.2-py2.py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
|
||||
clang-format --version
|
||||
- name: Download clang-format-diff.py
|
||||
run: wget https://rocksdb-deps.s3.us-west-2.amazonaws.com/llvm/llvm-project/release/12.x/clang/tools/clang-format/clang-format-diff.py
|
||||
# Mirrored on the "third-party-deps" GitHub release; add/update via
|
||||
# https://github.com/facebook/rocksdb/releases/edit/third-party-deps
|
||||
run: wget --tries=3 --retry-on-host-error https://github.com/facebook/rocksdb/releases/download/third-party-deps/clang-format-diff.py
|
||||
- name: Check format
|
||||
run: VERBOSE_CHECK=1 make check-format
|
||||
- name: Compare buckify output
|
||||
@@ -320,16 +322,46 @@ jobs:
|
||||
# Run the make target in STRICT mode so this core CI job HARD-FAILS rather
|
||||
# than silently skipping if a prerequisite (clang++, clang-format, or the
|
||||
# compat baseline ref) is ever missing. See `make check-c-api-gen`.
|
||||
env:
|
||||
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
PUSH_BEFORE_SHA: ${{ github.event.before }}
|
||||
run: |
|
||||
# The container checkout is owned by a different user, so git refuses to
|
||||
# operate on it ("dubious ownership") -- mark it safe for the `git fetch`
|
||||
# and the `git show` the compat checker runs.
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
git config --global --add safe.directory '*'
|
||||
git fetch --no-tags --depth=1 origin main
|
||||
|
||||
# A PR must preserve the API of its merge target, while a push must
|
||||
# preserve the API of the branch before the push. Comparing every event
|
||||
# with main produces false removals when a release branch predates APIs
|
||||
# added to main.
|
||||
case "$GITHUB_EVENT_NAME" in
|
||||
pull_request) api_compat_ref="$PR_BASE_SHA" ;;
|
||||
push) api_compat_ref="$PUSH_BEFORE_SHA" ;;
|
||||
*)
|
||||
echo "ERROR: unsupported event for C API compatibility: $GITHUB_EVENT_NAME" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$api_compat_ref" ]; then
|
||||
echo "ERROR: C API compatibility baseline is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
zero_sha=0000000000000000000000000000000000000000
|
||||
if [ "$api_compat_ref" = "$zero_sha" ]; then
|
||||
# A newly created branch or tag has no prior ref state to preserve.
|
||||
api_compat_ref=HEAD
|
||||
else
|
||||
git fetch --no-tags --depth=1 origin "$api_compat_ref"
|
||||
api_compat_ref=FETCH_HEAD
|
||||
fi
|
||||
|
||||
CXX=clang++-21 make check-c-api-gen \
|
||||
CHECK_C_API_GEN_STRICT=1 \
|
||||
API_COMPAT_REF=FETCH_HEAD \
|
||||
API_COMPAT_REF="$api_compat_ref" \
|
||||
CLANG_FORMAT_BINARY=clang-format-21
|
||||
- uses: "./.github/actions/teardown-ccache"
|
||||
if: always()
|
||||
@@ -394,13 +426,20 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: mini-crashtest
|
||||
- uses: "./.github/actions/cache-folly"
|
||||
id: cache-folly
|
||||
- uses: "./.github/actions/build-folly"
|
||||
with:
|
||||
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
|
||||
- run: |
|
||||
ulimit -S -n $(ulimit -Hn) # Raise open files soft limit to hard limit
|
||||
echo "Updated ulimit -Hn: $(ulimit -Hn) -Sn: $(ulimit -Sn)"
|
||||
make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=${{ matrix.crash_duration }} --max_key=2500000' ${{ matrix.crash_test_target }}
|
||||
USE_COROUTINES=1 USE_FOLLY=1 LIB_MODE=static make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=${{ matrix.crash_duration }} --max_key=2500000' ${{ matrix.crash_test_target }}
|
||||
- uses: "./.github/actions/teardown-ccache"
|
||||
if: always()
|
||||
- uses: "./.github/actions/post-steps"
|
||||
|
||||
@@ -59,6 +59,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/compaction/sst_partitioner.cc",
|
||||
"db/compaction/subcompaction_state.cc",
|
||||
"db/convenience.cc",
|
||||
"db/coro_db.cc",
|
||||
"db/db_filesnapshot.cc",
|
||||
"db/db_impl/compacted_db_impl.cc",
|
||||
"db/db_impl/db_impl.cc",
|
||||
@@ -103,7 +104,6 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/snapshot_impl.cc",
|
||||
"db/table_cache.cc",
|
||||
"db/table_properties_collector.cc",
|
||||
"db/transaction_log_impl.cc",
|
||||
"db/trim_history_scheduler.cc",
|
||||
"db/version_builder.cc",
|
||||
"db/version_edit.cc",
|
||||
@@ -111,7 +111,9 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/version_set.cc",
|
||||
"db/version_util.cc",
|
||||
"db/wal_edit.cc",
|
||||
"db/wal_iterator_impl.cc",
|
||||
"db/wal_manager.cc",
|
||||
"db/wide/lazy_wide_columns.cc",
|
||||
"db/wide/read_path_blob_resolver.cc",
|
||||
"db/wide/wide_column_serialization.cc",
|
||||
"db/wide/wide_columns.cc",
|
||||
@@ -4947,6 +4949,12 @@ cpp_unittest_wrapper(name="db_kv_checksum_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_lazy_entity_test",
|
||||
srcs=["db/wide/db_lazy_entity_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_log_iter_test",
|
||||
srcs=["db/db_log_iter_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
|
||||
@@ -55,7 +55,7 @@ This document provides guidance for generating and reviewing code in the RocksDB
|
||||
|
||||
**API Consistency:** New APIs should be consistent with existing patterns. Use similar naming conventions, parameter ordering, and return types. Reviewers will suggest changes to improve consistency with the broader codebase.
|
||||
|
||||
**Documentation:** Public APIs must be thoroughly documented. Include usage examples, parameter descriptions, and notes on thread safety, performance characteristics, and compatibility considerations.
|
||||
**Documentation:** Public APIs must be thoroughly documented, without unnecessary embelishment nor dwelling on implementation details nor project planning. When non-obvious, include usage examples, parameter descriptions, known bugs or limitations, notes on thread safety, performance characteristics, and compatibility considerations. Re-read comments for ambiguous terminology and phrasing, such as ambiguously re-purposed programming jargon.
|
||||
|
||||
---
|
||||
|
||||
@@ -259,6 +259,10 @@ will automatically clean object files if the build parameters/flavor have
|
||||
changed. The `build_tools/rockstest.sh` / `rocksptest.sh` helpers described
|
||||
below set `AUTO_CLEAN=1` for you.
|
||||
|
||||
`AUTO_CLEAN=1` does not fix Make failures associated with stale .d files
|
||||
referring to removed files. Resolve that manually or with `make clean`
|
||||
without complaining to the user.
|
||||
|
||||
### Source checks
|
||||
* Run `make check-sources` before committing. This catches non-ASCII
|
||||
characters in source files and other source-level issues that CI will
|
||||
@@ -396,6 +400,11 @@ architectures.
|
||||
|
||||
### Adding release note
|
||||
* Release note should be kept short at high level for external user consumption.
|
||||
Release notes identify what users might care about most in a release. They
|
||||
are not exhaustive and are not a guide. PLEASE learn from past agents who
|
||||
ried to build elaborate release notes with implementation details and
|
||||
elsewhere-documented nuance. That wastes time. Fight the bias that
|
||||
"my change" is important so must be worthy of release note mention.
|
||||
* If more than single markdown line, consider how their formatting will be
|
||||
integrated into HISTORY.md.
|
||||
|
||||
|
||||
+4
-1
@@ -801,6 +801,7 @@ set(SOURCES
|
||||
db/compaction/sst_partitioner.cc
|
||||
db/compaction/subcompaction_state.cc
|
||||
db/convenience.cc
|
||||
db/coro_db.cc
|
||||
db/db_filesnapshot.cc
|
||||
db/db_impl/compacted_db_impl.cc
|
||||
db/db_impl/db_impl.cc
|
||||
@@ -845,7 +846,6 @@ set(SOURCES
|
||||
db/snapshot_impl.cc
|
||||
db/table_cache.cc
|
||||
db/table_properties_collector.cc
|
||||
db/transaction_log_impl.cc
|
||||
db/trim_history_scheduler.cc
|
||||
db/version_builder.cc
|
||||
db/version_edit.cc
|
||||
@@ -853,7 +853,9 @@ set(SOURCES
|
||||
db/version_set.cc
|
||||
db/version_util.cc
|
||||
db/wal_edit.cc
|
||||
db/wal_iterator_impl.cc
|
||||
db/wal_manager.cc
|
||||
db/wide/lazy_wide_columns.cc
|
||||
db/wide/read_path_blob_resolver.cc
|
||||
db/wide/wide_column_serialization.cc
|
||||
db/wide/wide_columns.cc
|
||||
@@ -1560,6 +1562,7 @@ if(WITH_TESTS)
|
||||
db/version_set_test.cc
|
||||
db/wal_manager_test.cc
|
||||
db/wal_edit_test.cc
|
||||
db/wide/db_lazy_entity_test.cc
|
||||
db/wide/db_wide_basic_test.cc
|
||||
db/wide/db_wide_blob_direct_write_test.cc
|
||||
db/wide/wide_column_serialization_test.cc
|
||||
|
||||
+24
-1
@@ -1,9 +1,32 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 11.8.1 (07/28/2026)
|
||||
## 11.9.1 (08/18/2026)
|
||||
### Public API Changes
|
||||
* Added an immutable `external_table_config` option that is persisted in OPTIONS files and passed to external table factories during bootstrap.
|
||||
|
||||
## 11.9.0 (08/14/2026)
|
||||
### New Features
|
||||
* Added an integrated and improved compression auto-skip feature (new CompressionOptions, still experimental) that saves CPU by skipping compression when and where the overall ratio is inadequate. This improves on the previous auto-skip compression manager by (a) taking achieved compression ratios into account, (b) using a continuously evolving payoff estimate, and (c) combining natively with parallel compression.
|
||||
* Added the `blob_file_writable_file_max_buffer_size` mutable column family option. When set to a non-zero value, blob-file writers use it as their `WritableFileWriter` max buffer size instead of inheriting `DBOptions::writable_file_max_buffer_size`.
|
||||
* Added an experimental lazy wide-column read API, `DB::GetEntityLazy()` / `DB::MultiGetEntityLazy()` (see `include/rocksdb/lazy_wide_columns.h`). The returned `LazyWideColumns` leaves blob-backed columns as unresolved references and, like an iterator, stays valid after the call returns, so the caller resolves only the columns -- and only the byte ranges -- it needs; a byte-range read of an uncompressed blob (separate-file or embedded) reads just those bytes from storage. Adds `Env::IOActivity::kLazyResolve` and `rocksdb.blobdb.lazy.*` statistics. Requires `max_open_files == -1`.
|
||||
* Added DB property `rocksdb.num-running-remote-compactions`, reporting the number of compaction service jobs currently waiting in `CompactionService::Wait()`.
|
||||
* Added DB property `rocksdb.num-unscheduled-compactions`, reporting compactions waiting in the DB's internal compaction queue but not yet assigned to a background job.
|
||||
* Added `DB::AbortCompactions()` and `DB::ResumeCompactions()` for per-column-family compaction abort and resume, with the same active-abort semantics as the DB-wide `AbortAllCompactions()`/`ResumeAllCompactions()` but scoped to a single column family. Calls are reference-counted independently per column family and compose with the DB-wide abort.
|
||||
|
||||
### Public API Changes
|
||||
* Add IndexFactory compatibility names for the existing user-defined index API.
|
||||
* Added the experimental `CoroDB` capability, exposed by `DB::GetCoroDB()`, with lazy native coroutine variants of `Get` and `MultiGet`. The existing `DB::GetAsync()` and `DB::MultiGetAsync()` APIs dispatch through it.
|
||||
* Added `rocksdb_batched_multi_get_pinned_cf()` to return batch-owned pinned MultiGet values and per-key errors without allocating one wrapper per found value.
|
||||
* Add `DBOptions::read_io_executor_threads` to increase the maximum thread count of the shared filesystem read I/O executor used for asynchronous reads when opening a DB. Opening a DB never reduces the shared executor thread count. Change the experimental `FSRandomAccessFile::SubmitReadAsync()` API to return whether the asynchronous path was used, and add the `FILE_SUBMIT_ASYNC_READ_FALLBACK` ticker to count calls that fall back to synchronous reads.
|
||||
* `ReadOptions::table_filter` is now a *pointer* to a caller-owned `std::function` instead of a `std::function` value, to improve efficiency in copying and destroying `ReadOptions` objects (which RocksDB does internally). This is a breaking change for users of this relatively obscure option, who will now need to declare a `std::function` that outlives the read. As a side effect of `ReadOptions` being trivially copyable, new but legitimate compiler warnings may surface.
|
||||
* Removed the experimental `CreateAutoSkipCompressionManager()`. Auto-skip compression is now a first-class feature of the block-based table builder, controlled by the new experimental `CompressionOptions::auto_skip`.
|
||||
* Added `CoroStackableDB` for implementing wrappers over native coroutine reads and used it to add coroutine `Get` and `MultiGet` support to `DBWithTTL` and WritePrepared `TransactionDB`. A stackable wrapper advertises coroutine support only when its wrapped DB supports it; TTL value decoding and transaction visibility semantics are preserved.
|
||||
* Added a `kSstFileWriter` value to `TableFileCreationReason`, which `SstFileWriter` now reports instead of the catch-all `kMisc`. This, along with new `db_name` and `is_remote_compaction` fields in `FilterBuildingContext`, improves the contextual information available to custom filter and compression strategies.
|
||||
* Renamed `TransactionLogIterator` to `WalIterator` (and `include/rocksdb/transaction_log.h` to `include/rocksdb/wal_iterator.h`), because it iterates the write-ahead log and is unrelated to `TransactionDB`. The old names still work. `DB::GetUpdatesSince()` is unchanged. In Java, `WalIterator` is the new name and `TransactionLogIterator` remains as a subclass. Also documented existing behavior that was previously unstated: only writes that reached the WAL are visible (so `WriteOptions::disableWAL` and `IngestExternalFile()` leave holes that end iteration), what each iterator state means, and that `GetUpdatesSince()` silently starts later when the requested sequence number is unavailable.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a bug where `BlobFileBuilder` passed an SST path to `OnBlobFileCompleted()` instead of the completed blob path during BlobDB garbage-collection compactions, causing incorrect `SstFileManager` accounting and compaction failures on filesystems where the SST was not yet pathname-visible.
|
||||
|
||||
### Performance Improvements
|
||||
* When `DBOptions::avoid_unnecessary_blocking_io` is true, obsolete `OPTIONS-*` files found during DB open are deleted by background purge instead of synchronously on the opening thread.
|
||||
|
||||
@@ -86,11 +86,20 @@ else
|
||||
LIB_MODE?=shared
|
||||
endif
|
||||
|
||||
$(info $$DEBUG_LEVEL is $(DEBUG_LEVEL), $$LIB_MODE is $(LIB_MODE))
|
||||
# Set by the gen_check_scripts wrappers to mark a recursive make reusing the
|
||||
# parent's make_config.mk: silence the parse-time banners and skip
|
||||
# build_detect_platform (below).
|
||||
$(if $(ROCKSDB_RECURSIVE_MAKE_SAME_CONFIG),,$(info $$DEBUG_LEVEL is $(DEBUG_LEVEL), $$LIB_MODE is $(LIB_MODE)))
|
||||
|
||||
# Detect what platform we're building on.
|
||||
# Export some common variables that might have been passed as Make variables
|
||||
# instead of environment variables.
|
||||
#
|
||||
# Skipped by the gen_check_scripts wrappers (which run concurrently under
|
||||
# gnu_parallel): each would otherwise rewrite the shared make_config.mk
|
||||
# non-atomically and a concurrent reader could catch it mid-write ("missing
|
||||
# separator"). Requires an existing make_config.mk, so a normal build still detects.
|
||||
ifeq ($(and $(ROCKSDB_RECURSIVE_MAKE_SAME_CONFIG),$(wildcard make_config.mk)),)
|
||||
dummy := $(shell (export ROCKSDB_ROOT="$(CURDIR)"; \
|
||||
export CXXFLAGS="$(EXTRA_CXXFLAGS)"; \
|
||||
export LDFLAGS="$(EXTRA_LDFLAGS)"; \
|
||||
@@ -104,8 +113,9 @@ dummy := $(shell (export ROCKSDB_ROOT="$(CURDIR)"; \
|
||||
export ROCKSDB_CXX_STANDARD="$(ROCKSDB_CXX_STANDARD)"; \
|
||||
export USE_FOLLY="$(USE_FOLLY)"; \
|
||||
export USE_FOLLY_LITE="$(USE_FOLLY_LITE)"; \
|
||||
"$(CURDIR)/build_tools/build_detect_platform" "$(CURDIR)/make_config.mk"))
|
||||
# this file is generated by the previous line to set build flags and sources
|
||||
"$(CURDIR)/build_tools/build_detect_platform" "$(CURDIR)/make_config.mk" $(if $(ROCKSDB_RECURSIVE_MAKE_SAME_CONFIG),2>/dev/null)))
|
||||
endif
|
||||
# this file (generated above, or reused when skipped) sets build flags and sources
|
||||
include make_config.mk
|
||||
|
||||
# Figure out optimize level.
|
||||
@@ -181,7 +191,7 @@ ifeq ($(filter -DROCKSDB_ASSERT_STATUS_CHECKED,$(OPT)),)
|
||||
endif
|
||||
endif
|
||||
|
||||
$(warning Warning: Compiling in debug mode. Don't use the resulting binary in production)
|
||||
$(if $(ROCKSDB_RECURSIVE_MAKE_SAME_CONFIG),,$(warning Warning: Compiling in debug mode. Don't use the resulting binary in production))
|
||||
endif
|
||||
|
||||
# `USE_LTO=1` enables link-time optimizations. Among other things, this enables
|
||||
@@ -579,9 +589,12 @@ TESTS += $(PLUGIN_TESTS)
|
||||
ifneq ($(filter check-headers, $(MAKECMDGOALS)),)
|
||||
# TODO: add/support JNI headers
|
||||
DEV_HEADER_DIRS := $(sort include/ $(dir $(ALL_SOURCES)))
|
||||
FOLLY_DEPENDENT_HEADERS := \
|
||||
include/rocksdb/coro_db.h \
|
||||
include/rocksdb/utilities/coro_stackable_db.h
|
||||
# Some headers like in port/ are platform-specific
|
||||
DEV_HEADERS_TO_CHECK := $(shell $(FIND) $(DEV_HEADER_DIRS) -type f -name '*.h' | grep -E -v 'port/|plugin/|range_tree/|secondary_index/')
|
||||
PUBLIC_HEADERS_TO_CHECK := $(shell $(FIND) include/ -type f -name '*.h')
|
||||
DEV_HEADERS_TO_CHECK := $(filter-out $(FOLLY_DEPENDENT_HEADERS), $(shell $(FIND) $(DEV_HEADER_DIRS) -type f -name '*.h' | grep -E -v 'port/|plugin/|range_tree/|secondary_index/'))
|
||||
PUBLIC_HEADERS_TO_CHECK := $(filter-out $(FOLLY_DEPENDENT_HEADERS), $(shell $(FIND) include/ -type f -name '*.h'))
|
||||
else
|
||||
DEV_HEADERS_TO_CHECK :=
|
||||
PUBLIC_HEADERS_TO_CHECK :=
|
||||
@@ -949,6 +962,57 @@ gen_parallel_tests:
|
||||
$(AM_V_at)$(FIND) t -type f -name 'run-*' -exec rm -f {} \;
|
||||
$(MAKE) $(parallel_tests)
|
||||
|
||||
# Auxiliary (non-unit-test) checks that are part of `make check`. Kept as a
|
||||
# single list so the same set can run as parallel jobs alongside the C++ unit
|
||||
# tests (see gen_check_scripts + check_0) and serially in the J=1 fallback.
|
||||
CHECK_EXTRA_TARGETS =
|
||||
ifneq ($(PLATFORM), OS_AIX)
|
||||
CHECK_EXTRA_TARGETS += check-all-python
|
||||
ifndef ASSERT_STATUS_CHECKED # not yet working with these tests
|
||||
CHECK_EXTRA_TARGETS += ldb_tests db_crashtest_tests rocksdb_dump_test
|
||||
endif
|
||||
endif
|
||||
ifndef SKIP_FORMAT_BUCK_CHECKS
|
||||
CHECK_EXTRA_TARGETS += check-format check-buck-targets check-sources check-workflow-yaml check-c-api-gen
|
||||
endif
|
||||
|
||||
# Generate a t/run-<target> wrapper per enabled auxiliary check; check_0's
|
||||
# `$(FIND) t -name 'run-*'` then runs them as gnu_parallel jobs alongside the
|
||||
# unit tests, each framed with a gtest-style pass/fail line. Under CI sharding,
|
||||
# round-robin across shards so each check runs on exactly one shard. Kept
|
||||
# separate from gen_parallel_tests (which cleans t/run-* first) so the valgrind_*
|
||||
# targets, which also glob t/run-*, don't pick these up.
|
||||
.PHONY: gen_check_scripts
|
||||
gen_check_scripts:
|
||||
$(AM_V_at)mkdir -p t; \
|
||||
idx=0; \
|
||||
for tgt in $(CHECK_EXTRA_TARGETS); do \
|
||||
if [ -z "$(CI_TOTAL_SHARDS)" ] || [ $$(( idx % $(CI_TOTAL_SHARDS) )) -eq $(CI_SHARD_INDEX) ]; then \
|
||||
s=t/run-$$tgt; \
|
||||
printf '%s\n' \
|
||||
'#!/bin/sh' \
|
||||
'# Generated by `make gen_check_scripts`.' \
|
||||
"t=$$tgt" \
|
||||
'# Drop the parent jobserver from MAKEFLAGS (its fds are lost through' \
|
||||
'# gnu_parallel, causing a spurious warning); keep overrides after "--".' \
|
||||
'MAKEFLAGS=$$(printf "%s" "$$MAKEFLAGS" | sed -e "s/ --jobserver-auth=[^ ]*//g" -e "s/ --jobserver-fds=[^ ]*//g" -e "s/ -j[0-9]*//g")' \
|
||||
'export MAKEFLAGS' \
|
||||
'echo; echo "[----------]"; echo "[ RUN ] >>> make $$t <<<"' \
|
||||
'$(MAKE) --no-print-directory ROCKSDB_RECURSIVE_MAKE_SAME_CONFIG=1 "$$t"' \
|
||||
'rc=$$?' \
|
||||
'if [ "$$rc" = 0 ]; then' \
|
||||
' echo "[ OK ] >>> make $$t <<<"' \
|
||||
'else' \
|
||||
' echo "[ FAILED ] >>> make $$t <<<"' \
|
||||
'fi' \
|
||||
'echo "[----------]"' \
|
||||
'exit $$rc' \
|
||||
> $$s; \
|
||||
chmod a=rx $$s; \
|
||||
fi; \
|
||||
idx=$$((idx + 1)); \
|
||||
done
|
||||
|
||||
# Reorder input lines (one per test or per shard) so the longest-running
|
||||
# tests/shards start first under "make check" parallel scheduling. The trick
|
||||
# is to prefix matched names with a fake duration (100), sort numerically
|
||||
@@ -976,7 +1040,8 @@ gen_parallel_tests:
|
||||
slow_test_regexp = \
|
||||
^.*point_lock_manager_stress_test.*$$|^.*db_test.*$$|^.*external_sst_file_test.*$$|^.*compaction_service_test.*$$|^.*corruption_test.*$$|^.*comparator_db_test.*$$|^.*external_sst_file_basic_test.*$$|^.*rate_limiter_test.*$$|^.*db_compaction_test.*$$|^.*write_prepared_transaction_test.*$$|^.*db_merge_operator_test.*$$|\
|
||||
^.*db_dynamic_level_test.*$$|^.*db_bloom_filter_test.*$$|^.*error_handler_fs_test.*$$|^.*merge_helper_test.*$$|^.*transaction_test.*$$|^.*db_kv_checksum_test.*$$|^.*inlineskiplist_test.*$$|\
|
||||
^.*db_with_timestamp_basic_test.*$$|^.*table_test.*$$|^.*db_wal_test.*$$|^.*block_based_table_reader_test.*$$|^.*block_test.*$$
|
||||
^.*db_with_timestamp_basic_test.*$$|^.*table_test.*$$|^.*db_wal_test.*$$|^.*block_based_table_reader_test.*$$|^.*block_test.*$$|\
|
||||
^.*run-check-c-api-gen.*$$|^.*run-check-format.*$$
|
||||
prioritize_long_running_tests = \
|
||||
perl -pe 's,($(slow_test_regexp)),100 $$1,' \
|
||||
| sort -k1,1gr \
|
||||
@@ -1108,27 +1173,17 @@ check: all
|
||||
&& (build_tools/gnu_parallel --gnu --help 2>/dev/null) | \
|
||||
grep -q 'GNU Parallel'; \
|
||||
then \
|
||||
$(MAKE) gen_check_scripts; \
|
||||
$(MAKE) T="$$t" check_0; \
|
||||
else \
|
||||
for t in $(TESTS); do \
|
||||
echo "===== Running $$t (`date`)"; ./$$t || exit 1; done; \
|
||||
for t in $(CHECK_EXTRA_TARGETS); do \
|
||||
echo "===== Running $$t (`date`)"; $(MAKE) $$t || exit 1; done; \
|
||||
fi
|
||||
rm -rf $(TEST_TMPDIR)
|
||||
ifneq ($(PLATFORM), OS_AIX)
|
||||
$(PYTHON) tools/check_all_python.py
|
||||
ifndef ASSERT_STATUS_CHECKED # not yet working with these tests
|
||||
$(PYTHON) tools/ldb_test.py
|
||||
$(PYTHON) tools/db_crashtest_test.py
|
||||
sh tools/rocksdb_dump_test.sh
|
||||
endif
|
||||
endif
|
||||
ifndef SKIP_FORMAT_BUCK_CHECKS
|
||||
$(MAKE) check-format
|
||||
$(MAKE) check-buck-targets
|
||||
$(MAKE) check-sources
|
||||
$(MAKE) check-workflow-yaml
|
||||
$(MAKE) check-c-api-gen
|
||||
endif
|
||||
# NOTE: ^^^^^^^^ don't add new auxiliary checks here, but instead to
|
||||
# CHECK_EXTRA_TARGETS
|
||||
|
||||
# Check that the auto-generated C API files are up to date. It regenerates the
|
||||
# fragments and the inlined c.h/c.cc and compares them against a snapshot of the
|
||||
@@ -1149,8 +1204,8 @@ endif
|
||||
# Any non-empty value other than 0/no/false enables strict mode.
|
||||
CLANG_FORMAT_BINARY ?=
|
||||
# Backward-compatibility baseline for the C API (signature-level) check. CI
|
||||
# overrides this with the PR's merge target; locally it falls back to main /
|
||||
# origin/main and is skipped if neither resolves.
|
||||
# overrides this with the PR's merge target or the branch's pre-push tip;
|
||||
# locally it falls back to main or origin/main and is skipped if neither resolves.
|
||||
API_COMPAT_REF ?= main
|
||||
CHECK_C_API_GEN_STRICT ?=
|
||||
check-c-api-gen:
|
||||
@@ -1235,6 +1290,14 @@ ldb_tests: ldb
|
||||
db_crashtest_tests:
|
||||
$(PYTHON) tools/db_crashtest_test.py
|
||||
|
||||
.PHONY: check-all-python
|
||||
check-all-python:
|
||||
$(PYTHON) tools/check_all_python.py
|
||||
|
||||
.PHONY: rocksdb_dump_test
|
||||
rocksdb_dump_test: rocksdb_dump rocksdb_undump
|
||||
sh tools/rocksdb_dump_test.sh
|
||||
|
||||
include crash_test.mk
|
||||
|
||||
asan_check: clean
|
||||
@@ -1670,6 +1733,9 @@ db_readonly_with_timestamp_test: $(OBJ_DIR)/db/db_readonly_with_timestamp_test.o
|
||||
db_wide_basic_test: $(OBJ_DIR)/db/wide/db_wide_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_lazy_entity_test: $(OBJ_DIR)/db/wide/db_lazy_entity_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_wide_blob_direct_write_test: $(OBJ_DIR)/db/wide/db_wide_blob_direct_write_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -2374,22 +2440,27 @@ ROCKSDB_JAVADOCS_JAR = rocksdbjni-$(ROCKSDB_JAVA_VERSION)-javadoc.jar
|
||||
ROCKSDB_SOURCES_JAR = rocksdbjni-$(ROCKSDB_JAVA_VERSION)-sources.jar
|
||||
SHA256_CMD = sha256sum
|
||||
|
||||
# The *_DOWNLOAD_PREFIX defaults below point at the upstream projects; CI
|
||||
# overrides them to a mirror on the "third-party-deps" GitHub release (see
|
||||
# .github/actions/pre-steps/action.yml). When bumping a version, see
|
||||
# instructions in CI config comments for how to update that release.
|
||||
ZLIB_VER ?= 1.3.1
|
||||
ZLIB_SHA256 ?= 9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23
|
||||
ZLIB_DOWNLOAD_BASE ?= http://zlib.net
|
||||
ZLIB_DOWNLOAD_PREFIX ?= http://zlib.net/zlib-
|
||||
BZIP2_VER ?= 1.0.8
|
||||
BZIP2_SHA256 ?= ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269
|
||||
BZIP2_DOWNLOAD_BASE ?= http://sourceware.org/pub/bzip2
|
||||
BZIP2_DOWNLOAD_PREFIX ?= http://sourceware.org/pub/bzip2/bzip2-
|
||||
SNAPPY_VER ?= 1.2.2
|
||||
SNAPPY_SHA256 ?= 90f74bc1fbf78a6c56b3c4a082a05103b3a56bb17bca1a27e052ea11723292dc
|
||||
SNAPPY_DOWNLOAD_BASE ?= https://github.com/google/snappy/archive
|
||||
SNAPPY_DOWNLOAD_PREFIX ?= https://github.com/google/snappy/archive/
|
||||
LZ4_VER ?= 1.10.0
|
||||
LZ4_SHA256 ?= 537512904744b35e232912055ccf8ec66d768639ff3abe5788d90d792ec5f48b
|
||||
LZ4_DOWNLOAD_BASE ?= https://github.com/lz4/lz4/archive
|
||||
LZ4_DOWNLOAD_PREFIX ?= https://github.com/lz4/lz4/archive/v
|
||||
ZSTD_VER ?= 1.5.7
|
||||
ZSTD_SHA256 ?= 37d7284556b20954e56e1ca85b80226768902e2edabd3b649e9e72c0c9012ee3
|
||||
ZSTD_DOWNLOAD_BASE ?= https://github.com/facebook/zstd/archive
|
||||
CURL_SSL_OPTS ?= --tlsv1
|
||||
ZSTD_DOWNLOAD_PREFIX ?= https://github.com/facebook/zstd/archive/v
|
||||
# curl with the common options used to fetch the pinned third-party sources above
|
||||
CURL_DOWNLOAD = curl --fail --location --retry 3 --retry-all-errors
|
||||
|
||||
ifeq ($(PLATFORM), OS_MACOSX)
|
||||
ifeq (,$(findstring librocksdbjni-osx,$(ROCKSDBJNILIB)))
|
||||
@@ -2468,7 +2539,7 @@ BUILD_SIG_NONBUILD_GOALS := \
|
||||
check-sources check-workflow-yaml check-progress clang-tidy \
|
||||
tags tags0 package jclean checkout_folly build_folly \
|
||||
watch-log dump-log suggest-slow-tests list_all_tests gen-pc \
|
||||
gen_parallel_tests check-c-api-gen \
|
||||
gen_parallel_tests gen_check_scripts check-c-api-gen check-all-python \
|
||||
setup-hooks install-hooks uninstall-hooks uninstall db_crashtest_tests
|
||||
# Goals that clean before building (depend on or invoke `clean`): they manage
|
||||
# their own freshness, so the check must not block them (it would error before
|
||||
@@ -2530,7 +2601,7 @@ endif
|
||||
endif
|
||||
|
||||
zlib-$(ZLIB_VER).tar.gz:
|
||||
curl --fail --output zlib-$(ZLIB_VER).tar.gz --location ${ZLIB_DOWNLOAD_BASE}/zlib-$(ZLIB_VER).tar.gz
|
||||
$(CURL_DOWNLOAD) --output zlib-$(ZLIB_VER).tar.gz ${ZLIB_DOWNLOAD_PREFIX}$(ZLIB_VER).tar.gz
|
||||
ZLIB_SHA256_ACTUAL=`$(SHA256_CMD) zlib-$(ZLIB_VER).tar.gz | cut -d ' ' -f 1`; \
|
||||
if [ "$(ZLIB_SHA256)" != "$$ZLIB_SHA256_ACTUAL" ]; then \
|
||||
echo zlib-$(ZLIB_VER).tar.gz checksum mismatch, expected=\"$(ZLIB_SHA256)\" actual=\"$$ZLIB_SHA256_ACTUAL\"; \
|
||||
@@ -2548,7 +2619,7 @@ libz.a: zlib-$(ZLIB_VER).tar.gz
|
||||
cp zlib-$(ZLIB_VER)/libz.a .
|
||||
|
||||
bzip2-$(BZIP2_VER).tar.gz:
|
||||
curl --fail --output bzip2-$(BZIP2_VER).tar.gz --location ${CURL_SSL_OPTS} ${BZIP2_DOWNLOAD_BASE}/bzip2-$(BZIP2_VER).tar.gz
|
||||
$(CURL_DOWNLOAD) --output bzip2-$(BZIP2_VER).tar.gz ${BZIP2_DOWNLOAD_PREFIX}$(BZIP2_VER).tar.gz
|
||||
BZIP2_SHA256_ACTUAL=`$(SHA256_CMD) bzip2-$(BZIP2_VER).tar.gz | cut -d ' ' -f 1`; \
|
||||
if [ "$(BZIP2_SHA256)" != "$$BZIP2_SHA256_ACTUAL" ]; then \
|
||||
echo bzip2-$(BZIP2_VER).tar.gz checksum mismatch, expected=\"$(BZIP2_SHA256)\" actual=\"$$BZIP2_SHA256_ACTUAL\"; \
|
||||
@@ -2562,7 +2633,7 @@ libbz2.a: bzip2-$(BZIP2_VER).tar.gz
|
||||
cp bzip2-$(BZIP2_VER)/libbz2.a .
|
||||
|
||||
snappy-$(SNAPPY_VER).tar.gz:
|
||||
curl --fail --output snappy-$(SNAPPY_VER).tar.gz --location ${CURL_SSL_OPTS} ${SNAPPY_DOWNLOAD_BASE}/$(SNAPPY_VER).tar.gz
|
||||
$(CURL_DOWNLOAD) --output snappy-$(SNAPPY_VER).tar.gz ${SNAPPY_DOWNLOAD_PREFIX}$(SNAPPY_VER).tar.gz
|
||||
SNAPPY_SHA256_ACTUAL=`$(SHA256_CMD) snappy-$(SNAPPY_VER).tar.gz | cut -d ' ' -f 1`; \
|
||||
if [ "$(SNAPPY_SHA256)" != "$$SNAPPY_SHA256_ACTUAL" ]; then \
|
||||
echo snappy-$(SNAPPY_VER).tar.gz checksum mismatch, expected=\"$(SNAPPY_SHA256)\" actual=\"$$SNAPPY_SHA256_ACTUAL\"; \
|
||||
@@ -2577,7 +2648,7 @@ libsnappy.a: snappy-$(SNAPPY_VER).tar.gz
|
||||
cp snappy-$(SNAPPY_VER)/build/libsnappy.a .
|
||||
|
||||
lz4-$(LZ4_VER).tar.gz:
|
||||
curl --fail --output lz4-$(LZ4_VER).tar.gz --location ${CURL_SSL_OPTS} ${LZ4_DOWNLOAD_BASE}/v$(LZ4_VER).tar.gz
|
||||
$(CURL_DOWNLOAD) --output lz4-$(LZ4_VER).tar.gz ${LZ4_DOWNLOAD_PREFIX}$(LZ4_VER).tar.gz
|
||||
LZ4_SHA256_ACTUAL=`$(SHA256_CMD) lz4-$(LZ4_VER).tar.gz | cut -d ' ' -f 1`; \
|
||||
if [ "$(LZ4_SHA256)" != "$$LZ4_SHA256_ACTUAL" ]; then \
|
||||
echo lz4-$(LZ4_VER).tar.gz checksum mismatch, expected=\"$(LZ4_SHA256)\" actual=\"$$LZ4_SHA256_ACTUAL\"; \
|
||||
@@ -2591,7 +2662,7 @@ liblz4.a: lz4-$(LZ4_VER).tar.gz
|
||||
cp lz4-$(LZ4_VER)/lib/liblz4.a .
|
||||
|
||||
zstd-$(ZSTD_VER).tar.gz:
|
||||
curl --fail --output zstd-$(ZSTD_VER).tar.gz --location ${CURL_SSL_OPTS} ${ZSTD_DOWNLOAD_BASE}/v$(ZSTD_VER).tar.gz
|
||||
$(CURL_DOWNLOAD) --output zstd-$(ZSTD_VER).tar.gz ${ZSTD_DOWNLOAD_PREFIX}$(ZSTD_VER).tar.gz
|
||||
ZSTD_SHA256_ACTUAL=`$(SHA256_CMD) zstd-$(ZSTD_VER).tar.gz | cut -d ' ' -f 1`; \
|
||||
if [ "$(ZSTD_SHA256)" != "$$ZSTD_SHA256_ACTUAL" ]; then \
|
||||
echo zstd-$(ZSTD_VER).tar.gz checksum mismatch, expected=\"$(ZSTD_SHA256)\" actual=\"$$ZSTD_SHA256_ACTUAL\"; \
|
||||
|
||||
@@ -1305,6 +1305,16 @@ uint64_t rocksdb_options_get_blob_file_size(rocksdb_options_t* opt) {
|
||||
return opt->rep.blob_file_size;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_blob_file_writable_file_max_buffer_size(
|
||||
rocksdb_options_t* opt, uint64_t v) {
|
||||
opt->rep.blob_file_writable_file_max_buffer_size = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_options_get_blob_file_writable_file_max_buffer_size(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.blob_file_writable_file_max_buffer_size;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_blob_compression_type(rocksdb_options_t* opt, int v) {
|
||||
opt->rep.blob_compression_type =
|
||||
static_cast<decltype(opt->rep.blob_compression_type)>(v);
|
||||
|
||||
@@ -387,6 +387,14 @@ rocksdb_options_set_preserve_internal_time_seconds(rocksdb_options_t* opt,
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_options_get_preserve_internal_time_seconds(rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_options_set_blob_file_writable_file_max_buffer_size(
|
||||
rocksdb_options_t* opt, uint64_t v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_options_get_blob_file_writable_file_max_buffer_size(
|
||||
rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_enable_blob_direct_write(
|
||||
rocksdb_options_t* opt, unsigned char v);
|
||||
|
||||
|
||||
@@ -647,6 +647,12 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
|
||||
CheckCondition(rocksdb_options_get_blob_direct_write_partitions(obj) == 1);
|
||||
rocksdb_options_set_blob_direct_write_partitions(obj, 0);
|
||||
CheckCondition(rocksdb_options_get_blob_direct_write_partitions(obj) == 0);
|
||||
rocksdb_options_set_blob_file_writable_file_max_buffer_size(obj, 1);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_blob_file_writable_file_max_buffer_size(obj) == 1);
|
||||
rocksdb_options_set_blob_file_writable_file_max_buffer_size(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_blob_file_writable_file_max_buffer_size(obj) == 0);
|
||||
rocksdb_options_set_block_protection_bytes_per_key(obj, 1);
|
||||
CheckCondition(rocksdb_options_get_block_protection_bytes_per_key(obj) == 1);
|
||||
rocksdb_options_set_block_protection_bytes_per_key(obj, 0);
|
||||
|
||||
@@ -4,12 +4,19 @@
|
||||
Thorough multi-agent code review for RocksDB commits following CLAUDE.md guidelines,
|
||||
with structured codebase exploration, inter-agent debate, and verification.
|
||||
|
||||
IMPORTANT — Incremental output: After completing EACH major phase (Setup,
|
||||
Codebase Context, Initial Review, Debate, Synthesis), append your findings
|
||||
to review-findings.md using the Write tool. This ensures partial results are
|
||||
saved even if the review is interrupted by the turn limit. After the final
|
||||
synthesis, write the complete review to review-findings.md, replacing
|
||||
previous content. Also output the final review as your response text.
|
||||
IMPORTANT — Where the review goes: Your FINAL RESPONSE TEXT is the review.
|
||||
The PR comment is generated verbatim from your final response, so the
|
||||
complete, polished review MUST appear there in full. Do NOT end your turn with
|
||||
a meta-summary such as "the review has been written to review-findings.md" —
|
||||
paste the entire review as your response.
|
||||
|
||||
review-findings.md is a RECOVERY BACKSTOP, not the primary output: after
|
||||
completing EACH major phase (Setup, Codebase Context, Initial Review, Debate,
|
||||
Synthesis), append your current findings to it with the Write tool, and after
|
||||
the final synthesis overwrite it with the complete review. This preserves
|
||||
partial results if the turn limit interrupts the run (a recovery step reads
|
||||
this file); it does NOT substitute for outputting the full review as your
|
||||
final response.
|
||||
|
||||
## Workflow Steps
|
||||
|
||||
@@ -466,7 +473,9 @@ Team lead synthesizes the debate into a consensus document:
|
||||
- Include detailed bug analysis (root cause, vulnerable code paths)
|
||||
- Include suggested fixes
|
||||
- Note which findings were debated and the outcome
|
||||
- Write the complete review to review-findings.md and output as response
|
||||
- Output the complete review as your final response text (this becomes the PR
|
||||
comment verbatim), and also overwrite review-findings.md with the identical
|
||||
content as a recovery backstop
|
||||
|
||||
**Final report quality rules:**
|
||||
- The final report must be CLEAN and POLISHED. No stream-of-consciousness,
|
||||
@@ -483,9 +492,10 @@ Team lead synthesizes the debate into a consensus document:
|
||||
|
||||
**REQUIRED output structure (so the PR page stays scrollable):**
|
||||
|
||||
The final response (and contents of `review-findings.md`) MUST follow this
|
||||
exact structure. The summary appears first so reviewers can see HIGH findings
|
||||
at a glance; everything else is hidden behind a `<details>` block.
|
||||
Your final response — which becomes the PR comment verbatim — (and the
|
||||
identical contents of `review-findings.md`) MUST follow this exact structure.
|
||||
The summary appears first so reviewers can see HIGH findings at a glance;
|
||||
everything else is hidden behind a `<details>` block.
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
@@ -533,6 +543,9 @@ Rules for this structure:
|
||||
- Do NOT nest a `<details>` inside another `<details>`.
|
||||
- Do NOT add any text after the closing `</details>` — the comment-builder
|
||||
appends its own footer.
|
||||
- Your final response MUST be the review itself in this structure — never a
|
||||
meta-summary like "the review has been written to review-findings.md". The
|
||||
PR comment is generated verbatim from your final response.
|
||||
- If the review is partial (recovery path), still produce the summary block
|
||||
first, and put whatever was salvaged inside the `<details>` block.
|
||||
|
||||
@@ -571,9 +584,14 @@ Rules for this structure:
|
||||
- [ ] All callers enumerated with parameter range table
|
||||
|
||||
## Output Structure
|
||||
Write all review artifacts to the working directory root:
|
||||
- `review-findings.md` — Incremental findings (appended after each phase),
|
||||
then replaced with the final synthesized review at the end
|
||||
Your **final response text is the primary output** — it becomes the PR comment
|
||||
verbatim. The files below are working artifacts and recovery backstops only;
|
||||
they are NOT a substitute for outputting the full review in your final response.
|
||||
|
||||
Write these artifacts to the working directory root:
|
||||
- `review-findings.md` — Recovery backstop: incremental findings appended after
|
||||
each phase, overwritten with the final synthesized review at the end. Used
|
||||
only if the turn limit interrupts the run.
|
||||
- `context.md` — Codebase context (call chains, invariants)
|
||||
- `findings-*.md` — Per-agent findings (design, correctness, cross-component,
|
||||
invariant-adversary, caller-audit, performance, api, serialization, tests)
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
# Code Review Skill
|
||||
|
||||
This document defines the methodology for performing thorough, multi-perspective
|
||||
code reviews on RocksDB changes. It is used both by CI (GitHub Actions) and
|
||||
local review workflows.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting a review:
|
||||
- Read `CLAUDE.md` in the repository root for project-specific guidelines
|
||||
- Read any files in `claude_md/` for architecture context
|
||||
- Identify the commit/diff/PR to review and parse the changed files
|
||||
|
||||
## Review Methodology
|
||||
|
||||
Conduct the review from multiple perspectives, then synthesize findings into a
|
||||
single report. Each perspective catches different classes of bugs.
|
||||
|
||||
### Perspective 1: Codebase Context & Call-Chain Analysis
|
||||
|
||||
Before reviewing the diff itself, build deep context:
|
||||
- For each changed function/method, trace the call chain UPWARD 3-5 levels.
|
||||
Who consumes the changed behavior? What invariants do callers rely on?
|
||||
- Trace DOWNWARD into callees. What side effects do they have? What shared
|
||||
state do they mutate (counters, sequence numbers, flags, metadata)?
|
||||
- Identify the "sibling" or "reference" implementation that handles the same
|
||||
scenario the standard way. Compare behaviors.
|
||||
- For data written to shared structures (memtable, SST block, cache entry),
|
||||
find ALL readers and verify their visibility rules match the writer.
|
||||
|
||||
### Perspective 2: Correctness & Edge Cases
|
||||
|
||||
- Thread safety and concurrency (lock ordering, data races)
|
||||
- Error handling and Status propagation
|
||||
- Edge cases: empty inputs, overflow, boundary conditions
|
||||
- Data corruption scenarios
|
||||
- Behavioral contract changes: do return value semantics match callers?
|
||||
|
||||
### Perspective 3: Cross-Component & Adversarial Analysis
|
||||
|
||||
Check the change in ALL execution contexts:
|
||||
|
||||
| Context | Key difference |
|
||||
|---------|---------------|
|
||||
| WritePreparedTxnDB | read_callback_ controls visibility |
|
||||
| ReadOnly DB / SecondaryInstance | No mutable memtable |
|
||||
| CompactionService / Remote compaction | Different process |
|
||||
| User-defined timestamps | Extra key comparison dimension |
|
||||
| MemPurge | Memtable-to-memtable path |
|
||||
| BlobDB | Values in blob files, not inline |
|
||||
| Old snapshots | Seqno far behind current |
|
||||
| Concurrent writers | Lock-free vs locked paths |
|
||||
| FIFO / Universal compaction | Different invariants |
|
||||
| Prefix seek / total order seek | Different iterator behavior |
|
||||
|
||||
When the change claims a property ("logically redundant", "safe because X"),
|
||||
systematically try to break it: list preconditions, construct counterexamples.
|
||||
|
||||
### Perspective 4: Performance
|
||||
|
||||
RocksDB is a high-performance storage engine.
|
||||
- Memory allocation on hot paths
|
||||
- Unnecessary copies (prefer move semantics, Slice, string_view)
|
||||
- Cache efficiency and data locality
|
||||
- Loop optimization, branch prediction (LIKELY/UNLIKELY)
|
||||
|
||||
### Perspective 5: API, Compatibility & Testing
|
||||
|
||||
- Public API backwards compatibility
|
||||
- Serialization format correctness and versioning
|
||||
- Test coverage for edge cases, failure modes, and system-level interactions
|
||||
|
||||
## How to Explore the Codebase
|
||||
|
||||
Use available tools to explore deeply — do NOT just review the diff in isolation.
|
||||
The most critical bugs hide at component boundaries.
|
||||
|
||||
- Read the header files for changed implementations
|
||||
- Search for callers of changed functions (3-5 levels up)
|
||||
- Read existing tests to understand guaranteed behaviors
|
||||
- Check related implementations for conventions and patterns
|
||||
|
||||
## Output Format
|
||||
|
||||
### Summary
|
||||
Brief overall assessment (1-2 sentences).
|
||||
|
||||
### Issues Found
|
||||
Categorize by severity:
|
||||
- :red_circle: **Critical**: Must fix (correctness, data corruption, security)
|
||||
- :yellow_circle: **Suggestion**: Should consider (performance, edge cases)
|
||||
- :green_circle: **Nitpick**: Minor style issues
|
||||
|
||||
For each issue include:
|
||||
- File and line reference
|
||||
- Which review perspective found it
|
||||
- Description with root cause analysis
|
||||
- Suggested fix
|
||||
|
||||
### Cross-Component Analysis
|
||||
Execution context checks and assumption stress-test results.
|
||||
|
||||
### Positive Observations
|
||||
Good patterns, clever optimizations, or improvements.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Manual (Local) Code Review — Lightweight Wrapper
|
||||
|
||||
## What this is
|
||||
|
||||
A resource-light, **single-reviewer** variant of `claude_md/ci_review_prompt.md`
|
||||
for running a review by hand in a local Claude Code / Devmate session. It reuses
|
||||
the full CI review *methodology* but drops the multi-agent orchestration and the
|
||||
CI-only file-persistence machinery, to approximate the heavyweight result at a
|
||||
fraction of the cost.
|
||||
|
||||
**Scope: LOCAL manual review only. This file is NOT used by CI.** CI uses
|
||||
`claude_md/ci_review_prompt.md`.
|
||||
|
||||
## How to use
|
||||
|
||||
Read `claude_md/ci_review_prompt.md` for the complete methodology — the analysis
|
||||
techniques (caller-chain, callee side-effects, cross-component data consumers,
|
||||
alternative execution contexts, assumption stress-testing), the per-perspective
|
||||
reviewer instructions, the review checklist, and the anti-patterns table.
|
||||
|
||||
Then apply that methodology with the OVERRIDES below. **Where this file
|
||||
conflicts with `ci_review_prompt.md`, this file wins.**
|
||||
|
||||
## Overrides (this is what makes it lightweight)
|
||||
|
||||
### 1. Single reviewer — no team, no subagents
|
||||
- Do NOT create a team, spawn subagents, or run parallel/background agents.
|
||||
- Do NOT run the round-robin Debate phase or the Consensus phase.
|
||||
- Instead, YOU personally apply each reviewer role from `ci_review_prompt.md`
|
||||
(Design, Correctness, Cross-Component & Adversarial, Invariant Adversary,
|
||||
Caller-Context, Performance, API/Compat, Serialization, Test Coverage) as
|
||||
sequential *lenses* in one pass. Spend a few focused minutes per lens. Skip
|
||||
lenses that plainly don't apply (e.g., Serialization when there is no on-disk
|
||||
or wire-format change) and note the skip in one line.
|
||||
|
||||
### 2. No intermediate files
|
||||
- Do NOT write `context.md`, `findings-*.md`, `consensus.md`, or
|
||||
`review-findings.md`. Keep working notes in your reasoning, not on disk.
|
||||
- Rationale: those files exist only so CI can recover partial results when an
|
||||
ephemeral runner hits the turn limit — a local session has neither problem.
|
||||
`ci_review_prompt.md` already outputs the review as its final response, so
|
||||
skipping the file loses nothing here.
|
||||
|
||||
### 3. Right-size the effort
|
||||
- Do the Codebase Context phase, but proportionally: trace the 2-3 most
|
||||
load-bearing call chains and the highest-risk cross-component data consumers,
|
||||
rather than exhaustively enumerating every caller 3-5 levels up.
|
||||
- Prioritize the highest-signal lenses for RocksDB: **Correctness**,
|
||||
**Cross-Component & Adversarial**, and **Assumption Stress-Testing**. These
|
||||
catch the bugs that matter most; give them the most attention.
|
||||
- Escalation clause: if a thread looks dangerous, go deep on that specific
|
||||
thread even though the overall pass is modest. Modest is the default, not a cap.
|
||||
|
||||
### 4. Identify the review target locally
|
||||
- Unlike CI, the diff is NOT pre-pasted into this prompt. Determine what to
|
||||
review from the user's request against the local worktree — e.g. the current
|
||||
commit (`git show`), uncommitted changes (`git diff` / `git diff --staged`),
|
||||
a named commit range, or a PR — and read the changed files plus their
|
||||
surrounding context directly from the repo. When in doubt, include all changes
|
||||
not yet upstream, as in
|
||||
`git diff "$(git merge-base upstream/main HEAD 2> /dev/null || git merge-base origin/main HEAD 2> /dev/null)"`
|
||||
plus any untracked files (`git status`) that seem relevant.
|
||||
|
||||
## Reminder
|
||||
|
||||
The goal is a thorough *single* reviewer, not a 9-agent debate: depth where it
|
||||
matters, brevity everywhere else.
|
||||
@@ -44,6 +44,45 @@ Status VersionBlobFetcherBase::FetchBlob(const Slice& user_key,
|
||||
prefetch_buffer, blob_value, bytes_read);
|
||||
}
|
||||
|
||||
Status VersionBlobFetcherBase::FetchBlobRange(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
uint64_t range_offset,
|
||||
size_t range_length,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
// Only ever called when SupportsRangeRead() holds (a Version to read through,
|
||||
// no direct-write fallback); the direct-write path resolves whole records and
|
||||
// has no range variant.
|
||||
assert(SupportsRangeRead());
|
||||
assert(version_);
|
||||
return version_->GetBlobRange(read_options(), user_key, blob_index,
|
||||
range_offset, range_length, blob_value,
|
||||
bytes_read);
|
||||
}
|
||||
|
||||
Status VersionBlobFetcherBase::FetchBlobForceVerify(
|
||||
const Slice& user_key, const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer, PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
if (read_options().verify_checksums) {
|
||||
// Verification already happens on the normal path.
|
||||
return FetchBlob(user_key, blob_index, prefetch_buffer, blob_value,
|
||||
bytes_read);
|
||||
}
|
||||
|
||||
// Force verification via a transient fetcher over a verify-enabled copy of
|
||||
// the read options. `verify_read_options` outlives `verifying_fetcher` (both
|
||||
// are local to this call), so the borrowed-options VersionBlobFetcher is
|
||||
// safe.
|
||||
ReadOptions verify_read_options = read_options();
|
||||
verify_read_options.verify_checksums = true;
|
||||
VersionBlobFetcher verifying_fetcher(version_, verify_read_options,
|
||||
blob_file_cache_,
|
||||
allow_write_path_fallback_);
|
||||
return verifying_fetcher.FetchBlob(user_key, blob_index, prefetch_buffer,
|
||||
blob_value, bytes_read);
|
||||
}
|
||||
|
||||
Status EmbeddedAwareBlobFetcher::FetchBlob(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
|
||||
@@ -63,6 +63,37 @@ class VersionBlobFetcherBase : public BlobFetcher {
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const override;
|
||||
|
||||
// Fetches a byte sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed*, separate-file blob's value into *blob_value, reading only
|
||||
// those bytes on a cache miss (see Version::GetBlobRange /
|
||||
// BlobSource::GetBlobRange). Only valid when SupportsRangeRead() is true (a
|
||||
// Version to read through and no direct-write fallback); callers check that
|
||||
// first and take the whole-value FetchBlob path otherwise. Not routed through
|
||||
// the same-file (embedded) decorator -- same-file range reads are a later
|
||||
// phase.
|
||||
Status FetchBlobRange(const Slice& user_key, const BlobIndex& blob_index,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
PinnableSlice* blob_value, uint64_t* bytes_read) const;
|
||||
|
||||
// Fetches the whole blob with checksum verification forced on, regardless of
|
||||
// this fetcher's ReadOptions::verify_checksums. Used to honor a lazy read's
|
||||
// force_verify when verify_checksums is off: the caller wants the record's
|
||||
// checksum checked even though the global option would skip it. When
|
||||
// verify_checksums is already on this is just FetchBlob.
|
||||
//
|
||||
// TODO(lazy-blob-resolution-cleanup): fold FetchBlobRange /
|
||||
// FetchBlobForceVerify (and the whole-vs-range GetBlob twins throughout the
|
||||
// blob stack) into one parameterized primitive taking a BlobReadRequest -- a
|
||||
// range descriptor ({offset, len}; whole == [0, npos)) plus a 3-valued verify
|
||||
// policy (must-verify-if-present / verify-if-no-amplification / ignore). That
|
||||
// retires this method and the range/whole method pairs. See the lazy blob
|
||||
// resolution plan (deferred for feature velocity).
|
||||
Status FetchBlobForceVerify(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// True if this fetcher has enough context to resolve a (non-inlined,
|
||||
// non-same-file) blob reference: either a Version to read through, or the
|
||||
// direct-write fallback (a blob file cache) enabled. Callers that may hold a
|
||||
@@ -73,6 +104,13 @@ class VersionBlobFetcherBase : public BlobFetcher {
|
||||
(allow_write_path_fallback_ && blob_file_cache_ != nullptr);
|
||||
}
|
||||
|
||||
// True if this fetcher can serve an I/O-saving byte-range read: it reads
|
||||
// through a Version (FetchBlobRange goes to Version::GetBlobRange) and is not
|
||||
// in direct-write fallback mode (whose reads resolve whole records instead).
|
||||
bool SupportsRangeRead() const {
|
||||
return version_ != nullptr && !allow_write_path_fallback_;
|
||||
}
|
||||
|
||||
protected:
|
||||
VersionBlobFetcherBase(const Version* version, BlobFileCache* blob_file_cache,
|
||||
bool allow_write_path_fallback)
|
||||
|
||||
@@ -41,14 +41,14 @@ BlobFileBuilder::BlobFileBuilder(
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
BlobFileCompletionCallback* blob_callback,
|
||||
BlobFileCreationReason creation_reason,
|
||||
std::vector<std::string>* blob_file_paths,
|
||||
std::vector<std::string>* output_file_paths,
|
||||
std::vector<BlobFileAddition>* blob_file_additions)
|
||||
: BlobFileBuilder([versions]() { return versions->NewFileNumber(); }, fs,
|
||||
immutable_options, mutable_cf_options, file_options,
|
||||
write_options, db_id, db_session_id, job_id,
|
||||
column_family_id, column_family_name, write_hint,
|
||||
io_tracer, blob_callback, creation_reason,
|
||||
blob_file_paths, blob_file_additions) {}
|
||||
output_file_paths, blob_file_additions) {}
|
||||
|
||||
BlobFileBuilder::BlobFileBuilder(
|
||||
std::function<uint64_t()> file_number_generator, FileSystem* fs,
|
||||
@@ -60,13 +60,15 @@ BlobFileBuilder::BlobFileBuilder(
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
BlobFileCompletionCallback* blob_callback,
|
||||
BlobFileCreationReason creation_reason,
|
||||
std::vector<std::string>* blob_file_paths,
|
||||
std::vector<std::string>* output_file_paths,
|
||||
std::vector<BlobFileAddition>* blob_file_additions)
|
||||
: file_number_generator_(std::move(file_number_generator)),
|
||||
fs_(fs),
|
||||
immutable_options_(immutable_options),
|
||||
min_blob_size_(mutable_cf_options->min_blob_size),
|
||||
blob_file_size_(mutable_cf_options->blob_file_size),
|
||||
blob_file_writable_file_max_buffer_size_(
|
||||
mutable_cf_options->blob_file_writable_file_max_buffer_size),
|
||||
blob_compression_type_(mutable_cf_options->blob_compression_type),
|
||||
// TODO with schema change: support custom compression manager and options
|
||||
// such as max_compressed_bytes_per_kb
|
||||
@@ -88,7 +90,7 @@ BlobFileBuilder::BlobFileBuilder(
|
||||
io_tracer_(io_tracer),
|
||||
blob_callback_(blob_callback),
|
||||
creation_reason_(creation_reason),
|
||||
blob_file_paths_(blob_file_paths),
|
||||
output_file_paths_(output_file_paths),
|
||||
blob_file_additions_(blob_file_additions),
|
||||
blob_count_(0),
|
||||
blob_bytes_(0) {
|
||||
@@ -97,14 +99,21 @@ BlobFileBuilder::BlobFileBuilder(
|
||||
assert(immutable_options_);
|
||||
assert(file_options_);
|
||||
assert(write_options_);
|
||||
assert(blob_file_paths_);
|
||||
assert(blob_file_paths_->empty());
|
||||
assert(output_file_paths_);
|
||||
assert(output_file_paths_->empty());
|
||||
assert(blob_file_additions_);
|
||||
assert(blob_file_additions_->empty());
|
||||
}
|
||||
|
||||
BlobFileBuilder::~BlobFileBuilder() = default;
|
||||
|
||||
std::string BlobFileBuilder::GetBlobFilePath(uint64_t blob_file_number) const {
|
||||
assert(immutable_options_);
|
||||
assert(!immutable_options_->cf_paths.empty());
|
||||
return BlobFileName(immutable_options_->cf_paths.front().path,
|
||||
blob_file_number);
|
||||
}
|
||||
|
||||
Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
|
||||
std::string* blob_index) {
|
||||
assert(blob_index);
|
||||
@@ -186,10 +195,7 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
|
||||
assert(file_number_generator_);
|
||||
const uint64_t blob_file_number = file_number_generator_();
|
||||
|
||||
assert(immutable_options_);
|
||||
assert(!immutable_options_->cf_paths.empty());
|
||||
std::string blob_file_path =
|
||||
BlobFileName(immutable_options_->cf_paths.front().path, blob_file_number);
|
||||
std::string blob_file_path = GetBlobFilePath(blob_file_number);
|
||||
|
||||
if (blob_callback_) {
|
||||
blob_callback_->OnBlobFileCreationStarted(
|
||||
@@ -204,6 +210,10 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
|
||||
fo_copy.open_contract = FileOpenContract::kNoReopenForWrite |
|
||||
FileOpenContract::kNoReadersWhileOpenForWrite;
|
||||
fo_copy.write_hint = write_hint_;
|
||||
if (blob_file_writable_file_max_buffer_size_ > 0) {
|
||||
fo_copy.writable_file_max_buffer_size =
|
||||
blob_file_writable_file_max_buffer_size_;
|
||||
}
|
||||
Status s = NewWritableFile(fs_, blob_file_path, &file, fo_copy);
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
@@ -214,11 +224,11 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
|
||||
}
|
||||
}
|
||||
|
||||
// Note: files get added to blob_file_paths_ right after the open, so they
|
||||
// can be cleaned up upon failure. Contrast this with blob_file_additions_,
|
||||
// which only contains successfully written files.
|
||||
assert(blob_file_paths_);
|
||||
blob_file_paths_->emplace_back(std::move(blob_file_path));
|
||||
// Note: blob files get added to output_file_paths_ right after the open, so
|
||||
// they can be cleaned up upon failure. Contrast this with
|
||||
// blob_file_additions_, which only contains successfully written files.
|
||||
assert(output_file_paths_);
|
||||
output_file_paths_->emplace_back(std::move(blob_file_path));
|
||||
|
||||
assert(file);
|
||||
file->SetIOPriority(write_options_->rate_limiter_priority);
|
||||
@@ -228,7 +238,7 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
|
||||
FileTypeSet tmp_set = immutable_options_->checksum_handoff_file_types;
|
||||
Statistics* const statistics = immutable_options_->stats;
|
||||
std::unique_ptr<WritableFileWriter> file_writer(new WritableFileWriter(
|
||||
std::move(file), blob_file_paths_->back(), *file_options_,
|
||||
std::move(file), output_file_paths_->back(), fo_copy,
|
||||
immutable_options_->clock, io_tracer_, statistics,
|
||||
Histograms::BLOB_DB_BLOB_FILE_WRITE_MICROS, immutable_options_->listeners,
|
||||
immutable_options_->file_checksum_gen_factory.get(),
|
||||
@@ -341,7 +351,7 @@ Status BlobFileBuilder::CloseBlobFile() {
|
||||
|
||||
if (blob_callback_) {
|
||||
s = blob_callback_->OnBlobFileCompleted(
|
||||
blob_file_paths_->back(), column_family_name_, job_id_,
|
||||
GetBlobFilePath(blob_file_number), column_family_name_, job_id_,
|
||||
blob_file_number, creation_reason_, s, checksum_value, checksum_method,
|
||||
blob_count_, blob_bytes_);
|
||||
}
|
||||
@@ -385,9 +395,10 @@ void BlobFileBuilder::Abandon(const Status& s) {
|
||||
if (blob_callback_) {
|
||||
// BlobFileBuilder::Abandon() is called because of error while writing to
|
||||
// Blob files. So we can ignore the below error.
|
||||
const uint64_t blob_file_number = writer_->get_log_number();
|
||||
blob_callback_
|
||||
->OnBlobFileCompleted(blob_file_paths_->back(), column_family_name_,
|
||||
job_id_, writer_->get_log_number(),
|
||||
->OnBlobFileCompleted(GetBlobFilePath(blob_file_number),
|
||||
column_family_name_, job_id_, blob_file_number,
|
||||
creation_reason_, s, "", "", blob_count_,
|
||||
blob_bytes_)
|
||||
.PermitUncheckedError();
|
||||
|
||||
@@ -48,7 +48,7 @@ class BlobFileBuilder {
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
BlobFileCompletionCallback* blob_callback,
|
||||
BlobFileCreationReason creation_reason,
|
||||
std::vector<std::string>* blob_file_paths,
|
||||
std::vector<std::string>* output_file_paths,
|
||||
std::vector<BlobFileAddition>* blob_file_additions);
|
||||
|
||||
BlobFileBuilder(std::function<uint64_t()> file_number_generator,
|
||||
@@ -63,7 +63,7 @@ class BlobFileBuilder {
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
BlobFileCompletionCallback* blob_callback,
|
||||
BlobFileCreationReason creation_reason,
|
||||
std::vector<std::string>* blob_file_paths,
|
||||
std::vector<std::string>* output_file_paths,
|
||||
std::vector<BlobFileAddition>* blob_file_additions);
|
||||
|
||||
BlobFileBuilder(const BlobFileBuilder&) = delete;
|
||||
@@ -76,6 +76,7 @@ class BlobFileBuilder {
|
||||
void Abandon(const Status& s);
|
||||
|
||||
private:
|
||||
std::string GetBlobFilePath(uint64_t blob_file_number) const;
|
||||
bool IsBlobFileOpen() const;
|
||||
Status OpenBlobFileIfNeeded();
|
||||
Status CompressBlobIfNeeded(Slice* blob,
|
||||
@@ -93,6 +94,7 @@ class BlobFileBuilder {
|
||||
const ImmutableOptions* immutable_options_;
|
||||
uint64_t min_blob_size_;
|
||||
uint64_t blob_file_size_;
|
||||
uint64_t blob_file_writable_file_max_buffer_size_;
|
||||
CompressionType blob_compression_type_;
|
||||
std::unique_ptr<Compressor> blob_compressor_;
|
||||
mutable Compressor::ManagedWorkingArea blob_compressor_wa_;
|
||||
@@ -108,7 +110,7 @@ class BlobFileBuilder {
|
||||
std::shared_ptr<IOTracer> io_tracer_;
|
||||
BlobFileCompletionCallback* blob_callback_;
|
||||
BlobFileCreationReason creation_reason_;
|
||||
std::vector<std::string>* blob_file_paths_;
|
||||
std::vector<std::string>* output_file_paths_;
|
||||
std::vector<BlobFileAddition>* blob_file_additions_;
|
||||
std::unique_ptr<BlobLogWriter> writer_;
|
||||
uint64_t blob_count_;
|
||||
|
||||
@@ -100,7 +100,8 @@ BlobFilePartitionManager::BlobFilePartitionManager(
|
||||
FileNumberAllocator file_number_allocator, FileSystem* fs,
|
||||
SystemClock* clock, Statistics* statistics, const FileOptions& file_options,
|
||||
std::string db_path, std::string column_family_name,
|
||||
uint64_t blob_file_size, bool use_fsync, BlobFileCache* blob_file_cache,
|
||||
uint64_t blob_file_size, bool use_fsync,
|
||||
uint64_t blob_writer_max_buffer_size, BlobFileCache* blob_file_cache,
|
||||
BlobFileCompletionCallback* blob_callback,
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners,
|
||||
FileChecksumGenFactory* file_checksum_gen_factory,
|
||||
@@ -120,6 +121,7 @@ BlobFilePartitionManager::BlobFilePartitionManager(
|
||||
column_family_name_(std::move(column_family_name)),
|
||||
blob_file_size_(blob_file_size),
|
||||
use_fsync_(use_fsync),
|
||||
blob_writer_max_buffer_size_(blob_writer_max_buffer_size),
|
||||
blob_file_cache_(blob_file_cache),
|
||||
blob_callback_(blob_callback),
|
||||
listeners_(listeners),
|
||||
@@ -202,6 +204,11 @@ Status BlobFilePartitionManager::OpenNewBlobFile(Partition* partition,
|
||||
std::unique_ptr<FSWritableFile> file;
|
||||
FileOptions writer_file_options = file_options_;
|
||||
writer_file_options.open_contract = FileOpenContract::kNoReopenForWrite;
|
||||
const uint64_t max_buffer_size =
|
||||
blob_writer_max_buffer_size_.load(std::memory_order_relaxed);
|
||||
if (max_buffer_size > 0) {
|
||||
writer_file_options.writable_file_max_buffer_size = max_buffer_size;
|
||||
}
|
||||
Status s = NewWritableFile(fs_, blob_file_path, &file, writer_file_options);
|
||||
if (!s.ok()) {
|
||||
RemoveFilePartitionMapping(blob_file_number);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
@@ -61,7 +62,8 @@ class BlobFilePartitionManager {
|
||||
SystemClock* clock, Statistics* statistics,
|
||||
const FileOptions& file_options, std::string db_path,
|
||||
std::string column_family_name, uint64_t blob_file_size, bool use_fsync,
|
||||
BlobFileCache* blob_file_cache, BlobFileCompletionCallback* blob_callback,
|
||||
uint64_t blob_writer_max_buffer_size, BlobFileCache* blob_file_cache,
|
||||
BlobFileCompletionCallback* blob_callback,
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners,
|
||||
FileChecksumGenFactory* file_checksum_gen_factory,
|
||||
const FileTypeSet& checksum_handoff_file_types,
|
||||
@@ -129,6 +131,11 @@ class BlobFilePartitionManager {
|
||||
// protected by a live memtable / old SuperVersion.
|
||||
bool IsTrackedBlobFileNumber(uint64_t file_number) const;
|
||||
|
||||
void SetBlobWriterMaxBufferSize(uint64_t blob_writer_max_buffer_size) {
|
||||
blob_writer_max_buffer_size_.store(blob_writer_max_buffer_size,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// Increments / decrements memtable-held protection on sealed blob files.
|
||||
void ProtectSealedBlobFileNumbers(const std::vector<uint64_t>& file_numbers);
|
||||
void UnprotectSealedBlobFileNumbers(
|
||||
@@ -260,6 +267,7 @@ class BlobFilePartitionManager {
|
||||
// File creation policy and shared blob-file infrastructure.
|
||||
uint64_t blob_file_size_;
|
||||
bool use_fsync_;
|
||||
std::atomic<uint64_t> blob_writer_max_buffer_size_;
|
||||
BlobFileCache* blob_file_cache_;
|
||||
BlobFileCompletionCallback* blob_callback_;
|
||||
std::vector<std::shared_ptr<EventListener>> listeners_;
|
||||
|
||||
@@ -416,6 +416,77 @@ Status BlobFileReader::GetBlob(
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlobFileReader::GetBlobRange(const ReadOptions& read_options,
|
||||
const Slice& user_key, uint64_t offset,
|
||||
uint64_t value_size, uint64_t range_offset,
|
||||
size_t range_length,
|
||||
MemoryAllocator* allocator,
|
||||
std::unique_ptr<BlobContents>* result,
|
||||
uint64_t* bytes_read) const {
|
||||
assert(result);
|
||||
|
||||
// Partial reads are only meaningful for uncompressed blobs: a strict
|
||||
// sub-range of a compressed record cannot be decompressed in isolation.
|
||||
// Callers (BlobSource::GetBlobRange) must ensure this; enforce it here too.
|
||||
if (compression_type_ != kNoCompression) {
|
||||
return Status::Corruption("Cannot range-read a compressed blob");
|
||||
}
|
||||
|
||||
const uint64_t key_size = user_key.size();
|
||||
|
||||
// Validate that the full value region is within the file (same check GetBlob
|
||||
// performs); this guards the sub-range read below.
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
|
||||
has_footer_)) {
|
||||
return Status::Corruption("Invalid blob offset");
|
||||
}
|
||||
|
||||
// The requested sub-range must lie within the value.
|
||||
if (range_offset > value_size || range_length > value_size - range_offset) {
|
||||
return Status::InvalidArgument("Blob range out of bounds");
|
||||
}
|
||||
|
||||
// Read only the requested bytes, at the value's file position plus the range
|
||||
// offset. Unlike GetBlob there is no record-header adjustment (we do not read
|
||||
// the key/header) and no whole-record checksum verification (a strict
|
||||
// sub-range cannot cover it -- callers that require verification take the
|
||||
// full-read path instead).
|
||||
const uint64_t read_offset = offset + range_offset;
|
||||
const size_t read_size = range_length;
|
||||
|
||||
Slice range_slice;
|
||||
Buffer buf;
|
||||
AlignedBuffer direct_io_buffer;
|
||||
|
||||
TEST_SYNC_POINT("BlobFileReader::GetBlobRange:ReadFromFile");
|
||||
PERF_COUNTER_ADD(blob_read_count, 1);
|
||||
PERF_COUNTER_ADD(blob_read_byte, read_size);
|
||||
PERF_TIMER_GUARD(blob_read_time);
|
||||
|
||||
{
|
||||
const Status s =
|
||||
ReadFromFile(file_reader_.get(), read_options, read_offset, read_size,
|
||||
statistics_, &range_slice, &buf, &direct_io_buffer);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("BlobFileReader::GetBlobRange:TamperWithResult",
|
||||
&range_slice);
|
||||
|
||||
// The blob is uncompressed, so the bytes read are the requested value bytes;
|
||||
// copy them into an owned BlobContents (no decompression).
|
||||
BlobContentsCreator::Create(result, /*out_charge=*/nullptr, range_slice,
|
||||
kNoCompression, allocator);
|
||||
|
||||
if (bytes_read) {
|
||||
*bytes_read = read_size;
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void BlobFileReader::MultiGetBlob(
|
||||
const ReadOptions& read_options, MemoryAllocator* allocator,
|
||||
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>&
|
||||
|
||||
@@ -64,6 +64,23 @@ class BlobFileReader {
|
||||
std::unique_ptr<BlobContents>* result,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// Reads a byte sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed* blob's value directly from the file, reading only those
|
||||
// bytes. Unlike GetBlob it does not read the record header/key, does not
|
||||
// verify the whole-record checksum (a strict sub-range cannot cover it), and
|
||||
// never decompresses -- so it is only valid when this reader's blob file is
|
||||
// uncompressed (checked). `offset` and `value_size` are the blob value's file
|
||||
// offset and full size (from the BlobIndex); the caller must ensure
|
||||
// range_offset + range_length <= value_size. On success `*result` owns a copy
|
||||
// of the requested bytes and `*bytes_read` (when non-null) is the number of
|
||||
// bytes read from the file.
|
||||
Status GetBlobRange(const ReadOptions& read_options, const Slice& user_key,
|
||||
uint64_t offset, uint64_t value_size,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
MemoryAllocator* allocator,
|
||||
std::unique_ptr<BlobContents>* result,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// offsets must be sorted in ascending order by caller.
|
||||
void MultiGetBlob(
|
||||
const ReadOptions& read_options, MemoryAllocator* allocator,
|
||||
|
||||
@@ -76,6 +76,54 @@ Status ReadAndVerifySimpleGen2BlobRecord(
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file,
|
||||
uint64_t record_offset, size_t payload_size,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
CompressionType expected_compression,
|
||||
char* buf) {
|
||||
assert(file != nullptr);
|
||||
assert(buf != nullptr);
|
||||
|
||||
// A strict sub-range of a compressed payload cannot be decompressed in
|
||||
// isolation; callers resolve such payloads whole and slice instead.
|
||||
if (expected_compression != kNoCompression) {
|
||||
return Status::Corruption("Cannot range-read a compressed blob");
|
||||
}
|
||||
|
||||
// The requested sub-range must lie within the payload (the trailer that
|
||||
// follows the payload is never part of a range read).
|
||||
if (range_offset > payload_size ||
|
||||
range_length > payload_size - range_offset) {
|
||||
return Status::InvalidArgument("Blob range out of bounds");
|
||||
}
|
||||
|
||||
Slice result;
|
||||
IOOptions opts;
|
||||
IODebugContext dbg;
|
||||
Status s = file->PrepareIOOptions(read_options, opts, &dbg);
|
||||
if (s.ok()) {
|
||||
s = file->Read(opts, record_offset + range_offset, range_length, &result,
|
||||
buf, nullptr, &dbg);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (result.size() != range_length) {
|
||||
return Status::Corruption("Could not read complete blob range");
|
||||
}
|
||||
// With mmap reads the data lands outside `buf`; copy it in so the caller can
|
||||
// rely on `buf` owning the bytes (the only copy on the mmap path).
|
||||
if (result.data() != buf) {
|
||||
memcpy(buf, result.data(), range_length);
|
||||
}
|
||||
|
||||
// No trailer read and no checksum verification: a strict sub-range cannot
|
||||
// cover the record's checksum (callers that require it read the whole
|
||||
// record).
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
IOStatus WriteSimpleGen2BlobRecord(WritableFileWriter* file,
|
||||
const WriteOptions& write_options,
|
||||
ChecksumType checksum_type,
|
||||
|
||||
@@ -73,6 +73,24 @@ Status ReadAndVerifySimpleGen2BlobRecord(
|
||||
ChecksumType checksum_type, uint32_t base_context_checksum,
|
||||
CompressionType expected_compression, char* buf);
|
||||
|
||||
// Reads a byte sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed* SimpleGen2Blob payload directly into `buf` (capacity >=
|
||||
// range_length), reading only those bytes. Unlike
|
||||
// ReadAndVerifySimpleGen2BlobRecord it does not read the trailer and does not
|
||||
// verify the record's checksum (a strict sub-range cannot cover it); callers
|
||||
// that require verification read the whole record instead. `payload_size` is
|
||||
// the full payload size (from the blob index); the caller must ensure
|
||||
// range_offset + range_length <= payload_size. `expected_compression` must be
|
||||
// kNoCompression (a strict sub-range of a compressed payload cannot be
|
||||
// decompressed in isolation).
|
||||
//
|
||||
// On success, buf[0, range_length) holds the requested payload bytes.
|
||||
Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file,
|
||||
uint64_t record_offset, size_t payload_size,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
CompressionType expected_compression, char* buf);
|
||||
|
||||
// Writes a SimpleGen2Blob record for `payload` at the current end of `file`,
|
||||
// which the caller asserts is byte offset `record_offset`. Appends the payload
|
||||
// bytes followed by the 5-byte trailer (compression marker + context-modified
|
||||
|
||||
+283
-48
@@ -36,6 +36,102 @@ Status AppendBlobRefreshRetryFailure(const Status& stale_status,
|
||||
stale_status, "; refresh retry failed: ", retry_status.ToString());
|
||||
}
|
||||
|
||||
// Runs read(reader) against the blob-file reader for `file_number`. On a
|
||||
// Corruption -- which a stale cached reader (e.g. the physical file was
|
||||
// replaced) can surface -- evicts the cached reader, reopens it uncached,
|
||||
// retries read() once, and refreshes the cache on success. Shared by the
|
||||
// single-blob read paths (GetBlob / GetBlobRange) so the stale-file recovery
|
||||
// lives in one place.
|
||||
//
|
||||
// `read` is invoked with a BlobFileReader* and returns a Status; because it may
|
||||
// run twice, it must re-initialize any output it populates on each call (and is
|
||||
// taken by const reference rather than forwarded, since it is not moved-from).
|
||||
template <typename ReadFn>
|
||||
Status ReadBlobWithReaderRetry(BlobFileCache* blob_file_cache,
|
||||
const ReadOptions& read_options,
|
||||
uint64_t file_number, const ReadFn& read) {
|
||||
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
||||
Status s = blob_file_cache->GetBlobFileReader(read_options, file_number,
|
||||
&blob_file_reader);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
assert(blob_file_reader.GetValue());
|
||||
|
||||
s = read(blob_file_reader.GetValue());
|
||||
if (!s.IsCorruption()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
const Status stale_status = s;
|
||||
blob_file_reader.Reset();
|
||||
blob_file_cache->Evict(file_number);
|
||||
|
||||
std::unique_ptr<BlobFileReader> fresh_reader;
|
||||
s = blob_file_cache->OpenBlobFileReaderUncached(
|
||||
read_options, file_number, &fresh_reader,
|
||||
/*allow_footer_skip_retry=*/false);
|
||||
if (!s.ok()) {
|
||||
return AppendBlobRefreshRetryFailure(stale_status, s);
|
||||
}
|
||||
|
||||
s = read(fresh_reader.get());
|
||||
if (!s.ok()) {
|
||||
return AppendBlobRefreshRetryFailure(stale_status, s);
|
||||
}
|
||||
|
||||
CacheHandleGuard<BlobFileReader> ignored_reader;
|
||||
blob_file_cache
|
||||
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
|
||||
.PermitUncheckedError();
|
||||
return s;
|
||||
}
|
||||
|
||||
// Records lazy wide-column read metrics for one actual storage read issued
|
||||
// while resolving a lazy result (attributed via Env::IOActivity::kLazyResolve):
|
||||
// `bytes_read` bytes were read from storage. Covers both whole-column and
|
||||
// partial reads; not called for cache hits (no storage read).
|
||||
void RecordLazyRead(Statistics* statistics, uint64_t bytes_read) {
|
||||
RecordTick(statistics, BLOB_DB_LAZY_READ_COUNT);
|
||||
RecordTick(statistics, BLOB_DB_LAZY_READ_BYTES, bytes_read);
|
||||
}
|
||||
|
||||
// Additionally records the partial-read metrics for one actual partial
|
||||
// (byte-range) read: `bytes_read` bytes were fetched instead of the column's
|
||||
// full `value_size`. Call alongside RecordLazyRead (a partial read is also a
|
||||
// lazy read).
|
||||
void RecordLazyPartialRead(Statistics* statistics, uint64_t bytes_read,
|
||||
uint64_t value_size) {
|
||||
RecordTick(statistics, BLOB_DB_LAZY_PARTIAL_READ_COUNT);
|
||||
if (value_size > bytes_read) {
|
||||
RecordTick(statistics, BLOB_DB_LAZY_PARTIAL_BYTES_SAVED,
|
||||
value_size - bytes_read);
|
||||
}
|
||||
}
|
||||
|
||||
// Pins the sub-range [range_offset, range_offset + range_length) of a cache-hit
|
||||
// whole blob value into *value (clamped: an offset at/past the end yields
|
||||
// empty, length is clamped to the remainder), keeping the cached bytes alive by
|
||||
// transferring the cache handle into *value. Shared by the range-read cache-hit
|
||||
// paths (GetBlobRange / GetSimpleGen2BlobRange).
|
||||
void PinCacheHitSubRange(CacheHandleGuard<BlobContents>* blob_handle,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
PinnableSlice* value) {
|
||||
const Slice full = blob_handle->GetValue()->data();
|
||||
Slice sub;
|
||||
if (range_offset < full.size()) {
|
||||
const size_t off = static_cast<size_t>(range_offset);
|
||||
const size_t avail = full.size() - off;
|
||||
const size_t len = range_length > avail ? avail : range_length;
|
||||
sub = Slice(full.data() + off, len);
|
||||
} // else: offset at/past end -> empty (not an error)
|
||||
|
||||
value->Reset();
|
||||
constexpr Cleanable* cleanable = nullptr;
|
||||
value->PinSlice(sub, cleanable);
|
||||
blob_handle->TransferTo(value);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BlobSource::BlobSource(const ImmutableOptions& immutable_options,
|
||||
@@ -228,66 +324,37 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
std::unique_ptr<BlobContents> blob_contents;
|
||||
|
||||
{
|
||||
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
||||
s = blob_file_cache_->GetBlobFileReader(read_options, file_number,
|
||||
&blob_file_reader);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
assert(blob_file_reader.GetValue());
|
||||
|
||||
if (compression_type != blob_file_reader.GetValue()->GetCompressionType()) {
|
||||
return Status::Corruption("Compression type mismatch when reading blob");
|
||||
}
|
||||
|
||||
MemoryAllocator* const allocator =
|
||||
(blob_cache_ && read_options.fill_cache)
|
||||
? blob_cache_.get()->memory_allocator()
|
||||
: nullptr;
|
||||
|
||||
uint64_t read_size = 0;
|
||||
s = blob_file_reader.GetValue()->GetBlob(
|
||||
read_options, user_key, offset, value_size, compression_type,
|
||||
prefetch_buffer, allocator, &blob_contents, &read_size);
|
||||
if (s.IsCorruption()) {
|
||||
const Status stale_status = s;
|
||||
blob_file_reader.Reset();
|
||||
blob_file_cache_->Evict(file_number);
|
||||
|
||||
std::unique_ptr<BlobFileReader> fresh_reader;
|
||||
s = blob_file_cache_->OpenBlobFileReaderUncached(
|
||||
read_options, file_number, &fresh_reader,
|
||||
/*allow_footer_skip_retry=*/false);
|
||||
if (!s.ok()) {
|
||||
return AppendBlobRefreshRetryFailure(stale_status, s);
|
||||
}
|
||||
|
||||
if (compression_type != fresh_reader->GetCompressionType()) {
|
||||
return Status::Corruption(
|
||||
"Compression type mismatch when reading blob");
|
||||
}
|
||||
|
||||
blob_contents.reset();
|
||||
read_size = 0;
|
||||
s = fresh_reader->GetBlob(read_options, user_key, offset, value_size,
|
||||
compression_type, prefetch_buffer, allocator,
|
||||
&blob_contents, &read_size);
|
||||
if (!s.ok()) {
|
||||
s = AppendBlobRefreshRetryFailure(stale_status, s);
|
||||
} else {
|
||||
CacheHandleGuard<BlobFileReader> ignored_reader;
|
||||
blob_file_cache_
|
||||
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
s = ReadBlobWithReaderRetry(
|
||||
blob_file_cache_, read_options, file_number,
|
||||
[&](BlobFileReader* reader) {
|
||||
if (compression_type != reader->GetCompressionType()) {
|
||||
return Status::Corruption(
|
||||
"Compression type mismatch when reading blob");
|
||||
}
|
||||
blob_contents.reset();
|
||||
read_size = 0;
|
||||
return reader->GetBlob(read_options, user_key, offset, value_size,
|
||||
compression_type, prefetch_buffer, allocator,
|
||||
&blob_contents, &read_size);
|
||||
});
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (bytes_read) {
|
||||
*bytes_read = read_size;
|
||||
}
|
||||
// Whole-column read on the lazy resolve path (partial reads go through
|
||||
// GetBlobRange). Counts the storage read; partial reads are not counted
|
||||
// here.
|
||||
if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
|
||||
RecordLazyRead(statistics_, read_size);
|
||||
}
|
||||
}
|
||||
|
||||
if (blob_cache_ && read_options.fill_cache) {
|
||||
@@ -308,6 +375,92 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
return s;
|
||||
}
|
||||
|
||||
Status BlobSource::GetBlobRange(const ReadOptions& read_options,
|
||||
const Slice& user_key, uint64_t file_number,
|
||||
uint64_t offset, uint64_t file_size,
|
||||
uint64_t value_size,
|
||||
CompressionType compression_type,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
PinnableSlice* value, uint64_t* bytes_read) {
|
||||
assert(value);
|
||||
// Partial reads are for uncompressed blobs only; the caller decides this (a
|
||||
// compressed column takes the whole-value GetBlob path and slices).
|
||||
assert(compression_type == kNoCompression);
|
||||
// Range reads are (currently) only issued while resolving a lazy result, so
|
||||
// the lazy read stats below are recorded unconditionally (unlike GetBlob,
|
||||
// which gates on the activity). Enforce that invariant here.
|
||||
assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
|
||||
|
||||
Status s;
|
||||
|
||||
const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
|
||||
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
|
||||
// First, probe the blob cache for the whole value. On a hit, slice the
|
||||
// requested sub-range out of the cached value while pinning the cache handle
|
||||
// (zero-copy, no disk read). A partial read never inserts into the cache.
|
||||
if (blob_cache_) {
|
||||
Slice key = cache_key.AsSlice();
|
||||
s = GetBlobFromCache(key, &blob_handle);
|
||||
if (s.ok()) {
|
||||
PinCacheHitSubRange(&blob_handle, range_offset, range_length, value);
|
||||
if (bytes_read) {
|
||||
*bytes_read = 0; // served from cache; no disk read
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
assert(blob_handle.IsEmpty());
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
}
|
||||
|
||||
// Cache miss: read only the requested sub-range from the file. The result is
|
||||
// never inserted into the blob cache (see the header comment).
|
||||
std::unique_ptr<BlobContents> blob_contents;
|
||||
{
|
||||
// No cache-fill allocator: a partial value is never inserted into the
|
||||
// cache.
|
||||
constexpr MemoryAllocator* allocator = nullptr;
|
||||
|
||||
uint64_t read_size = 0;
|
||||
// Reuses the stale-reader retry shared with GetBlob. A range read skips
|
||||
// whole-record checksum verification, so the retry handles the stale-file
|
||||
// case (offset/size mismatch surfacing as Corruption), not a payload
|
||||
// checksum failure.
|
||||
s = ReadBlobWithReaderRetry(
|
||||
blob_file_cache_, read_options, file_number,
|
||||
[&](BlobFileReader* reader) {
|
||||
if (compression_type != reader->GetCompressionType()) {
|
||||
return Status::Corruption(
|
||||
"Compression type mismatch when reading blob");
|
||||
}
|
||||
blob_contents.reset();
|
||||
read_size = 0;
|
||||
return reader->GetBlobRange(read_options, user_key, offset,
|
||||
value_size, range_offset, range_length,
|
||||
allocator, &blob_contents, &read_size);
|
||||
});
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (bytes_read) {
|
||||
*bytes_read = read_size;
|
||||
}
|
||||
RecordLazyRead(statistics_, read_size);
|
||||
RecordLazyPartialRead(statistics_, read_size, value_size);
|
||||
}
|
||||
|
||||
PinOwnedBlob(&blob_contents, value);
|
||||
|
||||
assert(s.ok());
|
||||
return s;
|
||||
}
|
||||
|
||||
Status BlobSource::GetSimpleGen2Blob(
|
||||
const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file, uint64_t record_offset, uint64_t payload_size,
|
||||
@@ -379,6 +532,11 @@ Status BlobSource::GetSimpleGen2Blob(
|
||||
if (bytes_read) {
|
||||
*bytes_read = record_size;
|
||||
}
|
||||
// Whole-column read on the lazy resolve path (partial reads go through
|
||||
// GetSimpleGen2BlobRange).
|
||||
if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
|
||||
RecordLazyRead(statistics_, record_size);
|
||||
}
|
||||
|
||||
if (blob_cache_ && read_options.fill_cache) {
|
||||
// If filling cache is allowed and a cache is configured, try to put the
|
||||
@@ -398,6 +556,83 @@ Status BlobSource::GetSimpleGen2Blob(
|
||||
return s;
|
||||
}
|
||||
|
||||
Status BlobSource::GetSimpleGen2BlobRange(
|
||||
const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file, uint64_t record_offset, uint64_t payload_size,
|
||||
ChecksumType /*checksum_type*/, uint32_t /*base_context_checksum*/,
|
||||
CompressionType expected_compression, uint64_t range_offset,
|
||||
size_t range_length, PinnableSlice* value, uint64_t* bytes_read) {
|
||||
assert(value);
|
||||
assert(file);
|
||||
// Partial reads are for uncompressed payloads only; the caller decides this
|
||||
// (a compressed column takes the whole-payload GetSimpleGen2Blob path +
|
||||
// slice).
|
||||
assert(expected_compression == kNoCompression);
|
||||
// Range reads are (currently) only issued while resolving a lazy result, so
|
||||
// the lazy read stats below are recorded unconditionally (unlike
|
||||
// GetSimpleGen2Blob, which gates on the activity). Enforce that invariant
|
||||
// here.
|
||||
assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
|
||||
|
||||
const CacheKey cache_key =
|
||||
GetSimpleGen2BlobCacheKey(base_cache_key, record_offset);
|
||||
|
||||
Status s;
|
||||
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
|
||||
// First, probe the blob cache for the whole payload. On a hit, slice the
|
||||
// requested sub-range out of the cached payload while pinning the cache
|
||||
// handle (zero-copy, no disk read). A partial read never inserts into the
|
||||
// cache.
|
||||
if (blob_cache_) {
|
||||
Slice key = cache_key.AsSlice();
|
||||
s = GetBlobFromCache(key, &blob_handle);
|
||||
if (s.ok()) {
|
||||
PinCacheHitSubRange(&blob_handle, range_offset, range_length, value);
|
||||
if (bytes_read) {
|
||||
*bytes_read = 0; // served from cache; no disk read
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
assert(blob_handle.IsEmpty());
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
}
|
||||
|
||||
// Cache miss: read only the requested sub-range from the file. The result is
|
||||
// never inserted into the blob cache (a partial payload cannot represent the
|
||||
// whole-record cache entry).
|
||||
CacheAllocationPtr buf = AllocateBlock(range_length, /*allocator=*/nullptr);
|
||||
s = ReadSimpleGen2BlobRange(read_options, file, record_offset,
|
||||
static_cast<size_t>(payload_size), range_offset,
|
||||
range_length, expected_compression, buf.get());
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
std::unique_ptr<BlobContents> blob_contents(
|
||||
new BlobContents(std::move(buf), range_length));
|
||||
|
||||
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, range_length);
|
||||
PERF_COUNTER_ADD(blob_read_count, 1);
|
||||
PERF_COUNTER_ADD(blob_read_byte, range_length);
|
||||
RecordLazyRead(statistics_, range_length);
|
||||
RecordLazyPartialRead(statistics_, range_length, payload_size);
|
||||
if (bytes_read) {
|
||||
*bytes_read = range_length;
|
||||
}
|
||||
|
||||
PinOwnedBlob(&blob_contents, value);
|
||||
|
||||
assert(s.ok());
|
||||
return s;
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetBlob(const ReadOptions& read_options,
|
||||
autovector<BlobFileReadRequests>& blob_reqs,
|
||||
uint64_t* bytes_read) {
|
||||
|
||||
@@ -61,6 +61,31 @@ class BlobSource {
|
||||
FilePrefetchBuffer* prefetch_buffer, PinnableSlice* value,
|
||||
uint64_t* bytes_read);
|
||||
|
||||
// Reads a byte sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed* blob's value, reading only those bytes on a cache miss.
|
||||
//
|
||||
// First probes the blob cache for the whole value. On a hit, pins the cache
|
||||
// handle into *value and points it at the requested sub-range (zero-copy, no
|
||||
// disk I/O). On a miss, reads only the requested bytes from the blob file
|
||||
// (via BlobFileReader::GetBlobRange -- no record-header/key read and no
|
||||
// whole-record checksum verification) and pins the owned buffer into *value.
|
||||
//
|
||||
// Unlike GetBlob, a partial read never inserts into the blob cache: the cache
|
||||
// entry is keyed per (file, offset) and holds the *whole* BlobContents, so a
|
||||
// partial value would violate that invariant. `compression_type` must be
|
||||
// kNoCompression (a strict sub-range of a compressed blob cannot be
|
||||
// decompressed in isolation); callers that need a compressed column, a
|
||||
// whole-column read, or checksum verification use GetBlob and slice instead.
|
||||
//
|
||||
// The caller must ensure range_offset + range_length <= value_size. On a miss
|
||||
// *bytes_read (when non-null) is the number of bytes read from the file; on a
|
||||
// hit it is 0.
|
||||
Status GetBlobRange(const ReadOptions& read_options, const Slice& user_key,
|
||||
uint64_t file_number, uint64_t offset, uint64_t file_size,
|
||||
uint64_t value_size, CompressionType compression_type,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
PinnableSlice* value, uint64_t* bytes_read);
|
||||
|
||||
// Reads a SimpleGen2Blob payload (see db/blob/blob_gen2_format.h) through the
|
||||
// blob value cache and BLOB_DB_* statistics. This is the counterpart to
|
||||
// GetBlob() for the second-generation blob record format, which is read
|
||||
@@ -96,6 +121,31 @@ class BlobSource {
|
||||
CompressionType expected_compression,
|
||||
PinnableSlice* value, uint64_t* bytes_read);
|
||||
|
||||
// Reads a byte sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed* SimpleGen2Blob payload, reading only those bytes on a cache
|
||||
// miss. This is the embedded (same-file) counterpart of GetBlobRange().
|
||||
//
|
||||
// First probes the blob cache for the whole payload (keyed exactly as
|
||||
// GetSimpleGen2Blob). On a hit, pins the cache handle into *value and points
|
||||
// it at the requested sub-range (zero-copy, no disk I/O). On a miss, reads
|
||||
// only the requested bytes from the file (via ReadSimpleGen2BlobRange -- no
|
||||
// trailer read and no checksum verification) and pins the owned buffer.
|
||||
//
|
||||
// Like GetBlobRange, a partial read never inserts into the blob cache (the
|
||||
// cache entry holds the whole payload). `expected_compression` must be
|
||||
// kNoCompression. The caller must ensure range_offset + range_length <=
|
||||
// payload_size. On a miss *bytes_read (when non-null) is the number of bytes
|
||||
// read from the file; on a hit it is 0.
|
||||
Status GetSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file,
|
||||
uint64_t record_offset, uint64_t payload_size,
|
||||
ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum,
|
||||
CompressionType expected_compression,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
PinnableSlice* value, uint64_t* bytes_read);
|
||||
|
||||
// Read multiple blobs from the underlying cache or blob file(s).
|
||||
//
|
||||
// If successful, returns ok and sets "result" in the elements of "blob_reqs"
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -63,6 +65,75 @@ TEST_F(DBBlobBasicTest, GetBlob) {
|
||||
.IsIncomplete());
|
||||
}
|
||||
|
||||
TEST_F(DBBlobBasicTest, BlobFileWritableFileMaxBufferSize) {
|
||||
constexpr uint64_t kBlobWriterBufferSize = 128 * 1024;
|
||||
|
||||
Options options = GetDefaultOptions();
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 0;
|
||||
options.writable_file_max_buffer_size = 1024 * 1024;
|
||||
options.blob_file_writable_file_max_buffer_size = kBlobWriterBufferSize;
|
||||
|
||||
Reopen(options);
|
||||
|
||||
std::atomic<int> matching_blob_writer_count{0};
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"WritableFileWriter::WritableFileWriter:0", [&](void* arg) {
|
||||
const uint64_t max_buffer_size =
|
||||
static_cast<uint64_t>(reinterpret_cast<uintptr_t>(arg));
|
||||
if (max_buffer_size == kBlobWriterBufferSize) {
|
||||
++matching_blob_writer_count;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(Put("key", std::string(1024, 'v')));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_GE(matching_blob_writer_count.load(), 1);
|
||||
}
|
||||
|
||||
TEST_F(DBBlobBasicTest,
|
||||
DirectWriteBlobFileWritableFileMaxBufferSizeSetOptions) {
|
||||
constexpr uint64_t kBlobWriterBufferSize = 64 * 1024;
|
||||
|
||||
Options options = GetDefaultOptions();
|
||||
options.enable_blob_files = true;
|
||||
options.enable_blob_direct_write = true;
|
||||
options.allow_concurrent_memtable_write = false;
|
||||
options.min_blob_size = 0;
|
||||
options.blob_file_size = 200;
|
||||
options.writable_file_max_buffer_size = 1024 * 1024;
|
||||
|
||||
Reopen(options);
|
||||
|
||||
std::atomic<int> matching_blob_writer_count{0};
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"WritableFileWriter::WritableFileWriter:0", [&](void* arg) {
|
||||
const uint64_t max_buffer_size =
|
||||
static_cast<uint64_t>(reinterpret_cast<uintptr_t>(arg));
|
||||
if (max_buffer_size == kBlobWriterBufferSize) {
|
||||
++matching_blob_writer_count;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(Put("first", std::string(300, 'a')));
|
||||
ASSERT_OK(db_->SetOptions({{"blob_file_writable_file_max_buffer_size",
|
||||
std::to_string(kBlobWriterBufferSize)}}));
|
||||
ASSERT_OK(Put("second", std::string(300, 'b')));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_GE(matching_blob_writer_count.load(), 1);
|
||||
}
|
||||
|
||||
TEST_F(DBBlobBasicTest, EmptyValueNotStoredAsBlob) {
|
||||
// Regression test for crash when empty blob value is evicted to
|
||||
// CompressedSecondaryCache (T261142690). Empty values should always be
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "db/arena_wrapped_db_iter.h"
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/column_family.h"
|
||||
#include "db/db_iter.h"
|
||||
@@ -138,8 +139,11 @@ class DBBlobIndexTest : public DBTestBase {
|
||||
bool resolve_direct_write_value, const Version* current,
|
||||
PinnableSlice* value, PinnableWideColumns* columns, Status* s,
|
||||
bool* is_blob_index, bool* value_found = nullptr) {
|
||||
VersionBlobFetcher blob_fetcher(current, read_options,
|
||||
cfd()->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/true);
|
||||
return DBImpl::MaybeResolveDirectWriteValue(
|
||||
read_options, key, resolve_direct_write_value, current, cfd(), value,
|
||||
read_options, key, resolve_direct_write_value, blob_fetcher, value,
|
||||
columns, s, is_blob_index, value_found);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
@@ -34,6 +37,21 @@ class SameFileBlobReader {
|
||||
virtual Status GetSameFileBlob(const ReadOptions& read_options,
|
||||
const BlobIndex& blob_index,
|
||||
PinnableSlice* value) const = 0;
|
||||
|
||||
// Resolves a byte sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed* same-file blob reference into `value`, reading only those
|
||||
// bytes on a cache miss (the embedded counterpart of a separate-file blob
|
||||
// range read). Pins the payload zero-copy (a sliced cached record, or an
|
||||
// owned sub-range buffer). Unlike GetSameFileBlob it skips whole-record
|
||||
// checksum verification (a strict sub-range cannot cover it) and never
|
||||
// populates the blob cache. The caller must ensure range_offset +
|
||||
// range_length <= blob_index.size(). read_tier == kBlockCacheTier without a
|
||||
// cached record yields Status::Incomplete.
|
||||
virtual Status GetSameFileBlobRange(const ReadOptions& read_options,
|
||||
const BlobIndex& blob_index,
|
||||
uint64_t range_offset,
|
||||
size_t range_length,
|
||||
PinnableSlice* value) const = 0;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
@@ -182,7 +183,6 @@ using ROCKSDB_NAMESPACE::TraceReader;
|
||||
using ROCKSDB_NAMESPACE::Transaction;
|
||||
using ROCKSDB_NAMESPACE::TransactionDB;
|
||||
using ROCKSDB_NAMESPACE::TransactionDBOptions;
|
||||
using ROCKSDB_NAMESPACE::TransactionLogIterator;
|
||||
using ROCKSDB_NAMESPACE::TransactionOptions;
|
||||
using ROCKSDB_NAMESPACE::UserCollectedProperties;
|
||||
using ROCKSDB_NAMESPACE::UserDefinedIndexFactory;
|
||||
@@ -190,6 +190,7 @@ using ROCKSDB_NAMESPACE::VectorWalPtr;
|
||||
using ROCKSDB_NAMESPACE::WaitForCompactOptions;
|
||||
using ROCKSDB_NAMESPACE::WalFile;
|
||||
using ROCKSDB_NAMESPACE::WalFilter;
|
||||
using ROCKSDB_NAMESPACE::WalIterator;
|
||||
using ROCKSDB_NAMESPACE::WALRecoveryMode;
|
||||
using ROCKSDB_NAMESPACE::WritableFile;
|
||||
using ROCKSDB_NAMESPACE::WriteBatch;
|
||||
@@ -412,12 +413,16 @@ struct rocksdb_readoptions_t {
|
||||
void* table_filter_state = nullptr;
|
||||
void (*table_filter_destructor)(void*) = nullptr;
|
||||
rocksdb_readoptions_table_filter_cb table_filter_callback = nullptr;
|
||||
// Owns the std::function that rep.table_filter points to. rep.table_filter is
|
||||
// a pointer to a caller-owned std::function, and this struct is that owner.
|
||||
std::function<bool(const TableProperties&)> table_filter_func;
|
||||
std::shared_ptr<UserDefinedIndexFactory> table_index_factory;
|
||||
|
||||
~rocksdb_readoptions_t() { ClearTableFilter(); }
|
||||
|
||||
void ClearTableFilter() {
|
||||
rep.table_filter = {};
|
||||
rep.table_filter = nullptr;
|
||||
table_filter_func = {};
|
||||
if (table_filter_destructor != nullptr) {
|
||||
(*table_filter_destructor)(table_filter_state);
|
||||
}
|
||||
@@ -435,11 +440,12 @@ struct rocksdb_readoptions_t {
|
||||
if (callback != nullptr) {
|
||||
auto cb = table_filter_callback;
|
||||
auto filter_state = table_filter_state;
|
||||
rep.table_filter = [cb, filter_state](const TableProperties& props) {
|
||||
table_filter_func = [cb, filter_state](const TableProperties& props) {
|
||||
return cb(filter_state,
|
||||
reinterpret_cast<const rocksdb_table_properties_t*>(
|
||||
&props)) != 0;
|
||||
};
|
||||
rep.table_filter = &table_filter_func;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -480,10 +486,10 @@ struct rocksdb_writablefile_t {
|
||||
WritableFile* rep;
|
||||
};
|
||||
struct rocksdb_wal_iterator_t {
|
||||
TransactionLogIterator* rep;
|
||||
WalIterator* rep;
|
||||
};
|
||||
struct rocksdb_wal_readoptions_t {
|
||||
TransactionLogIterator::ReadOptions rep;
|
||||
WalIterator::ReadOptions rep;
|
||||
};
|
||||
|
||||
struct rocksdb_wal_files_t {
|
||||
@@ -594,6 +600,63 @@ struct rocksdb_perfcontext_t {
|
||||
struct rocksdb_pinnableslice_t {
|
||||
PinnableSlice rep;
|
||||
};
|
||||
struct rocksdb_pinnable_multi_get_t {
|
||||
static constexpr size_t kNotFound = std::numeric_limits<size_t>::max();
|
||||
|
||||
explicit rocksdb_pinnable_multi_get_t(size_t result_count)
|
||||
: result_indexes(result_count, kNotFound) {}
|
||||
|
||||
void ReservePayloads(const std::vector<Status>& statuses) {
|
||||
size_t value_count = 0;
|
||||
size_t error_count = 0;
|
||||
for (const Status& status : statuses) {
|
||||
value_count += status.ok();
|
||||
error_count += !status.ok() && !status.IsNotFound();
|
||||
}
|
||||
values.reserve(value_count);
|
||||
errors.reserve(error_count);
|
||||
}
|
||||
|
||||
void StoreResults(std::vector<PinnableSlice>* multi_get_values,
|
||||
const std::vector<Status>& statuses) {
|
||||
ReservePayloads(statuses);
|
||||
for (size_t i = 0; i < statuses.size(); ++i) {
|
||||
if (statuses[i].ok()) {
|
||||
result_indexes[i] = values.size();
|
||||
values.emplace_back(std::move((*multi_get_values)[i]));
|
||||
} else if (!statuses[i].IsNotFound()) {
|
||||
result_indexes[i] = kNotFound - errors.size() - 1;
|
||||
errors.emplace_back(statuses[i].ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int GetResult(size_t index, const char** value, size_t* value_size,
|
||||
const char** error, size_t* error_size) const {
|
||||
if (index >= result_indexes.size()) {
|
||||
return rocksdb_pinnable_multi_get_out_of_bounds;
|
||||
}
|
||||
const size_t result_index = result_indexes[index];
|
||||
if (result_index == kNotFound) {
|
||||
return rocksdb_pinnable_multi_get_not_found;
|
||||
}
|
||||
if (result_index < values.size()) {
|
||||
*value = values[result_index].data();
|
||||
*value_size = values[result_index].size();
|
||||
return rocksdb_pinnable_multi_get_found;
|
||||
}
|
||||
const std::string& message = errors[kNotFound - result_index - 1];
|
||||
*error = message.c_str();
|
||||
*error_size = message.size();
|
||||
return rocksdb_pinnable_multi_get_error;
|
||||
}
|
||||
|
||||
// Found entries store a value index. Error entries count back from
|
||||
// kNotFound - 1, leaving kNotFound to represent a missing key.
|
||||
std::vector<size_t> result_indexes;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<std::string> errors;
|
||||
};
|
||||
struct rocksdb_transactiondb_options_t {
|
||||
TransactionDBOptions rep;
|
||||
};
|
||||
@@ -2862,6 +2925,44 @@ void rocksdb_batched_multi_get_cf_slice(
|
||||
delete[] statuses;
|
||||
}
|
||||
|
||||
rocksdb_pinnable_multi_get_t* rocksdb_batched_multi_get_pinned_cf(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
rocksdb_column_family_handle_t* column_family, size_t num_keys,
|
||||
const rocksdb_slice_t* keys, unsigned char sorted_input) {
|
||||
auto results = std::make_unique<rocksdb_pinnable_multi_get_t>(num_keys);
|
||||
if (num_keys == 0) {
|
||||
return results.release();
|
||||
}
|
||||
const Slice* key_slices = reinterpret_cast<const Slice*>(keys);
|
||||
std::vector<PinnableSlice> values(num_keys);
|
||||
std::vector<Status> statuses(num_keys);
|
||||
db->rep->MultiGet(options->rep, column_family->rep, num_keys, key_slices,
|
||||
values.data(), statuses.data(), sorted_input != 0);
|
||||
results->StoreResults(&values, statuses);
|
||||
return results.release();
|
||||
}
|
||||
|
||||
size_t rocksdb_pinnable_multi_get_count(
|
||||
const rocksdb_pinnable_multi_get_t* multi_get) {
|
||||
return multi_get->result_indexes.size();
|
||||
}
|
||||
|
||||
int rocksdb_pinnable_multi_get_result(
|
||||
const rocksdb_pinnable_multi_get_t* multi_get, size_t index,
|
||||
const char** value, size_t* value_size, const char** error,
|
||||
size_t* error_size) {
|
||||
*value = nullptr;
|
||||
*value_size = 0;
|
||||
*error = nullptr;
|
||||
*error_size = 0;
|
||||
return multi_get->GetResult(index, value, value_size, error, error_size);
|
||||
}
|
||||
|
||||
void rocksdb_pinnable_multi_get_destroy(
|
||||
rocksdb_pinnable_multi_get_t* multi_get) {
|
||||
delete multi_get;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_key_may_exist(rocksdb_t* db,
|
||||
const rocksdb_readoptions_t* options,
|
||||
const char* key, size_t key_len,
|
||||
@@ -2921,8 +3022,8 @@ rocksdb_iterator_t* rocksdb_create_iterator(
|
||||
rocksdb_wal_iterator_t* rocksdb_get_updates_since(
|
||||
rocksdb_t* db, uint64_t seq_number,
|
||||
const rocksdb_wal_readoptions_t* options, char** errptr) {
|
||||
std::unique_ptr<TransactionLogIterator> iter;
|
||||
TransactionLogIterator::ReadOptions ro;
|
||||
std::unique_ptr<WalIterator> iter;
|
||||
WalIterator::ReadOptions ro;
|
||||
if (options != nullptr) {
|
||||
ro = options->rep;
|
||||
}
|
||||
@@ -10271,6 +10372,16 @@ uint64_t rocksdb_options_get_blob_file_size(rocksdb_options_t* opt) {
|
||||
return opt->rep.blob_file_size;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_blob_file_writable_file_max_buffer_size(
|
||||
rocksdb_options_t* opt, uint64_t v) {
|
||||
opt->rep.blob_file_writable_file_max_buffer_size = v;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_options_get_blob_file_writable_file_max_buffer_size(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.blob_file_writable_file_max_buffer_size;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_blob_compression_type(rocksdb_options_t* opt, int v) {
|
||||
opt->rep.blob_compression_type =
|
||||
static_cast<decltype(opt->rep.blob_compression_type)>(v);
|
||||
|
||||
+178
@@ -189,6 +189,57 @@ static void CheckMultiGetValues(size_t num_keys, char** values,
|
||||
}
|
||||
}
|
||||
|
||||
static void CheckPinnableMultiGetFound(
|
||||
const rocksdb_pinnable_multi_get_t* multi_get, size_t index,
|
||||
const char* expected, size_t expected_size) {
|
||||
const char* value;
|
||||
const char* error;
|
||||
size_t value_size;
|
||||
size_t error_size;
|
||||
int result = rocksdb_pinnable_multi_get_result(
|
||||
multi_get, index, &value, &value_size, &error, &error_size);
|
||||
CheckCondition(result == rocksdb_pinnable_multi_get_found);
|
||||
CheckCondition(value_size == expected_size);
|
||||
CheckCondition(error == NULL);
|
||||
CheckCondition(error_size == 0);
|
||||
if (expected_size > 0) {
|
||||
CheckCondition(value != NULL);
|
||||
CheckCondition(memcmp(value, expected, expected_size) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void CheckPinnableMultiGetNotFound(
|
||||
const rocksdb_pinnable_multi_get_t* multi_get, size_t index) {
|
||||
const char* value;
|
||||
const char* error;
|
||||
size_t value_size;
|
||||
size_t error_size;
|
||||
int result = rocksdb_pinnable_multi_get_result(
|
||||
multi_get, index, &value, &value_size, &error, &error_size);
|
||||
CheckCondition(result == rocksdb_pinnable_multi_get_not_found);
|
||||
CheckCondition(value == NULL);
|
||||
CheckCondition(value_size == 0);
|
||||
CheckCondition(error == NULL);
|
||||
CheckCondition(error_size == 0);
|
||||
}
|
||||
|
||||
static void CheckPinnableMultiGetError(
|
||||
const rocksdb_pinnable_multi_get_t* multi_get, size_t index,
|
||||
const char* expected) {
|
||||
const char* value;
|
||||
const char* error;
|
||||
size_t value_size;
|
||||
size_t error_size;
|
||||
int result = rocksdb_pinnable_multi_get_result(
|
||||
multi_get, index, &value, &value_size, &error, &error_size);
|
||||
CheckCondition(result == rocksdb_pinnable_multi_get_error);
|
||||
CheckCondition(value == NULL);
|
||||
CheckCondition(value_size == 0);
|
||||
CheckCondition(error != NULL);
|
||||
CheckCondition(error_size == strlen(error));
|
||||
CheckCondition(strstr(error, expected) != NULL);
|
||||
}
|
||||
|
||||
static void CheckIter(rocksdb_iterator_t* iter, const char* key,
|
||||
const char* val) {
|
||||
size_t len;
|
||||
@@ -834,6 +885,39 @@ static char* MergeOperatorPartialMerge(void* arg, const char* key,
|
||||
return result;
|
||||
}
|
||||
|
||||
static char* FailingMergeOperatorFullMerge(
|
||||
void* arg, const char* key, size_t key_length, const char* existing_value,
|
||||
size_t existing_value_length, const char* const* operands_list,
|
||||
const size_t* operands_list_length, int num_operands,
|
||||
unsigned char* success, size_t* new_value_length) {
|
||||
(void)arg;
|
||||
(void)key;
|
||||
(void)key_length;
|
||||
(void)existing_value;
|
||||
(void)existing_value_length;
|
||||
(void)operands_list;
|
||||
(void)operands_list_length;
|
||||
(void)num_operands;
|
||||
*success = 0;
|
||||
*new_value_length = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char* FailingMergeOperatorPartialMerge(
|
||||
void* arg, const char* key, size_t key_length,
|
||||
const char* const* operands_list, const size_t* operands_list_length,
|
||||
int num_operands, unsigned char* success, size_t* new_value_length) {
|
||||
(void)arg;
|
||||
(void)key;
|
||||
(void)key_length;
|
||||
(void)operands_list;
|
||||
(void)operands_list_length;
|
||||
(void)num_operands;
|
||||
*success = 0;
|
||||
*new_value_length = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void CheckTxnGet(rocksdb_transaction_t* txn,
|
||||
const rocksdb_readoptions_t* options, const char* key,
|
||||
const char* expected) {
|
||||
@@ -2639,12 +2723,17 @@ int main(int argc, char** argv) {
|
||||
// use dbpathname2 as the cf_path for "cf1"
|
||||
rocksdb_dbpath_t* dbpath2;
|
||||
char dbpathname2[200];
|
||||
rocksdb_mergeoperator_t* failing_merge_operator;
|
||||
snprintf(dbpathname2, sizeof(dbpathname2),
|
||||
"%s/rocksdb_c_test-%" PRIu64 "-dbpath2", GetTempDir(),
|
||||
GetTestId());
|
||||
dbpath2 = rocksdb_dbpath_create(dbpathname2, 1024 * 1024);
|
||||
const rocksdb_dbpath_t* cf_paths[1] = {dbpath2};
|
||||
rocksdb_options_set_cf_paths(cf_options_2, cf_paths, 1);
|
||||
failing_merge_operator = rocksdb_mergeoperator_create(
|
||||
NULL, MergeOperatorDestroy, FailingMergeOperatorFullMerge,
|
||||
FailingMergeOperatorPartialMerge, NULL, MergeOperatorName);
|
||||
rocksdb_options_set_merge_operator(cf_options_2, failing_merge_operator);
|
||||
|
||||
const char* cf_names[2] = {"default", "cf1"};
|
||||
const rocksdb_options_t* cf_opts[2] = {cf_options_1, cf_options_2};
|
||||
@@ -3167,6 +3256,95 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
rocksdb_slice_t pinned_keys[6] = {{"box", 3}, {"missing", 7},
|
||||
{"empty", 5}, {"merge-error", 11},
|
||||
{"box", 3}, {"owned", 5}};
|
||||
rocksdb_pinnable_multi_get_t* multi_get;
|
||||
const char* value;
|
||||
const char* error;
|
||||
size_t value_size;
|
||||
size_t error_size;
|
||||
int result;
|
||||
|
||||
rocksdb_put_cf(db, woptions, handles[1], "empty", 5, "", 0, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_put_cf(db, woptions, handles[1], "merge-error", 11, "base", 4,
|
||||
&err);
|
||||
CheckNoError(err);
|
||||
rocksdb_merge_cf(db, woptions, handles[1], "merge-error", 11, "operand",
|
||||
7, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_put_cf(db, woptions, handles[1], "owned", 5, "before", 6, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
multi_get = rocksdb_batched_multi_get_pinned_cf(db, roptions, handles[1],
|
||||
6, pinned_keys, 0);
|
||||
CheckCondition(rocksdb_pinnable_multi_get_count(multi_get) == 6);
|
||||
CheckPinnableMultiGetFound(multi_get, 0, "c", 1);
|
||||
CheckPinnableMultiGetNotFound(multi_get, 1);
|
||||
CheckPinnableMultiGetFound(multi_get, 2, "", 0);
|
||||
CheckPinnableMultiGetError(multi_get, 3, "Merge operator failed");
|
||||
CheckPinnableMultiGetFound(multi_get, 4, "c", 1);
|
||||
CheckPinnableMultiGetFound(multi_get, 5, "before", 6);
|
||||
|
||||
value = "unchanged";
|
||||
error = "unchanged";
|
||||
value_size = 9;
|
||||
error_size = 9;
|
||||
result = rocksdb_pinnable_multi_get_result(
|
||||
multi_get, 6, &value, &value_size, &error, &error_size);
|
||||
CheckCondition(result == rocksdb_pinnable_multi_get_out_of_bounds);
|
||||
CheckCondition(value == NULL);
|
||||
CheckCondition(value_size == 0);
|
||||
CheckCondition(error == NULL);
|
||||
CheckCondition(error_size == 0);
|
||||
|
||||
result = rocksdb_pinnable_multi_get_result(
|
||||
multi_get, 5, &value, &value_size, &error, &error_size);
|
||||
CheckCondition(result == rocksdb_pinnable_multi_get_found);
|
||||
const char* borrowed_value = value;
|
||||
size_t borrowed_value_size = value_size;
|
||||
result = rocksdb_pinnable_multi_get_result(
|
||||
multi_get, 3, &value, &value_size, &error, &error_size);
|
||||
CheckCondition(result == rocksdb_pinnable_multi_get_error);
|
||||
const char* borrowed_error = error;
|
||||
size_t borrowed_error_size = error_size;
|
||||
rocksdb_put_cf(db, woptions, handles[1], "owned", 5, "after", 5, &err);
|
||||
CheckNoError(err);
|
||||
CheckCondition(borrowed_value_size == 6);
|
||||
CheckCondition(memcmp(borrowed_value, "before", 6) == 0);
|
||||
CheckCondition(borrowed_error_size == strlen(borrowed_error));
|
||||
CheckCondition(strstr(borrowed_error, "Merge operator failed") != NULL);
|
||||
rocksdb_pinnable_multi_get_destroy(multi_get);
|
||||
}
|
||||
|
||||
{
|
||||
rocksdb_slice_t sorted_keys[4] = {
|
||||
{"box", 3}, {"box", 3}, {"empty", 5}, {"missing", 7}};
|
||||
rocksdb_pinnable_multi_get_t* multi_get =
|
||||
rocksdb_batched_multi_get_pinned_cf(db, roptions, handles[1], 4,
|
||||
sorted_keys, 1);
|
||||
CheckCondition(rocksdb_pinnable_multi_get_count(multi_get) == 4);
|
||||
CheckPinnableMultiGetFound(multi_get, 0, "c", 1);
|
||||
CheckPinnableMultiGetFound(multi_get, 1, "c", 1);
|
||||
CheckPinnableMultiGetFound(multi_get, 2, "", 0);
|
||||
CheckPinnableMultiGetNotFound(multi_get, 3);
|
||||
rocksdb_pinnable_multi_get_destroy(multi_get);
|
||||
|
||||
multi_get = rocksdb_batched_multi_get_pinned_cf(db, roptions, handles[1],
|
||||
0, sorted_keys, 1);
|
||||
CheckCondition(rocksdb_pinnable_multi_get_count(multi_get) == 0);
|
||||
rocksdb_pinnable_multi_get_destroy(multi_get);
|
||||
}
|
||||
|
||||
rocksdb_delete_cf(db, woptions, handles[1], "empty", 5, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_delete_cf(db, woptions, handles[1], "merge-error", 11, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_delete_cf(db, woptions, handles[1], "owned", 5, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
{
|
||||
unsigned char value_found = 0;
|
||||
|
||||
|
||||
+45
-2
@@ -29,6 +29,7 @@
|
||||
#include "trace_replay/block_cache_tracer.h"
|
||||
#include "util/cast_util.h"
|
||||
#include "util/hash_containers.h"
|
||||
#include "util/mutexlock.h"
|
||||
#include "util/thread_local.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -520,6 +521,32 @@ class ColumnFamilyData {
|
||||
bool queued_for_flush() { return queued_for_flush_; }
|
||||
bool queued_for_compaction() { return queued_for_compaction_; }
|
||||
|
||||
int compaction_aborted() const {
|
||||
return compaction_aborted_.obj_.load(std::memory_order_acquire);
|
||||
}
|
||||
void inc_compaction_aborted(InstrumentedMutex* db_mutex) {
|
||||
db_mutex->AssertHeld();
|
||||
compaction_aborted_.obj_.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
int dec_compaction_aborted(InstrumentedMutex* db_mutex) {
|
||||
db_mutex->AssertHeld();
|
||||
return compaction_aborted_.obj_.fetch_sub(1, std::memory_order_acq_rel) - 1;
|
||||
}
|
||||
|
||||
int num_in_flight_compactions(InstrumentedMutex* db_mutex) const {
|
||||
db_mutex->AssertHeld();
|
||||
return num_in_flight_compactions_;
|
||||
}
|
||||
void inc_num_in_flight_compactions(InstrumentedMutex* db_mutex) {
|
||||
db_mutex->AssertHeld();
|
||||
++num_in_flight_compactions_;
|
||||
}
|
||||
void dec_num_in_flight_compactions(InstrumentedMutex* db_mutex) {
|
||||
db_mutex->AssertHeld();
|
||||
assert(num_in_flight_compactions_ > 0);
|
||||
--num_in_flight_compactions_;
|
||||
}
|
||||
|
||||
static std::pair<WriteStallCondition, WriteStallCause>
|
||||
GetWriteStallConditionAndCause(
|
||||
int num_unflushed_memtables, int num_l0_files,
|
||||
@@ -711,10 +738,26 @@ class ColumnFamilyData {
|
||||
// If true --> this ColumnFamily is currently present in DBImpl::flush_queue_
|
||||
bool queued_for_flush_;
|
||||
|
||||
// If true --> this ColumnFamily is currently present in
|
||||
// DBImpl::compaction_queue_
|
||||
// If true, this ColumnFamily is in DBImpl::compaction_queue_ or its parked
|
||||
// compaction set.
|
||||
bool queued_for_compaction_;
|
||||
|
||||
// Read outside the DB mutex by compaction workers (acquire-loaded roughly
|
||||
// every 1024 records in ProcessKeyValueCompaction). Given its own cache line
|
||||
// via CacheAlignedWrapper to avoid false sharing with the DB-mutex-written
|
||||
// fields packed around it.
|
||||
CacheAlignedWrapper<std::atomic<int>> compaction_aborted_{};
|
||||
|
||||
// Per-CF count of compactions "in flight" for AbortCompactions(): a picked
|
||||
// Compaction (or one CompactFiles call), tracked from
|
||||
// BeginInFlightCompaction() until EndInFlightCompaction() at the end of
|
||||
// BackgroundCompaction, so it covers the NotifyOnCompactionCompleted window
|
||||
// that CompactionPicker's compactions_in_progress_ set does not. NOT the same
|
||||
// as DBImpl::num_running_compactions_ (which counts BackgroundCallCompaction
|
||||
// worker invocations, backs the public rocksdb.num-running-compactions
|
||||
// property, and excludes CompactFiles). Protected by the DB mutex.
|
||||
int num_in_flight_compactions_{0};
|
||||
|
||||
uint64_t prev_compaction_needed_bytes_;
|
||||
|
||||
// if the database was opened with 2pc enabled
|
||||
|
||||
@@ -152,7 +152,8 @@ CompactionJob::CompactionJob(
|
||||
const std::atomic<int>& compaction_aborted, const std::string& db_id,
|
||||
const std::string& db_session_id, std::string full_history_ts_low,
|
||||
std::string trim_ts, BlobFileCompletionCallback* blob_callback,
|
||||
int* bg_compaction_scheduled, int* bg_bottom_compaction_scheduled)
|
||||
int* bg_compaction_scheduled, int* bg_bottom_compaction_scheduled,
|
||||
std::atomic<int>* num_running_remote_compactions)
|
||||
: compact_(new CompactionState(compaction)),
|
||||
internal_stats_(compaction->compaction_reason(), 1),
|
||||
db_options_(db_options),
|
||||
@@ -198,7 +199,8 @@ CompactionJob::CompactionJob(
|
||||
blob_callback_(blob_callback),
|
||||
extra_num_subcompaction_threads_reserved_(0),
|
||||
bg_compaction_scheduled_(bg_compaction_scheduled),
|
||||
bg_bottom_compaction_scheduled_(bg_bottom_compaction_scheduled) {
|
||||
bg_bottom_compaction_scheduled_(bg_bottom_compaction_scheduled),
|
||||
num_running_remote_compactions_(num_running_remote_compactions) {
|
||||
assert(job_stats_ != nullptr);
|
||||
assert(log_buffer_ != nullptr);
|
||||
assert(job_context);
|
||||
@@ -1668,8 +1670,12 @@ Status CompactionJob::ProcessKeyValue(
|
||||
const uint64_t kCronEveryMask = (1 << 10) - 1;
|
||||
[[maybe_unused]] const std::optional<const Slice> end = sub_compact->end;
|
||||
|
||||
// Check for abort signal before starting key processing
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
// Check for abort signal before starting key processing. The DB-wide
|
||||
// (AbortAllCompactions) and per-CF (AbortCompactions) counters compose here;
|
||||
// either aborts this job. See DB::AbortCompactions() in db.h for how the
|
||||
// abort knobs relate to each other and to the other compaction controls.
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0 ||
|
||||
cfd->compaction_aborted() > 0) {
|
||||
return Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
}
|
||||
|
||||
@@ -1691,7 +1697,8 @@ Status CompactionJob::ProcessKeyValue(
|
||||
// Periodic cron operations: stats update, abort check.
|
||||
if ((num_records & kCronEveryMask) == kCronEveryMask) {
|
||||
// Check for abort signal periodically
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0 ||
|
||||
cfd->compaction_aborted() > 0) {
|
||||
status = Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
break;
|
||||
}
|
||||
@@ -2613,7 +2620,8 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
bottommost_level_, TableFileCreationReason::kCompaction,
|
||||
0 /* oldest_key_time */, current_time, db_id_, db_session_id_,
|
||||
sub_compact->compaction->max_output_file_size(), file_number,
|
||||
proximal_after_seqno_ /*last_level_inclusive_max_seqno_threshold*/);
|
||||
proximal_after_seqno_ /*last_level_inclusive_max_seqno_threshold*/,
|
||||
dbname_ /*db_name*/, IsRemoteCompaction() /*is_remote_compaction*/);
|
||||
|
||||
outputs.NewBuilder(tboptions);
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/memtablerep.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
#include "util/autovector.h"
|
||||
#include "util/stop_watch.h"
|
||||
#include "util/thread_local.h"
|
||||
@@ -166,7 +166,8 @@ class CompactionJob {
|
||||
std::string full_history_ts_low = "", std::string trim_ts = "",
|
||||
BlobFileCompletionCallback* blob_callback = nullptr,
|
||||
int* bg_compaction_scheduled = nullptr,
|
||||
int* bg_bottom_compaction_scheduled = nullptr);
|
||||
int* bg_bottom_compaction_scheduled = nullptr,
|
||||
std::atomic<int>* num_running_remote_compactions = nullptr);
|
||||
|
||||
virtual ~CompactionJob();
|
||||
|
||||
@@ -500,6 +501,12 @@ class CompactionJob {
|
||||
int* bg_compaction_scheduled_;
|
||||
int* bg_bottom_compaction_scheduled_;
|
||||
|
||||
// Stores the pointer to DBImpl::num_running_remote_compactions_, backing the
|
||||
// public rocksdb.num-running-remote-compactions property. Null when the job
|
||||
// has no owning DBImpl to report to (compaction service workers, unit tests).
|
||||
// Updated without the DB mutex held.
|
||||
std::atomic<int>* num_running_remote_compactions_;
|
||||
|
||||
// Stores the sequence number to time mapping gathered from all input files
|
||||
// it also collects the smallest_seqno -> oldest_ancester_time from the SST.
|
||||
SeqnoToTimeMapping seqno_to_time_mapping_;
|
||||
@@ -523,6 +530,10 @@ class CompactionJob {
|
||||
// Get table file name in where it's outputting to, which should also be in
|
||||
// `output_directory_`.
|
||||
virtual std::string GetTableFileName(uint64_t file_number);
|
||||
|
||||
// True if this job builds output files as a remote (offloaded)
|
||||
// CompactionService worker. Overridden by CompactionServiceCompactionJob.
|
||||
virtual bool IsRemoteCompaction() const { return false; }
|
||||
// The rate limiter priority (io_priority) is determined dynamically here.
|
||||
// The Compaction Read and Write priorities are the same for different
|
||||
// scenarios, such as write stalled.
|
||||
@@ -730,6 +741,7 @@ class CompactionServiceCompactionJob : private CompactionJob {
|
||||
private:
|
||||
// Get table file name in output_path
|
||||
std::string GetTableFileName(uint64_t file_number) override;
|
||||
bool IsRemoteCompaction() const override { return true; }
|
||||
// Specific the compaction output path, otherwise it uses default DB path
|
||||
const std::string output_path_;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "monitoring/thread_status_util.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "rocksdb/utilities/options_type.h"
|
||||
#include "util/defer.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
class SubcompactionState;
|
||||
@@ -114,9 +115,6 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
|
||||
break;
|
||||
}
|
||||
|
||||
std::string debug_str_before_wait =
|
||||
compaction->input_version()->DebugString(/*hex=*/true);
|
||||
|
||||
// TODO: Update CompactionService API to support abort and resume
|
||||
// functionality. Currently, remote compaction jobs cannot be aborted via
|
||||
// AbortAllCompactions() because the CompactionService interface lacks methods
|
||||
@@ -129,18 +127,29 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
|
||||
"[%s] [JOB %d] Waiting for remote compaction...",
|
||||
compaction->column_family_data()->GetName().c_str(), job_id_);
|
||||
std::string compaction_result_binary;
|
||||
CompactionServiceJobStatus compaction_status =
|
||||
db_options_.compaction_service->Wait(response.scheduled_job_id,
|
||||
&compaction_result_binary);
|
||||
CompactionServiceJobStatus compaction_status;
|
||||
{
|
||||
// Increment rocksdb.num-running-remote-compactions while this compaction
|
||||
// service job is waiting in CompactionService::Wait().
|
||||
std::atomic<int>* const num_running = num_running_remote_compactions_;
|
||||
if (num_running != nullptr) {
|
||||
num_running->fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
Defer decrement_num_running([num_running]() {
|
||||
if (num_running != nullptr) {
|
||||
num_running->fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
});
|
||||
compaction_status = db_options_.compaction_service->Wait(
|
||||
response.scheduled_job_id, &compaction_result_binary);
|
||||
}
|
||||
|
||||
if (compaction_status != CompactionServiceJobStatus::kSuccess) {
|
||||
ROCKS_LOG_ERROR(
|
||||
db_options_.info_log,
|
||||
"[%s] [JOB %d] Wait() status is not kSuccess. "
|
||||
"\nDebugString Before Wait():\n%s"
|
||||
"\nDebugString After Wait():\n%s",
|
||||
compaction->column_family_data()->GetName().c_str(), job_id_,
|
||||
debug_str_before_wait.c_str(),
|
||||
compaction->input_version()->DebugString(/*hex=*/true).c_str());
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "file/file_util.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/advanced_compression.h"
|
||||
#include "rocksdb/filter_policy.h"
|
||||
#include "rocksdb/utilities/object_registry.h"
|
||||
#include "rocksdb/utilities/options_util.h"
|
||||
#include "table/unique_id_impl.h"
|
||||
#include "utilities/merge_operators/string_append/stringappend.h"
|
||||
@@ -360,6 +364,7 @@ class CompactionServiceTest : public DBTestBase {
|
||||
|
||||
TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
Options options = CurrentOptions();
|
||||
options.max_open_files = -1;
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
Statistics* primary_statistics = GetPrimaryStatistics();
|
||||
@@ -480,6 +485,113 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// A CompressionManager that records, per GetCompressorForSST() call, whether
|
||||
// the FilterBuildingContext reports the SST as being built by a remote
|
||||
// (offloaded) compaction worker. Observations go into process-global counters
|
||||
// because the remote worker reconstructs its own manager instance from the
|
||||
// persisted OPTIONS file (same process in this test).
|
||||
struct RemoteAwareObservations {
|
||||
std::atomic<int> num_remote{0};
|
||||
std::atomic<int> num_non_remote{0};
|
||||
void Reset() {
|
||||
num_remote.store(0);
|
||||
num_non_remote.store(0);
|
||||
}
|
||||
};
|
||||
|
||||
RemoteAwareObservations& GetRemoteAwareObservations() {
|
||||
static RemoteAwareObservations obs;
|
||||
return obs;
|
||||
}
|
||||
|
||||
class RemoteAwareCompressionManager : public CompressionManagerWrapper {
|
||||
public:
|
||||
RemoteAwareCompressionManager()
|
||||
: CompressionManagerWrapper(GetBuiltinV2CompressionManager()) {}
|
||||
|
||||
static const char* kClassName() { return "RemoteAwareCompressionManager"; }
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
std::unique_ptr<Compressor> GetCompressorForSST(
|
||||
const FilterBuildingContext& context, const CompressionOptions& opts,
|
||||
CompressionType preferred) override {
|
||||
if (context.is_remote_compaction) {
|
||||
GetRemoteAwareObservations().num_remote.fetch_add(1);
|
||||
} else {
|
||||
GetRemoteAwareObservations().num_non_remote.fetch_add(1);
|
||||
}
|
||||
return CompressionManagerWrapper::GetCompressorForSST(context, opts,
|
||||
preferred);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(CompactionServiceTest, IsRemoteCompactionInCompressorContext) {
|
||||
// Register so the remote worker's DB::OpenAndCompact can reconstruct the
|
||||
// custom CompressionManager from the persisted OPTIONS file
|
||||
// (CompactionServiceOptionsOverride does not carry a compression_manager).
|
||||
ObjectLibrary::Default()->AddFactory<CompressionManager>(
|
||||
RemoteAwareCompressionManager::kClassName(),
|
||||
[](const std::string& /*uri*/, std::unique_ptr<CompressionManager>* guard,
|
||||
std::string* /*errmsg*/) {
|
||||
*guard = std::make_unique<RemoteAwareCompressionManager>();
|
||||
return guard->get();
|
||||
});
|
||||
|
||||
auto& obs = GetRemoteAwareObservations();
|
||||
obs.Reset();
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.compression_manager =
|
||||
std::make_shared<RemoteAwareCompressionManager>();
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
GenerateTestData();
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
VerifyTestData();
|
||||
|
||||
auto my_cs = GetCompactionService();
|
||||
ASSERT_GE(my_cs->GetCompactionNum(), 1);
|
||||
|
||||
// The offloaded worker built the compaction output SSTs and therefore must
|
||||
// have observed is_remote_compaction == true in GetCompressorForSST().
|
||||
ASSERT_GT(obs.num_remote.load(), 0);
|
||||
// The primary's own flushes, seen by the same manager, must report false
|
||||
ASSERT_GT(obs.num_non_remote.load(), 0);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, IsRemoteCompactionFalseForLocalCompaction) {
|
||||
// Same custom manager, but no compaction_service: everything runs locally,
|
||||
// so GetCompressorForSST() must never see is_remote_compaction == true.
|
||||
auto& obs = GetRemoteAwareObservations();
|
||||
obs.Reset();
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.compression_manager =
|
||||
std::make_shared<RemoteAwareCompressionManager>();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
Random rnd(301);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 100; j++) {
|
||||
ASSERT_OK(Put(Key(i * 100 + j), rnd.RandomString(64)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
// Local flushes and compactions must all report not-remote.
|
||||
ASSERT_GT(obs.num_non_remote.load(), 0);
|
||||
ASSERT_EQ(obs.num_remote.load(), 0);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, SkipWALRecoveryInOpenAndCompact) {
|
||||
// Verify that OpenAndCompact skips WAL recovery when opening the secondary
|
||||
// instance. WAL replay is unnecessary for remote compaction since it only
|
||||
@@ -1761,6 +1873,109 @@ TEST_F(CompactionServiceTest, SubCompaction) {
|
||||
ASSERT_GE(compaction_num, 2);
|
||||
}
|
||||
|
||||
class PropertySamplingCompactionService : public MyTestCompactionService {
|
||||
public:
|
||||
using MyTestCompactionService::MyTestCompactionService;
|
||||
|
||||
void SetPrimaryDB(DB* db) { db_ = db; }
|
||||
|
||||
CompactionServiceJobStatus Wait(const std::string& scheduled_job_id,
|
||||
std::string* result) override {
|
||||
EXPECT_TRUE(
|
||||
db_->GetIntProperty(DB::Properties::kNumRunningRemoteCompactions,
|
||||
&num_running_while_waiting_));
|
||||
return MyTestCompactionService::Wait(scheduled_job_id, result);
|
||||
}
|
||||
|
||||
uint64_t GetNumRunningWhileWaiting() const {
|
||||
return num_running_while_waiting_;
|
||||
}
|
||||
|
||||
private:
|
||||
DB* db_ = nullptr;
|
||||
uint64_t num_running_while_waiting_ = 0;
|
||||
};
|
||||
|
||||
TEST_F(CompactionServiceTest, NumRunningRemoteCompactionsProperty) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.env = env_;
|
||||
auto statistics = CreateDBStatistics();
|
||||
auto compaction_service = std::make_shared<PropertySamplingCompactionService>(
|
||||
dbname_, options, statistics,
|
||||
std::vector<std::shared_ptr<EventListener>>{},
|
||||
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>{});
|
||||
options.compaction_service = compaction_service;
|
||||
DestroyAndReopen(options);
|
||||
compaction_service->SetPrimaryDB(db_.get());
|
||||
|
||||
// Overlapping key ranges across files so the compaction is not a trivial
|
||||
// move, which would bypass the compaction service entirely.
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 10; j++) {
|
||||
ASSERT_OK(Put(Key(j), "value" + std::to_string(i * 10 + j)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
// No remote compaction is in flight yet.
|
||||
uint64_t num_running = 0;
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningRemoteCompactions,
|
||||
&num_running));
|
||||
ASSERT_EQ(num_running, 0U);
|
||||
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_GE(compaction_service->GetCompactionNum(), 1);
|
||||
ASSERT_GE(compaction_service->GetNumRunningWhileWaiting(), 1U);
|
||||
|
||||
// The counter is back to 0 once the remote jobs are installed.
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningRemoteCompactions,
|
||||
&num_running));
|
||||
ASSERT_EQ(num_running, 0U);
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, NumRunningRemoteCompactionsPropertyLocalOnly) {
|
||||
// Without a compaction_service configured, the property stays 0.
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Overlapping key ranges across files so the compaction is not a trivial
|
||||
// move, which would bypass the compaction service entirely.
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 10; j++) {
|
||||
ASSERT_OK(Put(Key(j), "value" + std::to_string(i * 10 + j)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
std::atomic<int> property_samples{0};
|
||||
std::atomic_bool observed_non_zero{false};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::ProcessKeyValueCompaction()::Processing",
|
||||
[&](void* /*arg*/) {
|
||||
uint64_t num_running = 0;
|
||||
EXPECT_TRUE(db_->GetIntProperty(
|
||||
DB::Properties::kNumRunningRemoteCompactions, &num_running));
|
||||
property_samples.fetch_add(1);
|
||||
if (num_running != 0) {
|
||||
observed_non_zero.store(true);
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_GT(property_samples.load(), 0);
|
||||
ASSERT_FALSE(observed_non_zero.load());
|
||||
|
||||
uint64_t num_running = 0;
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningRemoteCompactions,
|
||||
&num_running));
|
||||
ASSERT_EQ(num_running, 0U);
|
||||
}
|
||||
|
||||
class PartialDeleteCompactionFilter : public CompactionFilter {
|
||||
public:
|
||||
CompactionFilter::Decision FilterV2(
|
||||
@@ -1779,6 +1994,7 @@ class PartialDeleteCompactionFilter : public CompactionFilter {
|
||||
|
||||
TEST_F(CompactionServiceTest, CompactionFilter) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
std::unique_ptr<CompactionFilter> delete_comp_filter(
|
||||
new PartialDeleteCompactionFilter());
|
||||
options.compaction_filter = delete_comp_filter.get();
|
||||
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
|
||||
#if USE_COROUTINES
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
|
||||
#include "folly/Executor.h"
|
||||
#include "folly/coro/Nothrow.h"
|
||||
#include "folly/executors/IOExecutor.h"
|
||||
#include "folly/io/async/EventBase.h"
|
||||
#include "rocksdb/coro_db.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "table/multiget_context.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp) {
|
||||
assert(db != nullptr);
|
||||
CoroDB* coro_db = db->GetCoroDB();
|
||||
auto* file_system = db->GetFileSystem();
|
||||
auto* read_executor =
|
||||
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
|
||||
Status status;
|
||||
if (coro_db == nullptr || read_event_base == nullptr) {
|
||||
status = db->Get(options, column_family, key, value, timestamp);
|
||||
} else {
|
||||
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base),
|
||||
coro_db->GetCoroutine(options, column_family, key, value, timestamp)));
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, std::string* value,
|
||||
std::string* timestamp) {
|
||||
assert(value != nullptr);
|
||||
PinnableSlice pinnable_value(value);
|
||||
assert(!pinnable_value.IsPinned());
|
||||
Status status = co_await folly::coro::co_nothrow(
|
||||
CoGet(db, options, column_family, key, &pinnable_value, timestamp));
|
||||
if (status.ok() && pinnable_value.IsPinned()) {
|
||||
value->assign(pinnable_value.data(), pinnable_value.size());
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
|
||||
const Slice& key, std::string* value) {
|
||||
assert(db != nullptr);
|
||||
return CoGet(db, options, db->DefaultColumnFamily(), key, value);
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
|
||||
const Slice& key, std::string* value,
|
||||
std::string* timestamp) {
|
||||
assert(db != nullptr);
|
||||
return CoGet(db, options, db->DefaultColumnFamily(), key, value, timestamp);
|
||||
}
|
||||
|
||||
folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
|
||||
DB* db, const ReadOptions& options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
const std::vector<Slice>& keys, std::vector<std::string>* values,
|
||||
std::vector<std::string>* timestamps) {
|
||||
const size_t num_keys = keys.size();
|
||||
std::vector<Status> statuses(num_keys);
|
||||
std::vector<PinnableSlice> pinnable_values(num_keys);
|
||||
|
||||
values->resize(num_keys);
|
||||
if (timestamps != nullptr) {
|
||||
timestamps->resize(num_keys);
|
||||
}
|
||||
co_await folly::coro::co_nothrow(CoMultiGet(
|
||||
db, options, num_keys,
|
||||
const_cast<ColumnFamilyHandle**>(column_families.data()), keys.data(),
|
||||
pinnable_values.data(),
|
||||
timestamps == nullptr ? nullptr : timestamps->data(), statuses.data(),
|
||||
/*sorted_input=*/false));
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
if (statuses[i].ok()) {
|
||||
(*values)[i].assign(pinnable_values[i].data(), pinnable_values[i].size());
|
||||
}
|
||||
}
|
||||
co_return statuses;
|
||||
}
|
||||
|
||||
folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
|
||||
DB* db, const ReadOptions& options, const std::vector<Slice>& keys,
|
||||
std::vector<std::string>* values, std::vector<std::string>* timestamps) {
|
||||
assert(db != nullptr);
|
||||
std::vector<ColumnFamilyHandle*> column_families(keys.size(),
|
||||
db->DefaultColumnFamily());
|
||||
co_return co_await folly::coro::co_nothrow(
|
||||
CoMultiGet(db, options, column_families, keys, values, timestamps));
|
||||
}
|
||||
|
||||
folly::coro::Task<void> CoroDB::CoMultiGet(
|
||||
DB* db, const ReadOptions& options, size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps, Status* statuses,
|
||||
bool sorted_input) {
|
||||
assert(db != nullptr);
|
||||
CoroDB* coro_db = db->GetCoroDB();
|
||||
auto* file_system = db->GetFileSystem();
|
||||
auto* read_executor =
|
||||
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
|
||||
if (coro_db == nullptr || read_event_base == nullptr) {
|
||||
db->MultiGet(options, num_keys, column_families, keys, values, timestamps,
|
||||
statuses, sorted_input);
|
||||
} else {
|
||||
co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base),
|
||||
coro_db->MultiGetCoroutine(options, num_keys, column_families, keys,
|
||||
values, timestamps, statuses,
|
||||
sorted_input)));
|
||||
}
|
||||
}
|
||||
|
||||
folly::coro::Task<void> CoroDB::CoMultiGet(
|
||||
DB* db, const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
size_t num_keys, const Slice* keys, PinnableSlice* values,
|
||||
std::string* timestamps, Status* statuses, bool sorted_input) {
|
||||
if (num_keys > MultiGetContext::MAX_BATCH_SIZE) {
|
||||
std::vector<ColumnFamilyHandle*> column_families(num_keys, column_family);
|
||||
co_await folly::coro::co_nothrow(
|
||||
CoMultiGet(db, options, num_keys, column_families.data(), keys, values,
|
||||
timestamps, statuses, sorted_input));
|
||||
} else {
|
||||
std::array<ColumnFamilyHandle*, MultiGetContext::MAX_BATCH_SIZE>
|
||||
column_families;
|
||||
std::fill(column_families.begin(), column_families.begin() + num_keys,
|
||||
column_family);
|
||||
co_await folly::coro::co_nothrow(
|
||||
CoMultiGet(db, options, num_keys, column_families.data(), keys, values,
|
||||
timestamps, statuses, sorted_input));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
#endif // USE_COROUTINES
|
||||
+6
-3
@@ -4020,7 +4020,8 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL0) {
|
||||
AssertMultiGetIOBatchSize(3, 1);
|
||||
}
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 3 : 0);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
@@ -4047,7 +4048,8 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1) {
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 6 : 3);
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 6 : 0);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
@@ -4200,7 +4202,8 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL2WithRangeOverlapL0L1) {
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 3 : 2);
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
|
||||
UseCoroutineRead() ? 3 : 0);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
|
||||
@@ -1800,6 +1800,7 @@ static std::map<TableFileCreationReason, std::string>
|
||||
{TableFileCreationReason::kFlush, "kFlush"},
|
||||
{TableFileCreationReason::kMisc, "kMisc"},
|
||||
{TableFileCreationReason::kRecovery, "kRecovery"},
|
||||
{TableFileCreationReason::kSstFileWriter, "kSstFileWriter"},
|
||||
};
|
||||
|
||||
class TestingContextCustomFilterPolicy
|
||||
@@ -1814,6 +1815,15 @@ class TestingContextCustomFilterPolicy
|
||||
const FilterBuildingContext& context) const override {
|
||||
test_report_ += "cf=";
|
||||
test_report_ += context.column_family_name;
|
||||
// Token rather than path, so expectations don't depend on test directory
|
||||
test_report_ += ",db=";
|
||||
if (context.db_name.empty()) {
|
||||
test_report_ += "none";
|
||||
} else if (context.db_name == Slice(expected_db_name_)) {
|
||||
test_report_ += "expected";
|
||||
} else {
|
||||
test_report_ += "UNEXPECTED:" + context.db_name.ToString();
|
||||
}
|
||||
test_report_ += ",s=";
|
||||
test_report_ +=
|
||||
OptionsHelper::compaction_style_to_string[context.compaction_style];
|
||||
@@ -1836,8 +1846,13 @@ class TestingContextCustomFilterPolicy
|
||||
return rv;
|
||||
}
|
||||
|
||||
void SetExpectedDbName(std::string db_name) {
|
||||
expected_db_name_ = std::move(db_name);
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::string test_report_;
|
||||
std::string expected_db_name_;
|
||||
};
|
||||
} // anonymous namespace
|
||||
|
||||
@@ -1859,6 +1874,13 @@ TEST_F(DBBloomFilterTest, ContextCustomFilterPolicy) {
|
||||
|
||||
ASSERT_OK(TryReopen(options));
|
||||
CreateAndReopenWithCF({fifo ? "abe" : "bob"}, options);
|
||||
policy->SetExpectedDbName(dbname_);
|
||||
|
||||
const char* expected_flush_report =
|
||||
fifo
|
||||
? "cf=abe,db=expected,s=kCompactionStyleFIFO,n=7,l=0,b=0,r=kFlush\n"
|
||||
: "cf=bob,db=expected,s=kCompactionStyleLevel,n=7,l=0,b=0,r="
|
||||
"kFlush\n";
|
||||
|
||||
const int maxKey = 10000;
|
||||
for (int i = 0; i < maxKey / 2; i++) {
|
||||
@@ -1867,17 +1889,13 @@ TEST_F(DBBloomFilterTest, ContextCustomFilterPolicy) {
|
||||
// Add a large key to make the file contain wide range
|
||||
ASSERT_OK(Put(1, Key(maxKey + 55555), Key(maxKey + 55555)));
|
||||
ASSERT_OK(Flush(1));
|
||||
EXPECT_EQ(policy->DumpTestReport(),
|
||||
fifo ? "cf=abe,s=kCompactionStyleFIFO,n=7,l=0,b=0,r=kFlush\n"
|
||||
: "cf=bob,s=kCompactionStyleLevel,n=7,l=0,b=0,r=kFlush\n");
|
||||
EXPECT_EQ(policy->DumpTestReport(), expected_flush_report);
|
||||
|
||||
for (int i = maxKey / 2; i < maxKey; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), Key(i)));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
EXPECT_EQ(policy->DumpTestReport(),
|
||||
fifo ? "cf=abe,s=kCompactionStyleFIFO,n=7,l=0,b=0,r=kFlush\n"
|
||||
: "cf=bob,s=kCompactionStyleLevel,n=7,l=0,b=0,r=kFlush\n");
|
||||
EXPECT_EQ(policy->DumpTestReport(), expected_flush_report);
|
||||
|
||||
// Check that they can be found
|
||||
for (int i = 0; i < maxKey; i++) {
|
||||
@@ -1904,7 +1922,8 @@ TEST_F(DBBloomFilterTest, ContextCustomFilterPolicy) {
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), handles_[1], nullptr,
|
||||
nullptr));
|
||||
EXPECT_EQ(policy->DumpTestReport(),
|
||||
"cf=bob,s=kCompactionStyleLevel,n=7,l=1,b=1,r=kCompaction\n");
|
||||
"cf=bob,db=expected,s=kCompactionStyleLevel,n=7,l=1,b=1,r="
|
||||
"kCompaction\n");
|
||||
|
||||
// Check that we now have one filter, about 9.2% FP rate (5 bits per key)
|
||||
for (int i = 0; i < maxKey; i++) {
|
||||
@@ -1926,7 +1945,8 @@ TEST_F(DBBloomFilterTest, ContextCustomFilterPolicy) {
|
||||
}
|
||||
// Note: kCompactionStyleLevel is default, ignored if num_levels == -1
|
||||
EXPECT_EQ(policy->DumpTestReport(),
|
||||
"cf=abe,s=kCompactionStyleLevel,n=-1,l=-1,b=0,r=kMisc\n");
|
||||
"cf=abe,db=none,s=kCompactionStyleLevel,n=-1,l=-1,b=0,r="
|
||||
"kSstFileWriter\n");
|
||||
}
|
||||
|
||||
// Destroy
|
||||
@@ -3978,7 +3998,8 @@ static std::vector<std::string> RangeQueryKeys(
|
||||
ReadOptions ro;
|
||||
ro.iterate_lower_bound = &lb;
|
||||
ro.iterate_upper_bound = &ub;
|
||||
ro.table_filter = factory.GetTableFilterForRangeQuery(lb, ub);
|
||||
auto filter = factory.GetTableFilterForRangeQuery(lb, ub);
|
||||
ro.table_filter = &filter;
|
||||
auto it = db.NewIterator(ro);
|
||||
std::vector<std::string> ret;
|
||||
for (it->Seek(lb); it->Valid(); it->Next()) {
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
@@ -14,6 +17,7 @@
|
||||
#include "options/options_helper.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/listener.h"
|
||||
#include "rocksdb/sst_file_writer.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testharness.h"
|
||||
@@ -91,6 +95,19 @@ class AbortSynchronizer {
|
||||
}
|
||||
}
|
||||
|
||||
void TriggerAbort(DBImpl* db, ColumnFamilyHandle* column_family) {
|
||||
if (abort_triggered_.load() && abort_completed_.load()) {
|
||||
Reset();
|
||||
}
|
||||
|
||||
if (!abort_triggered_.exchange(true)) {
|
||||
abort_thread_ = std::thread([this, db, column_family]() {
|
||||
db->AbortCompactions(column_family);
|
||||
SignalAbortCompleted();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the abort thread to complete.
|
||||
// Call this AFTER compaction returns to ensure the abort thread has finished
|
||||
// before proceeding with Resume or other operations.
|
||||
@@ -459,6 +476,483 @@ TEST_F(DBCompactionAbortTest, AbortScheduledAutomaticCompactionBeforePick) {
|
||||
VerifyDataIntegrity(/*num_keys=*/100);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortCompactionsIsScopedToColumnFamily) {
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 100;
|
||||
options.max_background_compactions = 2;
|
||||
options.max_subcompactions = 1;
|
||||
options.disable_auto_compactions = true;
|
||||
CreateAndReopenWithCF({"target", "sibling"}, options);
|
||||
|
||||
Random rnd(301);
|
||||
for (int cf = 1; cf <= 2; ++cf) {
|
||||
for (int file = 0; file < 4; ++file) {
|
||||
for (int key = 0; key < 100; ++key) {
|
||||
ASSERT_OK(Put(cf, Key(key), rnd.RandomString(1000)));
|
||||
}
|
||||
ASSERT_OK(Flush(cf));
|
||||
}
|
||||
}
|
||||
|
||||
AbortSynchronizer abort_sync;
|
||||
std::atomic<int> compactions_started{0};
|
||||
std::mutex sibling_mutex;
|
||||
std::condition_variable sibling_cv;
|
||||
bool sibling_started = false;
|
||||
bool allow_sibling_to_finish = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImpl::AbortCompactions:FlagSet",
|
||||
"DBCompactionAbortTest::ScopedAbort:WaitForAbort"},
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::ProcessKeyValueCompaction:Start", [&](void* /*arg*/) {
|
||||
if (compactions_started.fetch_add(1) == 0) {
|
||||
std::unique_lock<std::mutex> lock(sibling_mutex);
|
||||
sibling_started = true;
|
||||
sibling_cv.notify_all();
|
||||
sibling_cv.wait(lock, [&]() { return allow_sibling_to_finish; });
|
||||
return;
|
||||
}
|
||||
abort_sync.TriggerAbort(dbfull(), handles_[1]);
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"DBCompactionAbortTest::ScopedAbort:WaitForAbort", nullptr);
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status sibling_status;
|
||||
std::thread sibling_thread([&]() {
|
||||
sibling_status = dbfull()->CompactRange(CompactRangeOptions(), handles_[2],
|
||||
nullptr, nullptr);
|
||||
});
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(sibling_mutex);
|
||||
sibling_cv.wait(lock, [&]() { return sibling_started; });
|
||||
}
|
||||
|
||||
Status target_status = dbfull()->CompactRange(CompactRangeOptions(),
|
||||
handles_[1], nullptr, nullptr);
|
||||
abort_sync.WaitForAbortCompletion();
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(sibling_mutex);
|
||||
allow_sibling_to_finish = true;
|
||||
}
|
||||
sibling_cv.notify_all();
|
||||
sibling_thread.join();
|
||||
CleanupSyncPoints();
|
||||
|
||||
ASSERT_TRUE(target_status.IsIncomplete());
|
||||
ASSERT_TRUE(target_status.IsCompactionAborted());
|
||||
ASSERT_OK(sibling_status);
|
||||
target_status = dbfull()->CompactRange(CompactRangeOptions(), handles_[1],
|
||||
nullptr, nullptr);
|
||||
ASSERT_TRUE(target_status.IsIncomplete());
|
||||
ASSERT_TRUE(target_status.IsCompactionAborted());
|
||||
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), handles_[1], nullptr,
|
||||
nullptr));
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, PerColumnFamilyAndGlobalAbortCountersCompose) {
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 100;
|
||||
options.disable_auto_compactions = true;
|
||||
CreateAndReopenWithCF({"target", "sibling"}, options);
|
||||
|
||||
for (int cf = 1; cf <= 2; ++cf) {
|
||||
for (int file = 0; file < 2; ++file) {
|
||||
ASSERT_OK(Put(cf, Key(file), "value"));
|
||||
ASSERT_OK(Flush(cf));
|
||||
}
|
||||
}
|
||||
|
||||
dbfull()->AbortAllCompactions();
|
||||
dbfull()->AbortCompactions(handles_[1]);
|
||||
dbfull()->AbortCompactions(handles_[1]);
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
|
||||
Status sibling_status = dbfull()->CompactRange(CompactRangeOptions(),
|
||||
handles_[2], nullptr, nullptr);
|
||||
ASSERT_TRUE(sibling_status.IsIncomplete());
|
||||
ASSERT_TRUE(sibling_status.IsCompactionAborted());
|
||||
|
||||
dbfull()->ResumeAllCompactions();
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), handles_[2], nullptr,
|
||||
nullptr));
|
||||
Status target_status = dbfull()->CompactRange(CompactRangeOptions(),
|
||||
handles_[1], nullptr, nullptr);
|
||||
ASSERT_TRUE(target_status.IsIncomplete());
|
||||
ASSERT_TRUE(target_status.IsCompactionAborted());
|
||||
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), handles_[1], nullptr,
|
||||
nullptr));
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortCompactionsRejectsCompactFilesTrivialMove) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
CreateAndReopenWithCF({"target"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "value"));
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
ColumnFamilyMetaData metadata;
|
||||
dbfull()->GetColumnFamilyMetaData(handles_[1], &metadata);
|
||||
ASSERT_EQ(metadata.levels[0].files.size(), 1);
|
||||
const std::vector<std::string> input_files = {
|
||||
metadata.levels[0].files.front().name};
|
||||
|
||||
CompactionOptions compaction_options;
|
||||
compaction_options.allow_trivial_move = true;
|
||||
dbfull()->AbortCompactions(handles_[1]);
|
||||
Status status =
|
||||
dbfull()->CompactFiles(compaction_options, handles_[1], input_files, 1);
|
||||
ASSERT_TRUE(status.IsIncomplete());
|
||||
ASSERT_TRUE(status.IsCompactionAborted());
|
||||
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
ASSERT_OK(
|
||||
dbfull()->CompactFiles(compaction_options, handles_[1], input_files, 1));
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, ResumeRestoresParkedAutomaticCompaction) {
|
||||
constexpr int kCompactionTrigger = 4;
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
||||
options.max_background_compactions = 1;
|
||||
options.disable_auto_compactions = false;
|
||||
CreateAndReopenWithCF({"target", "sibling"}, options);
|
||||
|
||||
AbortSynchronizer abort_sync;
|
||||
std::mutex parked_mutex;
|
||||
std::condition_variable parked_cv;
|
||||
bool compaction_parked = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImpl::AbortCompactions:FlagSet",
|
||||
"DBCompactionAbortTest::ParkedCompaction:WaitForAbort"},
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"BackgroundCallCompaction:0", [&](void* /*arg*/) {
|
||||
abort_sync.TriggerAbort(dbfull(), handles_[1]);
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"DBCompactionAbortTest::ParkedCompaction:WaitForAbort", nullptr);
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::PickCompactionFromQueue:Parked", [&](void* /*arg*/) {
|
||||
std::lock_guard<std::mutex> guard(parked_mutex);
|
||||
compaction_parked = true;
|
||||
parked_cv.notify_all();
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Random rnd(301);
|
||||
for (int file = 0; file < kCompactionTrigger; ++file) {
|
||||
for (int key = 0; key < 100; ++key) {
|
||||
ASSERT_OK(Put(1, Key(key), rnd.RandomString(1000)));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(parked_mutex);
|
||||
parked_cv.wait(lock, [&]() { return compaction_parked; });
|
||||
}
|
||||
|
||||
CleanupSyncPoints();
|
||||
abort_sync.WaitForAbortCompletion();
|
||||
const int l0_files_before_resume = NumTableFilesAtLevel(0, 1);
|
||||
ASSERT_GE(l0_files_before_resume, kCompactionTrigger);
|
||||
|
||||
WaitForCompactOptions wait_options;
|
||||
wait_options.timeout = std::chrono::milliseconds(1);
|
||||
ASSERT_TRUE(dbfull()->WaitForCompact(wait_options).IsTimedOut());
|
||||
|
||||
for (int file = 0; file < kCompactionTrigger; ++file) {
|
||||
for (int key = 0; key < 100; ++key) {
|
||||
ASSERT_OK(Put(2, Key(key), rnd.RandomString(1000)));
|
||||
}
|
||||
ASSERT_OK(Flush(2));
|
||||
}
|
||||
ASSERT_OK(dbfull()->TEST_WaitForBackgroundWork());
|
||||
ASSERT_LT(NumTableFilesAtLevel(0, 2), kCompactionTrigger);
|
||||
ASSERT_EQ(NumTableFilesAtLevel(0, 1), l0_files_before_resume);
|
||||
|
||||
std::atomic<bool> target_reenqueued{false};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::RestoreParkedCompaction:cfd", [&](void* arg) {
|
||||
auto* cfd = static_cast<ColumnFamilyData*>(arg);
|
||||
if (cfd->GetName() == "target") {
|
||||
target_reenqueued.store(true);
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
ASSERT_TRUE(target_reenqueued.load());
|
||||
CleanupSyncPoints();
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
ASSERT_LT(NumTableFilesAtLevel(0, 1), l0_files_before_resume);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, DropColumnFamilyReleasesParkedCompaction) {
|
||||
constexpr int kCompactionTrigger = 4;
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
||||
options.max_background_compactions = 1;
|
||||
options.disable_auto_compactions = false;
|
||||
CreateAndReopenWithCF({"target"}, options);
|
||||
|
||||
AbortSynchronizer abort_sync;
|
||||
std::mutex parked_mutex;
|
||||
std::condition_variable parked_cv;
|
||||
bool compaction_parked = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImpl::AbortCompactions:FlagSet",
|
||||
"DBCompactionAbortTest::DropParkedCompaction:WaitForAbort"},
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"BackgroundCallCompaction:0", [&](void* /*arg*/) {
|
||||
abort_sync.TriggerAbort(dbfull(), handles_[1]);
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"DBCompactionAbortTest::DropParkedCompaction:WaitForAbort",
|
||||
nullptr);
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::PickCompactionFromQueue:Parked", [&](void* /*arg*/) {
|
||||
std::lock_guard<std::mutex> guard(parked_mutex);
|
||||
compaction_parked = true;
|
||||
parked_cv.notify_all();
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Random rnd(301);
|
||||
for (int file = 0; file < kCompactionTrigger; ++file) {
|
||||
for (int key = 0; key < 100; ++key) {
|
||||
ASSERT_OK(Put(1, Key(key), rnd.RandomString(1000)));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(parked_mutex);
|
||||
parked_cv.wait(lock, [&]() { return compaction_parked; });
|
||||
}
|
||||
|
||||
CleanupSyncPoints();
|
||||
abort_sync.WaitForAbortCompletion();
|
||||
ASSERT_OK(dbfull()->DropColumnFamily(handles_[1]));
|
||||
|
||||
WaitForCompactOptions wait_options;
|
||||
wait_options.timeout = std::chrono::seconds(10);
|
||||
ASSERT_OK(dbfull()->WaitForCompact(wait_options));
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, DropColumnFamilyBeforeAbortedQueueEntryIsParked) {
|
||||
constexpr int kCompactionTrigger = 4;
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
||||
options.max_background_compactions = 1;
|
||||
options.disable_auto_compactions = false;
|
||||
CreateAndReopenWithCF({"target"}, options);
|
||||
|
||||
std::mutex worker_mutex;
|
||||
std::condition_variable worker_cv;
|
||||
bool worker_started = false;
|
||||
bool allow_worker_to_pick = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"BackgroundCallCompaction:0", [&](void* /*arg*/) {
|
||||
std::unique_lock<std::mutex> lock(worker_mutex);
|
||||
worker_started = true;
|
||||
worker_cv.notify_all();
|
||||
worker_cv.wait(lock, [&]() { return allow_worker_to_pick; });
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
for (int file = 0; file < kCompactionTrigger; ++file) {
|
||||
ASSERT_OK(Put(1, Key(file), "value"));
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(worker_mutex);
|
||||
worker_cv.wait(lock, [&]() { return worker_started; });
|
||||
}
|
||||
|
||||
dbfull()->AbortCompactions(handles_[1]);
|
||||
const Status drop_status = dbfull()->DropColumnFamily(handles_[1]);
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(worker_mutex);
|
||||
allow_worker_to_pick = true;
|
||||
}
|
||||
worker_cv.notify_all();
|
||||
|
||||
WaitForCompactOptions wait_options;
|
||||
wait_options.timeout = std::chrono::seconds(10);
|
||||
const Status wait_status = dbfull()->WaitForCompact(wait_options);
|
||||
CleanupSyncPoints();
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
|
||||
ASSERT_OK(drop_status);
|
||||
ASSERT_OK(wait_status);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, NewCompactionWorkWhileAbortedIsParked) {
|
||||
constexpr int kCompactionTrigger = 4;
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
||||
options.max_background_compactions = 1;
|
||||
options.disable_auto_compactions = false;
|
||||
CreateAndReopenWithCF({"target"}, options);
|
||||
|
||||
dbfull()->AbortCompactions(handles_[1]);
|
||||
for (int file = 0; file < kCompactionTrigger; ++file) {
|
||||
ASSERT_OK(Put(1, Key(file), "value"));
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
|
||||
const int l0_files_before_resume = NumTableFilesAtLevel(0, 1);
|
||||
WaitForCompactOptions wait_options;
|
||||
wait_options.timeout = std::chrono::milliseconds(1);
|
||||
const Status wait_status = dbfull()->WaitForCompact(wait_options);
|
||||
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
ASSERT_TRUE(wait_status.IsTimedOut());
|
||||
ASSERT_GE(l0_files_before_resume, kCompactionTrigger);
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
ASSERT_LT(NumTableFilesAtLevel(0, 1), l0_files_before_resume);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortWaitsForCompactionCompletionCallback) {
|
||||
constexpr int kCompactionTrigger = 4;
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
||||
options.max_background_compactions = 1;
|
||||
options.disable_auto_compactions = false;
|
||||
options.listeners.emplace_back(std::make_shared<EventListener>());
|
||||
CreateAndReopenWithCF({"target"}, options);
|
||||
|
||||
std::mutex callback_mutex;
|
||||
std::condition_variable callback_cv;
|
||||
bool callback_started = false;
|
||||
bool allow_callback_to_finish = false;
|
||||
enum class AbortState { kNotObserved, kWaiting, kComplete };
|
||||
AbortState abort_state = AbortState::kNotObserved;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::NotifyOnCompactionCompleted::UnlockMutex", [&](void* /*arg*/) {
|
||||
std::unique_lock<std::mutex> lock(callback_mutex);
|
||||
callback_started = true;
|
||||
callback_cv.notify_all();
|
||||
callback_cv.wait(lock, [&]() { return allow_callback_to_finish; });
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::AbortCompactions:Waiting", [&](void* /*arg*/) {
|
||||
std::lock_guard<std::mutex> guard(callback_mutex);
|
||||
abort_state = AbortState::kWaiting;
|
||||
callback_cv.notify_all();
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::AbortCompactions:Complete", [&](void* /*arg*/) {
|
||||
std::lock_guard<std::mutex> guard(callback_mutex);
|
||||
abort_state = AbortState::kComplete;
|
||||
callback_cv.notify_all();
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
for (int file = 0; file < kCompactionTrigger; ++file) {
|
||||
ASSERT_OK(Put(1, Key(file), "value"));
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(callback_mutex);
|
||||
callback_cv.wait(lock, [&]() { return callback_started; });
|
||||
}
|
||||
|
||||
std::thread abort_thread([&]() { dbfull()->AbortCompactions(handles_[1]); });
|
||||
bool returned_while_callback_blocked = false;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(callback_mutex);
|
||||
callback_cv.wait(lock,
|
||||
[&]() { return abort_state != AbortState::kNotObserved; });
|
||||
returned_while_callback_blocked = abort_state == AbortState::kComplete;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(callback_mutex);
|
||||
allow_callback_to_finish = true;
|
||||
}
|
||||
callback_cv.notify_all();
|
||||
abort_thread.join();
|
||||
CleanupSyncPoints();
|
||||
dbfull()->ResumeCompactions(handles_[1]);
|
||||
|
||||
ASSERT_FALSE(returned_while_callback_blocked);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest,
|
||||
DroppedColumnFamilyFilesPurgedAfterTrackedCompaction) {
|
||||
constexpr int kCompactionTrigger = 4;
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
||||
options.max_background_compactions = 1;
|
||||
options.disable_auto_compactions = false;
|
||||
CreateAndReopenWithCF({"target"}, options);
|
||||
|
||||
std::mutex compaction_mutex;
|
||||
std::condition_variable compaction_cv;
|
||||
bool compaction_started = false;
|
||||
bool allow_compaction_to_finish = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::Run():Start", [&](void* /*arg*/) {
|
||||
std::unique_lock<std::mutex> lock(compaction_mutex);
|
||||
compaction_started = true;
|
||||
compaction_cv.notify_all();
|
||||
compaction_cv.wait(lock, [&]() { return allow_compaction_to_finish; });
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Random rnd(301);
|
||||
for (int file = 0; file < kCompactionTrigger; ++file) {
|
||||
for (int key = 0; key < 100; ++key) {
|
||||
ASSERT_OK(Put(1, Key(key), rnd.RandomString(1000)));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(compaction_mutex);
|
||||
compaction_cv.wait(lock, [&]() { return compaction_started; });
|
||||
}
|
||||
|
||||
const int sst_files_before_drop = GetSstFileCount(dbname_);
|
||||
const Status drop_status = dbfull()->DropColumnFamily(handles_[1]);
|
||||
Status destroy_handle_status;
|
||||
if (drop_status.ok()) {
|
||||
destroy_handle_status = dbfull()->DestroyColumnFamilyHandle(handles_[1]);
|
||||
if (destroy_handle_status.ok()) {
|
||||
handles_.erase(handles_.begin() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(compaction_mutex);
|
||||
allow_compaction_to_finish = true;
|
||||
}
|
||||
compaction_cv.notify_all();
|
||||
const Status wait_status = dbfull()->TEST_WaitForCompact();
|
||||
CleanupSyncPoints();
|
||||
|
||||
ASSERT_OK(drop_status);
|
||||
ASSERT_OK(destroy_handle_status);
|
||||
ASSERT_OK(wait_status);
|
||||
ASSERT_GT(sst_files_before_drop, 0);
|
||||
ASSERT_EQ(0, GetSstFileCount(dbname_));
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, ResumeCompactionsAfterCloseIsNoOp) {
|
||||
auto* const default_column_family = dbfull()->DefaultColumnFamily();
|
||||
dbfull()->AbortCompactions(default_column_family);
|
||||
|
||||
ASSERT_OK(dbfull()->Close());
|
||||
dbfull()->ResumeCompactions(default_column_family);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortAndVerifyNoOutputFiles) {
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
|
||||
@@ -813,6 +813,23 @@ TEST_F(DBCompactionTest, CompactRangeBottomPri) {
|
||||
int low_pri_count = 0;
|
||||
int bottom_pri_count = 0;
|
||||
bool bottom_running_seen = false;
|
||||
std::atomic<bool> bottom_property_seen = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::Run():Start", [&](void*) {
|
||||
uint64_t total = 0;
|
||||
uint64_t bottom = 0;
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningCompactions,
|
||||
&total));
|
||||
ASSERT_TRUE(db_->GetIntProperty(
|
||||
DB::Properties::kNumRunningBottomCompactions, &bottom));
|
||||
ASSERT_GE(total, bottom);
|
||||
ASSERT_GE(total, 1);
|
||||
ASSERT_LE(total, 2);
|
||||
ASSERT_LE(bottom, 1);
|
||||
if (bottom > 0) {
|
||||
bottom_property_seen = true;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BackgroundCallCompaction:1", [&](void*) {
|
||||
if (dbfull()->TEST_NumRunningBottomCompactions() > 0) {
|
||||
@@ -838,6 +855,7 @@ TEST_F(DBCompactionTest, CompactRangeBottomPri) {
|
||||
ASSERT_EQ(1, low_pri_count);
|
||||
ASSERT_EQ(1, bottom_pri_count);
|
||||
ASSERT_TRUE(bottom_running_seen);
|
||||
ASSERT_TRUE(bottom_property_seen);
|
||||
ASSERT_EQ("0,0,2", FilesPerLevel(0));
|
||||
|
||||
// Recompact bottom most level uses bottom pool
|
||||
@@ -6156,6 +6174,7 @@ TEST_F(DBCompactionTest, CompactRangeFlushOverlappingMemtable) {
|
||||
TEST_F(DBCompactionTest, CompactionStatsTest) {
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 2;
|
||||
options.max_background_compactions = 1;
|
||||
CompactionStatsCollector* collector = new CompactionStatsCollector();
|
||||
options.listeners.emplace_back(collector);
|
||||
DestroyAndReopen(options);
|
||||
@@ -6163,9 +6182,13 @@ TEST_F(DBCompactionTest, CompactionStatsTest) {
|
||||
// Verify that the internal statistics for num_running_compactions and
|
||||
// num_running_compaction_sorted_runs start and end at valid states
|
||||
uint64_t num_running_compactions = 0;
|
||||
uint64_t num_running_bottom_compactions = 0;
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningCompactions,
|
||||
&num_running_compactions));
|
||||
ASSERT_EQ(num_running_compactions, 0);
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningBottomCompactions,
|
||||
&num_running_bottom_compactions));
|
||||
ASSERT_EQ(0, num_running_compactions);
|
||||
ASSERT_EQ(0, num_running_bottom_compactions);
|
||||
uint64_t num_running_compaction_sorted_runs = 0;
|
||||
ASSERT_TRUE(
|
||||
db_->GetIntProperty(DB::Properties::kNumRunningCompactionSortedRuns,
|
||||
@@ -6174,6 +6197,18 @@ TEST_F(DBCompactionTest, CompactionStatsTest) {
|
||||
// Check that the stat actually gets changed some time between the start and
|
||||
// end of compaction
|
||||
std::atomic<bool> sorted_runs_count_incremented = false;
|
||||
std::atomic<bool> running_compaction_seen = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::Run():Start", [&](void*) {
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningCompactions,
|
||||
&num_running_compactions));
|
||||
ASSERT_TRUE(
|
||||
db_->GetIntProperty(DB::Properties::kNumRunningBottomCompactions,
|
||||
&num_running_bottom_compactions));
|
||||
ASSERT_EQ(1, num_running_compactions);
|
||||
ASSERT_EQ(0, num_running_bottom_compactions);
|
||||
running_compaction_seen = true;
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionMergingIterator::UpdateInternalStats",
|
||||
[&](void*) { sorted_runs_count_incremented = true; });
|
||||
@@ -6197,11 +6232,15 @@ TEST_F(DBCompactionTest, CompactionStatsTest) {
|
||||
// to process
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningCompactions,
|
||||
&num_running_compactions));
|
||||
ASSERT_EQ(num_running_compactions, 0);
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumRunningBottomCompactions,
|
||||
&num_running_bottom_compactions));
|
||||
ASSERT_EQ(0, num_running_compactions);
|
||||
ASSERT_EQ(0, num_running_bottom_compactions);
|
||||
ASSERT_TRUE(
|
||||
db_->GetIntProperty(DB::Properties::kNumRunningCompactionSortedRuns,
|
||||
&num_running_compaction_sorted_runs));
|
||||
ASSERT_EQ(num_running_compaction_sorted_runs, 0);
|
||||
ASSERT_TRUE(running_compaction_seen);
|
||||
ASSERT_TRUE(sorted_runs_count_incremented);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
|
||||
+71
-1
@@ -8,6 +8,7 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "db/db_impl/db_impl.h"
|
||||
@@ -20,8 +21,8 @@
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/metadata.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/types.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/file_checksum_helper.h"
|
||||
#include "util/mutexlock.h"
|
||||
@@ -195,14 +196,64 @@ Status DBImpl::GetCurrentWalFile(std::unique_ptr<WalFile>* current_wal_file) {
|
||||
return wal_manager_.GetLiveWalFile(current_logfile_number, current_wal_file);
|
||||
}
|
||||
|
||||
Status DBImpl::AppendColumnFamilyDropsToManifest(
|
||||
const std::string& manifest_path, uint64_t manifest_size,
|
||||
const std::vector<uint32_t>& cf_ids) {
|
||||
const WriteOptions write_options;
|
||||
// Snapshot manifest_preallocation_size_ under the DB mutex per VersionSet's
|
||||
// contract (the field is mutated by SetDBOptions under this mutex).
|
||||
uint64_t preallocation_size = 0;
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
preallocation_size = versions_->manifest_preallocation_size_;
|
||||
}
|
||||
return versions_->AppendColumnFamilyDropsToManifest(
|
||||
manifest_path, manifest_size, cf_ids, write_options, preallocation_size);
|
||||
}
|
||||
|
||||
Status DBImpl::GetLiveFilesStorageInfo(
|
||||
const LiveFilesStorageInfoOptions& opts,
|
||||
std::vector<LiveFileStorageInfo>* files) {
|
||||
return GetLiveFilesStorageInfoImpl(opts, /*include_cf_ids=*/{}, files,
|
||||
/*excluded_cf_ids=*/nullptr);
|
||||
}
|
||||
|
||||
Status DBImpl::GetLiveFilesStorageInfoForSubsetCheckpoint(
|
||||
const LiveFilesStorageInfoOptions& opts,
|
||||
const std::vector<uint32_t>& include_cf_ids,
|
||||
std::vector<LiveFileStorageInfo>* files,
|
||||
std::vector<uint32_t>* excluded_cf_ids) {
|
||||
// Internal contract: caller must coalesce ids and always include the default
|
||||
// CF (it cannot be dropped, so a subset checkpoint without it would produce
|
||||
// an unopenable DB). CheckpointImpl enforces this before calling here.
|
||||
assert(!include_cf_ids.empty());
|
||||
assert(std::find(include_cf_ids.begin(), include_cf_ids.end(),
|
||||
default_cf_handle_->GetID()) != include_cf_ids.end());
|
||||
assert(excluded_cf_ids != nullptr);
|
||||
return GetLiveFilesStorageInfoImpl(opts, include_cf_ids, files,
|
||||
excluded_cf_ids);
|
||||
}
|
||||
|
||||
Status DBImpl::GetLiveFilesStorageInfoImpl(
|
||||
const LiveFilesStorageInfoOptions& opts,
|
||||
const std::vector<uint32_t>& include_cf_ids,
|
||||
std::vector<LiveFileStorageInfo>* files,
|
||||
std::vector<uint32_t>* excluded_cf_ids) {
|
||||
// To avoid returning partial results, only move results to files on success.
|
||||
assert(files);
|
||||
files->clear();
|
||||
std::vector<LiveFileStorageInfo> results;
|
||||
|
||||
const bool cf_subset = !include_cf_ids.empty();
|
||||
assert(!cf_subset || excluded_cf_ids != nullptr);
|
||||
const std::unordered_set<uint32_t> include_cf_id_set(include_cf_ids.begin(),
|
||||
include_cf_ids.end());
|
||||
// Tracks which requested ids remain to be observed as live under the DB
|
||||
// mutex; any id still present after the enumeration was either dropped
|
||||
// before or during the checkpoint (race) and must not silently disappear
|
||||
// from the checkpoint.
|
||||
std::unordered_set<uint32_t> unseen_include_cf_ids = include_cf_id_set;
|
||||
|
||||
// NOTE: This implementation was largely migrated from Checkpoint.
|
||||
|
||||
VectorWalPtr live_wal_files;
|
||||
@@ -282,6 +333,14 @@ Status DBImpl::GetLiveFilesStorageInfo(
|
||||
if (cfd->IsDropped()) {
|
||||
continue;
|
||||
}
|
||||
if (cf_subset &&
|
||||
include_cf_id_set.find(cfd->GetID()) == include_cf_id_set.end()) {
|
||||
excluded_cf_ids->push_back(cfd->GetID());
|
||||
continue;
|
||||
}
|
||||
if (cf_subset) {
|
||||
unseen_include_cf_ids.erase(cfd->GetID());
|
||||
}
|
||||
VersionStorageInfo& vsi = *cfd->current()->storage_info();
|
||||
auto& cf_paths = cfd->ioptions().cf_paths;
|
||||
|
||||
@@ -343,6 +402,17 @@ Status DBImpl::GetLiveFilesStorageInfo(
|
||||
}
|
||||
}
|
||||
|
||||
// Release-build validation: every id the subset-checkpoint caller asked for
|
||||
// must have been observed as live under the mutex. If any is missing, it was
|
||||
// dropped before we ran (stale handle) or lost a race with a concurrent
|
||||
// drop; either way the checkpoint would silently omit it, violating the
|
||||
// caller's contract that the checkpoint contains the requested CFs.
|
||||
if (cf_subset && !unseen_include_cf_ids.empty()) {
|
||||
mutex_.Unlock();
|
||||
return Status::InvalidArgument(
|
||||
"Requested column family id was not live at checkpoint time");
|
||||
}
|
||||
|
||||
// Capture some final info before releasing mutex
|
||||
const uint64_t manifest_number = versions_->manifest_file_number();
|
||||
const uint64_t manifest_size = versions_->manifest_file_size();
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
|
||||
#include "util/coro_utils.h"
|
||||
|
||||
#if defined(USE_COROUTINES) && defined(WITH_COROUTINES)
|
||||
#include "util/coro_stats_util.h"
|
||||
#endif // USE_COROUTINES && WITH_COROUTINES
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
|
||||
@@ -14,6 +18,10 @@ namespace ROCKSDB_NAMESPACE {
|
||||
DEFINE_SYNC_AND_ASYNC(Status, CompactedDBImpl::Get)
|
||||
(const ReadOptions& _read_options, ColumnFamilyHandle*, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGet) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
@@ -102,6 +110,10 @@ DEFINE_SYNC_AND_ASYNC(void, CompactedDBImpl::MultiGet)
|
||||
ColumnFamilyHandle** /*column_families*/, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps, Status* statuses,
|
||||
const bool /*sorted_input*/) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
assert(user_comparator_);
|
||||
Status s;
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
|
||||
+317
-114
@@ -15,13 +15,6 @@
|
||||
#include <alloca.h>
|
||||
#endif
|
||||
|
||||
#if USE_COROUTINES
|
||||
#include "folly/Executor.h"
|
||||
#include "folly/coro/Invoke.h"
|
||||
#include "folly/executors/IOExecutor.h"
|
||||
#include "folly/io/async/EventBase.h"
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstdio>
|
||||
#include <deque>
|
||||
@@ -67,8 +60,9 @@
|
||||
#include "db/range_tombstone_fragmenter.h"
|
||||
#include "db/table_cache.h"
|
||||
#include "db/table_properties_collector.h"
|
||||
#include "db/transaction_log_impl.h"
|
||||
#include "db/version_set.h"
|
||||
#include "db/wal_iterator_impl.h"
|
||||
#include "db/wide/lazy_wide_columns_helper.h"
|
||||
#include "db/wide/wide_column_serialization.h"
|
||||
#include "db/wide/wide_columns_helper.h"
|
||||
#include "db/write_batch_internal.h"
|
||||
@@ -91,6 +85,7 @@
|
||||
#include "options/cf_options.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "options/options_parser.h"
|
||||
#include "rocksdb/lazy_wide_columns.h"
|
||||
#include "util/udt_util.h"
|
||||
#ifdef ROCKSDB_JEMALLOC
|
||||
#include "port/jemalloc_helper.h"
|
||||
@@ -135,6 +130,15 @@
|
||||
#include "util/udt_util.h"
|
||||
#include "utilities/trace/replayer_impl.h"
|
||||
|
||||
#if USE_COROUTINES
|
||||
#include "folly/Executor.h"
|
||||
#include "folly/coro/Nothrow.h"
|
||||
#include "folly/coro/Task.h"
|
||||
#include "folly/executors/IOExecutor.h"
|
||||
#include "folly/io/async/EventBase.h"
|
||||
#include "rocksdb/coro_db.h"
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
const std::string kDefaultColumnFamilyName("default");
|
||||
@@ -670,8 +674,9 @@ void DBImpl::MaybeInitBlobDirectWriteColumnFamily(
|
||||
[vs = versions_.get()]() { return vs->NewFileNumber(); }, fs_.get(),
|
||||
immutable_db_options_.clock, stats_, file_options_, dbname_,
|
||||
column_family_name, cf_options.blob_file_size,
|
||||
immutable_db_options_.use_fsync, cfd->blob_file_cache(), &blob_callback_,
|
||||
immutable_db_options_.listeners,
|
||||
immutable_db_options_.use_fsync,
|
||||
cf_options.blob_file_writable_file_max_buffer_size,
|
||||
cfd->blob_file_cache(), &blob_callback_, immutable_db_options_.listeners,
|
||||
immutable_db_options_.file_checksum_gen_factory.get(),
|
||||
immutable_db_options_.checksum_handoff_file_types, io_tracer_, db_id_,
|
||||
db_session_id_, immutable_db_options_.info_log.get());
|
||||
@@ -883,6 +888,13 @@ Status DBImpl::CloseHelper() {
|
||||
cfd->UnrefAndTryDelete();
|
||||
}
|
||||
|
||||
for (auto* cfd : parked_compaction_cfds_) {
|
||||
assert(cfd->queued_for_compaction());
|
||||
cfd->set_queued_for_compaction(false);
|
||||
cfd->UnrefAndTryDelete();
|
||||
}
|
||||
parked_compaction_cfds_.clear();
|
||||
|
||||
if (default_cf_handle_ != nullptr || persist_stats_cf_handle_ != nullptr) {
|
||||
// we need to delete handle outside of lock because it does its own locking
|
||||
mutex_.Unlock();
|
||||
@@ -1680,7 +1692,13 @@ Status DBImpl::SetOptions(
|
||||
// options to file, otherwise there will be a deadlock with writer
|
||||
// thread.
|
||||
for (const auto& cfd_opts : column_family_datas) {
|
||||
InstallSuperVersionForConfigChange(cfd_opts.first, &sv_context);
|
||||
auto* cfd = cfd_opts.first;
|
||||
InstallSuperVersionForConfigChange(cfd, &sv_context);
|
||||
if (auto* blob_partition_manager = cfd->blob_partition_manager()) {
|
||||
blob_partition_manager->SetBlobWriterMaxBufferSize(
|
||||
cfd->GetLatestMutableCFOptions()
|
||||
.blob_file_writable_file_max_buffer_size);
|
||||
}
|
||||
}
|
||||
|
||||
persist_options_status =
|
||||
@@ -2537,6 +2555,17 @@ static void CleanupSuperVersionHandle(void* arg1, void* /*arg2*/) {
|
||||
delete sv_handle;
|
||||
}
|
||||
|
||||
void DBImpl::TransferSuperVersionPin(SuperVersion* super_version,
|
||||
Cleanable* pin) {
|
||||
assert(super_version != nullptr);
|
||||
assert(pin != nullptr);
|
||||
super_version->Ref();
|
||||
SuperVersionHandle* sv_handle = new SuperVersionHandle(
|
||||
this, &mutex_, super_version,
|
||||
immutable_db_options_.avoid_unnecessary_blocking_io);
|
||||
pin->RegisterCleanup(CleanupSuperVersionHandle, sv_handle, nullptr);
|
||||
}
|
||||
|
||||
struct GetMergeOperandsState {
|
||||
GetMergeOperandsState(MergeContext _merge_context,
|
||||
PinnedIteratorsManager _pinned_iters_mgr)
|
||||
@@ -2707,6 +2736,163 @@ Status DBImpl::GetEntity(const ReadOptions& _read_options,
|
||||
return GetImpl(read_options, key, get_impl_options);
|
||||
}
|
||||
|
||||
Status DBImpl::GetEntityLazyImpl(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, LazyWideColumns* result) {
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(column_family);
|
||||
auto cfd = cfh->cfd();
|
||||
|
||||
// Point-lookup output: the entity buffer owned by the result. GetImpl fills
|
||||
// it with the (unresolved) entity and hands back the resolution context.
|
||||
PinnableWideColumns* entity_buffer =
|
||||
LazyWideColumnsHelper::EntityBuffer(result);
|
||||
|
||||
const SameFileBlobReader* same_file_reader = nullptr;
|
||||
const Version* version = nullptr;
|
||||
Cleanable pin;
|
||||
|
||||
GetImplOptions get_impl_options;
|
||||
get_impl_options.column_family = column_family;
|
||||
get_impl_options.columns = entity_buffer;
|
||||
get_impl_options.lazy_columns_pin = &pin;
|
||||
get_impl_options.lazy_columns_version = &version;
|
||||
get_impl_options.lazy_columns_same_file_reader = &same_file_reader;
|
||||
|
||||
Status s = GetImpl(read_options, key, get_impl_options);
|
||||
if (!s.ok()) {
|
||||
result->Reset();
|
||||
return s;
|
||||
}
|
||||
|
||||
s = LazyWideColumnsHelper::Finalize(
|
||||
result, key, version, read_options, cfd->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/cfd->blob_partition_manager() != nullptr,
|
||||
same_file_reader, std::move(pin));
|
||||
if (!s.ok()) {
|
||||
result->Reset();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status DBImpl::GetEntityLazy(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, LazyWideColumns* result) {
|
||||
// Start from an empty result so a reused LazyWideColumns is left empty (not
|
||||
// stale) on every early-return / error path below.
|
||||
if (result != nullptr) {
|
||||
result->Reset();
|
||||
}
|
||||
if (!column_family) {
|
||||
return Status::InvalidArgument(
|
||||
"Cannot call GetEntityLazy without a column family handle");
|
||||
}
|
||||
if (!result) {
|
||||
return Status::InvalidArgument(
|
||||
"Cannot call GetEntityLazy without a LazyWideColumns object");
|
||||
}
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGetEntity) {
|
||||
return Status::InvalidArgument(
|
||||
"Can only call GetEntityLazy with `ReadOptions::io_activity` set to "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGetEntity`");
|
||||
}
|
||||
// Lazy resolution keeps blob references (including same-file/embedded ones)
|
||||
// resolvable after the call returns; that relies on immortal, pinned table
|
||||
// readers, which the DB only guarantees when max_open_files == -1.
|
||||
if (mutable_db_options_.max_open_files != -1) {
|
||||
return Status::InvalidArgument(
|
||||
"GetEntityLazy requires the DB to be opened with max_open_files == -1");
|
||||
}
|
||||
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kGetEntity;
|
||||
}
|
||||
|
||||
return GetEntityLazyImpl(read_options, column_family, key, result);
|
||||
}
|
||||
|
||||
void DBImpl::MultiGetEntityLazy(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
size_t num_keys, const Slice* keys,
|
||||
LazyWideColumnsBatch* result, Status* statuses,
|
||||
bool /* sorted_input */) {
|
||||
// Start from an empty batch so a reused LazyWideColumnsBatch is left empty
|
||||
// (not stale) on every early-return / error path below.
|
||||
if (result != nullptr) {
|
||||
result->Reset();
|
||||
}
|
||||
if (num_keys == 0) {
|
||||
return;
|
||||
}
|
||||
Status arg_status;
|
||||
if (!column_family) {
|
||||
arg_status = Status::InvalidArgument(
|
||||
"Cannot call MultiGetEntityLazy without a column family handle");
|
||||
} else if (!result) {
|
||||
arg_status = Status::InvalidArgument(
|
||||
"Cannot call MultiGetEntityLazy without a LazyWideColumnsBatch object");
|
||||
} else if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kMultiGetEntity) {
|
||||
arg_status = Status::InvalidArgument(
|
||||
"Can only call MultiGetEntityLazy with `ReadOptions::io_activity` set "
|
||||
"to `Env::IOActivity::kUnknown` or `Env::IOActivity::kMultiGetEntity`");
|
||||
} else if (mutable_db_options_.max_open_files != -1) {
|
||||
arg_status = Status::InvalidArgument(
|
||||
"MultiGetEntityLazy requires the DB to be opened with max_open_files "
|
||||
"== "
|
||||
"-1");
|
||||
}
|
||||
if (!arg_status.ok()) {
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] = arg_status;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
LazyWideColumnsHelper::InitBatch(result, num_keys);
|
||||
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kMultiGetEntity;
|
||||
}
|
||||
|
||||
// The current phase implements MultiGetEntityLazy as a loop of single-key
|
||||
// lazy lookups. Each iteration acquires (and hands back to the result) its
|
||||
// own SuperVersion reference, and each single-key lookup independently
|
||||
// derives its read sequence number. To still present one consistent
|
||||
// point-in-time view across all keys (as MultiGet guarantees), pin an
|
||||
// explicit snapshot for the duration of the loop when the caller did not
|
||||
// supply one: it fixes the read sequence number for every key and prevents
|
||||
// compaction from dropping a version some later key still needs to observe.
|
||||
//
|
||||
// TODO(lazy-blob-resolution-phase3): replace this loop with a genuinely
|
||||
// batched read that acquires a single SuperVersion and one consistent
|
||||
// (implicit) sequence number for the whole batch -- the mechanism batched
|
||||
// MultiGet already uses -- and holds one shared pin per column family instead
|
||||
// of one per key. That removes the need for an explicit snapshot here (which
|
||||
// takes the DB mutex and adds a snapshot list entry) and enables
|
||||
// coalescing/parallelizing the per-key work.
|
||||
const Snapshot* snapshot = read_options.snapshot;
|
||||
const bool own_snapshot = snapshot == nullptr;
|
||||
if (own_snapshot) {
|
||||
snapshot = GetSnapshot();
|
||||
read_options.snapshot = snapshot;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] =
|
||||
GetEntityLazyImpl(read_options, column_family, keys[i], &(*result)[i]);
|
||||
}
|
||||
// Link the populated entities back to the batch so batch reads can validate
|
||||
// column ownership.
|
||||
LazyWideColumnsHelper::FinalizeBatch(result);
|
||||
|
||||
if (own_snapshot) {
|
||||
ReleaseSnapshot(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
Status DBImpl::GetEntity(const ReadOptions& _read_options, const Slice& key,
|
||||
PinnableAttributeGroups* result) {
|
||||
if (!result) {
|
||||
@@ -2827,9 +3013,10 @@ static Status DecodeDirectWriteBlobIndex(const Slice& blob_index_slice,
|
||||
return status;
|
||||
}
|
||||
|
||||
Status DBImpl::ResolveDirectWritePlainValue(
|
||||
const ReadOptions& read_options, const Slice& key, const Version* current,
|
||||
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns) {
|
||||
Status DBImpl::ResolveDirectWritePlainValue(const Slice& key,
|
||||
const BlobFetcher& blob_fetcher,
|
||||
PinnableSlice* value,
|
||||
PinnableWideColumns* columns) {
|
||||
Slice blob_index_slice;
|
||||
std::string blob_index_storage;
|
||||
if (value != nullptr) {
|
||||
@@ -2867,9 +3054,9 @@ Status DBImpl::ResolveDirectWritePlainValue(
|
||||
BlobIndex blob_idx;
|
||||
Status status = DecodeDirectWriteBlobIndex(blob_index_slice, &blob_idx);
|
||||
if (status.ok()) {
|
||||
status = BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
|
||||
read_options, key, blob_idx, current, cfd->blob_file_cache(),
|
||||
nullptr /* prefetch_buffer */, target, nullptr /* bytes_read */);
|
||||
status =
|
||||
blob_fetcher.FetchBlob(key, blob_idx, nullptr /* prefetch_buffer */,
|
||||
target, nullptr /* bytes_read */);
|
||||
if (status.ok() && columns != nullptr) {
|
||||
columns->SetPlainValue(std::move(*target));
|
||||
}
|
||||
@@ -2877,10 +3064,8 @@ Status DBImpl::ResolveDirectWritePlainValue(
|
||||
return status;
|
||||
}
|
||||
|
||||
Status DBImpl::ResolveDirectWriteWideColumns(const ReadOptions& read_options,
|
||||
const Slice& key,
|
||||
const Version* current,
|
||||
ColumnFamilyData* cfd,
|
||||
Status DBImpl::ResolveDirectWriteWideColumns(const Slice& key,
|
||||
const BlobFetcher& blob_fetcher,
|
||||
PinnableWideColumns* columns) {
|
||||
assert(columns != nullptr);
|
||||
|
||||
@@ -2924,9 +3109,8 @@ Status DBImpl::ResolveDirectWriteWideColumns(const ReadOptions& read_options,
|
||||
} else {
|
||||
extra_buffers.emplace_front();
|
||||
PinnableSlice& blob_value = extra_buffers.front();
|
||||
s = BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
|
||||
read_options, key, blob_idx, current, cfd->blob_file_cache(),
|
||||
nullptr /* prefetch_buffer */, &blob_value, nullptr /* bytes_read */);
|
||||
s = blob_fetcher.FetchBlob(key, blob_idx, nullptr /* prefetch_buffer */,
|
||||
&blob_value, nullptr /* bytes_read */);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -3122,9 +3306,9 @@ Status DBImpl::PostprocessMemtableValueRead(
|
||||
|
||||
bool DBImpl::MaybeResolveDirectWriteValue(
|
||||
const ReadOptions& read_options, const Slice& key,
|
||||
bool resolve_direct_write_value, const Version* current,
|
||||
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index, bool* value_found) {
|
||||
bool resolve_direct_write_value, const BlobFetcher& blob_fetcher,
|
||||
PinnableSlice* value, PinnableWideColumns* columns, Status* s,
|
||||
bool* is_blob_index, bool* value_found) {
|
||||
if (!s->ok() || !resolve_direct_write_value || (!value && !columns)) {
|
||||
return false;
|
||||
}
|
||||
@@ -3169,15 +3353,14 @@ bool DBImpl::MaybeResolveDirectWriteValue(
|
||||
}
|
||||
|
||||
if (needs_plain_value_resolution) {
|
||||
*s = ResolveDirectWritePlainValue(read_options, key, current, cfd, value,
|
||||
columns);
|
||||
*s = ResolveDirectWritePlainValue(key, blob_fetcher, value, columns);
|
||||
assert(is_blob_index != nullptr);
|
||||
*is_blob_index = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
assert(columns != nullptr);
|
||||
*s = ResolveDirectWriteWideColumns(read_options, key, current, cfd, columns);
|
||||
*s = ResolveDirectWriteWideColumns(key, blob_fetcher, columns);
|
||||
|
||||
if (is_blob_index != nullptr) {
|
||||
*is_blob_index = false;
|
||||
@@ -3189,15 +3372,16 @@ bool DBImpl::MaybeResolveDirectWriteValue(
|
||||
void DBImpl::PostprocessDirectWriteValueRead(
|
||||
const ReadOptions& read_options, const Slice& key,
|
||||
const std::string* timestamp, bool resolve_direct_write_value,
|
||||
const Version* current, ColumnFamilyData* cfd, PinnableSlice* value,
|
||||
const BlobFetcher* blob_fetcher, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
|
||||
bool* value_found) {
|
||||
if (resolve_direct_write_value) {
|
||||
assert(blob_fetcher != nullptr);
|
||||
std::string blob_lookup_key_storage;
|
||||
const bool value_resolved = MaybeResolveDirectWriteValue(
|
||||
read_options,
|
||||
GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage),
|
||||
resolve_direct_write_value, current, cfd, value, columns, s,
|
||||
resolve_direct_write_value, *blob_fetcher, value, columns, s,
|
||||
is_blob_index, value_found);
|
||||
if (!value_resolved && value != nullptr) {
|
||||
value->PinSelf();
|
||||
@@ -4016,6 +4200,9 @@ Status DBImpl::DropColumnFamilyImpl(ColumnFamilyHandle* column_family) {
|
||||
if (s.ok() && cfd->blob_partition_manager() != nullptr) {
|
||||
UnregisterBlobDirectWriteColumnFamily();
|
||||
}
|
||||
if (s.ok()) {
|
||||
RestoreParkedCompaction(cfd);
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
auto& moptions = cfd->GetLatestMutableCFOptions();
|
||||
@@ -5212,9 +5399,9 @@ void DBImpl::ReleaseOptionsFileNumber(
|
||||
}
|
||||
}
|
||||
|
||||
Status DBImpl::GetUpdatesSince(
|
||||
SequenceNumber seq, std::unique_ptr<TransactionLogIterator>* iter,
|
||||
const TransactionLogIterator::ReadOptions& read_options) {
|
||||
Status DBImpl::GetUpdatesSince(SequenceNumber seq,
|
||||
std::unique_ptr<WalIterator>* iter,
|
||||
const WalIterator::ReadOptions& read_options) {
|
||||
RecordTick(stats_, GET_UPDATES_SINCE_CALLS);
|
||||
if (seq_per_batch_) {
|
||||
return Status::NotSupported(
|
||||
@@ -7594,9 +7781,24 @@ const IOStatsContext* CurrentIOStatsContextForAsyncCallback(
|
||||
}
|
||||
|
||||
#if USE_COROUTINES
|
||||
void InstallCoroutineStatsConfigToTLS(
|
||||
const CoroutineStatsConfig& stats_config) {
|
||||
#ifndef NPERF_CONTEXT
|
||||
if (stats_config.per_level_perf_context_enabled) {
|
||||
get_perf_context()->EnablePerLevelPerfContext();
|
||||
} else {
|
||||
get_perf_context()->per_level_perf_context_enabled = false;
|
||||
}
|
||||
#endif
|
||||
#ifndef NIOSTATS_CONTEXT
|
||||
get_iostats_context()->disable_iostats = stats_config.iostats_disabled;
|
||||
#endif
|
||||
SetPerfLevel(stats_config.perf_level);
|
||||
}
|
||||
|
||||
std::optional<CoroutineStatsConfig> CaptureCoroutineStatsConfigForCallback(
|
||||
bool stats_enabled) {
|
||||
if (!stats_enabled) {
|
||||
if (!stats_enabled || !IsCoroutineStatsEnabled()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return CaptureCoroutineStatsConfig();
|
||||
@@ -7606,49 +7808,49 @@ std::optional<CoroutineStatsConfig> CaptureCoroutineStatsConfigForCallback(
|
||||
|
||||
} // namespace
|
||||
|
||||
void DBImpl::GetAsync(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp,
|
||||
Status& status, AsyncCallback& callback) {
|
||||
void DB::GetAsync(const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp, Status& status,
|
||||
AsyncCallback& callback) {
|
||||
const bool stats_enabled = callback.EnableStats();
|
||||
#if USE_COROUTINES
|
||||
auto* read_executor = immutable_db_options_.fs->GetReadExecutor();
|
||||
if (read_executor != nullptr) {
|
||||
auto* read_event_base = read_executor->getEventBase();
|
||||
assert(read_event_base != nullptr);
|
||||
auto task =
|
||||
[](std::optional<CoroutineStatsConfig> task_stats_config, DBImpl* db,
|
||||
ReadOptions task_options, ColumnFamilyHandle* task_column_family,
|
||||
Slice task_key, PinnableSlice* task_value,
|
||||
std::string* task_timestamp, Status& task_status,
|
||||
AsyncCallback& task_callback) mutable -> folly::coro::Task<void> {
|
||||
std::optional<CoroutineStatsContextScope> coroutine_stats_scope;
|
||||
if (task_stats_config.has_value()) {
|
||||
coroutine_stats_scope.emplace(std::move(*task_stats_config),
|
||||
db->immutable_db_options_.env);
|
||||
}
|
||||
task_status = co_await folly::coro::co_nothrow(
|
||||
db->GetCoroutine(task_options, task_column_family, task_key,
|
||||
task_value, task_timestamp));
|
||||
if (coroutine_stats_scope.has_value()) {
|
||||
coroutine_stats_scope->Finalize();
|
||||
}
|
||||
task_callback.OnComplete(
|
||||
CurrentPerfContextForAsyncCallback(coroutine_stats_scope.has_value()),
|
||||
CurrentIOStatsContextForAsyncCallback(
|
||||
coroutine_stats_scope.has_value()));
|
||||
}(CaptureCoroutineStatsConfigForCallback(stats_enabled), this, options,
|
||||
column_family, key, value,
|
||||
timestamp, status, callback);
|
||||
folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task))
|
||||
.start();
|
||||
return;
|
||||
CoroDB* coro_db = GetCoroDB();
|
||||
if (coro_db != nullptr) {
|
||||
auto* read_executor = GetFileSystem()->GetReadExecutor();
|
||||
if (read_executor != nullptr) {
|
||||
auto* read_event_base = read_executor->getEventBase();
|
||||
assert(read_event_base != nullptr);
|
||||
auto stats_config = CaptureCoroutineStatsConfigForCallback(stats_enabled);
|
||||
auto task =
|
||||
[](std::optional<CoroutineStatsConfig> task_stats_config,
|
||||
CoroDB* task_db, ReadOptions task_options,
|
||||
ColumnFamilyHandle* task_column_family, Slice task_key,
|
||||
PinnableSlice* task_value, std::string* task_timestamp,
|
||||
Status& task_status,
|
||||
AsyncCallback& task_callback) mutable -> folly::coro::Task<void> {
|
||||
if (task_stats_config.has_value()) {
|
||||
InstallCoroutineStatsConfigToTLS(*task_stats_config);
|
||||
}
|
||||
task_status = co_await folly::coro::co_nothrow(
|
||||
task_db->GetCoroutine(task_options, task_column_family, task_key,
|
||||
task_value, task_timestamp));
|
||||
|
||||
const bool task_stats_enabled = task_stats_config.has_value();
|
||||
task_callback.OnComplete(
|
||||
CurrentPerfContextForAsyncCallback(task_stats_enabled),
|
||||
CurrentIOStatsContextForAsyncCallback(task_stats_enabled));
|
||||
}(std::move(stats_config), coro_db, options, column_family, key, value,
|
||||
timestamp, status, callback);
|
||||
folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task))
|
||||
.start();
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
if (stats_enabled) {
|
||||
// Mirror behavior of coroutine version where each coroutine gets a clean
|
||||
// context
|
||||
// Match coroutine reads: callback stats describe only this request.
|
||||
ResetThreadLocalStatsForAsyncCallback();
|
||||
}
|
||||
status = Get(options, column_family, key, value, timestamp);
|
||||
@@ -7656,51 +7858,52 @@ void DBImpl::GetAsync(const ReadOptions& options,
|
||||
CurrentIOStatsContextForAsyncCallback(stats_enabled));
|
||||
}
|
||||
|
||||
void DBImpl::MultiGetAsync(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input, AsyncCallback& callback) {
|
||||
void DB::MultiGetAsync(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps,
|
||||
Status* statuses, const bool sorted_input,
|
||||
AsyncCallback& callback) {
|
||||
const bool stats_enabled = callback.EnableStats();
|
||||
#if USE_COROUTINES
|
||||
auto* read_executor = immutable_db_options_.fs->GetReadExecutor();
|
||||
if (read_executor != nullptr) {
|
||||
auto* read_event_base = read_executor->getEventBase();
|
||||
assert(read_event_base != nullptr);
|
||||
auto task = [](std::optional<CoroutineStatsConfig> task_stats_config,
|
||||
DBImpl* db, ReadOptions task_options, size_t task_num_keys,
|
||||
ColumnFamilyHandle** task_column_families,
|
||||
const Slice* task_keys, PinnableSlice* task_values,
|
||||
std::string* task_timestamps, Status* task_statuses,
|
||||
bool task_sorted_input, AsyncCallback& task_callback) mutable
|
||||
-> folly::coro::Task<void> {
|
||||
std::optional<CoroutineStatsContextScope> coroutine_stats_scope;
|
||||
if (task_stats_config.has_value()) {
|
||||
coroutine_stats_scope.emplace(std::move(*task_stats_config),
|
||||
db->immutable_db_options_.env);
|
||||
}
|
||||
co_await folly::coro::co_nothrow(db->MultiGetCoroutine(
|
||||
task_options, task_num_keys, task_column_families, task_keys,
|
||||
task_values, task_timestamps, task_statuses, task_sorted_input));
|
||||
if (coroutine_stats_scope.has_value()) {
|
||||
coroutine_stats_scope->Finalize();
|
||||
}
|
||||
task_callback.OnComplete(
|
||||
CurrentPerfContextForAsyncCallback(coroutine_stats_scope.has_value()),
|
||||
CurrentIOStatsContextForAsyncCallback(
|
||||
coroutine_stats_scope.has_value()));
|
||||
}(CaptureCoroutineStatsConfigForCallback(stats_enabled), this, options,
|
||||
num_keys, column_families, keys, values, timestamps, statuses,
|
||||
sorted_input, callback);
|
||||
folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task))
|
||||
.start();
|
||||
return;
|
||||
CoroDB* coro_db = GetCoroDB();
|
||||
if (coro_db != nullptr) {
|
||||
auto* read_executor = GetFileSystem()->GetReadExecutor();
|
||||
if (read_executor != nullptr) {
|
||||
auto* read_event_base = read_executor->getEventBase();
|
||||
assert(read_event_base != nullptr);
|
||||
auto stats_config = CaptureCoroutineStatsConfigForCallback(stats_enabled);
|
||||
auto task =
|
||||
[](std::optional<CoroutineStatsConfig> task_stats_config,
|
||||
CoroDB* task_db, ReadOptions task_options, size_t task_num_keys,
|
||||
ColumnFamilyHandle** task_column_families, const Slice* task_keys,
|
||||
PinnableSlice* task_values, std::string* task_timestamps,
|
||||
Status* task_statuses, bool task_sorted_input,
|
||||
AsyncCallback& task_callback) mutable -> folly::coro::Task<void> {
|
||||
if (task_stats_config.has_value()) {
|
||||
InstallCoroutineStatsConfigToTLS(*task_stats_config);
|
||||
}
|
||||
co_await folly::coro::co_nothrow(task_db->MultiGetCoroutine(
|
||||
task_options, task_num_keys, task_column_families, task_keys,
|
||||
task_values, task_timestamps, task_statuses, task_sorted_input));
|
||||
|
||||
const bool task_stats_enabled = task_stats_config.has_value();
|
||||
task_callback.OnComplete(
|
||||
CurrentPerfContextForAsyncCallback(task_stats_enabled),
|
||||
CurrentIOStatsContextForAsyncCallback(task_stats_enabled));
|
||||
}(std::move(stats_config), coro_db, options, num_keys, column_families,
|
||||
keys, values, timestamps,
|
||||
statuses, sorted_input,
|
||||
callback);
|
||||
folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task))
|
||||
.start();
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
if (stats_enabled) {
|
||||
// Mirror behavior of coroutine version where each coroutine gets a clean
|
||||
// context
|
||||
// Match coroutine reads: callback stats describe only this request.
|
||||
ResetThreadLocalStatsForAsyncCallback();
|
||||
}
|
||||
MultiGet(options, num_keys, column_families, keys, values, timestamps,
|
||||
|
||||
+165
-45
@@ -21,6 +21,7 @@
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "db/column_family.h"
|
||||
@@ -53,6 +54,7 @@
|
||||
#include "memtable/wbwi_memtable.h"
|
||||
#include "monitoring/instrumented_mutex.h"
|
||||
#include "options/db_options.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/attribute_groups.h"
|
||||
#include "rocksdb/db.h"
|
||||
@@ -60,10 +62,10 @@
|
||||
#include "rocksdb/memtablerep.h"
|
||||
#include "rocksdb/status.h"
|
||||
#include "rocksdb/trace_reader_writer.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/user_write_callback.h"
|
||||
#include "rocksdb/utilities/replayer.h"
|
||||
#include "rocksdb/utilities/write_batch_with_index.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
#include "rocksdb/write_buffer_manager.h"
|
||||
#include "table/merging_iterator.h"
|
||||
#include "util/autovector.h"
|
||||
@@ -73,11 +75,17 @@
|
||||
#include "util/stop_watch.h"
|
||||
#include "util/thread_local.h"
|
||||
|
||||
#if USE_COROUTINES
|
||||
#include "rocksdb/coro_db.h"
|
||||
#endif
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class Arena;
|
||||
class ArenaWrappedDBIter;
|
||||
class Cleanable;
|
||||
class FileIngestionHandleImpl;
|
||||
class SameFileBlobReader;
|
||||
class InMemoryStatsHistoryIterator;
|
||||
class MemTable;
|
||||
class PersistentStatsHistoryIterator;
|
||||
@@ -196,14 +204,22 @@ struct DBOpenLogRecordReadReporter : public log::Reader::Reporter {
|
||||
//
|
||||
// Since it's a very large class, the definition of the functions is
|
||||
// divided in several db_impl_*.cc files, besides db_impl.cc.
|
||||
class DBImpl : public DB {
|
||||
#if USE_COROUTINES
|
||||
class DBImpl : public DB,
|
||||
public CoroDB
|
||||
#else
|
||||
class DBImpl : public DB
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
DBImpl(const DBOptions& options, const std::string& dbname,
|
||||
const bool seq_per_batch = false, const bool batch_per_txn = true,
|
||||
bool read_only = false);
|
||||
// No copying allowed
|
||||
// No copying or moving allowed
|
||||
DBImpl(const DBImpl&) = delete;
|
||||
void operator=(const DBImpl&) = delete;
|
||||
DBImpl(DBImpl&&) = delete;
|
||||
void operator=(DBImpl&&) = delete;
|
||||
|
||||
virtual ~DBImpl();
|
||||
|
||||
@@ -269,14 +285,10 @@ class DBImpl : public DB {
|
||||
bool HasAnyBlobDirectWriteColumnFamily();
|
||||
|
||||
using DB::Get;
|
||||
using DB::GetAsync;
|
||||
DECLARE_SYNC_ASYNC_AND_CALLBACK(
|
||||
Status, Get, GetAsync,
|
||||
(const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value, std::string* timestamp,
|
||||
Status& status, AsyncCallback& callback),
|
||||
const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value, std::string* timestamp);
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, Get, const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp);
|
||||
|
||||
using DB::GetEntity;
|
||||
Status GetEntity(const ReadOptions& options,
|
||||
@@ -285,6 +297,10 @@ class DBImpl : public DB {
|
||||
Status GetEntity(const ReadOptions& options, const Slice& key,
|
||||
PinnableAttributeGroups* result) override;
|
||||
|
||||
Status GetEntityLazy(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
LazyWideColumns* result) override;
|
||||
|
||||
using DB::GetMergeOperands;
|
||||
Status GetMergeOperands(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
@@ -301,7 +317,6 @@ class DBImpl : public DB {
|
||||
}
|
||||
|
||||
using DB::MultiGet;
|
||||
using DB::MultiGetAsync;
|
||||
// This MultiGet is a batched version, which may be faster than calling Get
|
||||
// multiple times, especially if the keys have some spatial locality that
|
||||
// enables them to be queried in the same SST files/set of files. The larger
|
||||
@@ -309,16 +324,13 @@ class DBImpl : public DB {
|
||||
// The values and statuses parameters are arrays with number of elements
|
||||
// equal to keys.size(). This allows the storage for those to be alloacted
|
||||
// by the caller on the stack for small batches
|
||||
DECLARE_SYNC_ASYNC_AND_CALLBACK(
|
||||
void, MultiGet, MultiGetAsync,
|
||||
(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input, AsyncCallback& callback),
|
||||
const ReadOptions& _read_options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input = false);
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(void, MultiGet,
|
||||
const ReadOptions& _read_options,
|
||||
const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input = false);
|
||||
|
||||
void MultiGetWithCallback(
|
||||
const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
@@ -340,6 +352,11 @@ class DBImpl : public DB {
|
||||
const Slice* keys,
|
||||
PinnableAttributeGroups* results) override;
|
||||
|
||||
void MultiGetEntityLazy(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, size_t num_keys,
|
||||
const Slice* keys, LazyWideColumnsBatch* result,
|
||||
Status* statuses, bool sorted_input) override;
|
||||
|
||||
void MultiGetEntityWithCallback(
|
||||
const ReadOptions& read_options, ColumnFamilyHandle* column_family,
|
||||
ReadCallback* callback,
|
||||
@@ -477,6 +494,8 @@ class DBImpl : public DB {
|
||||
void DisableManualCompaction() override;
|
||||
void AbortAllCompactions() override;
|
||||
void ResumeAllCompactions() override;
|
||||
void AbortCompactions(ColumnFamilyHandle* column_family) override;
|
||||
void ResumeCompactions(ColumnFamilyHandle* column_family) override;
|
||||
|
||||
using DB::SetOptions;
|
||||
Status SetOptions(
|
||||
@@ -573,10 +592,10 @@ class DBImpl : public DB {
|
||||
Status GetCurrentWalFile(std::unique_ptr<WalFile>* current_wal_file) override;
|
||||
Status GetCreationTimeOfOldestFile(uint64_t* creation_time) override;
|
||||
|
||||
Status GetUpdatesSince(
|
||||
SequenceNumber seq_number, std::unique_ptr<TransactionLogIterator>* iter,
|
||||
const TransactionLogIterator::ReadOptions& read_options =
|
||||
TransactionLogIterator::ReadOptions()) override;
|
||||
Status GetUpdatesSince(SequenceNumber seq_number,
|
||||
std::unique_ptr<WalIterator>* iter,
|
||||
const WalIterator::ReadOptions& read_options =
|
||||
WalIterator::ReadOptions()) override;
|
||||
Status DeleteFilesInRanges(ColumnFamilyHandle* column_family,
|
||||
const RangeOpt* ranges, size_t n,
|
||||
bool include_end = true);
|
||||
@@ -589,6 +608,35 @@ class DBImpl : public DB {
|
||||
const LiveFilesStorageInfoOptions& opts,
|
||||
std::vector<LiveFileStorageInfo>* files) override;
|
||||
|
||||
// Variant of GetLiveFilesStorageInfo used by subset-CF Checkpoint. Only
|
||||
// table/blob files of the given column families are captured; the rest are
|
||||
// reported in excluded_cf_ids so the caller can reconcile the copied
|
||||
// MANIFEST (typically via AppendColumnFamilyDropsToManifest below). Caller
|
||||
// must supply a non-empty include_cf_ids containing the default CF id and a
|
||||
// non-null excluded_cf_ids; both are internal-contract preconditions.
|
||||
Status GetLiveFilesStorageInfoForSubsetCheckpoint(
|
||||
const LiveFilesStorageInfoOptions& opts,
|
||||
const std::vector<uint32_t>& include_cf_ids,
|
||||
std::vector<LiveFileStorageInfo>* files,
|
||||
std::vector<uint32_t>* excluded_cf_ids);
|
||||
|
||||
// Shared body of GetLiveFilesStorageInfo and its subset-checkpoint variant.
|
||||
// include_cf_ids empty -> no filter; otherwise records skipped CF ids in
|
||||
// *excluded_cf_ids (which must be non-null in that case).
|
||||
Status GetLiveFilesStorageInfoImpl(
|
||||
const LiveFilesStorageInfoOptions& opts,
|
||||
const std::vector<uint32_t>& include_cf_ids,
|
||||
std::vector<LiveFileStorageInfo>* files,
|
||||
std::vector<uint32_t>* excluded_cf_ids);
|
||||
|
||||
// Appends kColumnFamilyDrop records to the MANIFEST file at manifest_path for
|
||||
// the given column family ids. Used by Checkpoint to make a subset-CF
|
||||
// checkpoint's copied MANIFEST consistent with the reduced file set. Operates
|
||||
// only on the given file; does not mutate this DB's live state.
|
||||
Status AppendColumnFamilyDropsToManifest(const std::string& manifest_path,
|
||||
uint64_t manifest_size,
|
||||
const std::vector<uint32_t>& cf_ids);
|
||||
|
||||
Status GetPreparedFileInfoForExternalSstIngestion(
|
||||
const std::string& file_path,
|
||||
std::shared_ptr<const PreparedFileInfo>* file_info) override;
|
||||
@@ -729,6 +777,24 @@ class DBImpl : public DB {
|
||||
PinnableSlice* merge_operands = nullptr;
|
||||
GetMergeOperandsOptions* get_merge_operands_options = nullptr;
|
||||
int* number_of_operands = nullptr;
|
||||
|
||||
// Lazy wide-column mode (used by GetEntityLazy / MultiGetEntityLazy). When
|
||||
// `lazy_columns_pin` is non-null, the SST point-lookup path leaves a
|
||||
// wide-column entity's blob references *unresolved* (see
|
||||
// GetContext::SaveWideColumnEntityToColumns), so the caller can resolve
|
||||
// them on demand later. On a successful lookup GetImpl then:
|
||||
// - sets `*lazy_columns_version` to the Version the entity was read from
|
||||
// (the resolution context for blob references), and
|
||||
// - transfers an extra SuperVersion reference by registering a cleanup on
|
||||
// `*lazy_columns_pin`, so the result may outlive this call (as an
|
||||
// iterator's result does).
|
||||
// The SameFileBlobReader for the SST that held the entity (if any) is
|
||||
// stored in `*lazy_columns_same_file_reader` by the read path, so same-file
|
||||
// ("embedded") references stay resolvable later. All three must be set
|
||||
// together. `columns` must also be set (the entity output).
|
||||
Cleanable* lazy_columns_pin = nullptr;
|
||||
const Version** lazy_columns_version = nullptr;
|
||||
const SameFileBlobReader** lazy_columns_same_file_reader = nullptr;
|
||||
};
|
||||
|
||||
DECLARE_SYNC_AND_ASYNC(Status, GetImpl, const ReadOptions& read_options,
|
||||
@@ -750,6 +816,17 @@ class DBImpl : public DB {
|
||||
const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
|
||||
// Shared implementation of GetEntityLazy / MultiGetEntityLazy for a single
|
||||
// key: performs a lazy point lookup for `key` (leaving blob references
|
||||
// unresolved) into `*result`, which takes ownership of a SuperVersion pin so
|
||||
// it may outlive this call. `read_options` must already be finalized (e.g.
|
||||
// io_activity set). Returns OK on success, NotFound (with an empty `*result`)
|
||||
// if there is no entry, or another non-OK status on error. REQUIRES:
|
||||
// max_open_files == -1 has already been validated by the caller.
|
||||
Status GetEntityLazyImpl(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
LazyWideColumns* result);
|
||||
|
||||
// If `snapshot` == kMaxSequenceNumber, set a recent one inside the file.
|
||||
ArenaWrappedDBIter* NewIteratorImpl(const ReadOptions& options,
|
||||
ColumnFamilyHandleImpl* cfh,
|
||||
@@ -901,6 +978,13 @@ class DBImpl : public DB {
|
||||
void CleanupIteratorSuperVersion(SuperVersion* super_version,
|
||||
bool background_purge);
|
||||
|
||||
// Transfer an extra reference to `super_version` into `pin` (via a cleanup
|
||||
// callback), so `pin`'s owner keeps the SuperVersion -- and thus the data it
|
||||
// references -- alive after the current call returns, as an iterator does.
|
||||
// Shared by the lazy wide-column read path (GetEntityLazy) across the
|
||||
// primary, read-only, and secondary GetImpl overrides.
|
||||
void TransferSuperVersionPin(SuperVersion* super_version, Cleanable* pin);
|
||||
|
||||
LogsWithPrepTracker* logs_with_prep_tracker() {
|
||||
return &logs_with_prep_tracker_;
|
||||
}
|
||||
@@ -1418,6 +1502,10 @@ class DBImpl : public DB {
|
||||
|
||||
bool seq_per_batch() const { return seq_per_batch_; }
|
||||
|
||||
#if USE_COROUTINES
|
||||
CoroDB* GetCoroDB() override { return this; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
const std::string dbname_;
|
||||
const bool read_only_;
|
||||
@@ -2801,9 +2889,24 @@ class DBImpl : public DB {
|
||||
ColumnFamilyData* PopFirstFromCompactionQueue();
|
||||
FlushRequest PopFirstFromFlushQueue();
|
||||
|
||||
struct CompactionQueueThrottled {};
|
||||
struct CompactionQueueParked {};
|
||||
struct CompactionQueuePicked {
|
||||
ColumnFamilyData* cfd;
|
||||
};
|
||||
using CompactionQueuePickResult =
|
||||
std::variant<CompactionQueuePicked, CompactionQueueThrottled,
|
||||
CompactionQueueParked>;
|
||||
|
||||
// Pick the first unthrottled compaction with task token from queue.
|
||||
ColumnFamilyData* PickCompactionFromQueue(
|
||||
CompactionQueuePickResult PickCompactionFromQueue(
|
||||
std::unique_ptr<TaskLimiterToken>* token, LogBuffer* log_buffer);
|
||||
bool IsCompactionAborted(ColumnFamilyData* cfd) const;
|
||||
bool MustWaitForCompaction(ColumnFamilyData* cfd);
|
||||
bool HasPendingManualCompaction(ColumnFamilyData* cfd);
|
||||
bool RestoreParkedCompaction(ColumnFamilyData* cfd);
|
||||
void BeginInFlightCompaction(ColumnFamilyData* cfd);
|
||||
bool EndInFlightCompaction(ColumnFamilyData* cfd);
|
||||
|
||||
IOStatus SyncWalImpl(bool include_current_wal,
|
||||
const WriteOptions& write_options,
|
||||
@@ -3084,39 +3187,34 @@ class DBImpl : public DB {
|
||||
// Resolves a plain value whose current payload is an encoded direct-write
|
||||
// blob index, either through `value` directly or through the default-column
|
||||
// view layered on `columns`.
|
||||
static Status ResolveDirectWritePlainValue(const ReadOptions& read_options,
|
||||
const Slice& key,
|
||||
const Version* current,
|
||||
ColumnFamilyData* cfd,
|
||||
static Status ResolveDirectWritePlainValue(const Slice& key,
|
||||
const BlobFetcher& blob_fetcher,
|
||||
PinnableSlice* value,
|
||||
PinnableWideColumns* columns);
|
||||
// Resolves each unresolved direct-write blob-valued column in `columns` and
|
||||
// rebuilds the serialized wide-column entity in place.
|
||||
static Status ResolveDirectWriteWideColumns(const ReadOptions& read_options,
|
||||
const Slice& key,
|
||||
const Version* current,
|
||||
ColumnFamilyData* cfd,
|
||||
static Status ResolveDirectWriteWideColumns(const Slice& key,
|
||||
const BlobFetcher& blob_fetcher,
|
||||
PinnableWideColumns* columns);
|
||||
// Dispatches between plain-value and wide-column direct-write resolution for
|
||||
// read results observed before flush makes the corresponding blob file
|
||||
// visible through normal Version metadata.
|
||||
static bool MaybeResolveDirectWriteValue(
|
||||
const ReadOptions& read_options, const Slice& key,
|
||||
bool resolve_direct_write_value, const Version* current,
|
||||
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index, bool* value_found = nullptr);
|
||||
bool resolve_direct_write_value, const BlobFetcher& blob_fetcher,
|
||||
PinnableSlice* value, PinnableWideColumns* columns, Status* s,
|
||||
bool* is_blob_index, bool* value_found = nullptr);
|
||||
// Completes primary memtable hits that can still carry direct-write blob
|
||||
// references before the blob file is visible in Version metadata.
|
||||
static void PostprocessDirectWriteValueRead(
|
||||
const ReadOptions& read_options, const Slice& key,
|
||||
const std::string* timestamp, bool resolve_direct_write_value,
|
||||
const Version* current, ColumnFamilyData* cfd, PinnableSlice* value,
|
||||
const BlobFetcher* blob_fetcher, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
|
||||
bool* value_found = nullptr);
|
||||
// Resolves memtable read results that still carry blob references through
|
||||
// either a raw blob-index payload in `value` or unresolved blob columns in
|
||||
// `columns`. Unlike the direct-write helper above, this path only depends on
|
||||
// a BlobFetcher and therefore works for read-only/secondary DBs. Sets
|
||||
// `columns`. This generic path is also used by read-only/secondary DBs. Sets
|
||||
// *did_resolve to true iff there was a reference to resolve -- in which case
|
||||
// this function has finalized `value`/`columns` (populated on success,
|
||||
// cleared on error) -- and false iff there was nothing to resolve (the caller
|
||||
@@ -3190,6 +3288,10 @@ class DBImpl : public DB {
|
||||
// DB mutex in compaction code paths.
|
||||
std::atomic<int> compaction_aborted_ = 0;
|
||||
|
||||
// Number of AbortCompactions() callers waiting for a targeted compaction to
|
||||
// finish. Protected by the DB mutex.
|
||||
int per_cf_compaction_abort_waiters_ = 0;
|
||||
|
||||
// This condition variable is signaled on these conditions:
|
||||
// * whenever bg_compaction_scheduled_ goes down to 0
|
||||
// * if AnyManualCompaction, whenever a compaction finishes, even if it hasn't
|
||||
@@ -3399,7 +3501,7 @@ class DBImpl : public DB {
|
||||
// cfd->imm()->IsFlushPending()
|
||||
// A column family is inserted into compaction_queue_ when it satisfied
|
||||
// condition cfd->NeedsCompaction()
|
||||
// Column families in this list are all Ref()-erenced
|
||||
// Column families in these collections are all Ref()-erenced.
|
||||
// TODO(icanadi) Provide some kind of ReferencedColumnFamily class that will
|
||||
// do RAII on ColumnFamilyData
|
||||
// Column families are in this queue when they need to be flushed or
|
||||
@@ -3414,9 +3516,23 @@ class DBImpl : public DB {
|
||||
// invariant(column family present in flush_queue_ <==>
|
||||
// ColumnFamilyData::pending_flush_ == true)
|
||||
std::deque<FlushRequest> flush_queue_;
|
||||
// invariant(column family present in compaction_queue_ <==>
|
||||
// ColumnFamilyData::pending_compaction_ == true)
|
||||
// invariant((column family in compaction_queue_ or
|
||||
// column family in parked_compaction_cfds_)
|
||||
// <=> queued_for_compaction() == true)
|
||||
//
|
||||
// "Parked" means a column family whose compaction was aborted via
|
||||
// AbortCompactions() while it was queued. When an aborted CF reaches the
|
||||
// front of compaction_queue_, PickCompactionFromQueue() moves it to
|
||||
// parked_compaction_cfds_ instead of scheduling it. The CF does not
|
||||
// immediately lose its place -- only when it reaches the front of the
|
||||
// queue is it parked. This preserves fairness for CFs behind it and
|
||||
// avoids restoring the scheduler credit (which would cause a hot re-pick
|
||||
// loop). On the final ResumeCompactions(), RestoreParkedCompaction()
|
||||
// re-enqueues the CF (or releases its ref if it no longer needs
|
||||
// compaction). Each parked CF carries exactly one Ref() from the
|
||||
// original AddToCompactionQueue() call.
|
||||
std::deque<ColumnFamilyData*> compaction_queue_;
|
||||
std::unordered_set<ColumnFamilyData*> parked_compaction_cfds_;
|
||||
|
||||
// A map to store file numbers and filenames of the files to be purged
|
||||
std::unordered_map<uint64_t, PurgeFileInfo> purge_files_;
|
||||
@@ -3447,6 +3563,10 @@ class DBImpl : public DB {
|
||||
// stores the number of BOTTOM-priority compactions currently running
|
||||
int num_running_bottom_compactions_ = 0;
|
||||
|
||||
// Number of compaction service jobs currently waiting in
|
||||
// CompactionService::Wait(), counted per executed subcompaction.
|
||||
std::atomic<int> num_running_remote_compactions_ = 0;
|
||||
|
||||
// number of background memtable flush jobs, submitted to the HIGH pool
|
||||
int bg_flush_scheduled_ = 0;
|
||||
|
||||
@@ -3770,7 +3890,7 @@ inline Status DBImpl::FailIfTsMismatchCf(ColumnFamilyHandle* column_family,
|
||||
inline Status DBImpl::FailIfTableFilterWithRangeConversion(
|
||||
const ReadOptions& read_options,
|
||||
const MutableCFOptions& mutable_cf_options) const {
|
||||
if (read_options.table_filter &&
|
||||
if (HasTableFilter(read_options) &&
|
||||
mutable_cf_options.min_tombstones_for_range_conversion > 0) {
|
||||
return Status::InvalidArgument(
|
||||
"ReadOptions::table_filter is not supported when "
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <deque>
|
||||
#include <unordered_map>
|
||||
@@ -1195,11 +1196,15 @@ Status DBImpl::CompactRange(const CompactRangeOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice* begin_without_ts,
|
||||
const Slice* end_without_ts) {
|
||||
auto* cfd =
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(column_family)->cfd();
|
||||
assert(cfd);
|
||||
|
||||
if (manual_compaction_paused_.load(std::memory_order_acquire) > 0) {
|
||||
return Status::Incomplete(Status::SubCode::kManualCompactionPaused);
|
||||
}
|
||||
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
if (IsCompactionAborted(cfd)) {
|
||||
return Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
}
|
||||
|
||||
@@ -1610,6 +1615,21 @@ Status DBImpl::CompactFiles(const CompactionOptions& compact_options,
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(column_family)->cfd();
|
||||
assert(cfd);
|
||||
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
BeginInFlightCompaction(cfd);
|
||||
}
|
||||
Defer end_in_flight_compaction([&]() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
// The caller holds a live handle to this CF, so this can never be the final
|
||||
// unref (the return can only be true when the last reference is dropped).
|
||||
[[maybe_unused]] const bool cfd_deleted = EndInFlightCompaction(cfd);
|
||||
assert(!cfd_deleted);
|
||||
if (per_cf_compaction_abort_waiters_ > 0) {
|
||||
bg_cv_.SignalAll();
|
||||
}
|
||||
});
|
||||
|
||||
Status s;
|
||||
JobContext job_context(next_job_id_.fetch_add(1), true);
|
||||
LogBuffer log_buffer(InfoLogLevel::INFO_LEVEL,
|
||||
@@ -1738,8 +1758,7 @@ Status DBImpl::CompactFilesImpl(
|
||||
return Status::ShutdownInProgress();
|
||||
}
|
||||
|
||||
// triggered by AbortAllCompactions
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
if (IsCompactionAborted(cfd)) {
|
||||
return Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
}
|
||||
|
||||
@@ -1866,7 +1885,7 @@ Status DBImpl::CompactFilesImpl(
|
||||
|
||||
c.reset();
|
||||
bg_compaction_scheduled_--;
|
||||
if (bg_compaction_scheduled_ == 0) {
|
||||
if (bg_compaction_scheduled_ == 0 || per_cf_compaction_abort_waiters_ > 0) {
|
||||
bg_cv_.SignalAll();
|
||||
}
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
@@ -1896,7 +1915,7 @@ Status DBImpl::CompactFilesImpl(
|
||||
kManualCompactionCanceledFalse_, compaction_aborted_, db_id_,
|
||||
db_session_id_, c->column_family_data()->GetFullHistoryTsLow(),
|
||||
c->trim_ts(), &blob_callback_, &bg_compaction_scheduled_,
|
||||
&bg_bottom_compaction_scheduled_);
|
||||
&bg_bottom_compaction_scheduled_, &num_running_remote_compactions_);
|
||||
|
||||
// Creating a compaction influences the compaction score because the score
|
||||
// takes running compactions into account (by skipping files that are already
|
||||
@@ -2001,7 +2020,7 @@ Status DBImpl::CompactFilesImpl(
|
||||
c.reset();
|
||||
|
||||
bg_compaction_scheduled_--;
|
||||
if (bg_compaction_scheduled_ == 0) {
|
||||
if (bg_compaction_scheduled_ == 0 || per_cf_compaction_abort_waiters_ > 0) {
|
||||
bg_cv_.SignalAll();
|
||||
}
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
@@ -2471,12 +2490,10 @@ Status DBImpl::RunManualCompaction(
|
||||
return manual.status;
|
||||
}
|
||||
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
// All compactions are being aborted. Return immediately.
|
||||
int counter = compaction_aborted_.load(std::memory_order_acquire);
|
||||
ROCKS_LOG_INFO(
|
||||
immutable_db_options_.info_log,
|
||||
"RunManualCompaction: Aborting due to compaction_aborted_=%d", counter);
|
||||
if (IsCompactionAborted(cfd)) {
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"[%s] RunManualCompaction: aborting compaction",
|
||||
cfd->GetName().c_str());
|
||||
manual.status = Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
manual.done = true;
|
||||
return manual.status;
|
||||
@@ -2506,7 +2523,7 @@ Status DBImpl::RunManualCompaction(
|
||||
// and `CompactRangeOptions::canceled` might not work well together.
|
||||
while (bg_bottom_compaction_scheduled_ > 0 ||
|
||||
bg_compaction_scheduled_ > 0) {
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
if (IsCompactionAborted(cfd)) {
|
||||
// Pretend the error came from compaction so the below cleanup/error
|
||||
// handling code can process it.
|
||||
manual.done = true;
|
||||
@@ -2542,6 +2559,11 @@ Status DBImpl::RunManualCompaction(
|
||||
// true.
|
||||
while (!manual.done) {
|
||||
assert(HasPendingManualCompaction());
|
||||
if (IsCompactionAborted(cfd) && !scheduled && !manual.in_progress) {
|
||||
manual.done = true;
|
||||
manual.status = Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
break;
|
||||
}
|
||||
manual_conflict = false;
|
||||
Compaction* compaction = nullptr;
|
||||
if (ShouldntRunManualCompaction(&manual) || (manual.in_progress == true) ||
|
||||
@@ -2631,7 +2653,7 @@ Status DBImpl::RunManualCompaction(
|
||||
if (!scheduled) {
|
||||
// There is nothing scheduled to wait on, so any cancellation can end the
|
||||
// manual now.
|
||||
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
if (IsCompactionAborted(cfd)) {
|
||||
// Stop waiting since it was canceled. Pretend the error came from
|
||||
// compaction so the below cleanup/error handling code can process it.
|
||||
manual.done = true;
|
||||
@@ -3299,6 +3321,52 @@ void DBImpl::EnableManualCompaction() {
|
||||
manual_compaction_paused_.fetch_sub(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool DBImpl::IsCompactionAborted(ColumnFamilyData* cfd) const {
|
||||
return compaction_aborted_.load(std::memory_order_acquire) > 0 ||
|
||||
cfd->compaction_aborted() > 0;
|
||||
}
|
||||
|
||||
bool DBImpl::MustWaitForCompaction(ColumnFamilyData* cfd) {
|
||||
mutex_.AssertHeld();
|
||||
// num_in_flight_compactions_ and the picker's compactions_in_progress_ set
|
||||
// cover DIFFERENT windows and neither subsumes the other:
|
||||
// - the counter covers a picked Compaction from pick through the end of
|
||||
// BackgroundCompaction (including NotifyOnCompactionCompleted), which the
|
||||
// picker set does not -- it is cleared during Install's manifest write.
|
||||
// - the picker set covers the bottom-priority handoff: when a LOW thread
|
||||
// forwards a bottom compaction, its EndInFlightCompaction() dips the
|
||||
// counter to 0 while the compaction is still registered and queued for the
|
||||
// BOTTOM pool, and the picker set keeps AbortCompactions() waiting across
|
||||
// that gap. (The re-registered "intended" bottom compaction inherits the
|
||||
// original compaction_reason(), so it is not filtered out below.)
|
||||
if (cfd->num_in_flight_compactions(&mutex_) > 0) {
|
||||
return true;
|
||||
}
|
||||
for (const auto* compaction :
|
||||
*cfd->compaction_picker()->compactions_in_progress()) {
|
||||
const auto reason = compaction->compaction_reason();
|
||||
// These are picker reservations for conflict tracking, not CompactionJobs,
|
||||
// so they never observe the abort atomic in ProcessKeyValue().
|
||||
if (reason != CompactionReason::kExternalSstIngestion &&
|
||||
reason != CompactionReason::kRefitLevel) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void DBImpl::BeginInFlightCompaction(ColumnFamilyData* cfd) {
|
||||
mutex_.AssertHeld();
|
||||
cfd->Ref();
|
||||
cfd->inc_num_in_flight_compactions(&mutex_);
|
||||
}
|
||||
|
||||
bool DBImpl::EndInFlightCompaction(ColumnFamilyData* cfd) {
|
||||
mutex_.AssertHeld();
|
||||
cfd->dec_num_in_flight_compactions(&mutex_);
|
||||
return cfd->UnrefAndTryDelete();
|
||||
}
|
||||
|
||||
void DBImpl::AbortAllCompactions() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
|
||||
@@ -3325,6 +3393,83 @@ void DBImpl::AbortAllCompactions() {
|
||||
}
|
||||
}
|
||||
|
||||
void DBImpl::AbortCompactions(ColumnFamilyHandle* column_family) {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
if (shutting_down_.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert(column_family != nullptr);
|
||||
auto* cfh = static_cast_with_check<ColumnFamilyHandleImpl>(column_family);
|
||||
assert(cfh->db() == this);
|
||||
auto* cfd = cfh->cfd();
|
||||
assert(cfd != nullptr);
|
||||
|
||||
cfd->inc_compaction_aborted(&mutex_);
|
||||
TEST_SYNC_POINT("DBImpl::AbortCompactions:FlagSet");
|
||||
|
||||
for (const auto* manual_compaction : manual_compaction_dequeue_) {
|
||||
if (manual_compaction->cfd == cfd) {
|
||||
manual_compaction->canceled.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
bg_cv_.SignalAll();
|
||||
|
||||
// Wait for this CF's running compactions to finish or abort. If the DB
|
||||
// is shutting down, return early -- the counter stays incremented but the
|
||||
// CFD and DB will be torn down anyway. Callers that use SCOPE_EXIT for
|
||||
// the matching ResumeCompactions() are safe: the resume is a no-op on a
|
||||
// DB that is already closed.
|
||||
++per_cf_compaction_abort_waiters_;
|
||||
while (!shutting_down_.load(std::memory_order_acquire) &&
|
||||
(MustWaitForCompaction(cfd) || HasPendingManualCompaction(cfd))) {
|
||||
TEST_SYNC_POINT("DBImpl::AbortCompactions:Waiting");
|
||||
bg_cv_.Wait();
|
||||
}
|
||||
--per_cf_compaction_abort_waiters_;
|
||||
TEST_SYNC_POINT("DBImpl::AbortCompactions:Complete");
|
||||
}
|
||||
|
||||
void DBImpl::ResumeCompactions(ColumnFamilyHandle* column_family) {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
if (shutting_down_.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert(column_family != nullptr);
|
||||
auto* cfh = static_cast_with_check<ColumnFamilyHandleImpl>(column_family);
|
||||
assert(cfh->db() == this);
|
||||
auto* cfd = cfh->cfd();
|
||||
assert(cfd != nullptr);
|
||||
|
||||
const int before = cfd->compaction_aborted();
|
||||
if (before <= 0) {
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"ResumeCompactions called without prior AbortCompactions "
|
||||
"for column family %s (counter=%d)",
|
||||
cfd->GetName().c_str(), before);
|
||||
return;
|
||||
}
|
||||
|
||||
const int current = cfd->dec_compaction_aborted(&mutex_);
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"[%s] ResumeCompactions: counter %d -> %d",
|
||||
cfd->GetName().c_str(), before, current);
|
||||
if (current == 0) {
|
||||
// TODO: Aborting an automatic compaction after PickCompaction()
|
||||
// releases its inputs without recomputing the compaction score, so this
|
||||
// EnqueuePendingCompaction() can observe a stale sub-trigger score and
|
||||
// queue nothing, leaving the CF idle until the next flush. Pre-existing
|
||||
// (ResumeAllCompactions() has the same gap); fix separately for both APIs
|
||||
// by recomputing the score on the abort path.
|
||||
if (!RestoreParkedCompaction(cfd) && !cfd->IsDropped()) {
|
||||
EnqueuePendingCompaction(cfd);
|
||||
}
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
bg_cv_.SignalAll();
|
||||
}
|
||||
}
|
||||
|
||||
void DBImpl::ResumeAllCompactions() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
int before = compaction_aborted_.load(std::memory_order_acquire);
|
||||
@@ -3350,6 +3495,7 @@ void DBImpl::ResumeAllCompactions() {
|
||||
// If this is the last resume call (abort counter back to 0), schedule
|
||||
// compactions that may have been waiting
|
||||
if (current == 0) {
|
||||
// The stale-score TODO in ResumeCompactions() also applies here.
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
}
|
||||
}
|
||||
@@ -3576,6 +3722,27 @@ void DBImpl::AddToCompactionQueue(ColumnFamilyData* cfd) {
|
||||
++unscheduled_compactions_;
|
||||
}
|
||||
|
||||
bool DBImpl::RestoreParkedCompaction(ColumnFamilyData* cfd) {
|
||||
mutex_.AssertHeld();
|
||||
auto it = parked_compaction_cfds_.find(cfd);
|
||||
if (it == parked_compaction_cfds_.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
parked_compaction_cfds_.erase(it);
|
||||
assert(cfd->queued_for_compaction());
|
||||
if (!cfd->IsDropped() && cfd->NeedsCompaction()) {
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::RestoreParkedCompaction:cfd",
|
||||
static_cast<void*>(cfd));
|
||||
compaction_queue_.push_back(cfd);
|
||||
++unscheduled_compactions_;
|
||||
} else {
|
||||
cfd->set_queued_for_compaction(false);
|
||||
cfd->UnrefAndTryDelete();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ColumnFamilyData* DBImpl::PopFirstFromCompactionQueue() {
|
||||
assert(!compaction_queue_.empty());
|
||||
auto cfd = *compaction_queue_.begin();
|
||||
@@ -3603,22 +3770,37 @@ DBImpl::FlushRequest DBImpl::PopFirstFromFlushQueue() {
|
||||
return flush_req;
|
||||
}
|
||||
|
||||
ColumnFamilyData* DBImpl::PickCompactionFromQueue(
|
||||
DBImpl::CompactionQueuePickResult DBImpl::PickCompactionFromQueue(
|
||||
std::unique_ptr<TaskLimiterToken>* token, LogBuffer* log_buffer) {
|
||||
assert(!compaction_queue_.empty());
|
||||
assert(*token == nullptr);
|
||||
autovector<ColumnFamilyData*> throttled_candidates;
|
||||
ColumnFamilyData* cfd = nullptr;
|
||||
CompactionQueuePickResult result = CompactionQueueThrottled{};
|
||||
while (!compaction_queue_.empty()) {
|
||||
auto first_cfd = *compaction_queue_.begin();
|
||||
compaction_queue_.pop_front();
|
||||
assert(first_cfd->queued_for_compaction());
|
||||
if (first_cfd->IsDropped()) {
|
||||
first_cfd->set_queued_for_compaction(false);
|
||||
result = CompactionQueuePicked{first_cfd};
|
||||
break;
|
||||
}
|
||||
if (first_cfd->compaction_aborted() > 0) {
|
||||
// Parking consumes this worker's credit. Remaining queue entries retain
|
||||
// their own credits and are scheduled when this worker returns.
|
||||
[[maybe_unused]] const bool inserted =
|
||||
parked_compaction_cfds_.insert(first_cfd).second;
|
||||
assert(inserted);
|
||||
TEST_SYNC_POINT("DBImpl::PickCompactionFromQueue:Parked");
|
||||
result = CompactionQueueParked{};
|
||||
break;
|
||||
}
|
||||
if (!RequestCompactionToken(first_cfd, false, token, log_buffer)) {
|
||||
throttled_candidates.push_back(first_cfd);
|
||||
continue;
|
||||
}
|
||||
cfd = first_cfd;
|
||||
cfd->set_queued_for_compaction(false);
|
||||
first_cfd->set_queued_for_compaction(false);
|
||||
result = CompactionQueuePicked{first_cfd};
|
||||
break;
|
||||
}
|
||||
// Add throttled compaction candidates back to queue in the original order.
|
||||
@@ -3626,7 +3808,7 @@ ColumnFamilyData* DBImpl::PickCompactionFromQueue(
|
||||
iter != throttled_candidates.rend(); ++iter) {
|
||||
compaction_queue_.push_front(*iter);
|
||||
}
|
||||
return cfd;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DBImpl::EnqueuePendingFlush(const FlushRequest& flush_req) {
|
||||
@@ -3673,13 +3855,21 @@ bool DBImpl::EnqueuePendingFlush(const FlushRequest& flush_req) {
|
||||
|
||||
void DBImpl::EnqueuePendingCompaction(ColumnFamilyData* cfd) {
|
||||
mutex_.AssertHeld();
|
||||
if (reject_new_background_jobs_) {
|
||||
if (reject_new_background_jobs_ || cfd->IsDropped()) {
|
||||
return;
|
||||
}
|
||||
if (!cfd->queued_for_compaction() && cfd->NeedsCompaction()) {
|
||||
TEST_SYNC_POINT_CALLBACK("EnqueuePendingCompaction::cfd",
|
||||
static_cast<void*>(cfd));
|
||||
AddToCompactionQueue(cfd);
|
||||
if (cfd->compaction_aborted() > 0) {
|
||||
cfd->Ref();
|
||||
cfd->set_queued_for_compaction(true);
|
||||
[[maybe_unused]] const bool inserted =
|
||||
parked_compaction_cfds_.insert(cfd).second;
|
||||
assert(inserted);
|
||||
} else {
|
||||
AddToCompactionQueue(cfd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4139,7 +4329,7 @@ void DBImpl::BackgroundCallCompaction(PrepickedCompaction* prepicked_compaction,
|
||||
prepicked_compaction->task_token.reset();
|
||||
}
|
||||
|
||||
if (made_progress ||
|
||||
if (made_progress || per_cf_compaction_abort_waiters_ > 0 ||
|
||||
(bg_compaction_scheduled_ == 0 &&
|
||||
bg_bottom_compaction_scheduled_ == 0) ||
|
||||
HasPendingManualCompaction() || unscheduled_compactions_ == 0) {
|
||||
@@ -4181,7 +4371,29 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
prepicked_compaction->compaction != nullptr) {
|
||||
c.reset(prepicked_compaction->compaction);
|
||||
}
|
||||
// Track this CF as having an in-flight compaction for the duration of this
|
||||
// BackgroundCompaction (which covers NotifyOnCompactionCompleted). The Defer
|
||||
// ends tracking on every return path. BeginInFlightCompaction()'s Ref keeps a
|
||||
// dropped CF alive across any c.reset() until then; if this ends the CF's
|
||||
// last reference, the FindObsoleteFiles() pass in BackgroundCallCompaction
|
||||
// cleans up its files.
|
||||
ColumnFamilyData* in_flight_cfd = nullptr;
|
||||
Defer end_in_flight_compaction([&]() {
|
||||
if (in_flight_cfd != nullptr) {
|
||||
EndInFlightCompaction(in_flight_cfd);
|
||||
}
|
||||
});
|
||||
if (c != nullptr) {
|
||||
in_flight_cfd = c->column_family_data();
|
||||
BeginInFlightCompaction(in_flight_cfd);
|
||||
}
|
||||
bool is_prepicked = is_manual || c;
|
||||
ColumnFamilyData* prepicked_cfd = nullptr;
|
||||
if (c) {
|
||||
prepicked_cfd = c->column_family_data();
|
||||
} else if (manual_compaction != nullptr) {
|
||||
prepicked_cfd = manual_compaction->cfd;
|
||||
}
|
||||
|
||||
// (manual_compaction->in_progress == false);
|
||||
bool trivial_move_disallowed =
|
||||
@@ -4209,6 +4421,9 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
// can schedule the still-queued work.
|
||||
unscheduled_compactions_++;
|
||||
}
|
||||
} else if (prepicked_cfd != nullptr &&
|
||||
prepicked_cfd->compaction_aborted() > 0) {
|
||||
status = Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
} else if (is_manual &&
|
||||
manual_compaction->canceled.load(std::memory_order_acquire)) {
|
||||
status = Status::Incomplete(Status::SubCode::kManualCompactionPaused);
|
||||
@@ -4313,13 +4528,18 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
ColumnFamilyData* cfd = nullptr;
|
||||
|
||||
if (!need_repick) {
|
||||
cfd = PickCompactionFromQueue(&task_token, log_buffer);
|
||||
if (cfd == nullptr) {
|
||||
auto pick_result = PickCompactionFromQueue(&task_token, log_buffer);
|
||||
if (std::holds_alternative<CompactionQueueThrottled>(pick_result)) {
|
||||
// Can't find any executable task from the compaction queue.
|
||||
// All tasks have been throttled by compaction thread limiter.
|
||||
++unscheduled_compactions_;
|
||||
return Status::Busy();
|
||||
}
|
||||
if (std::holds_alternative<CompactionQueueParked>(pick_result)) {
|
||||
return Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
}
|
||||
cfd = std::get<CompactionQueuePicked>(pick_result).cfd;
|
||||
assert(cfd != nullptr);
|
||||
|
||||
// We unreference here because the following code will take a Ref() on
|
||||
// this cfd if it is going to use it (Compaction class holds a
|
||||
@@ -4360,6 +4580,10 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
mutable_cf_options, mutable_db_options_, job_context->snapshot_seqs,
|
||||
job_context->snapshot_checker, log_buffer,
|
||||
thread_pri == Env::Priority::BOTTOM /* require_max_output_level */));
|
||||
if (c != nullptr && in_flight_cfd == nullptr) {
|
||||
in_flight_cfd = cfd;
|
||||
BeginInFlightCompaction(in_flight_cfd);
|
||||
}
|
||||
if (thread_pri == Env::Priority::LOW) {
|
||||
TEST_SYNC_POINT("DBImpl::BackgroundCompaction():AfterPickCompaction");
|
||||
} else if (thread_pri == Env::Priority::BOTTOM) {
|
||||
@@ -4840,7 +5064,7 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
compaction_aborted_, db_id_, db_session_id_,
|
||||
c->column_family_data()->GetFullHistoryTsLow(), c->trim_ts(),
|
||||
&blob_callback_, &bg_compaction_scheduled_,
|
||||
&bg_bottom_compaction_scheduled_);
|
||||
&bg_bottom_compaction_scheduled_, &num_running_remote_compactions_);
|
||||
compaction_job.Prepare(std::nullopt /*subcompact to be computed*/);
|
||||
|
||||
std::unique_ptr<std::list<uint64_t>::iterator> min_options_file_number_elem;
|
||||
@@ -5082,6 +5306,14 @@ bool DBImpl::HasPendingManualCompaction() {
|
||||
return (!manual_compaction_dequeue_.empty());
|
||||
}
|
||||
|
||||
bool DBImpl::HasPendingManualCompaction(ColumnFamilyData* cfd) {
|
||||
return std::any_of(manual_compaction_dequeue_.begin(),
|
||||
manual_compaction_dequeue_.end(),
|
||||
[cfd](const ManualCompactionState* manual) {
|
||||
return manual->cfd == cfd;
|
||||
});
|
||||
}
|
||||
|
||||
void DBImpl::AddManualCompaction(DBImpl::ManualCompactionState* m) {
|
||||
assert(manual_compaction_paused_ == 0);
|
||||
manual_compaction_dequeue_.push_back(m);
|
||||
@@ -5432,6 +5664,7 @@ Status DBImpl::WaitForCompact(
|
||||
}
|
||||
if ((bg_bottom_compaction_scheduled_ || bg_compaction_scheduled_ ||
|
||||
bg_flush_scheduled_ || unscheduled_compactions_ ||
|
||||
!parked_compaction_cfds_.empty() ||
|
||||
(wait_for_compact_options.wait_for_purge && bg_purge_scheduled_) ||
|
||||
unscheduled_flushes_ || error_handler_.IsRecoveryInProgress()) &&
|
||||
(error_handler_.GetBGError().ok())) {
|
||||
|
||||
@@ -55,6 +55,8 @@ Env::IOActivity GetCurrentThreadIOActivityForMetadataRead() {
|
||||
case ThreadStatus::OperationType::
|
||||
OP_GET_FILE_CHECKSUMS_FROM_CURRENT_MANIFEST:
|
||||
return Env::IOActivity::kGetFileChecksumsFromCurrentManifest;
|
||||
case ThreadStatus::OperationType::OP_LAZY_RESOLVE:
|
||||
return Env::IOActivity::kLazyResolve;
|
||||
default:
|
||||
return Env::IOActivity::kUnknown;
|
||||
}
|
||||
|
||||
@@ -2176,7 +2176,7 @@ Status DBImpl::WriteLevel0TableForRecovery(int job_id, ColumnFamilyData* cfd,
|
||||
false /* is_bottommost */, TableFileCreationReason::kRecovery,
|
||||
0 /* oldest_key_time */, 0 /* file_creation_time */, db_id_,
|
||||
db_session_id_, 0 /* target_file_size */, meta.fd.GetNumber(),
|
||||
kMaxSequenceNumber);
|
||||
kMaxSequenceNumber, dbname_ /* db_name */);
|
||||
Version* version = cfd->current();
|
||||
version->Ref();
|
||||
TableProperties temp_table_proerties;
|
||||
|
||||
@@ -128,7 +128,8 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
/*value_found*/ nullptr,
|
||||
/*key_exists*/ nullptr, /*seq*/ nullptr, &read_cb,
|
||||
/*is_blob*/ nullptr,
|
||||
/*do_merge=*/get_impl_options.get_value);
|
||||
/*do_merge=*/get_impl_options.get_value,
|
||||
get_impl_options.lazy_columns_same_file_reader);
|
||||
RecordTick(stats_, MEMTABLE_MISS);
|
||||
}
|
||||
{
|
||||
@@ -156,6 +157,17 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
PERF_COUNTER_ADD(get_read_bytes, size);
|
||||
}
|
||||
}
|
||||
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy): hand back the Version the entity's blob
|
||||
// references must be resolved against, and transfer a SuperVersion pin into
|
||||
// the result so it -- and deferred blob reads -- stay valid after this call
|
||||
// (as an iterator's pin does). Read-only borrows the current SuperVersion
|
||||
// without a reference, so the pin takes its own.
|
||||
if (get_impl_options.lazy_columns_version != nullptr) {
|
||||
*get_impl_options.lazy_columns_version = super_version->current;
|
||||
}
|
||||
TransferSuperVersionPin(super_version, get_impl_options.lazy_columns_pin);
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
|
||||
|
||||
@@ -1362,9 +1362,6 @@ Status DB::OpenAndCompact(
|
||||
db_options.statistics = override_options.statistics;
|
||||
db_options.listeners = override_options.listeners;
|
||||
db_options.compaction_service = nullptr;
|
||||
// We will close the DB after the compaction anyway.
|
||||
// Open as many files as needed for the compaction.
|
||||
db_options.max_open_files = -1;
|
||||
db_options.info_log = override_options.info_log;
|
||||
|
||||
// 4. Filter CFs that are needed for OpenAndCompact()
|
||||
|
||||
@@ -185,11 +185,22 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
/*value_found*/ nullptr,
|
||||
/*key_exists*/ nullptr, /*seq*/ nullptr, &read_cb,
|
||||
/*is_blob*/ nullptr,
|
||||
/*do_merge=*/get_impl_options.get_value);
|
||||
/*do_merge=*/get_impl_options.get_value,
|
||||
get_impl_options.lazy_columns_same_file_reader);
|
||||
RecordTick(stats_, MEMTABLE_MISS);
|
||||
}
|
||||
{
|
||||
PERF_TIMER_GUARD(get_post_process_time);
|
||||
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy): hand back the resolution Version and take
|
||||
// a SuperVersion pin for the result before the borrowed reference below
|
||||
// is released, so the result -- and deferred blob reads -- stay valid
|
||||
// after this call (as an iterator's pin does).
|
||||
if (get_impl_options.lazy_columns_version != nullptr) {
|
||||
*get_impl_options.lazy_columns_version = super_version->current;
|
||||
}
|
||||
TransferSuperVersionPin(super_version, get_impl_options.lazy_columns_pin);
|
||||
}
|
||||
#if defined(WITH_COROUTINES)
|
||||
CleanupSuperVersion(super_version);
|
||||
#else
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
#include "util/coro_utils.h"
|
||||
|
||||
#if defined(USE_COROUTINES) && defined(WITH_COROUTINES)
|
||||
#include "folly/executors/IOExecutor.h"
|
||||
#include "folly/io/async/EventBase.h"
|
||||
#include "util/coro_stats_util.h"
|
||||
#endif // USE_COROUTINES && WITH_COROUTINES
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
@@ -26,6 +25,10 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::Get)
|
||||
(const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value, std::string* timestamp) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
assert(value != nullptr);
|
||||
value->Reset();
|
||||
|
||||
@@ -41,18 +44,8 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::Get)
|
||||
read_options.io_activity = Env::IOActivity::kGet;
|
||||
}
|
||||
|
||||
#ifdef WITH_COROUTINES
|
||||
auto* read_executor = immutable_db_options_.fs->GetReadExecutor();
|
||||
if (read_executor != nullptr) {
|
||||
auto* read_event_base = read_executor->getEventBase();
|
||||
assert(read_event_base != nullptr);
|
||||
Status s = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base),
|
||||
GetImplCoroutine(read_options, column_family, key, value, timestamp)));
|
||||
co_return s;
|
||||
}
|
||||
#endif
|
||||
CO_RETURN GetImpl(read_options, column_family, key, value, timestamp);
|
||||
CO_RETURN CO_AWAIT(GetImpl, read_options, column_family, key, value,
|
||||
timestamp);
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
@@ -249,8 +242,9 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
done = true;
|
||||
PostprocessDirectWriteValueRead(
|
||||
read_options, key, timestamp, resolve_direct_write_value,
|
||||
sv->current, cfd, get_impl_options.value, get_impl_options.columns,
|
||||
&s, &is_blob_index, get_impl_options.value_found);
|
||||
memtable_blob_fetcher_ptr, get_impl_options.value,
|
||||
get_impl_options.columns, &s, &is_blob_index,
|
||||
get_impl_options.value_found);
|
||||
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
@@ -265,8 +259,9 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
done = true;
|
||||
PostprocessDirectWriteValueRead(
|
||||
read_options, key, timestamp, resolve_direct_write_value,
|
||||
sv->current, cfd, get_impl_options.value, get_impl_options.columns,
|
||||
&s, &is_blob_index, get_impl_options.value_found);
|
||||
memtable_blob_fetcher_ptr, get_impl_options.value,
|
||||
get_impl_options.columns, &s, &is_blob_index,
|
||||
get_impl_options.value_found);
|
||||
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
}
|
||||
@@ -306,14 +301,16 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
nullptr, nullptr,
|
||||
get_impl_options.get_value ? get_impl_options.callback : nullptr,
|
||||
get_impl_options.get_value ? is_blob_ptr : nullptr,
|
||||
get_impl_options.get_value);
|
||||
get_impl_options.get_value,
|
||||
get_impl_options.lazy_columns_same_file_reader);
|
||||
if (get_impl_options.get_value && resolve_direct_write_value) {
|
||||
assert(memtable_blob_fetcher_ptr != nullptr);
|
||||
std::string blob_lookup_key_storage;
|
||||
MaybeResolveDirectWriteValue(
|
||||
read_options,
|
||||
GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage),
|
||||
resolve_direct_write_value, sv->current, cfd, get_impl_options.value,
|
||||
get_impl_options.columns, &s, &is_blob_index,
|
||||
resolve_direct_write_value, *memtable_blob_fetcher_ptr,
|
||||
get_impl_options.value, get_impl_options.columns, &s, &is_blob_index,
|
||||
get_impl_options.value_found);
|
||||
}
|
||||
RecordTick(stats_, MEMTABLE_MISS);
|
||||
@@ -406,6 +403,19 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
PERF_COUNTER_ADD(get_read_bytes, size);
|
||||
}
|
||||
|
||||
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy): the entity's blob references were left
|
||||
// unresolved. Hand back the Version they must be resolved against and
|
||||
// transfer an extra SuperVersion reference into the result's pin, so the
|
||||
// result -- and thus deferred blob reads -- stay valid after this call
|
||||
// returns (as an iterator's SuperVersion pin does). The reference is
|
||||
// released when the pin's cleanup runs (result destruction); the
|
||||
// borrowed reference for this call is dropped by the sv_cleanup Defer.
|
||||
if (get_impl_options.lazy_columns_version != nullptr) {
|
||||
*get_impl_options.lazy_columns_version = sv->current;
|
||||
}
|
||||
TransferSuperVersionPin(sv, get_impl_options.lazy_columns_pin);
|
||||
}
|
||||
RecordInHistogram(stats_, BYTES_PER_READ, size);
|
||||
}
|
||||
CO_RETURN s;
|
||||
@@ -573,12 +583,13 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::MultiGetImpl)
|
||||
key->is_blob_index = false;
|
||||
*key->s = Status::Aborted();
|
||||
} else {
|
||||
assert(memtable_blob_fetcher_ptr != nullptr);
|
||||
std::string blob_lookup_key_storage;
|
||||
MaybeResolveDirectWriteValue(
|
||||
read_options,
|
||||
GetBlobLookupUserKey(*key->key, key->timestamp,
|
||||
&blob_lookup_key_storage),
|
||||
/*resolve_direct_write_value=*/true, super_version->current, cfd,
|
||||
/*resolve_direct_write_value=*/true, *memtable_blob_fetcher_ptr,
|
||||
key->value, key->columns, key->s, &key->is_blob_index);
|
||||
}
|
||||
}
|
||||
@@ -790,6 +801,10 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGet)
|
||||
(const ReadOptions& _read_options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys, PinnableSlice* values,
|
||||
std::string* timestamps, Status* statuses, const bool sorted_input) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kMultiGet) {
|
||||
Status s = Status::InvalidArgument(
|
||||
@@ -806,21 +821,10 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGet)
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kMultiGet;
|
||||
}
|
||||
#ifdef WITH_COROUTINES
|
||||
auto* read_executor = immutable_db_options_.fs->GetReadExecutor();
|
||||
if (read_executor != nullptr) {
|
||||
auto* read_event_base = read_executor->getEventBase();
|
||||
assert(read_event_base != nullptr);
|
||||
co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base),
|
||||
MultiGetCommonCoroutine(
|
||||
read_options, num_keys, column_families, keys, values,
|
||||
/* columns */ nullptr, timestamps, statuses, sorted_input)));
|
||||
co_return;
|
||||
}
|
||||
#endif
|
||||
MultiGetCommon(read_options, num_keys, column_families, keys, values,
|
||||
/* columns */ nullptr, timestamps, statuses, sorted_input);
|
||||
CO_AWAIT(MultiGetCommon, read_options, num_keys, column_families, keys,
|
||||
values,
|
||||
/* columns */ nullptr, timestamps, statuses, sorted_input);
|
||||
CO_RETURN;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
+4
-3
@@ -26,6 +26,7 @@
|
||||
#include "logging/logging.h"
|
||||
#include "memory/arena.h"
|
||||
#include "monitoring/perf_context_imp.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "port/likely.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/io_dispatcher.h"
|
||||
@@ -78,8 +79,8 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
|
||||
merge_operator_(ioptions.merge_operator.get()),
|
||||
iter_(iter),
|
||||
// Enable the direct-write blob fallback only when this CF has a
|
||||
// write-path partition manager; otherwise the fetcher stays on the plain
|
||||
// Version::GetBlob fast path.
|
||||
// write-path partition manager; otherwise the fetcher stays on the
|
||||
// version-backed path.
|
||||
blob_state_(version, read_options, cfd ? cfd->blob_file_cache() : nullptr,
|
||||
cfd != nullptr && cfd->blob_partition_manager() != nullptr),
|
||||
read_callback_(read_callback),
|
||||
@@ -122,7 +123,7 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
|
||||
// prefix_same_as_start do not guarantee complete scans, so conversion
|
||||
// must stay disabled for the iterator lifetime.
|
||||
min_tombstones_for_range_conversion_(
|
||||
active_mem != nullptr && !read_options.table_filter &&
|
||||
active_mem != nullptr && !HasTableFilter(read_options) &&
|
||||
(expect_total_order_inner_iter_ || prefix_same_as_start_) &&
|
||||
HasFullTimestampVisibility(read_options)
|
||||
? mutable_cf_options.min_tombstones_for_range_conversion
|
||||
|
||||
+18
-16
@@ -3230,16 +3230,18 @@ TEST_P(DBIteratorTest, TableFilter) {
|
||||
{
|
||||
std::set<uint64_t> unseen{1, 2, 3};
|
||||
ReadOptions opts;
|
||||
opts.table_filter = [&](const TableProperties& props) {
|
||||
auto it = unseen.find(props.num_entries);
|
||||
if (it == unseen.end()) {
|
||||
ADD_FAILURE() << "saw table properties with an unexpected "
|
||||
<< props.num_entries << " entries";
|
||||
} else {
|
||||
unseen.erase(it);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
std::function<bool(const TableProperties&)> filter =
|
||||
[&](const TableProperties& props) {
|
||||
auto it = unseen.find(props.num_entries);
|
||||
if (it == unseen.end()) {
|
||||
ADD_FAILURE() << "saw table properties with an unexpected "
|
||||
<< props.num_entries << " entries";
|
||||
} else {
|
||||
unseen.erase(it);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
opts.table_filter = &filter;
|
||||
auto iter = NewIterator(opts);
|
||||
iter->SeekToFirst();
|
||||
ASSERT_EQ(IterStatus(iter), "a->1");
|
||||
@@ -3264,9 +3266,9 @@ TEST_P(DBIteratorTest, TableFilter) {
|
||||
// during iteration.
|
||||
{
|
||||
ReadOptions opts;
|
||||
opts.table_filter = [](const TableProperties& props) {
|
||||
return props.num_entries != 2;
|
||||
};
|
||||
std::function<bool(const TableProperties&)> filter =
|
||||
[](const TableProperties& props) { return props.num_entries != 2; };
|
||||
opts.table_filter = &filter;
|
||||
auto iter = NewIterator(opts);
|
||||
iter->SeekToFirst();
|
||||
ASSERT_EQ(IterStatus(iter), "a->1");
|
||||
@@ -7151,9 +7153,9 @@ TEST_P(ReadPathRangeTombstoneTest, TableFilterNotAllowed) {
|
||||
|
||||
{
|
||||
ReadOptions filtered_ro;
|
||||
filtered_ro.table_filter = [](const TableProperties& props) {
|
||||
return props.num_entries != 2;
|
||||
};
|
||||
std::function<bool(const TableProperties&)> filter =
|
||||
[](const TableProperties& props) { return props.num_entries != 2; };
|
||||
filtered_ro.table_filter = &filter;
|
||||
// Hiding the two-delete SST would otherwise leave this iterator with a
|
||||
// partial SST view plus the previously converted memtable tombstone,
|
||||
// allowing hidden SST state to affect the filtered read result.
|
||||
|
||||
+104
-28
@@ -18,14 +18,12 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class DBTestXactLogIterator : public DBTestBase {
|
||||
class DBWalIteratorTest : public DBTestBase {
|
||||
public:
|
||||
DBTestXactLogIterator()
|
||||
: DBTestBase("db_log_iter_test", /*env_do_fsync=*/true) {}
|
||||
DBWalIteratorTest() : DBTestBase("db_log_iter_test", /*env_do_fsync=*/true) {}
|
||||
|
||||
std::unique_ptr<TransactionLogIterator> OpenTransactionLogIter(
|
||||
const SequenceNumber seq) {
|
||||
std::unique_ptr<TransactionLogIterator> iter;
|
||||
std::unique_ptr<WalIterator> OpenWalIter(const SequenceNumber seq) {
|
||||
std::unique_ptr<WalIterator> iter;
|
||||
Status status = dbfull()->GetUpdatesSince(seq, &iter);
|
||||
EXPECT_OK(status);
|
||||
EXPECT_TRUE(iter->Valid());
|
||||
@@ -34,8 +32,8 @@ class DBTestXactLogIterator : public DBTestBase {
|
||||
};
|
||||
|
||||
namespace {
|
||||
SequenceNumber ReadRecords(std::unique_ptr<TransactionLogIterator>& iter,
|
||||
int& count, bool expect_ok = true) {
|
||||
SequenceNumber ReadRecords(std::unique_ptr<WalIterator>& iter, int& count,
|
||||
bool expect_ok = true) {
|
||||
count = 0;
|
||||
SequenceNumber lastSequence = 0;
|
||||
BatchResult res;
|
||||
@@ -56,14 +54,14 @@ SequenceNumber ReadRecords(std::unique_ptr<TransactionLogIterator>& iter,
|
||||
}
|
||||
|
||||
void ExpectRecords(const int expected_no_records,
|
||||
std::unique_ptr<TransactionLogIterator>& iter) {
|
||||
std::unique_ptr<WalIterator>& iter) {
|
||||
int num_records;
|
||||
ReadRecords(iter, num_records);
|
||||
ASSERT_EQ(num_records, expected_no_records);
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
TEST_F(DBTestXactLogIterator, TransactionLogIterator) {
|
||||
TEST_F(DBWalIteratorTest, Basic) {
|
||||
do {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
@@ -73,7 +71,7 @@ TEST_F(DBTestXactLogIterator, TransactionLogIterator) {
|
||||
ASSERT_OK(Put(1, "key2", DummyString(1024)));
|
||||
ASSERT_EQ(dbfull()->GetLatestSequenceNumber(), 3U);
|
||||
{
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
ExpectRecords(3, iter);
|
||||
}
|
||||
ReopenWithColumnFamilies({"default", "pikachu"}, options);
|
||||
@@ -84,14 +82,14 @@ TEST_F(DBTestXactLogIterator, TransactionLogIterator) {
|
||||
ASSERT_OK(Put(0, "key6", DummyString(1024)));
|
||||
}
|
||||
{
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
ExpectRecords(6, iter);
|
||||
}
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
#ifndef NDEBUG // sync point is not included with DNDEBUG build
|
||||
TEST_F(DBTestXactLogIterator, TransactionLogIteratorRace) {
|
||||
TEST_F(DBWalIteratorTest, Race) {
|
||||
static const int LOG_ITERATOR_RACE_TEST_COUNT = 2;
|
||||
static const char* sync_points[LOG_ITERATOR_RACE_TEST_COUNT][4] = {
|
||||
{"WalManager::GetSortedWalFiles:1", "WalManager::PurgeObsoleteFiles:1",
|
||||
@@ -123,7 +121,7 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorRace) {
|
||||
ASSERT_OK(dbfull()->FlushWAL(false));
|
||||
|
||||
{
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
ExpectRecords(4, iter);
|
||||
}
|
||||
|
||||
@@ -140,14 +138,14 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorRace) {
|
||||
ASSERT_OK(dbfull()->FlushWAL(false));
|
||||
{
|
||||
// this iter would miss "key4" if not fixed
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
ExpectRecords(5, iter);
|
||||
}
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBTestXactLogIterator, TransactionLogIteratorCheckWhenArchive) {
|
||||
TEST_F(DBWalIteratorTest, CheckWhenArchive) {
|
||||
RelaxedAtomic<bool> callback_hit{};
|
||||
do {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearTrace();
|
||||
@@ -173,7 +171,7 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorCheckWhenArchive) {
|
||||
callback_hit.StoreRelaxed(false);
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"WalManager::PurgeObsoleteFiles:1", [&](void*) {
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
ExpectRecords(4, iter);
|
||||
callback_hit.StoreRelaxed(true);
|
||||
});
|
||||
@@ -192,12 +190,12 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorCheckWhenArchive) {
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_F(DBTestXactLogIterator, TransactionLogIteratorStallAtLastRecord) {
|
||||
TEST_F(DBWalIteratorTest, StallAtLastRecord) {
|
||||
do {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("key1", DummyString(1024)));
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
iter->Next();
|
||||
@@ -210,7 +208,7 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorStallAtLastRecord) {
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBTestXactLogIterator, TransactionLogIteratorCheckAfterRestart) {
|
||||
TEST_F(DBWalIteratorTest, CheckAfterRestart) {
|
||||
do {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
@@ -218,12 +216,12 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorCheckAfterRestart) {
|
||||
ASSERT_OK(Put("key2", DummyString(1023)));
|
||||
ASSERT_OK(dbfull()->Flush(FlushOptions()));
|
||||
Reopen(options);
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
ExpectRecords(2, iter);
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBTestXactLogIterator, TransactionLogIteratorCorruptedLog) {
|
||||
TEST_F(DBWalIteratorTest, CorruptedLog) {
|
||||
do {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
@@ -254,18 +252,18 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorCorruptedLog) {
|
||||
|
||||
// Try to read from the beginning. Should stop before the gap and read less
|
||||
// than 1025 entries
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
int count = 0;
|
||||
SequenceNumber last_sequence_read = ReadRecords(iter, count, false);
|
||||
ASSERT_LT(last_sequence_read, 1025U);
|
||||
|
||||
// Try to read past the gap, should be able to seek to key1025
|
||||
auto iter2 = OpenTransactionLogIter(last_sequence_read + 1);
|
||||
auto iter2 = OpenWalIter(last_sequence_read + 1);
|
||||
ExpectRecords(1, iter2);
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBTestXactLogIterator, TransactionLogIteratorBatchOperations) {
|
||||
TEST_F(DBWalIteratorTest, BatchOperations) {
|
||||
do {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
@@ -280,12 +278,12 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorBatchOperations) {
|
||||
ASSERT_OK(Flush(0));
|
||||
ReopenWithColumnFamilies({"default", "pikachu"}, options);
|
||||
ASSERT_OK(Put(1, "key4", DummyString(1024)));
|
||||
auto iter = OpenTransactionLogIter(3);
|
||||
auto iter = OpenWalIter(3);
|
||||
ExpectRecords(2, iter);
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBTestXactLogIterator, TransactionLogIteratorBlobs) {
|
||||
TEST_F(DBWalIteratorTest, Blobs) {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
@@ -301,7 +299,7 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorBlobs) {
|
||||
ReopenWithColumnFamilies({"default", "pikachu"}, options);
|
||||
}
|
||||
|
||||
auto res = OpenTransactionLogIter(0)->GetBatch();
|
||||
auto res = OpenWalIter(0)->GetBatch();
|
||||
struct Handler : public WriteBatch::Handler {
|
||||
std::string seen;
|
||||
Status PutCF(uint32_t cf, const Slice& key, const Slice& value) override {
|
||||
@@ -332,6 +330,84 @@ TEST_F(DBTestXactLogIterator, TransactionLogIteratorBlobs) {
|
||||
"Delete(0, key2)",
|
||||
handler.seen);
|
||||
}
|
||||
|
||||
// The tests below pin the semantics documented on WalIterator and
|
||||
// DB::GetUpdatesSince, including the sharp edges, so that changing any of
|
||||
// them is a deliberate and visible decision rather than an accident.
|
||||
|
||||
// A caught-up iterator is usable, not finished: Next() is how a consumer
|
||||
// tails a DB.
|
||||
TEST_F(DBWalIteratorTest, CaughtUpThenResumes) {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("key1", DummyString(128)));
|
||||
|
||||
auto iter = OpenWalIter(0);
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
iter->Next();
|
||||
|
||||
// Caught up: !Valid(), but status() is OK and the iterator is not spent.
|
||||
ASSERT_TRUE(!iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
// A later write to the same WAL is picked up by calling Next() again,
|
||||
// without rebuilding the iterator.
|
||||
ASSERT_OK(Put("key2", DummyString(128)));
|
||||
iter->Next();
|
||||
ASSERT_TRUE(iter->Valid()) << iter->status().ToString();
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_EQ(2U, iter->GetBatch().sequence);
|
||||
}
|
||||
|
||||
// Writes that bypass the WAL still consume sequence numbers, so they leave
|
||||
// holes that this API surfaces as the end of the run.
|
||||
TEST_F(DBWalIteratorTest, GapFromDisableWAL) {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
WriteOptions no_wal;
|
||||
no_wal.disableWAL = true;
|
||||
ASSERT_OK(Put("key1", DummyString(128))); // seq 1, in the WAL
|
||||
ASSERT_OK(Put("key2", DummyString(128), no_wal)); // seq 2, not in the WAL
|
||||
ASSERT_OK(Put("key3", DummyString(128))); // seq 3, in the WAL
|
||||
|
||||
auto iter = OpenWalIter(0);
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(1U, iter->GetBatch().sequence);
|
||||
|
||||
// The run stops at the hole rather than silently jumping to seq 3.
|
||||
iter->Next();
|
||||
ASSERT_TRUE(!iter->Valid());
|
||||
ASSERT_TRUE(iter->status().IsNotFound()) << iter->status().ToString();
|
||||
|
||||
// And the iterator is spent: further Next() calls do nothing and the
|
||||
// status never changes.
|
||||
iter->Next();
|
||||
iter->Next();
|
||||
ASSERT_TRUE(!iter->Valid());
|
||||
ASSERT_TRUE(iter->status().IsNotFound());
|
||||
}
|
||||
|
||||
// GetUpdatesSince is permissive about its starting point: if the requested
|
||||
// sequence number is not in the WAL it silently starts later and still
|
||||
// reports OK. This is the path by which a consumer recovering from a spent
|
||||
// iterator can lose data without noticing.
|
||||
TEST_F(DBWalIteratorTest, SilentlySkipsUnavailableStart) {
|
||||
Options options = OptionsForLogIterTest();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
WriteOptions no_wal;
|
||||
no_wal.disableWAL = true;
|
||||
ASSERT_OK(Put("key1", DummyString(128), no_wal)); // seq 1, not in the WAL
|
||||
ASSERT_OK(Put("key2", DummyString(128))); // seq 2, in the WAL
|
||||
|
||||
std::unique_ptr<WalIterator> iter;
|
||||
ASSERT_OK(dbfull()->GetUpdatesSince(1, &iter));
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
// Asked for seq 1, silently given seq 2, with no error anywhere.
|
||||
ASSERT_EQ(2U, iter->GetBatch().sequence);
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -566,6 +566,25 @@ TEST_F(DBOptionsTest, SetOptionsAndReopen) {
|
||||
ASSERT_OK(TryReopen(options));
|
||||
}
|
||||
|
||||
TEST_F(DBOptionsTest, SetBlobFileWritableFileMaxBufferSize) {
|
||||
constexpr uint64_t kBlobWriterBufferSize = 128 * 1024;
|
||||
|
||||
Options options;
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 0;
|
||||
Reopen(options);
|
||||
|
||||
ASSERT_EQ(db_->GetOptions().blob_file_writable_file_max_buffer_size, 0U);
|
||||
|
||||
ASSERT_OK(db_->SetOptions({{"blob_file_writable_file_max_buffer_size",
|
||||
std::to_string(kBlobWriterBufferSize)}}));
|
||||
|
||||
ASSERT_EQ(db_->GetOptions().blob_file_writable_file_max_buffer_size,
|
||||
kBlobWriterBufferSize);
|
||||
}
|
||||
|
||||
TEST_F(DBOptionsTest, EnableAutoCompactionAndTriggerStall) {
|
||||
const std::string kValue(1024, 'v');
|
||||
for (int method_type = 0; method_type < 2; method_type++) {
|
||||
|
||||
@@ -1148,6 +1148,60 @@ TEST_F(DBPropertiesTest, EstimatePendingCompBytes) {
|
||||
ASSERT_EQ(int_num, 0U);
|
||||
}
|
||||
|
||||
TEST_F(DBPropertiesTest, NumUnscheduledCompactions) {
|
||||
if (env_ == nullptr) {
|
||||
ADD_FAILURE() << "Missing test environment";
|
||||
return;
|
||||
}
|
||||
auto& env = *env_;
|
||||
env.SetBackgroundThreads(1, Env::HIGH);
|
||||
env.SetBackgroundThreads(1, Env::LOW);
|
||||
|
||||
Options options = CurrentOptions();
|
||||
WriteOptions writeOpt = WriteOptions();
|
||||
writeOpt.disableWAL = true;
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.level0_file_num_compaction_trigger = 2;
|
||||
options.max_background_compactions = 1;
|
||||
options.max_background_flushes = 1;
|
||||
options.write_buffer_size = 1000000;
|
||||
DestroyAndReopen(options);
|
||||
CreateAndReopenWithCF({"one"}, options);
|
||||
|
||||
uint64_t int_num;
|
||||
|
||||
ASSERT_TRUE(dbfull()->GetIntProperty(
|
||||
DB::Properties::kNumUnscheduledCompactions, &int_num));
|
||||
ASSERT_EQ(int_num, 0U);
|
||||
|
||||
// Keep one scheduled compaction from completing.
|
||||
test::SleepingBackgroundTask sleeping_task_low;
|
||||
env.Schedule(&test::SleepingBackgroundTask::DoSleepTask, &sleeping_task_low,
|
||||
Env::Priority::LOW);
|
||||
sleeping_task_low.WaitUntilSleeping();
|
||||
|
||||
std::string big_value(1000000 * 2, 'x');
|
||||
for (int cf = 0; cf < 2; cf++) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
ASSERT_OK(dbfull()->Put(writeOpt, handles_[cf], "k" + std::to_string(i),
|
||||
big_value));
|
||||
ASSERT_OK(Flush(cf));
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_TRUE(dbfull()->GetIntProperty(
|
||||
DB::Properties::kNumUnscheduledCompactions, &int_num));
|
||||
ASSERT_GT(int_num, 0U);
|
||||
|
||||
sleeping_task_low.WakeUp();
|
||||
sleeping_task_low.WaitUntilDone();
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
ASSERT_TRUE(dbfull()->GetIntProperty(
|
||||
DB::Properties::kNumUnscheduledCompactions, &int_num));
|
||||
ASSERT_EQ(int_num, 0U);
|
||||
}
|
||||
|
||||
TEST_F(DBPropertiesTest, EstimateCompressionRatio) {
|
||||
if (!Snappy_Supported()) {
|
||||
return;
|
||||
|
||||
@@ -749,8 +749,7 @@ TEST_F(DBSecondaryTest, OptionsOverrideTest) {
|
||||
"blob_compaction_readahead_size=4194304;"
|
||||
"some_invalid_option=ignore_me;"
|
||||
"env=this_should_not_fail;"
|
||||
"max_open_files=100;", // this should be always overriden as
|
||||
// -1 in remote compaction
|
||||
"max_open_files=100;",
|
||||
&override_options.options_map));
|
||||
|
||||
bool verified = false;
|
||||
@@ -761,7 +760,7 @@ TEST_F(DBSecondaryTest, OptionsOverrideTest) {
|
||||
auto secondary_db_options = secondary_db->GetOptions();
|
||||
// DBOption
|
||||
ASSERT_EQ(secondary_db_options.compaction_readahead_size, 8388608);
|
||||
ASSERT_EQ(secondary_db_options.max_open_files, -1);
|
||||
ASSERT_EQ(secondary_db_options.max_open_files, 100);
|
||||
// CFOption
|
||||
ASSERT_EQ(secondary_db_options.blob_compaction_readahead_size, 4194304);
|
||||
verified = true;
|
||||
|
||||
@@ -701,6 +701,44 @@ TEST_F(DBSSTTest, DBWithSstFileManagerForBlobFilesWithGC) {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(DBSSTTest, SstFileManagerTracksMultipleCompactionBlobOutputs) {
|
||||
constexpr uint64_t kBlobFileSize = 1024;
|
||||
constexpr size_t kValueSize = 600;
|
||||
|
||||
std::shared_ptr<SstFileManager> sst_file_manager(NewSstFileManager(env_));
|
||||
auto* sfm = static_cast<SstFileManagerImpl*>(sst_file_manager.get());
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.sst_file_manager = sst_file_manager;
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 0;
|
||||
options.blob_file_size = kBlobFileSize;
|
||||
options.blob_compression_type = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.enable_blob_garbage_collection = true;
|
||||
options.blob_garbage_collection_age_cutoff = 1.0;
|
||||
options.blob_garbage_collection_force_threshold = 0.0;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string value(kValueSize, 'a');
|
||||
ASSERT_OK(Put("key1", value));
|
||||
ASSERT_OK(Put("key3", value));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("key2", value));
|
||||
ASSERT_OK(Put("key4", value));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
sfm->WaitForEmptyTrash();
|
||||
ASSERT_EQ(2U, GetBlobFileNumbers().size());
|
||||
|
||||
std::unordered_map<std::string, uint64_t> files_in_db;
|
||||
ASSERT_OK(GetAllDataFiles(kTableFile, &files_in_db));
|
||||
ASSERT_OK(GetAllDataFiles(kBlobFile, &files_in_db));
|
||||
ASSERT_EQ(files_in_db, sfm->GetTrackedFiles());
|
||||
}
|
||||
|
||||
class DBSSTTestRateLimit : public DBSSTTest,
|
||||
public ::testing::WithParamInterface<bool> {
|
||||
public:
|
||||
|
||||
+6
-5
@@ -3725,6 +3725,8 @@ class ModelDB : public DB {
|
||||
void DisableManualCompaction() override {}
|
||||
void AbortAllCompactions() override {}
|
||||
void ResumeAllCompactions() override {}
|
||||
void AbortCompactions(ColumnFamilyHandle* /*column_family*/) override {}
|
||||
void ResumeCompactions(ColumnFamilyHandle* /*column_family*/) override {}
|
||||
|
||||
Status WaitForCompact(
|
||||
const WaitForCompactOptions& /* wait_for_compact_options */) override {
|
||||
@@ -3804,11 +3806,10 @@ class ModelDB : public DB {
|
||||
return Status::NotSupported();
|
||||
}
|
||||
|
||||
Status GetUpdatesSince(
|
||||
ROCKSDB_NAMESPACE::SequenceNumber,
|
||||
std::unique_ptr<ROCKSDB_NAMESPACE::TransactionLogIterator>*,
|
||||
const TransactionLogIterator::ReadOptions& /*read_options*/ =
|
||||
TransactionLogIterator::ReadOptions()) override {
|
||||
Status GetUpdatesSince(ROCKSDB_NAMESPACE::SequenceNumber,
|
||||
std::unique_ptr<ROCKSDB_NAMESPACE::WalIterator>*,
|
||||
const WalIterator::ReadOptions& /*read_options*/ =
|
||||
WalIterator::ReadOptions()) override {
|
||||
return Status::NotSupported("Not supported in Model DB");
|
||||
}
|
||||
|
||||
|
||||
+16
-12
@@ -25,6 +25,7 @@
|
||||
|
||||
#if USE_COROUTINES
|
||||
#include "folly/coro/BlockingWait.h"
|
||||
#include "rocksdb/coro_db.h"
|
||||
#endif // USE_COROUTINES
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -872,8 +873,8 @@ std::string DBTestBase::Get(const std::string& k, const Snapshot* snapshot,
|
||||
#if USE_COROUTINES
|
||||
if (use_coroutine) {
|
||||
PinnableSlice pinnable_value(&result);
|
||||
s = folly::coro::blockingWait(dbfull()->GetCoroutine(
|
||||
options, dbfull()->DefaultColumnFamily(), k, &pinnable_value,
|
||||
s = folly::coro::blockingWait(CoroDB::CoGet(
|
||||
dbfull(), options, dbfull()->DefaultColumnFamily(), k, &pinnable_value,
|
||||
/*timestamp=*/nullptr));
|
||||
if (s.ok() && pinnable_value.IsPinned()) {
|
||||
result.assign(pinnable_value.data(), pinnable_value.size());
|
||||
@@ -903,8 +904,9 @@ std::string DBTestBase::Get(int cf, const std::string& k,
|
||||
#if USE_COROUTINES
|
||||
if (use_coroutine) {
|
||||
PinnableSlice pinnable_value(&result);
|
||||
s = folly::coro::blockingWait(dbfull()->GetCoroutine(
|
||||
options, handles_[cf], k, &pinnable_value, /*timestamp=*/nullptr));
|
||||
s = folly::coro::blockingWait(CoroDB::CoGet(dbfull(), options, handles_[cf],
|
||||
k, &pinnable_value,
|
||||
/*timestamp=*/nullptr));
|
||||
if (s.ok() && pinnable_value.IsPinned()) {
|
||||
result.assign(pinnable_value.data(), pinnable_value.size());
|
||||
}
|
||||
@@ -956,9 +958,10 @@ std::vector<std::string> DBTestBase::MultiGet(std::vector<int> cfs,
|
||||
s.resize(cfs.size());
|
||||
#if USE_COROUTINES
|
||||
if (use_coroutine) {
|
||||
folly::coro::blockingWait(dbfull()->MultiGetCoroutine(
|
||||
options, cfs.size(), handles.data(), keys.data(), pin_values.data(),
|
||||
/*timestamps=*/nullptr, s.data(), /*sorted_input=*/false));
|
||||
folly::coro::blockingWait(CoroDB::CoMultiGet(
|
||||
dbfull(), options, cfs.size(), handles.data(), keys.data(),
|
||||
pin_values.data(), /*timestamps=*/nullptr, s.data(),
|
||||
/*sorted_input=*/false));
|
||||
} else
|
||||
#else
|
||||
(void)use_coroutine;
|
||||
@@ -1005,9 +1008,10 @@ std::vector<std::string> DBTestBase::MultiGet(const std::vector<std::string>& k,
|
||||
if (use_coroutine) {
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(),
|
||||
dbfull()->DefaultColumnFamily());
|
||||
folly::coro::blockingWait(dbfull()->MultiGetCoroutine(
|
||||
options, keys.size(), cfs.data(), keys.data(), pin_values.data(),
|
||||
/*timestamps=*/nullptr, statuses.data(), /*sorted_input=*/false));
|
||||
folly::coro::blockingWait(CoroDB::CoMultiGet(
|
||||
dbfull(), options, keys.size(), cfs.data(), keys.data(),
|
||||
pin_values.data(), /*timestamps=*/nullptr, statuses.data(),
|
||||
/*sorted_input=*/false));
|
||||
} else
|
||||
#else
|
||||
(void)use_coroutine;
|
||||
@@ -1038,8 +1042,8 @@ Status DBTestBase::Get(const std::string& k, PinnableSlice* v,
|
||||
#if USE_COROUTINES
|
||||
if (use_coroutine) {
|
||||
return folly::coro::blockingWait(
|
||||
dbfull()->GetCoroutine(options, dbfull()->DefaultColumnFamily(), k, v,
|
||||
/*timestamp=*/nullptr));
|
||||
CoroDB::CoGet(dbfull(), options, dbfull()->DefaultColumnFamily(), k, v,
|
||||
/*timestamp=*/nullptr));
|
||||
}
|
||||
#else
|
||||
(void)use_coroutine;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "test_util/testutil.h"
|
||||
|
||||
@@ -2415,6 +2415,253 @@ TEST_F(ExternalSSTFileTest, SstFileWriterNonSharedKeys) {
|
||||
ASSERT_OK(DeprecatedAddFile({file_path}));
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, IngestFlushRequestNotLostBehindQueuedFlush) {
|
||||
// These waits only prevent broken test paths from hanging forever. Keep them
|
||||
// large enough to avoid treating slow CI scheduling as a regression.
|
||||
constexpr uint64_t kHangGuardMicros = 30 * 1000 * 1000;
|
||||
constexpr uint64_t kSetupWaitMicros = kHangGuardMicros;
|
||||
constexpr uint64_t kIngestWaitMicros = kHangGuardMicros;
|
||||
constexpr uint64_t kPollMicros = 1000;
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.atomic_flush = false;
|
||||
options.disable_auto_compactions = true;
|
||||
options.write_buffer_size = 1024;
|
||||
options.max_write_buffer_number = 8;
|
||||
options.min_write_buffer_number_to_merge = 2;
|
||||
env_->SetBackgroundThreads(1, Env::HIGH);
|
||||
Reopen(options);
|
||||
|
||||
std::string external_file_path;
|
||||
std::vector<std::pair<std::string, std::string>> external_file_data = {
|
||||
{"ingest_key", "ingested_value"}};
|
||||
ASSERT_OK(GenerateOneExternalFile(options, nullptr, external_file_data, -1,
|
||||
true /* sort_data */, &external_file_path,
|
||||
nullptr /* true_data */));
|
||||
|
||||
auto sleeping_task = std::make_shared<test::SleepingBackgroundTask>();
|
||||
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask,
|
||||
sleeping_task.get(), Env::Priority::HIGH);
|
||||
sleeping_task->WaitUntilSleeping();
|
||||
|
||||
std::atomic<int> atomic_flush_scheduled{0};
|
||||
std::atomic<int> get_live_files_done{0};
|
||||
std::atomic<bool> ingest_waiting_for_flush{false};
|
||||
std::atomic<bool> ingest_done{false};
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::AtomicFlushMemTables:AfterScheduleFlush", [&](void*) {
|
||||
atomic_flush_scheduled.fetch_add(1, std::memory_order_acq_rel);
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::FlushMemTable:BeforeWaitForBgFlush", [&](void*) {
|
||||
ingest_waiting_for_flush.store(true, std::memory_order_release);
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
auto wait_until = [&](const std::function<bool()>& predicate,
|
||||
uint64_t timeout_micros) {
|
||||
for (uint64_t waited_micros = 0; waited_micros < timeout_micros;
|
||||
waited_micros += kPollMicros) {
|
||||
if (predicate()) {
|
||||
return true;
|
||||
}
|
||||
env_->SleepForMicroseconds(kPollMicros);
|
||||
}
|
||||
return predicate();
|
||||
};
|
||||
|
||||
auto wake_sleeping_task = [&] {
|
||||
if (!sleeping_task->WokenUp()) {
|
||||
sleeping_task->WakeUp();
|
||||
}
|
||||
return !sleeping_task->TimedWaitUntilDone(kSetupWaitMicros);
|
||||
};
|
||||
|
||||
auto wait_or_cancel_background_work =
|
||||
[&](const std::function<bool()>& predicate, uint64_t timeout_micros) {
|
||||
if (wait_until(predicate, timeout_micros)) {
|
||||
return true;
|
||||
}
|
||||
dbfull()->CancelAllBackgroundWork(false /* wait */);
|
||||
if (wait_until(predicate, timeout_micros)) {
|
||||
return true;
|
||||
}
|
||||
dbfull()->CancelAllBackgroundWork(true /* wait */);
|
||||
return wait_until(predicate, timeout_micros);
|
||||
};
|
||||
|
||||
ASSERT_OK(Put("atomic_flush_key", "value"));
|
||||
|
||||
Status get_live_files_status[2];
|
||||
std::vector<LiveFileStorageInfo> live_files[2];
|
||||
ThreadGuard get_live_files_thread_1(port::Thread([&] {
|
||||
LiveFilesStorageInfoOptions lfsi_opts;
|
||||
lfsi_opts.wal_size_for_flush = 0;
|
||||
lfsi_opts.atomic_flush = true;
|
||||
get_live_files_status[0] =
|
||||
db_->GetLiveFilesStorageInfo(lfsi_opts, &live_files[0]);
|
||||
get_live_files_done.fetch_add(1, std::memory_order_acq_rel);
|
||||
}));
|
||||
ThreadGuard get_live_files_thread_2(port::Thread([&] {
|
||||
LiveFilesStorageInfoOptions lfsi_opts;
|
||||
lfsi_opts.wal_size_for_flush = 0;
|
||||
lfsi_opts.atomic_flush = true;
|
||||
get_live_files_status[1] =
|
||||
db_->GetLiveFilesStorageInfo(lfsi_opts, &live_files[1]);
|
||||
get_live_files_done.fetch_add(1, std::memory_order_acq_rel);
|
||||
}));
|
||||
ThreadGuard ingest_thread;
|
||||
|
||||
auto get_live_files_finished = [&] {
|
||||
return get_live_files_done.load(std::memory_order_acquire) == 2;
|
||||
};
|
||||
auto ingest_finished = [&] {
|
||||
return ingest_done.load(std::memory_order_acquire);
|
||||
};
|
||||
auto wait_for_get_live_files_finished = [&] {
|
||||
return wait_or_cancel_background_work(get_live_files_finished,
|
||||
kSetupWaitMicros);
|
||||
};
|
||||
auto wait_for_ingest_finished = [&](uint64_t timeout_micros) {
|
||||
return wait_or_cancel_background_work(ingest_finished, timeout_micros);
|
||||
};
|
||||
auto join_get_live_files_threads = [&] {
|
||||
if (get_live_files_thread_1.GetThread().joinable()) {
|
||||
get_live_files_thread_1.GetThread().join();
|
||||
}
|
||||
if (get_live_files_thread_2.GetThread().joinable()) {
|
||||
get_live_files_thread_2.GetThread().join();
|
||||
}
|
||||
};
|
||||
auto join_ingest_thread = [&] {
|
||||
if (ingest_thread.GetThread().joinable()) {
|
||||
ingest_thread.GetThread().join();
|
||||
}
|
||||
};
|
||||
|
||||
class CleanupGuard {
|
||||
public:
|
||||
explicit CleanupGuard(const std::function<void()>& cleanup)
|
||||
: cleanup_(cleanup) {}
|
||||
|
||||
~CleanupGuard() {
|
||||
if (armed_) {
|
||||
cleanup_();
|
||||
}
|
||||
}
|
||||
|
||||
void Disarm() { armed_ = false; }
|
||||
|
||||
private:
|
||||
std::function<void()> cleanup_;
|
||||
bool armed_ = true;
|
||||
};
|
||||
|
||||
CleanupGuard cleanup_guard([&] {
|
||||
wake_sleeping_task();
|
||||
wait_for_get_live_files_finished();
|
||||
if (ingest_thread.GetThread().joinable()) {
|
||||
wait_for_ingest_finished(kSetupWaitMicros);
|
||||
}
|
||||
join_get_live_files_threads();
|
||||
join_ingest_thread();
|
||||
});
|
||||
|
||||
const bool saw_both_atomic_flushes = wait_until(
|
||||
[&] {
|
||||
return atomic_flush_scheduled.load(std::memory_order_acquire) >= 2;
|
||||
},
|
||||
kSetupWaitMicros);
|
||||
|
||||
if (!saw_both_atomic_flushes) {
|
||||
const bool sleeping_task_done = wake_sleeping_task();
|
||||
const bool get_live_files_finished_after_cleanup =
|
||||
wait_for_get_live_files_finished();
|
||||
join_get_live_files_threads();
|
||||
EXPECT_OK(get_live_files_status[0]);
|
||||
EXPECT_OK(get_live_files_status[1]);
|
||||
EXPECT_TRUE(get_live_files_finished_after_cleanup);
|
||||
ASSERT_TRUE(saw_both_atomic_flushes)
|
||||
<< "Timed out waiting for both GetLiveFilesStorageInfo calls to "
|
||||
"schedule their flushes. scheduled="
|
||||
<< atomic_flush_scheduled.load(std::memory_order_acquire)
|
||||
<< " get_live_files_done="
|
||||
<< get_live_files_done.load(std::memory_order_acquire)
|
||||
<< " sleeping_task_done=" << sleeping_task_done;
|
||||
}
|
||||
|
||||
ASSERT_OK(Put("ingest_key", "memtable_value"));
|
||||
|
||||
Status ingest_status;
|
||||
ingest_thread = ThreadGuard(port::Thread([&] {
|
||||
IngestExternalFileOptions ifo;
|
||||
ingest_status = db_->IngestExternalFile({external_file_path}, ifo);
|
||||
ingest_done.store(true, std::memory_order_release);
|
||||
}));
|
||||
|
||||
const bool saw_ingest_wait = wait_until(
|
||||
[&] {
|
||||
return ingest_waiting_for_flush.load(std::memory_order_acquire) ||
|
||||
ingest_done.load(std::memory_order_acquire);
|
||||
},
|
||||
kSetupWaitMicros);
|
||||
const bool ingest_finished_before_wait =
|
||||
ingest_done.load(std::memory_order_acquire);
|
||||
|
||||
if (!saw_ingest_wait || ingest_finished_before_wait) {
|
||||
const bool sleeping_task_done = wake_sleeping_task();
|
||||
const bool get_live_files_finished_after_cleanup =
|
||||
wait_for_get_live_files_finished();
|
||||
join_get_live_files_threads();
|
||||
const bool ingest_finished_after_cleanup =
|
||||
wait_for_ingest_finished(kSetupWaitMicros);
|
||||
join_ingest_thread();
|
||||
EXPECT_OK(get_live_files_status[0]);
|
||||
EXPECT_OK(get_live_files_status[1]);
|
||||
EXPECT_OK(ingest_status);
|
||||
EXPECT_TRUE(get_live_files_finished_after_cleanup);
|
||||
EXPECT_TRUE(ingest_finished_after_cleanup);
|
||||
ASSERT_TRUE(sleeping_task_done);
|
||||
ASSERT_TRUE(saw_ingest_wait)
|
||||
<< "Timed out waiting for IngestExternalFile to enter its flush wait";
|
||||
ASSERT_FALSE(ingest_finished_before_wait)
|
||||
<< "IngestExternalFile finished before entering the expected flush "
|
||||
"wait";
|
||||
}
|
||||
|
||||
const bool sleeping_task_done = wake_sleeping_task();
|
||||
join_get_live_files_threads();
|
||||
|
||||
const bool ingest_completed_without_unblock =
|
||||
wait_until(ingest_finished, kIngestWaitMicros);
|
||||
|
||||
if (!ingest_completed_without_unblock) {
|
||||
FlushOptions unblock_flush_options;
|
||||
unblock_flush_options.wait = false;
|
||||
const Status unblock_flush_status = db_->Flush(unblock_flush_options);
|
||||
const bool ingest_finished_after_unblock =
|
||||
wait_for_ingest_finished(kIngestWaitMicros);
|
||||
join_ingest_thread();
|
||||
ASSERT_TRUE(sleeping_task_done);
|
||||
ASSERT_OK(get_live_files_status[0]);
|
||||
ASSERT_OK(get_live_files_status[1]);
|
||||
ASSERT_OK(unblock_flush_status);
|
||||
ASSERT_TRUE(ingest_finished_after_unblock);
|
||||
FAIL() << "IngestExternalFile remained blocked waiting for a flush that "
|
||||
"was deduped behind an older queued flush request";
|
||||
}
|
||||
|
||||
join_ingest_thread();
|
||||
ASSERT_TRUE(sleeping_task_done);
|
||||
ASSERT_OK(get_live_files_status[0]);
|
||||
ASSERT_OK(get_live_files_status[1]);
|
||||
ASSERT_OK(ingest_status);
|
||||
ASSERT_EQ("ingested_value", Get("ingest_key"));
|
||||
cleanup_guard.Disarm();
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, WithUnorderedWrite) {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->LoadDependency(
|
||||
|
||||
+2
-1
@@ -1042,7 +1042,8 @@ Status FlushJob::WriteLevel0Table() {
|
||||
meta_.fd.GetNumber(),
|
||||
preclude_last_level_min_seqno_ == kMaxSequenceNumber
|
||||
? preclude_last_level_min_seqno_
|
||||
: std::min(earliest_snapshot_, preclude_last_level_min_seqno_));
|
||||
: std::min(earliest_snapshot_, preclude_last_level_min_seqno_),
|
||||
dbname_ /*db_name*/);
|
||||
s = BuildTable(
|
||||
dbname_, versions_, db_options_, tboptions, file_options_,
|
||||
cfd_->table_cache(), iter.get(), std::move(range_del_iters), &meta_,
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/listener.h"
|
||||
#include "rocksdb/memtablerep.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
#include "util/autovector.h"
|
||||
#include "util/stop_watch.h"
|
||||
#include "util/thread_local.h"
|
||||
|
||||
@@ -308,9 +308,15 @@ static const std::string aggregated_table_properties =
|
||||
static const std::string aggregated_table_properties_at_level =
|
||||
aggregated_table_properties + "-at-level";
|
||||
static const std::string num_running_compactions = "num-running-compactions";
|
||||
static const std::string num_running_remote_compactions =
|
||||
"num-running-remote-compactions";
|
||||
static const std::string num_running_bottom_compactions =
|
||||
"num-running-bottom-compactions";
|
||||
static const std::string num_running_compaction_sorted_runs =
|
||||
"num-running-compaction-sorted-runs";
|
||||
static const std::string compaction_abort_count = "compaction-abort-count";
|
||||
static const std::string num_unscheduled_compactions =
|
||||
"num-unscheduled-compactions";
|
||||
static const std::string num_running_flushes = "num-running-flushes";
|
||||
static const std::string actual_delayed_write_rate =
|
||||
"actual-delayed-write-rate";
|
||||
@@ -361,10 +367,16 @@ const std::string DB::Properties::kCompactionPending =
|
||||
rocksdb_prefix + compaction_pending;
|
||||
const std::string DB::Properties::kNumRunningCompactions =
|
||||
rocksdb_prefix + num_running_compactions;
|
||||
const std::string DB::Properties::kNumRunningRemoteCompactions =
|
||||
rocksdb_prefix + num_running_remote_compactions;
|
||||
const std::string DB::Properties::kNumRunningBottomCompactions =
|
||||
rocksdb_prefix + num_running_bottom_compactions;
|
||||
const std::string DB::Properties::kNumRunningCompactionSortedRuns =
|
||||
rocksdb_prefix + num_running_compaction_sorted_runs;
|
||||
const std::string DB::Properties::kCompactionAbortCount =
|
||||
rocksdb_prefix + compaction_abort_count;
|
||||
const std::string DB::Properties::kNumUnscheduledCompactions =
|
||||
rocksdb_prefix + num_unscheduled_compactions;
|
||||
const std::string DB::Properties::kNumRunningFlushes =
|
||||
rocksdb_prefix + num_running_flushes;
|
||||
const std::string DB::Properties::kBackgroundErrors =
|
||||
@@ -594,12 +606,21 @@ const UnorderedMap<std::string, DBPropertyInfo>
|
||||
{DB::Properties::kNumRunningCompactions,
|
||||
{false, nullptr, &InternalStats::HandleNumRunningCompactions, nullptr,
|
||||
nullptr}},
|
||||
{DB::Properties::kNumRunningRemoteCompactions,
|
||||
{false, nullptr, &InternalStats::HandleNumRunningRemoteCompactions,
|
||||
nullptr, nullptr}},
|
||||
{DB::Properties::kNumRunningBottomCompactions,
|
||||
{false, nullptr, &InternalStats::HandleNumRunningBottomCompactions,
|
||||
nullptr, nullptr}},
|
||||
{DB::Properties::kNumRunningCompactionSortedRuns,
|
||||
{false, nullptr, &InternalStats::HandleNumRunningCompactionSortedRuns,
|
||||
nullptr, nullptr}},
|
||||
{DB::Properties::kCompactionAbortCount,
|
||||
{false, nullptr, &InternalStats::HandleCompactionAbortCount, nullptr,
|
||||
nullptr}},
|
||||
{DB::Properties::kNumUnscheduledCompactions,
|
||||
{false, nullptr, &InternalStats::HandleNumUnscheduledCompactions,
|
||||
nullptr, nullptr}},
|
||||
{DB::Properties::kActualDelayedWriteRate,
|
||||
{false, nullptr, &InternalStats::HandleActualDelayedWriteRate, nullptr,
|
||||
nullptr}},
|
||||
@@ -1286,6 +1307,23 @@ bool InternalStats::HandleNumRunningCompactions(uint64_t* value, DBImpl* db,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleNumRunningRemoteCompactions(uint64_t* value,
|
||||
DBImpl* db,
|
||||
Version* /*version*/) {
|
||||
const int num_running =
|
||||
db->num_running_remote_compactions_.load(std::memory_order_relaxed);
|
||||
assert(num_running >= 0);
|
||||
*value = static_cast<uint64_t>(num_running);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleNumRunningBottomCompactions(uint64_t* value,
|
||||
DBImpl* db,
|
||||
Version* /*version*/) {
|
||||
*value = static_cast<uint64_t>(db->num_running_bottom_compactions_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleNumRunningCompactionSortedRuns(uint64_t* value,
|
||||
DBImpl* db,
|
||||
Version* /*version*/) {
|
||||
@@ -1305,6 +1343,14 @@ bool InternalStats::HandleCompactionAbortCount(uint64_t* value, DBImpl* db,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleNumUnscheduledCompactions(uint64_t* value, DBImpl* db,
|
||||
Version* /*version*/) {
|
||||
db->mutex()->AssertHeld();
|
||||
assert(db->unscheduled_compactions_ >= 0);
|
||||
*value = static_cast<uint64_t>(db->unscheduled_compactions_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleBackgroundErrors(uint64_t* value, DBImpl* /*db*/,
|
||||
Version* /*version*/) {
|
||||
// Accumulated number of errors in background flushes or compactions.
|
||||
|
||||
@@ -850,10 +850,16 @@ class InternalStats {
|
||||
bool HandleCompactionPending(uint64_t* value, DBImpl* db, Version* version);
|
||||
bool HandleNumRunningCompactions(uint64_t* value, DBImpl* db,
|
||||
Version* version);
|
||||
bool HandleNumRunningRemoteCompactions(uint64_t* value, DBImpl* db,
|
||||
Version* version);
|
||||
bool HandleNumRunningBottomCompactions(uint64_t* value, DBImpl* db,
|
||||
Version* version);
|
||||
bool HandleNumRunningCompactionSortedRuns(uint64_t* value, DBImpl* db,
|
||||
Version* version);
|
||||
bool HandleCompactionAbortCount(uint64_t* value, DBImpl* db,
|
||||
Version* version);
|
||||
bool HandleNumUnscheduledCompactions(uint64_t* value, DBImpl* db,
|
||||
Version* version);
|
||||
bool HandleBackgroundErrors(uint64_t* value, DBImpl* db, Version* version);
|
||||
bool HandleCurSizeActiveMemTable(uint64_t* value, DBImpl* db,
|
||||
Version* version);
|
||||
|
||||
+6
-5
@@ -498,7 +498,6 @@ void MemTableList::PickMemtablesToFlush(uint64_t max_memtable_id,
|
||||
AutoThreadOperationStageUpdater stage_updater(
|
||||
ThreadStatus::STAGE_PICK_MEMTABLES_TO_FLUSH);
|
||||
const auto& memlist = current_->memlist_;
|
||||
bool atomic_flush = false;
|
||||
|
||||
// Note: every time MemTableList::Add(mem) is called, it adds the new mem
|
||||
// at the FRONT of the memlist (memlist.push_front(mem)). Therefore, by
|
||||
@@ -509,9 +508,6 @@ void MemTableList::PickMemtablesToFlush(uint64_t max_memtable_id,
|
||||
auto it = memlist.rbegin();
|
||||
for (; it != memlist.rend(); ++it) {
|
||||
ReadOnlyMemTable* m = *it;
|
||||
if (!atomic_flush && m->atomic_flush_seqno_ != kMaxSequenceNumber) {
|
||||
atomic_flush = true;
|
||||
}
|
||||
if (m->GetID() > max_memtable_id) {
|
||||
break;
|
||||
}
|
||||
@@ -542,7 +538,12 @@ void MemTableList::PickMemtablesToFlush(uint64_t max_memtable_id,
|
||||
// since they map to the same WAL and have the same NextLogNumber().
|
||||
assert(strcmp((*it)->Name(), "WBWIMemTable") != 0);
|
||||
}
|
||||
if (!atomic_flush || num_flush_not_started_ == 0) {
|
||||
// The flush request is complete only after every unstarted immutable
|
||||
// memtable has been picked. A request can be older than a newer immutable
|
||||
// memtable; clearing flush_requested_ after a partial or empty pick would
|
||||
// make the queued newer request look unnecessary and leave that memtable
|
||||
// unflushed.
|
||||
if (num_flush_not_started_ == 0) {
|
||||
flush_requested_ = false; // start-flush request is complete
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -402,11 +402,11 @@ class MemTableList {
|
||||
// Returns an estimate of the timestamp of the earliest key.
|
||||
uint64_t ApproximateOldestKeyTime() const;
|
||||
|
||||
// Request a flush of all existing memtables to storage. This will
|
||||
// cause future calls to IsFlushPending() to return true if this list is
|
||||
// non-empty (regardless of the min_write_buffer_number_to_merge
|
||||
// parameter). This flush request will persist until the next time
|
||||
// PickMemtablesToFlush() is called.
|
||||
// Request a flush of all existing memtables to storage. This will cause
|
||||
// future calls to IsFlushPending() to return true if this list is non-empty
|
||||
// (regardless of the min_write_buffer_number_to_merge parameter). This flush
|
||||
// request will persist until PickMemtablesToFlush() has picked all unstarted
|
||||
// memtables.
|
||||
void FlushRequested() {
|
||||
flush_requested_ = true;
|
||||
// If there are some memtables stored in imm() that don't trigger
|
||||
|
||||
@@ -890,8 +890,8 @@ TEST_F(MemTableListTest, FlushPendingTest) {
|
||||
ASSERT_TRUE(to_flush5.empty());
|
||||
ASSERT_EQ(1, list.NumNotFlushed());
|
||||
ASSERT_TRUE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
ASSERT_FALSE(list.HasFlushRequested());
|
||||
ASSERT_TRUE(list.IsFlushPending());
|
||||
ASSERT_TRUE(list.HasFlushRequested());
|
||||
|
||||
// Pick tables to flush. The tables to pick must have ID smaller than or
|
||||
// equal to 5. Therefore, only tables[5] will be selected.
|
||||
@@ -920,6 +920,52 @@ TEST_F(MemTableListTest, FlushPendingTest) {
|
||||
to_delete.clear();
|
||||
}
|
||||
|
||||
TEST_F(MemTableListTest, FlushRequestPersistsAfterPartialPick) {
|
||||
SequenceNumber seq = 1;
|
||||
|
||||
auto factory = std::make_shared<SkipListFactory>();
|
||||
options.memtable_factory = factory;
|
||||
ImmutableOptions ioptions(options);
|
||||
InternalKeyComparator cmp(BytewiseComparator());
|
||||
WriteBufferManager wb(options.db_write_buffer_size);
|
||||
MutableCFOptions mutable_cf_options(options);
|
||||
autovector<ReadOnlyMemTable*> to_delete;
|
||||
|
||||
MemTableList list(10 /* min_write_buffer_number_to_merge */,
|
||||
0 /* max_write_buffer_size_to_maintain */);
|
||||
|
||||
for (uint64_t memtable_id = 0; memtable_id < 2; ++memtable_id) {
|
||||
MemTable* mem = new MemTable(cmp, ioptions, mutable_cf_options, &wb,
|
||||
kMaxSequenceNumber, 0 /* column_family_id */);
|
||||
mem->SetID(memtable_id);
|
||||
mem->Ref();
|
||||
ASSERT_OK(mem->Add(++seq, kTypeValue, "key" + std::to_string(memtable_id),
|
||||
"value", nullptr /* kv_prot_info */));
|
||||
list.Add(mem, &to_delete);
|
||||
}
|
||||
|
||||
list.FlushRequested();
|
||||
ASSERT_TRUE(list.IsFlushPending());
|
||||
|
||||
autovector<ReadOnlyMemTable*> to_flush;
|
||||
list.PickMemtablesToFlush(0 /* max_memtable_id */, &to_flush);
|
||||
ASSERT_EQ(1, to_flush.size());
|
||||
ASSERT_TRUE(list.IsFlushPending());
|
||||
ASSERT_TRUE(list.HasFlushRequested());
|
||||
|
||||
autovector<ReadOnlyMemTable*> to_flush2;
|
||||
list.PickMemtablesToFlush(1 /* max_memtable_id */, &to_flush2);
|
||||
ASSERT_EQ(1, to_flush2.size());
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
ASSERT_FALSE(list.HasFlushRequested());
|
||||
|
||||
list.current()->Unref(&to_delete);
|
||||
ASSERT_EQ(2, to_delete.size());
|
||||
for (ReadOnlyMemTable* m : to_delete) {
|
||||
delete m;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MemTableListTest, EmptyAtomicFlushTest) {
|
||||
autovector<MemTableList*> lists;
|
||||
autovector<uint32_t> cf_ids;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "test_util/testutil.h"
|
||||
|
||||
+69
-66
@@ -112,44 +112,45 @@ TEST_F(PerfContextTest, CoroutineStatsContextScopeCollectsStats) {
|
||||
std::move(stats_config)]() mutable -> folly::coro::Task<void> {
|
||||
EXPECT_TRUE(event_base->isInEventBaseThread());
|
||||
|
||||
CoroutineStatsContextScope scope(std::move(stats_config),
|
||||
Env::Default());
|
||||
|
||||
EXPECT_EQ(PerfLevel::kEnableTimeAndCPUTimeExceptForMutex,
|
||||
GetPerfLevel());
|
||||
EXPECT_EQ(0, get_perf_context()->block_read_count);
|
||||
EXPECT_TRUE(get_perf_context()->per_level_perf_context_enabled);
|
||||
|
||||
get_perf_context()->block_read_count += 3;
|
||||
(*get_perf_context()->level_to_perf_context)[2].block_cache_hit_count +=
|
||||
5;
|
||||
EXPECT_EQ(0, get_iostats_context()->bytes_read);
|
||||
get_iostats_context()->bytes_read += 7;
|
||||
get_iostats_context()->cpu_read_nanos += 11;
|
||||
get_iostats_context()
|
||||
->file_io_stats_by_temperature.hot_file_read_count += 1;
|
||||
|
||||
{
|
||||
PERF_TIMER_GUARD_WITH_CLOCK(block_read_time, clock);
|
||||
clock->SleepForMicroseconds(7);
|
||||
CoroutineStatsContextScope scope(std::move(stats_config),
|
||||
Env::Default());
|
||||
|
||||
EXPECT_EQ(PerfLevel::kEnableTimeAndCPUTimeExceptForMutex,
|
||||
GetPerfLevel());
|
||||
EXPECT_EQ(0, get_perf_context()->block_read_count);
|
||||
EXPECT_TRUE(get_perf_context()->per_level_perf_context_enabled);
|
||||
|
||||
get_perf_context()->block_read_count += 3;
|
||||
(*get_perf_context()->level_to_perf_context)[2]
|
||||
.block_cache_hit_count += 5;
|
||||
EXPECT_EQ(0, get_iostats_context()->bytes_read);
|
||||
get_iostats_context()->bytes_read += 7;
|
||||
get_iostats_context()->cpu_read_nanos += 11;
|
||||
get_iostats_context()
|
||||
->file_io_stats_by_temperature.hot_file_read_count += 1;
|
||||
|
||||
{
|
||||
PERF_TIMER_GUARD_WITH_CLOCK(block_read_time, clock);
|
||||
clock->SleepForMicroseconds(7);
|
||||
}
|
||||
|
||||
co_await folly::coro::co_reschedule_on_current_executor;
|
||||
|
||||
EXPECT_TRUE(event_base->isInEventBaseThread());
|
||||
EXPECT_EQ(3, get_perf_context()->block_read_count);
|
||||
EXPECT_TRUE(get_perf_context()->per_level_perf_context_enabled);
|
||||
|
||||
get_perf_context()->block_read_count += 5;
|
||||
(*get_perf_context()->level_to_perf_context)[2]
|
||||
.block_cache_hit_count += 2;
|
||||
EXPECT_EQ(7, get_iostats_context()->bytes_read);
|
||||
get_iostats_context()->bytes_read += 11;
|
||||
get_iostats_context()->cpu_read_nanos += 13;
|
||||
get_iostats_context()
|
||||
->file_io_stats_by_temperature.hot_file_read_count += 2;
|
||||
}
|
||||
|
||||
co_await folly::coro::co_reschedule_on_current_executor;
|
||||
|
||||
EXPECT_TRUE(event_base->isInEventBaseThread());
|
||||
EXPECT_EQ(3, get_perf_context()->block_read_count);
|
||||
EXPECT_TRUE(get_perf_context()->per_level_perf_context_enabled);
|
||||
|
||||
get_perf_context()->block_read_count += 5;
|
||||
(*get_perf_context()->level_to_perf_context)[2].block_cache_hit_count +=
|
||||
2;
|
||||
EXPECT_EQ(7, get_iostats_context()->bytes_read);
|
||||
get_iostats_context()->bytes_read += 11;
|
||||
get_iostats_context()->cpu_read_nanos += 13;
|
||||
get_iostats_context()
|
||||
->file_io_stats_by_temperature.hot_file_read_count += 2;
|
||||
|
||||
scope.Finalize();
|
||||
auto verify_stats = [] {
|
||||
const PerfContext* captured_perf_context = get_perf_context();
|
||||
ASSERT_NE(nullptr, captured_perf_context);
|
||||
@@ -199,32 +200,33 @@ TEST_F(PerfContextTest, CoroutineStatsContextsRemainIsolatedWhenInterleaved) {
|
||||
const bool expected_per_level_perf_context_enabled =
|
||||
stats_config.per_level_perf_context_enabled;
|
||||
const bool expected_iostats_disabled = stats_config.iostats_disabled;
|
||||
CoroutineStatsContextScope scope(std::move(stats_config),
|
||||
Env::Default());
|
||||
{
|
||||
CoroutineStatsContextScope scope(std::move(stats_config),
|
||||
Env::Default());
|
||||
|
||||
EXPECT_EQ(expected_perf_level, GetPerfLevel());
|
||||
EXPECT_EQ(expected_per_level_perf_context_enabled,
|
||||
get_perf_context()->per_level_perf_context_enabled);
|
||||
EXPECT_EQ(expected_iostats_disabled,
|
||||
get_iostats_context()->disable_iostats);
|
||||
EXPECT_EQ(0, get_perf_context()->block_read_count);
|
||||
EXPECT_EQ(0, get_iostats_context()->bytes_read);
|
||||
EXPECT_EQ(expected_perf_level, GetPerfLevel());
|
||||
EXPECT_EQ(expected_per_level_perf_context_enabled,
|
||||
get_perf_context()->per_level_perf_context_enabled);
|
||||
EXPECT_EQ(expected_iostats_disabled,
|
||||
get_iostats_context()->disable_iostats);
|
||||
EXPECT_EQ(0, get_perf_context()->block_read_count);
|
||||
EXPECT_EQ(0, get_iostats_context()->bytes_read);
|
||||
|
||||
get_perf_context()->block_read_count += first;
|
||||
get_iostats_context()->bytes_read += first;
|
||||
co_await folly::coro::co_reschedule_on_current_executor;
|
||||
get_perf_context()->block_read_count += first;
|
||||
get_iostats_context()->bytes_read += first;
|
||||
co_await folly::coro::co_reschedule_on_current_executor;
|
||||
|
||||
EXPECT_EQ(expected_perf_level, GetPerfLevel());
|
||||
EXPECT_EQ(expected_per_level_perf_context_enabled,
|
||||
get_perf_context()->per_level_perf_context_enabled);
|
||||
EXPECT_EQ(expected_iostats_disabled,
|
||||
get_iostats_context()->disable_iostats);
|
||||
EXPECT_EQ(first, get_perf_context()->block_read_count);
|
||||
EXPECT_EQ(first, get_iostats_context()->bytes_read);
|
||||
get_perf_context()->block_read_count += second;
|
||||
get_iostats_context()->bytes_read += second;
|
||||
EXPECT_EQ(expected_perf_level, GetPerfLevel());
|
||||
EXPECT_EQ(expected_per_level_perf_context_enabled,
|
||||
get_perf_context()->per_level_perf_context_enabled);
|
||||
EXPECT_EQ(expected_iostats_disabled,
|
||||
get_iostats_context()->disable_iostats);
|
||||
EXPECT_EQ(first, get_perf_context()->block_read_count);
|
||||
EXPECT_EQ(first, get_iostats_context()->bytes_read);
|
||||
get_perf_context()->block_read_count += second;
|
||||
get_iostats_context()->bytes_read += second;
|
||||
}
|
||||
|
||||
scope.Finalize();
|
||||
EXPECT_EQ(first + second, get_iostats_context()->bytes_read);
|
||||
co_return get_perf_context()->block_read_count;
|
||||
};
|
||||
@@ -250,17 +252,18 @@ TEST_F(PerfContextTest, CoroutineStatsContextScopeCollectsGetCpuNanos) {
|
||||
CompositeEnvWrapper env(Env::Default(), clock);
|
||||
clock->SetCurrentCPUTimeNanos(0);
|
||||
{
|
||||
CoroutineStatsContextScope scope(CaptureCoroutineStatsConfig(), &env);
|
||||
|
||||
clock->MockSleepForCPUNanos(17);
|
||||
clock->MockSleepForCPUNanos(5);
|
||||
// Time while the request context is unset is not charged to the request.
|
||||
{
|
||||
folly::RequestContextScopeGuard context_scope;
|
||||
clock->MockSleepForCPUNanos(13);
|
||||
CoroutineStatsContextScope scope(CaptureCoroutineStatsConfig(), &env);
|
||||
|
||||
clock->MockSleepForCPUNanos(17);
|
||||
clock->MockSleepForCPUNanos(5);
|
||||
// Time while the request context is unset is not charged to the request.
|
||||
{
|
||||
folly::RequestContextScopeGuard context_scope;
|
||||
clock->MockSleepForCPUNanos(13);
|
||||
}
|
||||
clock->MockSleepForCPUNanos(7);
|
||||
}
|
||||
clock->MockSleepForCPUNanos(7);
|
||||
scope.Finalize();
|
||||
EXPECT_EQ(29, get_perf_context()->get_cpu_nanos);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -482,7 +482,9 @@ class Repairer {
|
||||
current_time /* newest_key_time */, false /* is_bottommost */,
|
||||
TableFileCreationReason::kRecovery, 0 /* oldest_key_time */,
|
||||
0 /* file_creation_time */, "DB Repairer" /* db_id */, db_session_id_,
|
||||
0 /*target_file_size*/, meta.fd.GetNumber());
|
||||
0 /*target_file_size*/, meta.fd.GetNumber(),
|
||||
kMaxSequenceNumber /*last_level_inclusive_max_seqno_threshold*/,
|
||||
dbname_ /*db_name*/);
|
||||
|
||||
SeqnoToTimeMapping empty_seqno_to_time_mapping;
|
||||
status = BuildTable(
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
#include "rocksdb/comparator.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
#include "table/unique_id_impl.h"
|
||||
#include "util/string_util.h"
|
||||
|
||||
|
||||
+3
-2
@@ -19,6 +19,7 @@
|
||||
#include "logging/logging.h"
|
||||
#include "monitoring/file_read_sample.h"
|
||||
#include "monitoring/perf_context_imp.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "rocksdb/advanced_options.h"
|
||||
#include "rocksdb/statistics.h"
|
||||
#include "table/block_based/block_based_table_reader.h"
|
||||
@@ -391,8 +392,8 @@ InternalIterator* TableCache::NewIterator(
|
||||
open_options);
|
||||
InternalIterator* result = nullptr;
|
||||
if (s.ok()) {
|
||||
if (options.table_filter &&
|
||||
!options.table_filter(*table_reader->GetTableProperties())) {
|
||||
if (HasTableFilter(options) &&
|
||||
!(*options.table_filter)(*table_reader->GetTableProperties())) {
|
||||
result = NewEmptyInternalIterator<Slice>(arena);
|
||||
} else {
|
||||
result = table_reader->NewIterator(
|
||||
|
||||
+163
-17
@@ -2770,23 +2770,6 @@ Version::Version(ColumnFamilyData* column_family_data, VersionSet* vset,
|
||||
}
|
||||
}
|
||||
|
||||
Status Version::GetBlob(const ReadOptions& read_options, const Slice& user_key,
|
||||
const Slice& blob_index_slice,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* value, uint64_t* bytes_read) const {
|
||||
BlobIndex blob_index;
|
||||
|
||||
{
|
||||
Status s = blob_index.DecodeFrom(blob_index_slice);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
return GetBlob(read_options, user_key, blob_index, prefetch_buffer, value,
|
||||
bytes_read);
|
||||
}
|
||||
|
||||
Status Version::GetBlob(const ReadOptions& read_options, const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
@@ -2820,6 +2803,40 @@ Status Version::GetBlob(const ReadOptions& read_options, const Slice& user_key,
|
||||
return s;
|
||||
}
|
||||
|
||||
Status Version::GetBlobRange(const ReadOptions& read_options,
|
||||
const Slice& user_key, const BlobIndex& blob_index,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
PinnableSlice* value, uint64_t* bytes_read) const {
|
||||
assert(value);
|
||||
|
||||
if (blob_index.HasTTL() || blob_index.IsInlined()) {
|
||||
return Status::Corruption("Unexpected TTL/inlined blob index");
|
||||
}
|
||||
|
||||
// A strict sub-range of a compressed blob cannot be decompressed in
|
||||
// isolation; the caller resolves such columns whole and slices instead.
|
||||
if (blob_index.compression() != kNoCompression) {
|
||||
return Status::Corruption("Cannot range-read a compressed blob");
|
||||
}
|
||||
|
||||
const uint64_t blob_file_number = blob_index.file_number();
|
||||
|
||||
auto blob_file_meta = storage_info_.GetBlobFileMetaData(blob_file_number);
|
||||
if (!blob_file_meta) {
|
||||
// INTEGRITY CHECK -- see Version::GetBlob. No metadata (including
|
||||
// file_number 0, the same-file "embedded" sentinel) means a corrupt index
|
||||
// or a same-file reference that the lazy caller must not route here.
|
||||
return Status::Corruption("Invalid blob file number");
|
||||
}
|
||||
|
||||
assert(blob_source_);
|
||||
value->Reset();
|
||||
return blob_source_->GetBlobRange(
|
||||
read_options, user_key, blob_file_number, blob_index.offset(),
|
||||
blob_file_meta->GetBlobFileSize(), blob_index.size(),
|
||||
blob_index.compression(), range_offset, range_length, value, bytes_read);
|
||||
}
|
||||
|
||||
void Version::MultiGetBlob(
|
||||
const ReadOptions& read_options, MultiGetRange& range,
|
||||
std::unordered_map<uint64_t, BlobReadContexts>& blob_ctxs) {
|
||||
@@ -6545,6 +6562,135 @@ Status VersionSet::ReopenManifestForAppend(const std::string& manifest_path) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status VersionSet::AppendColumnFamilyDropsToManifest(
|
||||
const std::string& manifest_path, uint64_t manifest_size,
|
||||
const std::vector<uint32_t>& cf_ids, const WriteOptions& write_options,
|
||||
uint64_t manifest_preallocation_size) {
|
||||
if (cf_ids.empty()) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
// Rewrite the checkpoint MANIFEST via a bounded-memory buffered copy into a
|
||||
// .tmp file (append drops, sync, close), then atomically rename over the
|
||||
// original. Avoids depending on FileSystem::ReopenWritableFile (optional,
|
||||
// unsupported on some backends) and avoids reading the whole MANIFEST into
|
||||
// memory (which can reach ~1GB depending on configuration).
|
||||
const std::string tmp_path = manifest_path + ".tmp";
|
||||
|
||||
// Best-effort cleanup of any stale .tmp left by a prior aborted run.
|
||||
fs_->DeleteFile(tmp_path, IOOptions(), /*dbg=*/nullptr)
|
||||
.PermitUncheckedError();
|
||||
|
||||
std::unique_ptr<FSSequentialFile> src_file;
|
||||
{
|
||||
IOStatus io_s = fs_->NewSequentialFile(manifest_path, FileOptions(),
|
||||
&src_file, /*dbg=*/nullptr);
|
||||
if (!io_s.ok()) {
|
||||
return io_s;
|
||||
}
|
||||
}
|
||||
|
||||
FileOptions opt_file_opts = GetFileOptionsForManifestWrite();
|
||||
// Buffered writes only: direct writes would need alignment / tail-pad
|
||||
// reasoning that isn't worth the complexity for a checkpoint MANIFEST.
|
||||
opt_file_opts.use_direct_writes = false;
|
||||
|
||||
std::unique_ptr<FSWritableFile> dst_file;
|
||||
{
|
||||
IOStatus io_s = fs_->NewWritableFile(tmp_path, opt_file_opts, &dst_file,
|
||||
/*dbg=*/nullptr);
|
||||
if (!io_s.ok()) {
|
||||
return io_s;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy exactly manifest_size bytes through a fixed-size buffer.
|
||||
constexpr size_t kBufSize = 1 * 1024 * 1024;
|
||||
std::string buffer(kBufSize, '\0');
|
||||
uint64_t remaining = manifest_size;
|
||||
while (remaining > 0) {
|
||||
Slice result;
|
||||
const size_t to_read =
|
||||
static_cast<size_t>(std::min<uint64_t>(kBufSize, remaining));
|
||||
IOStatus read_s = src_file->Read(to_read, opt_file_opts.io_options, &result,
|
||||
buffer.data(), /*dbg=*/nullptr);
|
||||
if (!read_s.ok()) {
|
||||
return read_s;
|
||||
}
|
||||
if (result.empty()) {
|
||||
return Status::Corruption(
|
||||
"Unexpected EOF reading source checkpoint MANIFEST");
|
||||
}
|
||||
IOStatus write_s = dst_file->Append(result, opt_file_opts.io_options,
|
||||
/*dbg=*/nullptr);
|
||||
if (!write_s.ok()) {
|
||||
return write_s;
|
||||
}
|
||||
remaining -= result.size();
|
||||
}
|
||||
src_file.reset();
|
||||
|
||||
// Seeds initial_block_offset = manifest_size % kBlockSize so drop-record
|
||||
// framing continues from where the copied contents left off, matching the
|
||||
// position-based log reader.
|
||||
std::unique_ptr<log::Writer> writer =
|
||||
CreateManifestWriter(std::move(dst_file), tmp_path, opt_file_opts,
|
||||
manifest_preallocation_size, manifest_size);
|
||||
|
||||
// Always attempt to Close the writer on any exit path, folding any Close
|
||||
// error into the primary status. Guards against dropping the underlying
|
||||
// FSWritableFile with buffered data / no filesystem close hook.
|
||||
Status s;
|
||||
auto close_writer = [&]() {
|
||||
if (writer == nullptr) {
|
||||
return;
|
||||
}
|
||||
Status close_s = writer->Close(write_options);
|
||||
if (s.ok() && !close_s.ok()) {
|
||||
s = close_s;
|
||||
}
|
||||
writer.reset();
|
||||
};
|
||||
|
||||
for (uint32_t cf_id : cf_ids) {
|
||||
VersionEdit edit;
|
||||
edit.SetColumnFamily(cf_id);
|
||||
edit.DropColumnFamily();
|
||||
std::string record;
|
||||
if (!edit.EncodeTo(&record)) {
|
||||
s = Status::Corruption(
|
||||
"Unable to encode column family drop VersionEdit for checkpoint");
|
||||
close_writer();
|
||||
return s;
|
||||
}
|
||||
IOStatus add_s = writer->AddRecord(write_options, record);
|
||||
if (!add_s.ok()) {
|
||||
s = add_s;
|
||||
close_writer();
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
IOStatus sync_s = SyncManifest(db_options_, write_options, writer->file());
|
||||
if (!sync_s.ok()) {
|
||||
s = sync_s;
|
||||
close_writer();
|
||||
return s;
|
||||
}
|
||||
close_writer();
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// Replace the original with the rewritten .tmp.
|
||||
IOStatus rename_s =
|
||||
fs_->RenameFile(tmp_path, manifest_path, IOOptions(), /*dbg=*/nullptr);
|
||||
if (!rename_s.ok()) {
|
||||
return rename_s;
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status VersionSet::Recover(
|
||||
const std::vector<ColumnFamilyDescriptor>& column_families, bool read_only,
|
||||
std::string* db_id, bool no_error_if_files_missing, bool is_retry,
|
||||
|
||||
+26
-10
@@ -72,6 +72,7 @@ class Compaction;
|
||||
class LogBuffer;
|
||||
class LookupKey;
|
||||
class MemTable;
|
||||
class SameFileBlobReader;
|
||||
class Version;
|
||||
class VersionSet;
|
||||
class WriteBufferManager;
|
||||
@@ -969,21 +970,13 @@ class Version {
|
||||
PinnedIteratorsManager* pinned_iters_mgr, bool* value_found = nullptr,
|
||||
bool* key_exists = nullptr, SequenceNumber* seq = nullptr,
|
||||
ReadCallback* callback = nullptr, bool* is_blob = nullptr,
|
||||
bool do_merge = true);
|
||||
bool do_merge = true,
|
||||
const SameFileBlobReader** lazy_columns_same_file_reader = nullptr);
|
||||
|
||||
DECLARE_SYNC_AND_ASYNC(void, MultiGet, const ReadOptions&,
|
||||
MultiGetRange* range,
|
||||
ReadCallback* callback = nullptr);
|
||||
|
||||
// Interprets blob_index_slice as a blob reference, and (assuming the
|
||||
// corresponding blob file is part of this Version) retrieves the blob and
|
||||
// saves it in *value.
|
||||
// REQUIRES: blob_index_slice stores an encoded blob reference
|
||||
Status GetBlob(const ReadOptions& read_options, const Slice& user_key,
|
||||
const Slice& blob_index_slice,
|
||||
FilePrefetchBuffer* prefetch_buffer, PinnableSlice* value,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// Retrieves a blob using a blob reference and saves it in *value,
|
||||
// assuming the corresponding blob file is part of this Version.
|
||||
Status GetBlob(const ReadOptions& read_options, const Slice& user_key,
|
||||
@@ -991,6 +984,19 @@ class Version {
|
||||
FilePrefetchBuffer* prefetch_buffer, PinnableSlice* value,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// Retrieves a byte sub-range [range_offset, range_offset + range_length) of
|
||||
// an *uncompressed*, separate-file blob's value into *value, reading only
|
||||
// those bytes on a cache miss (see BlobSource::GetBlobRange). The blob file
|
||||
// must be part of this Version. Returns Corruption for a
|
||||
// TTL/inlined/same-file or compressed blob index (the lazy caller only routes
|
||||
// uncompressed separate-file references here; other cases take the
|
||||
// whole-value GetBlob path). The caller must ensure range_offset +
|
||||
// range_length <= blob_index.size().
|
||||
Status GetBlobRange(const ReadOptions& read_options, const Slice& user_key,
|
||||
const BlobIndex& blob_index, uint64_t range_offset,
|
||||
size_t range_length, PinnableSlice* value,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
struct BlobReadContext {
|
||||
BlobReadContext(const BlobIndex& blob_idx, KeyContext* key_ctx)
|
||||
: blob_index(blob_idx), key_context(key_ctx) {}
|
||||
@@ -1684,6 +1690,16 @@ class VersionSet {
|
||||
return manifest_preallocation_size_;
|
||||
}
|
||||
|
||||
// Appends a kColumnFamilyDrop record for each id in cf_ids to the MANIFEST
|
||||
// file at manifest_path, whose valid content length is manifest_size bytes.
|
||||
// Intended for post-processing a checkpoint's copied MANIFEST so that column
|
||||
// families whose SST/blob files were not copied are recorded as dropped and
|
||||
// thus not opened during recovery.
|
||||
Status AppendColumnFamilyDropsToManifest(
|
||||
const std::string& manifest_path, uint64_t manifest_size,
|
||||
const std::vector<uint32_t>& cf_ids, const WriteOptions& write_options,
|
||||
uint64_t manifest_preallocation_size);
|
||||
|
||||
protected:
|
||||
struct ManifestWriter;
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ DEFINE_SYNC_AND_ASYNC(void, Version::Get)
|
||||
PinnableWideColumns* columns, std::string* timestamp, Status* status,
|
||||
MergeContext* merge_context, SequenceNumber* max_covering_tombstone_seq,
|
||||
PinnedIteratorsManager* pinned_iters_mgr, bool* value_found, bool* key_exists,
|
||||
SequenceNumber* seq, ReadCallback* callback, bool* is_blob, bool do_merge) {
|
||||
SequenceNumber* seq, ReadCallback* callback, bool* is_blob, bool do_merge,
|
||||
const SameFileBlobReader** lazy_columns_same_file_reader) {
|
||||
Slice ikey = k.internal_key();
|
||||
Slice user_key = k.user_key();
|
||||
|
||||
@@ -48,7 +49,7 @@ DEFINE_SYNC_AND_ASYNC(void, Version::Get)
|
||||
do_merge ? timestamp : nullptr, value_found, merge_context, do_merge,
|
||||
max_covering_tombstone_seq, clock_, seq,
|
||||
merge_operator_ ? pinned_iters_mgr : nullptr, callback, is_blob_to_use,
|
||||
tracing_get_id, &blob_fetcher);
|
||||
tracing_get_id, &blob_fetcher, lazy_columns_same_file_reader);
|
||||
|
||||
// Pin blocks that we read to hold merge operands
|
||||
if (merge_operator_) {
|
||||
@@ -143,8 +144,9 @@ DEFINE_SYNC_AND_ASYNC(void, Version::Get)
|
||||
|
||||
constexpr uint64_t* bytes_read = nullptr;
|
||||
|
||||
*status = GetBlob(read_options, get_context.ukey_to_get_blob_value(),
|
||||
blob_index, prefetch_buffer, &result, bytes_read);
|
||||
*status = blob_fetcher.FetchBlob(get_context.ukey_to_get_blob_value(),
|
||||
blob_index, prefetch_buffer, &result,
|
||||
bytes_read);
|
||||
if (!status->ok()) {
|
||||
if (status->IsIncomplete()) {
|
||||
get_context.MarkKeyMayExist();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "db/transaction_log_impl.h"
|
||||
#include "db/wal_iterator_impl.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
TransactionLogIteratorImpl::TransactionLogIteratorImpl(
|
||||
WalIteratorImpl::WalIteratorImpl(
|
||||
const std::string& dir, const ImmutableDBOptions* options,
|
||||
const TransactionLogIterator::ReadOptions& read_options,
|
||||
const EnvOptions& soptions, const SequenceNumber seq,
|
||||
std::unique_ptr<VectorWalPtr> files, VersionSet const* const versions,
|
||||
const bool seq_per_batch, const std::shared_ptr<IOTracer>& io_tracer)
|
||||
const WalIterator::ReadOptions& read_options, const EnvOptions& soptions,
|
||||
const SequenceNumber seq, std::unique_ptr<VectorWalPtr> files,
|
||||
VersionSet const* const versions, [[maybe_unused]] const bool seq_per_batch,
|
||||
const std::shared_ptr<IOTracer>& io_tracer)
|
||||
: dir_(dir),
|
||||
options_(options),
|
||||
read_options_(read_options),
|
||||
@@ -26,7 +26,6 @@ TransactionLogIteratorImpl::TransactionLogIteratorImpl(
|
||||
starting_sequence_number_(seq),
|
||||
files_(std::move(files)),
|
||||
versions_(versions),
|
||||
seq_per_batch_(seq_per_batch),
|
||||
io_tracer_(io_tracer),
|
||||
started_(false),
|
||||
is_valid_(false),
|
||||
@@ -35,14 +34,16 @@ TransactionLogIteratorImpl::TransactionLogIteratorImpl(
|
||||
current_last_seq_(0) {
|
||||
assert(files_ != nullptr);
|
||||
assert(versions_ != nullptr);
|
||||
assert(!seq_per_batch_);
|
||||
// WalManager::GetUpdatesSince() rejects seq_per_batch before we get here, so
|
||||
// one update always consumes exactly one sequence number below.
|
||||
assert(!seq_per_batch);
|
||||
current_status_.PermitUncheckedError(); // Clear on start
|
||||
reporter_.env = options_->env;
|
||||
reporter_.info_log = options_->info_log.get();
|
||||
SeekToStartSequence(); // Seek till starting sequence
|
||||
}
|
||||
|
||||
Status TransactionLogIteratorImpl::OpenLogFile(
|
||||
Status WalIteratorImpl::OpenLogFile(
|
||||
const WalFile* log_file,
|
||||
std::unique_ptr<SequentialFileReader>* file_reader) {
|
||||
FileSystemPtr fs(options_->fs, io_tracer_);
|
||||
@@ -71,7 +72,7 @@ Status TransactionLogIteratorImpl::OpenLogFile(
|
||||
return s;
|
||||
}
|
||||
|
||||
BatchResult TransactionLogIteratorImpl::GetBatch() {
|
||||
BatchResult WalIteratorImpl::GetBatch() {
|
||||
assert(is_valid_); // cannot call in a non valid state.
|
||||
BatchResult result;
|
||||
result.sequence = current_batch_seq_;
|
||||
@@ -79,11 +80,11 @@ BatchResult TransactionLogIteratorImpl::GetBatch() {
|
||||
return result;
|
||||
}
|
||||
|
||||
Status TransactionLogIteratorImpl::status() { return current_status_; }
|
||||
Status WalIteratorImpl::status() { return current_status_; }
|
||||
|
||||
bool TransactionLogIteratorImpl::Valid() { return started_ && is_valid_; }
|
||||
bool WalIteratorImpl::Valid() { return started_ && is_valid_; }
|
||||
|
||||
bool TransactionLogIteratorImpl::RestrictedRead(Slice* record) {
|
||||
bool WalIteratorImpl::RestrictedRead(Slice* record) {
|
||||
// Don't read if no more complete entries to read from logs
|
||||
if (current_last_seq_ >= versions_->LastSequence()) {
|
||||
return false;
|
||||
@@ -91,13 +92,12 @@ bool TransactionLogIteratorImpl::RestrictedRead(Slice* record) {
|
||||
return current_log_reader_->ReadRecord(record, &scratch_);
|
||||
}
|
||||
|
||||
void TransactionLogIteratorImpl::SeekToStartSequence(uint64_t start_file_index,
|
||||
bool strict) {
|
||||
void WalIteratorImpl::SeekToStartSequence(uint64_t start_file_index,
|
||||
bool strict) {
|
||||
Slice record;
|
||||
started_ = false;
|
||||
is_valid_ = false;
|
||||
// Check invariant of TransactionLogIterator when SeekToStartSequence()
|
||||
// succeeds.
|
||||
// Check invariant of WalIterator when SeekToStartSequence() succeeds.
|
||||
const Defer defer([this]() {
|
||||
if (is_valid_) {
|
||||
assert(current_status_.ok());
|
||||
@@ -110,6 +110,7 @@ void TransactionLogIteratorImpl::SeekToStartSequence(uint64_t start_file_index,
|
||||
if (files_->size() <= start_file_index) {
|
||||
return;
|
||||
} else if (!current_status_.ok()) {
|
||||
// Already spent; see the comment on current_status_.
|
||||
return;
|
||||
}
|
||||
Status s =
|
||||
@@ -166,14 +167,15 @@ void TransactionLogIteratorImpl::SeekToStartSequence(uint64_t start_file_index,
|
||||
}
|
||||
}
|
||||
|
||||
void TransactionLogIteratorImpl::Next() {
|
||||
void WalIteratorImpl::Next() {
|
||||
if (!current_status_.ok()) {
|
||||
// Spent; the run ended and cannot be resumed. See WalIterator docs.
|
||||
return;
|
||||
}
|
||||
return NextImpl(false);
|
||||
}
|
||||
|
||||
void TransactionLogIteratorImpl::NextImpl(bool internal) {
|
||||
void WalIteratorImpl::NextImpl(bool internal) {
|
||||
Slice record;
|
||||
is_valid_ = false;
|
||||
if (!internal && !started_) {
|
||||
@@ -215,8 +217,13 @@ void TransactionLogIteratorImpl::NextImpl(bool internal) {
|
||||
} else {
|
||||
is_valid_ = false;
|
||||
if (current_last_seq_ == versions_->LastSequence()) {
|
||||
// Caught up. Not an error: the caller may call Next() again later to
|
||||
// pick up writes that have not happened yet.
|
||||
current_status_ = Status::OK();
|
||||
} else {
|
||||
// The DB has moved on but this iterator's set of WAL files is
|
||||
// exhausted, typically because the WAL was rotated after the file
|
||||
// list was collected. The caller must build a new iterator.
|
||||
const char* msg = "Create a new iterator to fetch the new tail.";
|
||||
current_status_ = Status::TryAgain(msg);
|
||||
}
|
||||
@@ -225,16 +232,15 @@ void TransactionLogIteratorImpl::NextImpl(bool internal) {
|
||||
}
|
||||
}
|
||||
|
||||
bool TransactionLogIteratorImpl::IsBatchExpected(
|
||||
const WriteBatch* batch, const SequenceNumber expected_seq) {
|
||||
bool WalIteratorImpl::IsBatchExpected(const WriteBatch* batch,
|
||||
const SequenceNumber expected_seq) {
|
||||
assert(batch);
|
||||
SequenceNumber batchSeq = WriteBatchInternal::Sequence(batch);
|
||||
if (batchSeq != expected_seq) {
|
||||
std::ostringstream oss;
|
||||
oss << "Discontinuity in log records. " << "Got seq=" << batchSeq << ", "
|
||||
<< "Expected seq=" << expected_seq << ", "
|
||||
<< "Last flushed seq=" << versions_->LastSequence() << ". "
|
||||
<< "Log iterator will reseek the correct batch.";
|
||||
<< "Last flushed seq=" << versions_->LastSequence() << ".";
|
||||
|
||||
reporter_.Info(oss.str().c_str());
|
||||
return false;
|
||||
@@ -242,7 +248,7 @@ bool TransactionLogIteratorImpl::IsBatchExpected(
|
||||
return true;
|
||||
}
|
||||
|
||||
void TransactionLogIteratorImpl::UpdateCurrentWriteBatch(const Slice& record) {
|
||||
void WalIteratorImpl::UpdateCurrentWriteBatch(const Slice& record) {
|
||||
std::unique_ptr<WriteBatch> batch(new WriteBatch());
|
||||
Status s = WriteBatchInternal::SetContents(batch.get(), record);
|
||||
s.PermitUncheckedError(); // TODO: What should we do with this error?
|
||||
@@ -250,26 +256,15 @@ void TransactionLogIteratorImpl::UpdateCurrentWriteBatch(const Slice& record) {
|
||||
SequenceNumber expected_seq = current_last_seq_ + 1;
|
||||
// If the iterator has started, then confirm that we get continuous batches
|
||||
if (started_ && !IsBatchExpected(batch.get(), expected_seq)) {
|
||||
// Seek to the batch having expected sequence number
|
||||
if (expected_seq < files_->at(current_file_index_)->StartSequence()) {
|
||||
// Expected batch must lie in the previous log file
|
||||
// Avoid underflow.
|
||||
if (current_file_index_ != 0) {
|
||||
current_file_index_--;
|
||||
}
|
||||
}
|
||||
starting_sequence_number_ = expected_seq;
|
||||
// currentStatus_ will be set to Ok if reseek succeeds
|
||||
// Note: this is still ok in seq_pre_batch_ && two_write_queuesp_ mode
|
||||
// that allows gaps in the WAL since it will still skip over the gap.
|
||||
// A run is contiguous, so a discontinuity ends it. Note that this is a
|
||||
// normal consequence of writes that bypass the WAL (WriteOptions::
|
||||
// disableWAL, IngestExternalFile()), not necessarily of data loss.
|
||||
is_valid_ = false;
|
||||
current_status_ = Status::NotFound("Gap in sequence numbers");
|
||||
// In seq_per_batch_ mode, gaps in the seq are possible so the strict mode
|
||||
// should be disabled
|
||||
return SeekToStartSequence(current_file_index_, !seq_per_batch_);
|
||||
return;
|
||||
}
|
||||
|
||||
current_batch_seq_ = WriteBatchInternal::Sequence(batch.get());
|
||||
assert(!seq_per_batch_);
|
||||
current_last_seq_ =
|
||||
current_batch_seq_ + WriteBatchInternal::Count(batch.get()) - 1;
|
||||
// currentBatchSeq_ can only change here
|
||||
@@ -280,7 +275,7 @@ void TransactionLogIteratorImpl::UpdateCurrentWriteBatch(const Slice& record) {
|
||||
current_status_ = Status::OK();
|
||||
}
|
||||
|
||||
Status TransactionLogIteratorImpl::OpenLogReader(const WalFile* log_file) {
|
||||
Status WalIteratorImpl::OpenLogReader(const WalFile* log_file) {
|
||||
std::unique_ptr<SequentialFileReader> file;
|
||||
Status s = OpenLogFile(log_file, &file);
|
||||
if (!s.ok()) {
|
||||
@@ -14,8 +14,8 @@
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/types.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -54,14 +54,14 @@ class WalFileImpl : public WalFile {
|
||||
uint64_t sizeFileBytes_;
|
||||
};
|
||||
|
||||
class TransactionLogIteratorImpl : public TransactionLogIterator {
|
||||
class WalIteratorImpl : public WalIterator {
|
||||
public:
|
||||
TransactionLogIteratorImpl(
|
||||
const std::string& dir, const ImmutableDBOptions* options,
|
||||
const TransactionLogIterator::ReadOptions& read_options,
|
||||
const EnvOptions& soptions, const SequenceNumber seqNum,
|
||||
std::unique_ptr<VectorWalPtr> files, VersionSet const* const versions,
|
||||
const bool seq_per_batch, const std::shared_ptr<IOTracer>& io_tracer);
|
||||
WalIteratorImpl(const std::string& dir, const ImmutableDBOptions* options,
|
||||
const WalIterator::ReadOptions& read_options,
|
||||
const EnvOptions& soptions, const SequenceNumber seqNum,
|
||||
std::unique_ptr<VectorWalPtr> files,
|
||||
VersionSet const* const versions, const bool seq_per_batch,
|
||||
const std::shared_ptr<IOTracer>& io_tracer);
|
||||
|
||||
bool Valid() override;
|
||||
|
||||
@@ -74,19 +74,20 @@ class TransactionLogIteratorImpl : public TransactionLogIterator {
|
||||
private:
|
||||
const std::string& dir_;
|
||||
const ImmutableDBOptions* options_;
|
||||
const TransactionLogIterator::ReadOptions read_options_;
|
||||
const WalIterator::ReadOptions read_options_;
|
||||
const EnvOptions& soptions_;
|
||||
SequenceNumber starting_sequence_number_;
|
||||
std::unique_ptr<VectorWalPtr> files_;
|
||||
// Used only to get latest seq. num
|
||||
// TODO(icanadi) can this be just a callback?
|
||||
VersionSet const* const versions_;
|
||||
const bool seq_per_batch_;
|
||||
std::shared_ptr<IOTracer> io_tracer_;
|
||||
|
||||
// State variables
|
||||
bool started_;
|
||||
bool is_valid_; // not valid when it starts of.
|
||||
// Once this is set to a non-OK status the iterator is spent: Next() is a
|
||||
// no-op and the status never changes again.
|
||||
Status current_status_;
|
||||
size_t current_file_index_;
|
||||
std::unique_ptr<WriteBatch> current_batch_;
|
||||
@@ -126,4 +127,5 @@ class TransactionLogIteratorImpl : public TransactionLogIterator {
|
||||
void UpdateCurrentWriteBatch(const Slice& record);
|
||||
Status OpenLogReader(const WalFile* file);
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+14
-12
@@ -16,7 +16,7 @@
|
||||
|
||||
#include "db/log_reader.h"
|
||||
#include "db/log_writer.h"
|
||||
#include "db/transaction_log_impl.h"
|
||||
#include "db/wal_iterator_impl.h"
|
||||
#include "db/write_batch_internal.h"
|
||||
#include "file/file_util.h"
|
||||
#include "file/filename.h"
|
||||
@@ -58,7 +58,7 @@ Status WalManager::GetSortedWalFiles(VectorWalPtr& files, bool need_seqnos,
|
||||
|
||||
// Reproduce the race condition where a log file is moved
|
||||
// to archived dir, between these two sync points, used in
|
||||
// (DBTest,TransactionLogIteratorRace)
|
||||
// (DBWalIteratorTest,Race)
|
||||
TEST_SYNC_POINT("WalManager::GetSortedWalFiles:1");
|
||||
TEST_SYNC_POINT("WalManager::GetSortedWalFiles:2");
|
||||
|
||||
@@ -100,10 +100,10 @@ Status WalManager::GetSortedWalFiles(VectorWalPtr& files, bool need_seqnos,
|
||||
return s;
|
||||
}
|
||||
|
||||
Status WalManager::GetUpdatesSince(
|
||||
SequenceNumber seq, std::unique_ptr<TransactionLogIterator>* iter,
|
||||
const TransactionLogIterator::ReadOptions& read_options,
|
||||
VersionSet* version_set) {
|
||||
Status WalManager::GetUpdatesSince(SequenceNumber seq,
|
||||
std::unique_ptr<WalIterator>* iter,
|
||||
const WalIterator::ReadOptions& read_options,
|
||||
VersionSet* version_set) {
|
||||
if (seq_per_batch_) {
|
||||
return Status::NotSupported();
|
||||
}
|
||||
@@ -123,9 +123,9 @@ Status WalManager::GetUpdatesSince(
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
iter->reset(new TransactionLogIteratorImpl(
|
||||
wal_dir_, &db_options_, read_options, file_options_, seq,
|
||||
std::move(wal_files), version_set, seq_per_batch_, io_tracer_));
|
||||
iter->reset(new WalIteratorImpl(wal_dir_, &db_options_, read_options,
|
||||
file_options_, seq, std::move(wal_files),
|
||||
version_set, seq_per_batch_, io_tracer_));
|
||||
return (*iter)->status();
|
||||
}
|
||||
|
||||
@@ -286,11 +286,13 @@ void WalManager::PurgeObsoleteWALFiles() {
|
||||
|
||||
void WalManager::ArchiveWALFile(const std::string& fname, uint64_t number) {
|
||||
auto archived_log_name = ArchivedLogFileName(wal_dir_, number);
|
||||
// The sync point below is used in (DBTest,TransactionLogIteratorRace)
|
||||
// The sync point below is used in
|
||||
// (DBWalIteratorTest,Race)
|
||||
TEST_SYNC_POINT("WalManager::PurgeObsoleteFiles:1");
|
||||
Status s = env_->RenameFile(fname, archived_log_name);
|
||||
IGNORE_STATUS_IF_ERROR(s);
|
||||
// The sync point below is used in (DBTest,TransactionLogIteratorRace)
|
||||
// The sync point below is used in
|
||||
// (DBWalIteratorTest,Race)
|
||||
TEST_SYNC_POINT("WalManager::PurgeObsoleteFiles:2");
|
||||
// The sync point below is used in
|
||||
// (CheckPointTest, CheckpointWithArchievedLog)
|
||||
@@ -329,7 +331,7 @@ Status WalManager::GetSortedWalsOfType(const std::string& path,
|
||||
|
||||
// Reproduce the race condition where a log file is moved
|
||||
// to archived dir, between these two sync points, used in
|
||||
// (DBTest,TransactionLogIteratorRace)
|
||||
// (DBWalIteratorTest,Race)
|
||||
TEST_SYNC_POINT("WalManager::GetSortedWalsOfType:1");
|
||||
TEST_SYNC_POINT("WalManager::GetSortedWalsOfType:2");
|
||||
|
||||
|
||||
+6
-6
@@ -23,8 +23,8 @@
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/status.h"
|
||||
#include "rocksdb/transaction_log.h"
|
||||
#include "rocksdb/types.h"
|
||||
#include "rocksdb/wal_iterator.h"
|
||||
#include "util/atomic.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -51,12 +51,12 @@ class WalManager {
|
||||
Status GetSortedWalFiles(VectorWalPtr& files, bool need_seqnos = true,
|
||||
bool include_archived = true);
|
||||
|
||||
// Allow user to tail transaction log to find all recent changes to the
|
||||
// Allow user to tail the WAL to find all recent changes to the
|
||||
// database that are newer than `seq_number`.
|
||||
Status GetUpdatesSince(
|
||||
SequenceNumber seq_number, std::unique_ptr<TransactionLogIterator>* iter,
|
||||
const TransactionLogIterator::ReadOptions& read_options,
|
||||
VersionSet* version_set);
|
||||
Status GetUpdatesSince(SequenceNumber seq_number,
|
||||
std::unique_ptr<WalIterator>* iter,
|
||||
const WalIterator::ReadOptions& read_options,
|
||||
VersionSet* version_set);
|
||||
|
||||
void PurgeObsoleteWALFiles();
|
||||
|
||||
|
||||
+11
-12
@@ -105,11 +105,10 @@ class WalManagerTest : public testing::Test {
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<TransactionLogIterator> OpenTransactionLogIter(
|
||||
const SequenceNumber seq) {
|
||||
std::unique_ptr<TransactionLogIterator> iter;
|
||||
std::unique_ptr<WalIterator> OpenWalIter(const SequenceNumber seq) {
|
||||
std::unique_ptr<WalIterator> iter;
|
||||
Status status = wal_manager_->GetUpdatesSince(
|
||||
seq, &iter, TransactionLogIterator::ReadOptions(), versions_.get());
|
||||
seq, &iter, WalIterator::ReadOptions(), versions_.get());
|
||||
EXPECT_OK(status);
|
||||
return iter;
|
||||
}
|
||||
@@ -206,7 +205,7 @@ std::vector<std::uint64_t> ListSpecificFiles(
|
||||
return file_numbers;
|
||||
}
|
||||
|
||||
int CountRecords(TransactionLogIterator* iter) {
|
||||
int CountRecords(WalIterator* iter) {
|
||||
int count = 0;
|
||||
SequenceNumber lastSequence = 0;
|
||||
BatchResult res;
|
||||
@@ -320,7 +319,7 @@ TEST_F(WalManagerTest, WALArchivalTtlClockGoesBackwards) {
|
||||
ListSpecificFiles(env_.get(), archive_dir, kWalFile).size());
|
||||
}
|
||||
|
||||
TEST_F(WalManagerTest, TransactionLogIteratorMoveOverZeroFiles) {
|
||||
TEST_F(WalManagerTest, WalIteratorMoveOverZeroFiles) {
|
||||
Init(nullptr /* clock_override */);
|
||||
RollTheLog(false);
|
||||
Put("key1", std::string(1024, 'a'));
|
||||
@@ -330,22 +329,22 @@ TEST_F(WalManagerTest, TransactionLogIteratorMoveOverZeroFiles) {
|
||||
|
||||
Put("key2", std::string(1024, 'a'));
|
||||
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
ASSERT_EQ(2, CountRecords(iter.get()));
|
||||
}
|
||||
|
||||
TEST_F(WalManagerTest, TransactionLogIteratorJustEmptyFile) {
|
||||
TEST_F(WalManagerTest, WalIteratorJustEmptyFile) {
|
||||
Init(nullptr /* clock_override */);
|
||||
RollTheLog(false);
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
// Check that an empty iterator is returned
|
||||
ASSERT_TRUE(!iter->Valid());
|
||||
}
|
||||
|
||||
TEST_F(WalManagerTest, TransactionLogIteratorNewFileWhileScanning) {
|
||||
TEST_F(WalManagerTest, WalIteratorNewFileWhileScanning) {
|
||||
Init(nullptr /* clock_override */);
|
||||
CreateArchiveLogs(2, 100);
|
||||
auto iter = OpenTransactionLogIter(0);
|
||||
auto iter = OpenWalIter(0);
|
||||
CreateArchiveLogs(1, 100);
|
||||
int i = 0;
|
||||
for (; iter->Valid(); iter->Next()) {
|
||||
@@ -356,7 +355,7 @@ TEST_F(WalManagerTest, TransactionLogIteratorNewFileWhileScanning) {
|
||||
// TryAgain indicates a new iterator is needed to fetch the new data
|
||||
ASSERT_TRUE(iter->status().IsTryAgain());
|
||||
|
||||
iter = OpenTransactionLogIter(0);
|
||||
iter = OpenWalIter(0);
|
||||
i = 0;
|
||||
for (; iter->Valid(); iter->Next()) {
|
||||
i++;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,440 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "rocksdb/lazy_wide_columns.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/wide/blob_column_resolver_util.h"
|
||||
#include "db/wide/lazy_wide_columns_helper.h"
|
||||
#include "db/wide/read_path_blob_resolver.h"
|
||||
#include "db/wide/wide_column_serialization.h"
|
||||
#include "db/wide/wide_columns_helper.h"
|
||||
#include "monitoring/thread_status_util.h"
|
||||
#include "rocksdb/cleanable.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/wide_columns.h"
|
||||
|
||||
// Current implementation of the lazy blob resolution API. Enumeration and
|
||||
// whole-column resolution are functional. Byte-range reads of an uncompressed
|
||||
// blob -- whether it lives in a separate blob file or is embedded (same-file)
|
||||
// in the SST -- are served by reading only the requested bytes from storage
|
||||
// (skipping checksum verification and cache-fill); other cases (compressed,
|
||||
// whole-column, already-cached, or force_verify) resolve the whole column and
|
||||
// slice it. Cross-key coalescing and async execution are future work; here
|
||||
// LazyWideColumnsBatch::MultiResolve simply routes each read to its owning
|
||||
// per-key result, and MultiGetEntityLazy fills the batch key-by-key.
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace {
|
||||
// Scope guard used while resolving a lazy result: sets the thread's
|
||||
// ThreadStatus operation to OP_LAZY_RESOLVE for the duration, then returns it
|
||||
// to OP_UNKNOWN on exit. A resolve is a self-contained top-level operation, so
|
||||
// it starts and ends with no active op rather than restoring a prior one --
|
||||
// thread operations do not stack, and any op lingering from the originating
|
||||
// GetEntity/MultiGetEntity call is stale. (This mirrors the SetThreadOperation
|
||||
// + ResetThreadStatus pattern other top-level ops such as compaction/DBOpen
|
||||
// use.)
|
||||
//
|
||||
// Setting the parallel operation type keeps the io_activity and thread
|
||||
// operation consistent while the deferred reads (which carry
|
||||
// Env::IOActivity::kLazyResolve) run: db_stress asserts every file read's
|
||||
// io_activity matches the one implied by the current thread operation (see
|
||||
// db_stress_env_wrapper.h / TEST_GetExpectedIOActivity), and ThreadStatus
|
||||
// reporting then attributes these reads to the lazy-resolve operation.
|
||||
//
|
||||
// TODO: unified RAII wrappers for thread status updates
|
||||
class LazyResolveThreadOpScope {
|
||||
public:
|
||||
LazyResolveThreadOpScope() {
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_LAZY_RESOLVE);
|
||||
}
|
||||
~LazyResolveThreadOpScope() {
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_UNKNOWN);
|
||||
}
|
||||
LazyResolveThreadOpScope(const LazyResolveThreadOpScope&) = delete;
|
||||
LazyResolveThreadOpScope& operator=(const LazyResolveThreadOpScope&) = delete;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
// Internal representation. Owns the serialized-entity backing buffer + inline
|
||||
// columns (via `entity_`), the decoded blob references, the per-column views,
|
||||
// the on-demand blob resolver, and the SuperVersion pin that keeps all of it
|
||||
// (blob files, immortal table readers) valid after the producing DB call
|
||||
// returns. The Rep is heap-allocated and never relocated (it lives behind a
|
||||
// unique_ptr that only ever transfers ownership), so pointers to it and to its
|
||||
// `columns_` elements stay stable across LazyWideColumns/Batch moves -- which
|
||||
// is what lets a LazyColumnReadRequest identify its column (and owning result)
|
||||
// by pointer without any move-time fix-ups.
|
||||
class LazyWideColumns::Rep {
|
||||
public:
|
||||
// Holds the serialized entity as a single backing buffer and, after
|
||||
// SetWideColumnValue (without resolution), the full sorted column set --
|
||||
// inline columns' value() are zero-copy Slices into that buffer; blob
|
||||
// columns' value() are the raw serialized BlobIndex bytes (the resolver reads
|
||||
// decoded indices from blob_columns_, not from here).
|
||||
//
|
||||
// Lifetime invariant: `entity_` is populated once (by the point lookup, via
|
||||
// EntityBuffer(), before Finalize) and is never mutated afterwards, and the
|
||||
// Rep is heap-allocated and never relocated. So this backing buffer is stable
|
||||
// for the result's whole lifetime -- which is what keeps valid every Slice
|
||||
// that points into it: the inline column values, the decoded
|
||||
// inlined-BlobIndex values stored in blob_columns_, the per-column
|
||||
// inline_data_ in columns_, and the resolver's cached bytes.
|
||||
PinnableWideColumns entity_;
|
||||
|
||||
// Decoded blob references (column_index -> BlobIndex), for the columns whose
|
||||
// value is a blob reference. Empty for a fully inline entity.
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_columns_;
|
||||
|
||||
// Per-column view returned by LazyWideColumns::operator[]. Derives from the
|
||||
// public LazyWideColumn (its ctor sets the protected base fields) and adds a
|
||||
// back-pointer to this Rep, used to validate that a read's column belongs to
|
||||
// this result (and, via owning_batch_rep_, to this batch). Nested (rather
|
||||
// than in an anonymous namespace) so it shares the enclosing class's linkage
|
||||
// -- an internal-linkage subobject type in this external-linkage class trips
|
||||
// -Wsubobject-linkage in unity builds.
|
||||
struct ColumnImpl : public LazyWideColumn {
|
||||
ColumnImpl(Rep* parent, size_t index, Slice name, const char* inline_data,
|
||||
std::optional<uint64_t> size)
|
||||
: parent_rep_(parent) {
|
||||
name_ = name;
|
||||
inline_data_ = inline_data;
|
||||
size_ = size;
|
||||
index_ = index;
|
||||
}
|
||||
Rep* parent_rep_ = nullptr;
|
||||
};
|
||||
|
||||
// One entry per column in `entity_.columns()`, in sorted order.
|
||||
std::vector<ColumnImpl> columns_;
|
||||
|
||||
// Identity of the owning batch's Rep (stable across batch move), or null for
|
||||
// a standalone result. Used to validate that a batch read targets an entity
|
||||
// of that batch. Stored as void* to avoid naming LazyWideColumnsBatch::Rep
|
||||
// here (it is only an identity token, never dereferenced).
|
||||
const void* owning_batch_rep_ = nullptr;
|
||||
|
||||
// Stable copy of the user key, referenced by the resolver for blob
|
||||
// verification (the originating key argument does not outlive the call).
|
||||
std::string user_key_;
|
||||
|
||||
// Pins the SuperVersion for this result's lifetime. Declared before
|
||||
// `resolver_` so the resolver (which references the Version) is destroyed
|
||||
// first.
|
||||
Cleanable pin_;
|
||||
|
||||
// On-demand blob resolver, bound to entity_.columns() + blob_columns_.
|
||||
// Emplaced at finalize time (needs the Version). Absent for an unpopulated
|
||||
// (e.g. NotFound) result.
|
||||
std::optional<ReadPathBlobResolver> resolver_;
|
||||
|
||||
~Rep() {
|
||||
// Releasing the SuperVersion pin (pin_) can trigger obsolete-file cleanup
|
||||
// I/O -- e.g. FindObsoleteFiles closing an obsolete WAL -- when this result
|
||||
// held the last reference to it. Mirror DBIter::~DBIter and run that
|
||||
// teardown with the thread operation reset to OP_UNKNOWN, so the incidental
|
||||
// I/O is not attributed to whatever read operation happens to be active on
|
||||
// the destroying thread (which otherwise misattributes I/O stats and trips
|
||||
// db_stress's io_activity invariant).
|
||||
const ThreadStatus::OperationType saved_op =
|
||||
ThreadStatusUtil::GetThreadOperation();
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_UNKNOWN);
|
||||
resolver_.reset(); // borrows the pinned Version; drop before the pin
|
||||
pin_.Reset(); // runs CleanupSuperVersionHandle if this was last ref
|
||||
ThreadStatusUtil::SetThreadOperation(saved_op);
|
||||
}
|
||||
|
||||
// Resolve one read against column `index` of this result (index < size()),
|
||||
// writing the per-read outcome to read.status / read.result. Shared by the
|
||||
// single-entity and batch MultiResolve paths.
|
||||
void ResolveOneRead(size_t index, LazyColumnReadRequest& read) {
|
||||
assert(resolver_);
|
||||
|
||||
// Delegate to the resolver, which owns the partial-vs-whole decision: for a
|
||||
// strict sub-range of an uncompressed blob reference (separate-file or
|
||||
// embedded, and not force_verify) it reads only the requested bytes,
|
||||
// skipping checksum and cache-fill; every other case resolves the whole
|
||||
// column (caching it) and slices. Repeated whole-column reads therefore
|
||||
// read the blob at most once; partial reads own their own bytes and are not
|
||||
// cached. A null read.result still resolves (to surface I/O/integrity
|
||||
// errors on read.status and honor force_verify), just without producing
|
||||
// output -- ResolveColumnRange handles that case.
|
||||
const Status s = resolver_->ResolveColumnRange(
|
||||
index, read.offset, read.length, read.force_verify, read.result);
|
||||
|
||||
if (read.status) {
|
||||
*read.status = s;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
LazyWideColumns::LazyWideColumns() = default;
|
||||
LazyWideColumns::~LazyWideColumns() = default;
|
||||
LazyWideColumns::LazyWideColumns(LazyWideColumns&&) noexcept = default;
|
||||
LazyWideColumns& LazyWideColumns::operator=(LazyWideColumns&&) noexcept =
|
||||
default;
|
||||
|
||||
size_t LazyWideColumns::size() const {
|
||||
return rep_ ? rep_->columns_.size() : 0;
|
||||
}
|
||||
|
||||
const LazyWideColumn& LazyWideColumns::operator[](size_t i) const {
|
||||
assert(rep_);
|
||||
assert(i < rep_->columns_.size());
|
||||
return rep_->columns_[i];
|
||||
}
|
||||
|
||||
Status LazyWideColumns::MultiResolve(size_t num_reads,
|
||||
LazyColumnReadRequest* reads) {
|
||||
LazyResolveThreadOpScope thread_op_scope;
|
||||
for (size_t i = 0; i < num_reads; ++i) {
|
||||
LazyColumnReadRequest& read = reads[i];
|
||||
if (read.status) {
|
||||
*read.status = Status::OK();
|
||||
}
|
||||
if (read.result) {
|
||||
read.result->Reset(); // failure paths below leave an empty result
|
||||
}
|
||||
// The column must belong to this result (it carries a back-pointer to the
|
||||
// Rep that owns it).
|
||||
if (read.column == nullptr ||
|
||||
static_cast<const Rep::ColumnImpl*>(read.column)->parent_rep_ !=
|
||||
rep_.get()) {
|
||||
if (read.status) {
|
||||
*read.status = Status::InvalidArgument(
|
||||
"Column does not belong to this LazyWideColumns");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
rep_->ResolveOneRead(read.column->index(), read);
|
||||
}
|
||||
// Every read was dispatched synchronously; per-read outcomes are in
|
||||
// read.status.
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status LazyWideColumns::ResolveColumnRange(const LazyWideColumn& column,
|
||||
uint64_t offset, size_t length,
|
||||
PinnableSlice* result) {
|
||||
// Sugar over a one-entry batch, so the batch path is the single real,
|
||||
// optimizable (coalescing/async) code path.
|
||||
Status status;
|
||||
LazyColumnReadRequest read;
|
||||
read.column = &column;
|
||||
read.offset = offset;
|
||||
read.length = length;
|
||||
read.result = result;
|
||||
read.status = &status;
|
||||
|
||||
const Status batch_status = MultiResolve(/*num_reads=*/1, &read);
|
||||
return batch_status.ok() ? status : batch_status;
|
||||
}
|
||||
|
||||
Status LazyWideColumns::ResolveColumn(const LazyWideColumn& column,
|
||||
PinnableSlice* result) {
|
||||
return ResolveColumnRange(column, /*offset=*/0, kLazyWholeColumn, result);
|
||||
}
|
||||
|
||||
void LazyWideColumns::Reset() { rep_.reset(); }
|
||||
|
||||
// ---- LazyWideColumnsBatch ----
|
||||
|
||||
// Internal representation. Owns the per-key LazyWideColumns. (In the current
|
||||
// phase each entity holds its own SuperVersion pin; cf_pins is reserved for the
|
||||
// future shared-pin, cross-CF design and is currently unused.)
|
||||
class LazyWideColumnsBatch::Rep {
|
||||
public:
|
||||
// One result per key of the MultiGetEntityLazy call, in key order.
|
||||
std::vector<LazyWideColumns> entities;
|
||||
|
||||
// TODO(lazy-blob-resolution-phase3): switch to one shared SuperVersion pin
|
||||
// per column family here (instead of one self-pin per entity), populated by a
|
||||
// batched MultiGetEntityLazy.
|
||||
std::map<uint32_t /* column_family_id */, Cleanable> cf_pins;
|
||||
};
|
||||
|
||||
LazyWideColumnsBatch::LazyWideColumnsBatch() = default;
|
||||
LazyWideColumnsBatch::~LazyWideColumnsBatch() = default;
|
||||
LazyWideColumnsBatch::LazyWideColumnsBatch(LazyWideColumnsBatch&&) noexcept =
|
||||
default;
|
||||
LazyWideColumnsBatch& LazyWideColumnsBatch::operator=(
|
||||
LazyWideColumnsBatch&&) noexcept = default;
|
||||
|
||||
size_t LazyWideColumnsBatch::size() const {
|
||||
return rep_ ? rep_->entities.size() : 0;
|
||||
}
|
||||
|
||||
const LazyWideColumns& LazyWideColumnsBatch::operator[](size_t i) const {
|
||||
assert(rep_);
|
||||
assert(i < rep_->entities.size());
|
||||
return rep_->entities[i];
|
||||
}
|
||||
|
||||
LazyWideColumns& LazyWideColumnsBatch::operator[](size_t i) {
|
||||
assert(rep_);
|
||||
assert(i < rep_->entities.size());
|
||||
return rep_->entities[i];
|
||||
}
|
||||
|
||||
Status LazyWideColumnsBatch::MultiResolve(size_t num_reads,
|
||||
LazyColumnReadRequest* reads) {
|
||||
LazyResolveThreadOpScope thread_op_scope;
|
||||
const void* this_rep = rep_.get();
|
||||
for (size_t i = 0; i < num_reads; ++i) {
|
||||
LazyColumnReadRequest& read = reads[i];
|
||||
if (read.status) {
|
||||
*read.status = Status::OK();
|
||||
}
|
||||
if (read.result) {
|
||||
read.result->Reset(); // failure paths below leave an empty result
|
||||
}
|
||||
if (read.column == nullptr) {
|
||||
if (read.status) {
|
||||
*read.status = Status::InvalidArgument("Null column in batch read");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Route the read to the entity that owns its column, and require that
|
||||
// entity to belong to this batch. The owning_batch_rep_ == nullptr check is
|
||||
// essential: a standalone column (from GetEntityLazy) has
|
||||
// owning_batch_rep_ == nullptr, and an empty/default-constructed batch has
|
||||
// this_rep == nullptr, so without it a foreign standalone column would slip
|
||||
// through the nullptr == nullptr comparison instead of being rejected.
|
||||
// TODO(lazy-blob-resolution-phase3): group reads per (CF, Version, blob
|
||||
// file) across entities and coalesce them; the request shape is unchanged.
|
||||
LazyWideColumns::Rep* entity_rep =
|
||||
static_cast<const LazyWideColumns::Rep::ColumnImpl*>(read.column)
|
||||
->parent_rep_;
|
||||
if (entity_rep == nullptr || entity_rep->owning_batch_rep_ == nullptr ||
|
||||
entity_rep->owning_batch_rep_ != this_rep) {
|
||||
if (read.status) {
|
||||
*read.status =
|
||||
Status::InvalidArgument("Column does not belong to this batch");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
entity_rep->ResolveOneRead(read.column->index(), read);
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void LazyWideColumnsBatch::Reset() { rep_.reset(); }
|
||||
|
||||
// ---- LazyWideColumnsHelper (DB-side construction) ----
|
||||
|
||||
PinnableWideColumns* LazyWideColumnsHelper::EntityBuffer(
|
||||
LazyWideColumns* result) {
|
||||
assert(result);
|
||||
if (!result->rep_) {
|
||||
result->rep_ = std::make_unique<LazyWideColumns::Rep>();
|
||||
}
|
||||
return &result->rep_->entity_;
|
||||
}
|
||||
|
||||
Status LazyWideColumnsHelper::Finalize(
|
||||
LazyWideColumns* result, const Slice& user_key, const Version* version,
|
||||
const ReadOptions& read_options, BlobFileCache* blob_file_cache,
|
||||
bool allow_write_path_fallback, const SameFileBlobReader* same_file_reader,
|
||||
Cleanable&& pin) {
|
||||
assert(result);
|
||||
assert(result->rep_);
|
||||
LazyWideColumns::Rep& rep = *result->rep_;
|
||||
|
||||
// Decode the blob references (if the entity still has unresolved ones -- it
|
||||
// does not for a fully inline entity, or one resolved eagerly on the memtable
|
||||
// path). When unresolved indices are present the entity occupies a single
|
||||
// backing buffer, so GetSerializedEntity() is valid.
|
||||
if (!PinnableWideColumnsHelper::GetUnresolvedBlobColumnIndices(rep.entity_)
|
||||
.empty()) {
|
||||
WideColumns unused_columns;
|
||||
const Status s = WideColumnSerialization::Deserialize(
|
||||
PinnableWideColumnsHelper::GetSerializedEntity(rep.entity_),
|
||||
unused_columns, &rep.blob_columns_);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
// Build the per-column views from the (sorted) column set + blob references.
|
||||
const WideColumns& columns = rep.entity_.columns();
|
||||
rep.columns_.clear();
|
||||
rep.columns_.reserve(columns.size());
|
||||
for (size_t i = 0; i < columns.size(); ++i) {
|
||||
const BlobIndex* blob_index =
|
||||
blob_resolver_util::FindBlobColumn(&rep.blob_columns_, i);
|
||||
const char* inline_data = nullptr;
|
||||
std::optional<uint64_t> size;
|
||||
if (blob_index == nullptr) {
|
||||
// Inline column: bytes available with no I/O. Slice guarantees a non-null
|
||||
// data(), so a non-null inline_data marks this as inline (not a
|
||||
// reference) even for an empty value.
|
||||
inline_data = columns[i].value().data();
|
||||
size = columns[i].value().size();
|
||||
} else if (blob_index->IsInlined()) {
|
||||
// TTL-inlined blob: an unresolved reference whose logical size is known
|
||||
// (the value lives in the index).
|
||||
size = blob_index->value().size();
|
||||
} else if (blob_index->compression() == kNoCompression) {
|
||||
// Uncompressed reference: on-disk size equals logical size.
|
||||
size = blob_index->size();
|
||||
}
|
||||
// else: compressed reference -- logical size unknown until resolved.
|
||||
rep.columns_.emplace_back(&rep, i, columns[i].name(), inline_data, size);
|
||||
}
|
||||
|
||||
// Take ownership of the SuperVersion pin and stand up the resolver bound to
|
||||
// the (address-stable, since Rep is heap-allocated) entity columns + blob
|
||||
// references. The resolver's deferred blob-byte reads are attributed to
|
||||
// Env::IOActivity::kLazyResolve (distinct from the kGetEntity/kMultiGetEntity
|
||||
// of the initial entity read that already completed via GetImpl).
|
||||
rep.pin_ = std::move(pin);
|
||||
rep.user_key_.assign(user_key.data(), user_key.size());
|
||||
ReadOptions resolve_read_options(read_options);
|
||||
resolve_read_options.io_activity = Env::IOActivity::kLazyResolve;
|
||||
rep.resolver_.emplace(version, resolve_read_options, blob_file_cache,
|
||||
allow_write_path_fallback);
|
||||
rep.resolver_->Reset(Slice(rep.user_key_), &rep.entity_.columns(),
|
||||
&rep.blob_columns_, same_file_reader);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void LazyWideColumnsHelper::InitBatch(LazyWideColumnsBatch* batch,
|
||||
size_t num_entities) {
|
||||
assert(batch);
|
||||
if (!batch->rep_) {
|
||||
batch->rep_ = std::make_unique<LazyWideColumnsBatch::Rep>();
|
||||
}
|
||||
batch->rep_->entities.clear();
|
||||
batch->rep_->entities.resize(num_entities);
|
||||
}
|
||||
|
||||
void LazyWideColumnsHelper::FinalizeBatch(LazyWideColumnsBatch* batch) {
|
||||
assert(batch);
|
||||
if (!batch->rep_) {
|
||||
return;
|
||||
}
|
||||
// Link each populated entity to this batch (by the batch Rep's stable
|
||||
// identity) so batch reads can validate that a column belongs to this batch.
|
||||
const void* batch_rep = batch->rep_.get();
|
||||
for (LazyWideColumns& entity : batch->rep_->entities) {
|
||||
if (entity.rep_) {
|
||||
entity.rep_->owning_batch_rep_ = batch_rep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "rocksdb/cleanable.h"
|
||||
#include "rocksdb/lazy_wide_columns.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class BlobFileCache;
|
||||
class PinnableWideColumns;
|
||||
class SameFileBlobReader;
|
||||
class Version;
|
||||
struct ReadOptions;
|
||||
|
||||
// Internal (DB-side) helper for populating LazyWideColumns /
|
||||
// LazyWideColumnsBatch results without exposing their construction machinery on
|
||||
// the public API (the same pattern as PinnableWideColumnsHelper). Only the DB
|
||||
// read path uses this.
|
||||
class LazyWideColumnsHelper {
|
||||
public:
|
||||
// Creates (if needed) the internal representation of `result` and returns the
|
||||
// PinnableWideColumns to use as the columns output of a lazy point lookup.
|
||||
// The returned buffer is owned by `result`; after the lookup fills it, call
|
||||
// Finalize().
|
||||
static PinnableWideColumns* EntityBuffer(LazyWideColumns* result);
|
||||
|
||||
// Finalizes a lazy result after its EntityBuffer() was populated by a point
|
||||
// lookup: decodes any remaining blob references, builds the enumeration
|
||||
// metadata, constructs the on-demand resolver bound to the entity, and takes
|
||||
// ownership of the SuperVersion `pin`. `version` is the Version blob
|
||||
// references resolve against; `same_file_reader` (may be null) resolves
|
||||
// same-file/embedded references. Returns non-OK only on a decode error.
|
||||
static Status Finalize(LazyWideColumns* result, const Slice& user_key,
|
||||
const Version* version,
|
||||
const ReadOptions& read_options,
|
||||
BlobFileCache* blob_file_cache,
|
||||
bool allow_write_path_fallback,
|
||||
const SameFileBlobReader* same_file_reader,
|
||||
Cleanable&& pin);
|
||||
|
||||
// Prepares `batch` to hold `num_entities` empty per-key results (creating the
|
||||
// batch's representation). Use (*batch)[i] to access each for filling, then
|
||||
// call FinalizeBatch() once all entities are populated.
|
||||
static void InitBatch(LazyWideColumnsBatch* batch, size_t num_entities);
|
||||
|
||||
// Links every populated entity of `batch` back to the batch, so a batch read
|
||||
// can validate that a column belongs to this batch. Call after all entities
|
||||
// have been filled (and individually Finalize()d).
|
||||
static void FinalizeBatch(LazyWideColumnsBatch* batch);
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/blob/same_file_blob_reader.h"
|
||||
#include "db/version_set.h"
|
||||
#include "db/wide/blob_column_resolver_util.h"
|
||||
|
||||
@@ -21,15 +23,24 @@ ReadPathBlobResolver::ReadPathBlobResolver(const Version* version,
|
||||
|
||||
void ReadPathBlobResolver::Reset(
|
||||
const Slice& user_key, const std::vector<WideColumn>* columns,
|
||||
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns) {
|
||||
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns,
|
||||
const SameFileBlobReader* same_file_reader) {
|
||||
user_key_ = user_key;
|
||||
columns_ = columns;
|
||||
blob_columns_ = blob_columns;
|
||||
same_file_reader_ = same_file_reader;
|
||||
resolved_cache_.clear();
|
||||
}
|
||||
|
||||
Status ReadPathBlobResolver::ResolveColumn(size_t column_index,
|
||||
Slice* resolved_value) {
|
||||
return ResolveColumnInternal(column_index, /*force_verify=*/false,
|
||||
resolved_value);
|
||||
}
|
||||
|
||||
Status ReadPathBlobResolver::ResolveColumnInternal(size_t column_index,
|
||||
bool force_verify,
|
||||
Slice* resolved_value) {
|
||||
assert(columns_);
|
||||
assert(resolved_value);
|
||||
|
||||
@@ -64,9 +75,50 @@ Status ReadPathBlobResolver::ResolveColumn(size_t column_index,
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
constexpr uint64_t* bytes_read = nullptr;
|
||||
|
||||
status =
|
||||
blob_fetcher_.FetchBlob(user_key_, blob_index, prefetch_buffer,
|
||||
new_entry.second.get(), bytes_read);
|
||||
// TODO(lazy-blob-resolution-cleanup): the force_verify branching here
|
||||
// (and the whole-vs-range split in ResolveColumnRange) is really a
|
||||
// single 3-valued verify policy -- must-verify-if-present (today's
|
||||
// force_verify), verify-if-no-amplification (verify_checksums on: a
|
||||
// whole read verifies, a partial read skips), and ignore. Map
|
||||
// (verify_checksums, force_verify) to that internal enum once at this
|
||||
// boundary and thread it down, collapsing these branches. Deferred
|
||||
// for feature velocity; see the lazy blob resolution plan.
|
||||
if (force_verify && !blob_index.IsSameFile()) {
|
||||
// Honor force_verify for separate-file references: read and verify
|
||||
// the whole record even if ReadOptions::verify_checksums is off.
|
||||
status = blob_fetcher_.FetchBlobForceVerify(
|
||||
user_key_, blob_index, prefetch_buffer, new_entry.second.get(),
|
||||
bytes_read);
|
||||
} else if (force_verify && blob_index.IsSameFile()) {
|
||||
// Honor force_verify for same-file (embedded) references the same
|
||||
// way: force checksum verification on via a verify-enabled copy of
|
||||
// the read options. A same-file reference is only produced when the
|
||||
// entity was read from an SST that supplied its SameFileBlobReader,
|
||||
// so it is expected to be set; surface a clean error rather than
|
||||
// silently dropping force_verify (and taking the unverified path
|
||||
// below) if it somehow is not.
|
||||
assert(same_file_reader_ != nullptr);
|
||||
if (same_file_reader_ == nullptr) {
|
||||
status = Status::Corruption(
|
||||
"Cannot force-verify same-file blob: no same-file reader");
|
||||
} else {
|
||||
ReadOptions verify_read_options = blob_fetcher_.read_options();
|
||||
verify_read_options.verify_checksums = true;
|
||||
status = same_file_reader_->GetSameFileBlob(
|
||||
verify_read_options, blob_index, new_entry.second.get());
|
||||
}
|
||||
} else {
|
||||
// Route same-file ("embedded") references through the current SST's
|
||||
// SameFileBlobReader when one is set; separate-file references (and
|
||||
// the no-same-file-reader case) go straight to the Version-backed
|
||||
// fetcher. EffectiveFetcher() returns the plain base fetcher when
|
||||
// there is no same-file reader, adding no indirection.
|
||||
EmbeddedAwareBlobFetcher embedded_fetcher(&blob_fetcher_,
|
||||
same_file_reader_);
|
||||
status = embedded_fetcher.EffectiveFetcher()->FetchBlob(
|
||||
user_key_, blob_index, prefetch_buffer, new_entry.second.get(),
|
||||
bytes_read);
|
||||
}
|
||||
if (!status.ok()) {
|
||||
resolved_cache_.pop_back();
|
||||
} else {
|
||||
@@ -79,6 +131,104 @@ Status ReadPathBlobResolver::ResolveColumn(size_t column_index,
|
||||
return status;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Pin [range_offset, range_offset + range_length) of `whole` into *result,
|
||||
// clamped to `whole`'s size (offset at/past end -> empty). The bytes are not
|
||||
// copied or owned here: *result points into `whole`, whose storage must outlive
|
||||
// *result (the resolver's whole-column cache / entity buffer, which lives as
|
||||
// long as this resolver).
|
||||
void PinClampedSubRange(const Slice& whole, uint64_t range_offset,
|
||||
size_t range_length, PinnableSlice* result) {
|
||||
result->Reset();
|
||||
if (range_offset >= whole.size()) {
|
||||
result->PinSlice(Slice(), nullptr);
|
||||
return;
|
||||
}
|
||||
const size_t off = static_cast<size_t>(range_offset);
|
||||
const size_t avail = whole.size() - off;
|
||||
const size_t len = range_length > avail ? avail : range_length;
|
||||
result->PinSlice(Slice(whole.data() + off, len), nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status ReadPathBlobResolver::ResolveColumnRange(size_t column_index,
|
||||
uint64_t range_offset,
|
||||
size_t range_length,
|
||||
bool force_verify,
|
||||
PinnableSlice* result) {
|
||||
assert(columns_);
|
||||
|
||||
if (column_index >= columns_->size()) {
|
||||
return Status::InvalidArgument("Column index out of bounds");
|
||||
}
|
||||
|
||||
// No output buffer: the caller only wants to surface any I/O / integrity
|
||||
// error (and honor force_verify). Resolve the whole column and return; there
|
||||
// is nothing to slice into.
|
||||
if (result == nullptr) {
|
||||
Slice ignored;
|
||||
return ResolveColumnInternal(column_index, force_verify, &ignored);
|
||||
}
|
||||
|
||||
const BlobIndex* blob_index_ptr =
|
||||
blob_resolver_util::FindBlobColumn(blob_columns_, column_index);
|
||||
|
||||
// Decide whether the I/O-saving partial path applies. It requires a blob
|
||||
// reference that is: not already resolved (else we slice the cached whole
|
||||
// value), uncompressed (a strict sub-range of a compressed record can't be
|
||||
// decompressed in isolation), a strict sub-range (a whole-column read takes
|
||||
// the verifying + cache-filling path), and not force_verify. The read then
|
||||
// goes to either the separate-file range fetcher (needs a Version) or, for a
|
||||
// same-file / embedded reference, the current SST's SameFileBlobReader.
|
||||
if (blob_index_ptr != nullptr && !force_verify &&
|
||||
blob_resolver_util::FindInCache(resolved_cache_, column_index) ==
|
||||
nullptr) {
|
||||
const BlobIndex& blob_index = *blob_index_ptr;
|
||||
const bool separate_file_ok =
|
||||
!blob_index.IsSameFile() && blob_fetcher_.SupportsRangeRead();
|
||||
const bool same_file_ok =
|
||||
blob_index.IsSameFile() && same_file_reader_ != nullptr;
|
||||
if (!blob_index.IsInlined() && blob_index.compression() == kNoCompression &&
|
||||
(separate_file_ok || same_file_ok)) {
|
||||
const uint64_t value_size = blob_index.size();
|
||||
const bool strict_subrange =
|
||||
range_offset > 0 || range_length < value_size;
|
||||
if (strict_subrange) {
|
||||
result->Reset();
|
||||
if (range_offset >= value_size || range_length == 0) {
|
||||
// Nothing to read; empty (not an error), no I/O.
|
||||
result->PinSlice(Slice(), nullptr);
|
||||
return Status::OK();
|
||||
}
|
||||
const size_t avail = static_cast<size_t>(value_size - range_offset);
|
||||
const size_t actual_len = range_length > avail ? avail : range_length;
|
||||
if (blob_index.IsSameFile()) {
|
||||
return same_file_reader_->GetSameFileBlobRange(
|
||||
blob_fetcher_.read_options(), blob_index, range_offset,
|
||||
actual_len, result);
|
||||
}
|
||||
return blob_fetcher_.FetchBlobRange(user_key_, blob_index, range_offset,
|
||||
actual_len, result,
|
||||
/*bytes_read=*/nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Full path: resolve the whole column (inline value directly, blob reference
|
||||
// via the resolver's cache, filling it on a miss and verifying under
|
||||
// ReadOptions::verify_checksums or force_verify), then slice out the
|
||||
// requested range.
|
||||
Slice whole;
|
||||
Status s = ResolveColumnInternal(column_index, force_verify, &whole);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
PinClampedSubRange(whole, range_offset, range_length, result);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status ReadPathBlobResolver::ResolveColumns(
|
||||
const std::vector<size_t>& column_indices,
|
||||
std::vector<Slice>* resolved_values) {
|
||||
|
||||
@@ -21,13 +21,14 @@
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class Version;
|
||||
class SameFileBlobReader;
|
||||
|
||||
// TODO: ReadPathBlobResolver and CompactionBlobResolver (in
|
||||
// compaction_iterator.h) share significant logic for blob column resolution
|
||||
// and caching. A refactoring into a common base class or shared utility
|
||||
// could reduce duplication. The two classes differ in their blob-fetching
|
||||
// backends (Version::GetBlob vs BlobFetcher::FetchBlob) and contexts
|
||||
// (read path vs compaction path with stats tracking).
|
||||
// could reduce duplication. The two classes differ in fetcher ownership and
|
||||
// their surrounding contexts (read path vs compaction path with stats
|
||||
// tracking).
|
||||
//
|
||||
// Enables lazy (on-demand) resolution of blob column values in the read path.
|
||||
// When a wide-column entity contains blob references (V2 format), the resolver
|
||||
@@ -50,8 +51,17 @@ class ReadPathBlobResolver {
|
||||
// Reset the resolver for a new entity. Clears all cached values.
|
||||
// The columns and blob_columns pointers must remain valid for the lifetime
|
||||
// of the resolver (or until the next Reset call).
|
||||
//
|
||||
// same_file_reader: when non-null, same-file ("embedded") blob references are
|
||||
// resolved against it (the SST that held the entity) via an
|
||||
// EmbeddedAwareBlobFetcher; it must outlive the resolver (ensured, on the
|
||||
// lazy read path, by the same SuperVersion pin that keeps the Version alive,
|
||||
// combined with immortal table readers when max_open_files == -1). When null
|
||||
// (e.g. the DBIter path, which resolves embedded refs separately), only
|
||||
// separate-file references are resolvable.
|
||||
void Reset(const Slice& user_key, const std::vector<WideColumn>* columns,
|
||||
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns);
|
||||
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns,
|
||||
const SameFileBlobReader* same_file_reader = nullptr);
|
||||
|
||||
// Resolve the value for the column at the given index.
|
||||
// For blob columns, fetches the blob value from the blob file (or returns
|
||||
@@ -62,6 +72,29 @@ class ReadPathBlobResolver {
|
||||
// - I/O error occurred while fetching the blob
|
||||
Status ResolveColumn(size_t column_index, Slice* resolved_value);
|
||||
|
||||
// Resolve a byte sub-range [range_offset, range_offset + range_length) of the
|
||||
// column at `column_index` into *result (zero-copy). `range_length` may be
|
||||
// larger than the remaining bytes (or SIZE_MAX for "to the end"); it is
|
||||
// clamped to the column's logical size. An offset at/past the end yields an
|
||||
// empty result (not an error).
|
||||
//
|
||||
// Reads only the requested bytes -- skipping whole-record checksum
|
||||
// verification and blob-cache population -- only for a strict sub-range of an
|
||||
// uncompressed blob reference (in a separate blob file, or
|
||||
// embedded/same-file) that is not already resolved, when !force_verify and
|
||||
// the read is servable (a Version-backed range read for separate-file refs,
|
||||
// or a SameFileBlobReader for embedded refs). Every other case (inline
|
||||
// column, already-resolved column, inlined blob, compressed reference, a
|
||||
// whole-column read, or force_verify) resolves the whole column (caching it,
|
||||
// verifying under ReadOptions::verify_checksums) and slices the requested
|
||||
// range out of it.
|
||||
//
|
||||
// Unlike ResolveColumn, a partial read is NOT cached: *result owns (or pins)
|
||||
// its own bytes, independent of this resolver's whole-column cache.
|
||||
Status ResolveColumnRange(size_t column_index, uint64_t range_offset,
|
||||
size_t range_length, bool force_verify,
|
||||
PinnableSlice* result);
|
||||
|
||||
// Resolve multiple columns in the order provided by `column_indices`.
|
||||
// Resolved blob values are cached exactly as if ResolveColumn() were called
|
||||
// repeatedly.
|
||||
@@ -91,6 +124,13 @@ class ReadPathBlobResolver {
|
||||
}
|
||||
|
||||
private:
|
||||
// Shared implementation of ResolveColumn / the whole-column path of
|
||||
// ResolveColumnRange. When force_verify is set, a separate-file blob
|
||||
// reference is read with checksum verification forced on even if
|
||||
// ReadOptions::verify_checksums is off (see LazyColumnReadRequest).
|
||||
Status ResolveColumnInternal(size_t column_index, bool force_verify,
|
||||
Slice* resolved_value);
|
||||
|
||||
// Owns its ReadOptions: a resolver's lifetime is independent of the caller
|
||||
// that created it (e.g. it may outlive the originating ReadOptions once
|
||||
// returned as part of a lazy result).
|
||||
@@ -99,6 +139,9 @@ class ReadPathBlobResolver {
|
||||
Slice user_key_;
|
||||
const std::vector<WideColumn>* columns_ = nullptr;
|
||||
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns_ = nullptr;
|
||||
// Non-null on the lazy read path for entities with same-file blob references;
|
||||
// see Reset().
|
||||
const SameFileBlobReader* same_file_reader_ = nullptr;
|
||||
|
||||
// Cache for resolved blob values to avoid re-fetching.
|
||||
// Uses a vector of (column_index, PinnableSlice) pairs. Typical entities
|
||||
|
||||
@@ -381,6 +381,19 @@ class BatchedOpsStressTest : public StressTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reuse one of the sibling entities we just read (under read_opts_copy's
|
||||
// snapshot, and cross-checked above) as the lazy read's reference, rather
|
||||
// than issuing another eager read.
|
||||
const size_t lazy_idx = thread->rand.Uniform(static_cast<int>(num_keys));
|
||||
const std::string lazy_key = std::to_string(lazy_idx) + key_suffix;
|
||||
const WideColumns& eager_ref_columns =
|
||||
FLAGS_use_attribute_group
|
||||
? attribute_group_results[lazy_idx].front().columns()
|
||||
: column_results[lazy_idx].columns();
|
||||
const WideColumns* const eager_ref =
|
||||
eager_ref_columns.empty() ? nullptr : &eager_ref_columns;
|
||||
MaybeTestGetEntityLazy(thread, read_opts_copy, cfh, lazy_key, eager_ref);
|
||||
}
|
||||
|
||||
void TestMultiGetEntity(ThreadState* thread, const ReadOptions& read_opts,
|
||||
@@ -485,6 +498,16 @@ class BatchedOpsStressTest : public StressTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<EagerEntityRef> eager_refs(num_prefixes);
|
||||
for (size_t j = 0; j < num_prefixes; ++j) {
|
||||
eager_refs[j].status = results[j][0].status();
|
||||
if (eager_refs[j].status.ok()) {
|
||||
eager_refs[j].columns = &results[j][0].columns();
|
||||
}
|
||||
}
|
||||
MaybeTestMultiGetEntityLazy(thread, read_opts_copy, cfh, num_prefixes,
|
||||
key_slices.data(), &eager_refs);
|
||||
} else {
|
||||
// Non-AttributeGroup MultiGetEntity verification
|
||||
|
||||
@@ -546,6 +569,16 @@ class BatchedOpsStressTest : public StressTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<EagerEntityRef> eager_refs(num_prefixes);
|
||||
for (size_t j = 0; j < num_prefixes; ++j) {
|
||||
eager_refs[j].status = statuses[j];
|
||||
if (statuses[j].ok()) {
|
||||
eager_refs[j].columns = &results[j].columns();
|
||||
}
|
||||
}
|
||||
MaybeTestMultiGetEntityLazy(thread, read_opts_copy, cfh, num_prefixes,
|
||||
key_slices.data(), &eager_refs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -574,6 +607,8 @@ class BatchedOpsStressTest : public StressTest {
|
||||
std::array<ReadOptions, num_prefixes> ro_copies;
|
||||
std::array<std::string, num_prefixes> upper_bounds;
|
||||
std::array<Slice, num_prefixes> ub_slices;
|
||||
std::array<std::function<bool(const TableProperties&)>, num_prefixes>
|
||||
table_filters;
|
||||
std::array<std::unique_ptr<Iterator>, num_prefixes> iters;
|
||||
|
||||
const Snapshot* const snapshot = db_->GetSnapshot();
|
||||
@@ -597,9 +632,9 @@ class BatchedOpsStressTest : public StressTest {
|
||||
ub_slices[i] = upper_bounds[i];
|
||||
ro_copies[i].iterate_upper_bound = &(ub_slices[i]);
|
||||
if (FLAGS_use_sqfc_for_range_queries) {
|
||||
ro_copies[i].table_filter =
|
||||
sqfc_factory_->GetTableFilterForRangeQuery(prefix_slices[i],
|
||||
ub_slices[i]);
|
||||
table_filters[i] = sqfc_factory_->GetTableFilterForRangeQuery(
|
||||
prefix_slices[i], ub_slices[i]);
|
||||
ro_copies[i].table_filter = &table_filters[i];
|
||||
}
|
||||
} else {
|
||||
// Otherwise, bound by prefix
|
||||
@@ -685,7 +720,7 @@ class BatchedOpsStressTest : public StressTest {
|
||||
// if the first iterator finished, they should have all finished
|
||||
assert(!iters[i]->Valid() ||
|
||||
!iters[i]->key().starts_with(prefix_slices[i]));
|
||||
assert(iters[i]->status().ok());
|
||||
DB_STRESS_ASSERT_OK(iters[i]->status());
|
||||
}
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
|
||||
@@ -382,6 +382,10 @@ class CfConsistencyStressTest : public StressTest {
|
||||
is_consistent = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Random single-CF read: no pinned snapshot to share with a reference, so
|
||||
// run a self-contained lazy read (it pins its own snapshot).
|
||||
MaybeTestGetEntityLazy(thread, read_opts, cfh, key);
|
||||
} else {
|
||||
// With a 1/2 chance, compare one key across all CFs
|
||||
ManagedSnapshot snapshot_guard(db_);
|
||||
@@ -422,6 +426,15 @@ class CfConsistencyStressTest : public StressTest {
|
||||
}
|
||||
}
|
||||
|
||||
// Reuse the CF[0] entity we just read (under read_opts_copy's snapshot,
|
||||
// and verified above) as the lazy read's reference for that CF.
|
||||
if (cmp_found && is_consistent) {
|
||||
const WideColumns& eager_ref = cmp_result.columns();
|
||||
MaybeTestGetEntityLazy(thread, read_opts_copy,
|
||||
column_families_[rand_column_families[0]], key,
|
||||
&eager_ref);
|
||||
}
|
||||
|
||||
if (is_consistent) {
|
||||
if (FLAGS_use_attribute_group) {
|
||||
PinnableAttributeGroups result;
|
||||
@@ -680,7 +693,7 @@ class CfConsistencyStressTest : public StressTest {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert(s.ok());
|
||||
DB_STRESS_ASSERT_OK(s);
|
||||
if (cmp_s.IsNotFound()) {
|
||||
fprintf(stderr,
|
||||
"MultiGetEntity (AttributeGroup) returns different results "
|
||||
@@ -736,6 +749,16 @@ class CfConsistencyStressTest : public StressTest {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<EagerEntityRef> eager_refs(num_keys);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
eager_refs[i].status = results[i][0].status();
|
||||
if (eager_refs[i].status.ok()) {
|
||||
eager_refs[i].columns = &results[i][0].columns();
|
||||
}
|
||||
}
|
||||
MaybeTestMultiGetEntityLazy(thread, read_opts_copy, cfhs[0], num_keys,
|
||||
key_slices.data(), &eager_refs);
|
||||
|
||||
} else {
|
||||
// Non-AttributeGroup MultiGetEntity verification
|
||||
|
||||
@@ -793,7 +816,7 @@ class CfConsistencyStressTest : public StressTest {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert(s.ok());
|
||||
DB_STRESS_ASSERT_OK(s);
|
||||
if (cmp_s.IsNotFound()) {
|
||||
fprintf(
|
||||
stderr,
|
||||
@@ -860,6 +883,26 @@ class CfConsistencyStressTest : public StressTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-CF note: the lazy read API has no cross-column-family (`CF**`)
|
||||
// overload yet -- MultiGetEntityLazy is single-CF -- so we exercise it on
|
||||
// just cfhs[0] (a cross-CF lazy overload is a planned follow-up; see the
|
||||
// lazy blob resolution plan). The attribute-group branch above reuses its
|
||||
// already-read cfhs[0] columns as the reference; the non-attribute branch
|
||||
// reads each key across CFs one at a time and so holds no batch of cfhs[0]
|
||||
// results to reuse -- fall back to a self-contained lazy read there.
|
||||
if (!FLAGS_use_attribute_group && LazyEntityReadEnabled()) {
|
||||
std::vector<std::string> lazy_key_strs;
|
||||
std::vector<Slice> lazy_key_slices;
|
||||
lazy_key_strs.reserve(num_keys);
|
||||
lazy_key_slices.reserve(num_keys);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
lazy_key_strs.emplace_back(Key(rand_keys[i]));
|
||||
lazy_key_slices.emplace_back(lazy_key_strs.back());
|
||||
}
|
||||
MaybeTestMultiGetEntityLazy(thread, read_opts_copy, cfhs[0], num_keys,
|
||||
lazy_key_slices.data());
|
||||
}
|
||||
}
|
||||
|
||||
Status TestPrefixScan(ThreadState* thread, const ReadOptions& readoptions,
|
||||
@@ -877,6 +920,7 @@ class CfConsistencyStressTest : public StressTest {
|
||||
|
||||
std::string upper_bound;
|
||||
Slice ub_slice;
|
||||
std::function<bool(const TableProperties&)> table_filter;
|
||||
|
||||
ReadOptions ro_copy = readoptions;
|
||||
std::unique_ptr<ManagedSnapshot> snapshot = nullptr;
|
||||
@@ -891,8 +935,9 @@ class CfConsistencyStressTest : public StressTest {
|
||||
ub_slice = Slice(upper_bound);
|
||||
ro_copy.iterate_upper_bound = &ub_slice;
|
||||
if (FLAGS_use_sqfc_for_range_queries) {
|
||||
ro_copy.table_filter =
|
||||
table_filter =
|
||||
sqfc_factory_->GetTableFilterForRangeQuery(prefix, ub_slice);
|
||||
ro_copy.table_filter = &table_filter;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,11 @@
|
||||
#ifdef GFLAGS
|
||||
#include "db_stress_tool/db_stress_common.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
|
||||
#include "db_stress_tool/db_stress_test_base.h"
|
||||
@@ -170,6 +173,245 @@ int64_t GetOneHotKeyID(double rand_seed, int64_t max_key) {
|
||||
return rand_local.Next() % max_key;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint64_t kLongActiveOperationTimeoutMultiplier = 4;
|
||||
|
||||
int LivenessSleepMicros() {
|
||||
const std::chrono::microseconds max_sleep_micros(
|
||||
std::numeric_limits<int>::max());
|
||||
const uint64_t max_sleep_seconds =
|
||||
std::chrono::duration_cast<std::chrono::seconds>(max_sleep_micros)
|
||||
.count();
|
||||
const std::chrono::seconds sleep_seconds(
|
||||
std::min(FLAGS_liveness_check_interval_sec, max_sleep_seconds));
|
||||
return static_cast<int>(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(sleep_seconds)
|
||||
.count());
|
||||
}
|
||||
|
||||
bool StopBgThreadIfNeeded(SharedState* shared) {
|
||||
port::Mutex* mu = shared->GetMutex();
|
||||
// The liveness watchdog must not block on the shared harness mutex; a stuck
|
||||
// holder is one of the failure modes it needs to report.
|
||||
if (!mu->TryLock()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!shared->ShouldStopBgThread()) {
|
||||
mu->Unlock();
|
||||
return false;
|
||||
}
|
||||
|
||||
shared->IncBgThreadsFinished();
|
||||
if (shared->BgThreadsFinished()) {
|
||||
shared->GetCondVar()->SignalAll();
|
||||
}
|
||||
mu->Unlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t ElapsedMicros(uint64_t now, uint64_t start) {
|
||||
return now >= start ? now - start : 0;
|
||||
}
|
||||
|
||||
uint64_t ElapsedSeconds(uint64_t now, uint64_t start) {
|
||||
constexpr uint64_t kMicrosPerSecond = 1000 * 1000;
|
||||
return ElapsedMicros(now, start) / kMicrosPerSecond;
|
||||
}
|
||||
|
||||
uint64_t ActiveOperationTimeoutSeconds(StressOperationType type,
|
||||
uint64_t no_progress_timeout_sec) {
|
||||
switch (type) {
|
||||
case StressOperationType::kNone:
|
||||
case StressOperationType::kCount:
|
||||
return 0;
|
||||
case StressOperationType::kCompactFiles:
|
||||
case StressOperationType::kCompactRange:
|
||||
case StressOperationType::kFlush:
|
||||
case StressOperationType::kVerifyDb:
|
||||
case StressOperationType::kVerifyChecksum:
|
||||
case StressOperationType::kVerifyFileChecksums:
|
||||
case StressOperationType::kIngestExternalFile:
|
||||
case StressOperationType::kBackup:
|
||||
case StressOperationType::kCheckpoint:
|
||||
case StressOperationType::kApproximateSize:
|
||||
case StressOperationType::kPauseBackground:
|
||||
case StressOperationType::kGetLiveFiles:
|
||||
return no_progress_timeout_sec * kLongActiveOperationTimeoutMultiplier;
|
||||
case StressOperationType::kReopen:
|
||||
case StressOperationType::kSetOptions:
|
||||
case StressOperationType::kManualWalFlush:
|
||||
case StressOperationType::kLockWal:
|
||||
case StressOperationType::kSyncWal:
|
||||
case StressOperationType::kMetadata:
|
||||
case StressOperationType::kDisableFileDeletions:
|
||||
case StressOperationType::kDisableManualCompaction:
|
||||
case StressOperationType::kAbortResumeCompactions:
|
||||
case StressOperationType::kGetProperty:
|
||||
case StressOperationType::kTableProperties:
|
||||
case StressOperationType::kSnapshot:
|
||||
case StressOperationType::kKeyMayExist:
|
||||
case StressOperationType::kRead:
|
||||
case StressOperationType::kPrefixScan:
|
||||
case StressOperationType::kWrite:
|
||||
case StressOperationType::kDelete:
|
||||
case StressOperationType::kDeleteRange:
|
||||
case StressOperationType::kIterate:
|
||||
case StressOperationType::kCustom:
|
||||
return no_progress_timeout_sec;
|
||||
}
|
||||
return no_progress_timeout_sec;
|
||||
}
|
||||
|
||||
void PrintLivenessState(SharedState* shared, uint64_t now) {
|
||||
fprintf(stderr, "Finished db_stress operations=%" PRIu64 "\n",
|
||||
shared->GetFinishedOps());
|
||||
fprintf(stderr, "Successful compactions=%" PRIu64 "\n",
|
||||
shared->GetSuccessfulCompactions());
|
||||
fprintf(stderr, "Completed db_stress operation scopes by type:");
|
||||
bool printed_completed_op_count = false;
|
||||
for (size_t i = 1; i < kStressOperationTypeCount; ++i) {
|
||||
const auto type = static_cast<StressOperationType>(i);
|
||||
const uint64_t count = shared->GetCompletedOpsForDiagnostics(type);
|
||||
if (count > 0) {
|
||||
fprintf(stderr, " %s=%" PRIu64, StressOperationTypeName(type), count);
|
||||
printed_completed_op_count = true;
|
||||
}
|
||||
}
|
||||
if (!printed_completed_op_count) {
|
||||
fprintf(stderr, " none");
|
||||
}
|
||||
fprintf(stderr, "\n");
|
||||
|
||||
for (uint32_t tid = 0; tid < shared->GetNumThreads(); ++tid) {
|
||||
const ThreadOperationSnapshot snapshot =
|
||||
shared->GetThreadOperationSnapshot(tid);
|
||||
if (snapshot.type == StressOperationType::kNone ||
|
||||
snapshot.started_micros == 0) {
|
||||
fprintf(stderr, "thread %" PRIu32 " active_op=none\n", tid);
|
||||
continue;
|
||||
}
|
||||
|
||||
fprintf(stderr,
|
||||
"thread %" PRIu32 " active_op=%s active_op_started_micros=%" PRIu64
|
||||
" active_op_elapsed_micros=%" PRIu64 "\n",
|
||||
tid, StressOperationTypeName(snapshot.type),
|
||||
snapshot.started_micros,
|
||||
ElapsedMicros(now, snapshot.started_micros));
|
||||
}
|
||||
}
|
||||
|
||||
bool HasStuckWriteOperation(SharedState* shared, uint64_t now,
|
||||
uint64_t timeout_sec) {
|
||||
for (uint32_t tid = 0; tid < shared->GetNumThreads(); ++tid) {
|
||||
const ThreadOperationSnapshot snapshot =
|
||||
shared->GetThreadOperationSnapshot(tid);
|
||||
const uint64_t elapsed_micros = ElapsedMicros(now, snapshot.started_micros);
|
||||
const uint64_t elapsed_seconds =
|
||||
ElapsedSeconds(now, snapshot.started_micros);
|
||||
if (snapshot.type == StressOperationType::kWrite &&
|
||||
snapshot.started_micros != 0 && elapsed_seconds >= timeout_sec) {
|
||||
fprintf(stderr,
|
||||
"Liveness watchdog detected a stuck write operation on thread "
|
||||
"%" PRIu32 " for %" PRIu64
|
||||
" seconds. active_op_elapsed_micros=%" PRIu64
|
||||
" active_op_timeout_seconds=%" PRIu64 "\n",
|
||||
tid, elapsed_seconds, elapsed_micros, timeout_sec);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HasTimedOutNonWriteOperation(SharedState* shared, uint64_t now,
|
||||
uint64_t no_progress_timeout_sec) {
|
||||
for (uint32_t tid = 0; tid < shared->GetNumThreads(); ++tid) {
|
||||
const ThreadOperationSnapshot snapshot =
|
||||
shared->GetThreadOperationSnapshot(tid);
|
||||
if (snapshot.type == StressOperationType::kWrite ||
|
||||
snapshot.started_micros == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint64_t timeout_sec =
|
||||
ActiveOperationTimeoutSeconds(snapshot.type, no_progress_timeout_sec);
|
||||
if (timeout_sec != 0 &&
|
||||
ElapsedSeconds(now, snapshot.started_micros) >= timeout_sec) {
|
||||
fprintf(stderr,
|
||||
"Liveness watchdog detected a timed out active operation on "
|
||||
"thread %" PRIu32
|
||||
". active_op=%s active_op_elapsed_micros=%" PRIu64
|
||||
" active_op_timeout_seconds=%" PRIu64 "\n",
|
||||
tid, StressOperationTypeName(snapshot.type),
|
||||
ElapsedMicros(now, snapshot.started_micros), timeout_sec);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void LivenessWatchdogThread(void* v) {
|
||||
assert(FLAGS_liveness_check_interval_sec > 0);
|
||||
assert(FLAGS_liveness_no_progress_timeout_sec > 0);
|
||||
|
||||
auto* thread = static_cast<ThreadState*>(v);
|
||||
SharedState* shared = thread->shared;
|
||||
|
||||
uint64_t last_finished_ops = shared->GetFinishedOps();
|
||||
uint64_t last_progress_time_micros = raw_env->NowMicros();
|
||||
const uint64_t no_progress_timeout_sec =
|
||||
FLAGS_liveness_no_progress_timeout_sec;
|
||||
|
||||
while (true) {
|
||||
if (StopBgThreadIfNeeded(shared)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool started = shared->OperationStarted();
|
||||
const bool all_operated = shared->OperationFinished();
|
||||
|
||||
const uint64_t now = raw_env->NowMicros();
|
||||
if (!started || all_operated) {
|
||||
last_finished_ops = shared->GetFinishedOps();
|
||||
last_progress_time_micros = now;
|
||||
raw_env->SleepForMicroseconds(LivenessSleepMicros());
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint64_t finished_ops = shared->GetFinishedOps();
|
||||
if (HasStuckWriteOperation(shared, now, no_progress_timeout_sec)) {
|
||||
PrintLivenessState(shared, now);
|
||||
fflush(stderr);
|
||||
shared->TerminateWithoutMutex();
|
||||
} else if (HasTimedOutNonWriteOperation(shared, now,
|
||||
no_progress_timeout_sec)) {
|
||||
PrintLivenessState(shared, now);
|
||||
fflush(stderr);
|
||||
shared->TerminateWithoutMutex();
|
||||
} else if (finished_ops != last_finished_ops) {
|
||||
last_finished_ops = finished_ops;
|
||||
last_progress_time_micros = now;
|
||||
} else if (now >= last_progress_time_micros &&
|
||||
ElapsedSeconds(now, last_progress_time_micros) >=
|
||||
no_progress_timeout_sec) {
|
||||
fprintf(stderr,
|
||||
"Liveness watchdog detected no completed db_stress operations "
|
||||
"for %" PRIu64 " seconds. finished_ops=%" PRIu64 "\n",
|
||||
FLAGS_liveness_no_progress_timeout_sec, finished_ops);
|
||||
PrintLivenessState(shared, now);
|
||||
fflush(stderr);
|
||||
shared->TerminateWithoutMutex();
|
||||
} else if (now < last_progress_time_micros) {
|
||||
last_progress_time_micros = now;
|
||||
}
|
||||
|
||||
raw_env->SleepForMicroseconds(LivenessSleepMicros());
|
||||
}
|
||||
}
|
||||
|
||||
void PoolSizeChangeThread(void* v) {
|
||||
assert(FLAGS_compaction_thread_pool_adjust_interval > 0);
|
||||
ThreadState* thread = static_cast<ThreadState*>(v);
|
||||
@@ -754,6 +996,19 @@ std::string GetNowNanos() {
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string GetReadTimestamp() {
|
||||
uint64_t read_timestamp = raw_env->NowNanos();
|
||||
if (!FLAGS_persist_user_defined_timestamps) {
|
||||
// Add 10 seconds of headroom because a concurrent flush can advance
|
||||
// full_history_ts_low making the read timestamp invalid.
|
||||
read_timestamp += 10'000'000'000ULL;
|
||||
}
|
||||
|
||||
std::string ret;
|
||||
PutFixed64(&ret, read_timestamp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint64_t GetWriteUnixTime(ThreadState* thread) {
|
||||
static uint64_t kPreserveSeconds =
|
||||
std::max(FLAGS_preserve_internal_time_seconds,
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "db_stress_tool/db_stress_env_wrapper.h"
|
||||
#include "db_stress_tool/db_stress_listener.h"
|
||||
#include "db_stress_tool/db_stress_shared_state.h"
|
||||
#include "db_stress_tool/db_stress_status.h"
|
||||
#include "db_stress_tool/db_stress_test_base.h"
|
||||
#include "logging/logging.h"
|
||||
#include "monitoring/histogram.h"
|
||||
@@ -228,6 +229,10 @@ DECLARE_uint64(sst_file_manager_bytes_per_truncate);
|
||||
DECLARE_int32(backup_one_in);
|
||||
DECLARE_uint64(backup_max_size);
|
||||
DECLARE_int32(checkpoint_one_in);
|
||||
DECLARE_int32(checkpoint_engine_max_background_operations);
|
||||
DECLARE_bool(checkpoint_engine_use_link_file_when_available);
|
||||
DECLARE_int32(parallel_checkpoint_one_in);
|
||||
DECLARE_int32(subset_cf_checkpoint_one_in);
|
||||
DECLARE_int32(ingest_external_file_one_in);
|
||||
DECLARE_int32(ingest_external_file_width);
|
||||
DECLARE_int32(ingest_external_file_prepare_commit_one_in);
|
||||
@@ -244,6 +249,7 @@ DECLARE_int32(pause_background_one_in);
|
||||
DECLARE_int32(disable_file_deletions_one_in);
|
||||
DECLARE_int32(disable_manual_compaction_one_in);
|
||||
DECLARE_int32(abort_and_resume_compactions_one_in);
|
||||
DECLARE_int32(abort_and_resume_cf_compactions_one_in);
|
||||
DECLARE_int32(compact_range_width);
|
||||
DECLARE_int32(acquire_snapshot_one_in);
|
||||
DECLARE_bool(compare_full_db_state_snapshot);
|
||||
@@ -253,6 +259,7 @@ DECLARE_bool(use_multiget);
|
||||
DECLARE_bool(use_async_db_api);
|
||||
DECLARE_bool(use_get_entity);
|
||||
DECLARE_bool(use_multi_get_entity);
|
||||
DECLARE_int32(lazy_entity_read_one_in);
|
||||
DECLARE_int32(readpercent);
|
||||
DECLARE_int32(prefixpercent);
|
||||
DECLARE_int32(writepercent);
|
||||
@@ -268,6 +275,8 @@ DECLARE_int32(compression_max_dict_bytes);
|
||||
DECLARE_int32(compression_zstd_max_train_bytes);
|
||||
DECLARE_int32(compression_parallel_threads);
|
||||
DECLARE_uint64(compression_max_dict_buffer_bytes);
|
||||
DECLARE_bool(compression_auto_skip);
|
||||
DECLARE_int32(compression_auto_skip_min_sample_every);
|
||||
DECLARE_bool(compression_use_zstd_dict_trainer);
|
||||
DECLARE_bool(compression_checksum);
|
||||
DECLARE_string(checksum_type);
|
||||
@@ -340,6 +349,7 @@ DECLARE_bool(enable_blob_direct_write);
|
||||
DECLARE_uint64(blob_direct_write_partitions);
|
||||
DECLARE_uint64(min_blob_size);
|
||||
DECLARE_uint64(blob_file_size);
|
||||
DECLARE_uint64(blob_file_writable_file_max_buffer_size);
|
||||
DECLARE_string(blob_compression_type);
|
||||
DECLARE_bool(enable_blob_garbage_collection);
|
||||
DECLARE_double(blob_garbage_collection_age_cutoff);
|
||||
@@ -818,6 +828,8 @@ void PoolSizeChangeThread(void* v);
|
||||
|
||||
void DbVerificationThread(void* v);
|
||||
|
||||
void LivenessWatchdogThread(void* v);
|
||||
|
||||
void RemoteCompactionWorkerThread(void* v);
|
||||
|
||||
void CompressedCacheSetCapacityThread(void* v);
|
||||
@@ -880,6 +892,8 @@ int64_t GetOneHotKeyID(double rand_seed, int64_t max_key);
|
||||
|
||||
std::string GetNowNanos();
|
||||
|
||||
std::string GetReadTimestamp();
|
||||
|
||||
uint64_t GetWriteUnixTime(ThreadState* thread);
|
||||
|
||||
std::shared_ptr<FileChecksumGenFactory> GetFileChecksumImpl(
|
||||
|
||||
@@ -78,11 +78,8 @@ bool RunStressTestImpl(SharedState* shared) {
|
||||
if (s.ok() && !ev_dir.empty()) {
|
||||
s = InitUnverifiedSubdir(ev_dir);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "%sFailed to setup unverified state dir: %s\n",
|
||||
db_label.c_str(), s.ToString().c_str());
|
||||
exit(1);
|
||||
}
|
||||
DB_STRESS_ASSERT_OK_MSG(s, "%sFailed to setup unverified state dir",
|
||||
db_label.c_str());
|
||||
}
|
||||
|
||||
stress->InitDb(shared);
|
||||
@@ -95,6 +92,13 @@ bool RunStressTestImpl(SharedState* shared) {
|
||||
|
||||
shared->SetThreads(n);
|
||||
|
||||
const bool liveness_watchdog_enabled =
|
||||
FLAGS_liveness_check_interval_sec > 0 &&
|
||||
FLAGS_liveness_no_progress_timeout_sec > 0;
|
||||
if (liveness_watchdog_enabled) {
|
||||
shared->IncBgThreads();
|
||||
}
|
||||
|
||||
if (FLAGS_continuous_verification_interval > 0) {
|
||||
shared->IncBgThreads();
|
||||
}
|
||||
@@ -131,6 +135,11 @@ bool RunStressTestImpl(SharedState* shared) {
|
||||
raw_env->StartThread(DbVerificationThread, &continuous_verification_thread);
|
||||
}
|
||||
|
||||
ThreadState liveness_watchdog_thread(0, shared);
|
||||
if (liveness_watchdog_enabled) {
|
||||
raw_env->StartThread(LivenessWatchdogThread, &liveness_watchdog_thread);
|
||||
}
|
||||
|
||||
// Spawn at most one CompressedCacheSetCapacityThread globally. The cache
|
||||
// is shared across all DBs, and the thread's SetCapacity(0)/assert(==0)
|
||||
// sequence would race if multiple DBs each spawned their own copy.
|
||||
@@ -177,11 +186,8 @@ bool RunStressTestImpl(SharedState* shared) {
|
||||
if (s.ok() && !ev_dir.empty()) {
|
||||
s = DestroyUnverifiedSubdir(ev_dir);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "%sFailed to cleanup unverified state dir: %s\n",
|
||||
db_label.c_str(), s.ToString().c_str());
|
||||
exit(1);
|
||||
}
|
||||
DB_STRESS_ASSERT_OK_MSG(s, "%sFailed to cleanup unverified state dir",
|
||||
db_label.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +209,7 @@ bool RunStressTestImpl(SharedState* shared) {
|
||||
}
|
||||
if (ShouldDisableAutoCompactionsBeforeVerifyDb()) {
|
||||
Status s = stress->EnableAutoCompaction();
|
||||
assert(s.ok());
|
||||
DB_STRESS_ASSERT_OK(s);
|
||||
}
|
||||
fprintf(stdout, "%s %sStarting database operations\n",
|
||||
clock->TimeToString(now / 1000000).c_str(), db_label.c_str());
|
||||
@@ -268,7 +274,7 @@ bool RunStressTestImpl(SharedState* shared) {
|
||||
}
|
||||
|
||||
if (FLAGS_compaction_thread_pool_adjust_interval > 0 ||
|
||||
FLAGS_continuous_verification_interval > 0 ||
|
||||
FLAGS_continuous_verification_interval > 0 || liveness_watchdog_enabled ||
|
||||
FLAGS_compressed_secondary_cache_size > 0 ||
|
||||
FLAGS_compressed_secondary_cache_ratio > 0.0 ||
|
||||
remote_compaction_worker_thread_count > 0) {
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include "db_stress_tool/db_stress_status.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
#ifdef GFLAGS
|
||||
@@ -82,7 +84,7 @@ SstQueryFilterConfigsManager& DbStressSqfcManager() {
|
||||
static std::shared_ptr<SstQueryFilterConfigsManager> mgr;
|
||||
std::call_once(flag, []() {
|
||||
Status s = SstQueryFilterConfigsManager::MakeShared(data, &mgr);
|
||||
assert(s.ok());
|
||||
DB_STRESS_ASSERT_OK(s);
|
||||
assert(mgr);
|
||||
});
|
||||
return *mgr;
|
||||
|
||||
@@ -156,6 +156,16 @@ DEFINE_bool(verbose, false, "Verbose");
|
||||
DEFINE_bool(progress_reports, true,
|
||||
"If true, db_stress will report number of finished operations");
|
||||
|
||||
DEFINE_uint64(
|
||||
liveness_check_interval_sec, 0,
|
||||
"If non-zero, check periodically whether db_stress operations are making "
|
||||
"progress. Requires --liveness_no_progress_timeout_sec to be non-zero.");
|
||||
|
||||
DEFINE_uint64(
|
||||
liveness_no_progress_timeout_sec, 0,
|
||||
"If non-zero with --liveness_check_interval_sec, terminate db_stress when "
|
||||
"no operation completes for this many seconds during the operation phase.");
|
||||
|
||||
DEFINE_uint64(db_write_buffer_size,
|
||||
ROCKSDB_NAMESPACE::Options().db_write_buffer_size,
|
||||
"Number of bytes to buffer in all memtables before compacting");
|
||||
@@ -527,6 +537,13 @@ DEFINE_uint64(blob_file_size,
|
||||
ROCKSDB_NAMESPACE::AdvancedColumnFamilyOptions().blob_file_size,
|
||||
"[Integrated BlobDB] The size limit for blob files.");
|
||||
|
||||
DEFINE_uint64(
|
||||
blob_file_writable_file_max_buffer_size,
|
||||
ROCKSDB_NAMESPACE::AdvancedColumnFamilyOptions()
|
||||
.blob_file_writable_file_max_buffer_size,
|
||||
"[Integrated BlobDB] Max WritableFileWriter buffer size for blob files. "
|
||||
"0 means inherit writable_file_max_buffer_size.");
|
||||
|
||||
DEFINE_string(blob_compression_type, "none",
|
||||
"[Integrated BlobDB] The compression algorithm to use for large "
|
||||
"values stored in blob files.");
|
||||
@@ -861,6 +878,25 @@ DEFINE_int32(checkpoint_one_in, 0,
|
||||
"every N operations on average. 0 indicates CreateCheckpoint() "
|
||||
"is disabled.");
|
||||
|
||||
DEFINE_int32(checkpoint_engine_max_background_operations, 1,
|
||||
"Background-thread parallelism of the shared CheckpointEngine "
|
||||
"(always opened). Values <= 0 are treated as 1.");
|
||||
|
||||
DEFINE_bool(checkpoint_engine_use_link_file_when_available, true,
|
||||
"Whether the parallel CheckpointEngine may hard-link data files "
|
||||
"instead of copying them. False forces the parallel copy path.");
|
||||
|
||||
DEFINE_int32(parallel_checkpoint_one_in, 0,
|
||||
"Each checkpoint uses the parallel CheckpointEngine with "
|
||||
"probability 1/N, else the legacy serial API (0 = always serial, "
|
||||
"1 = always parallel).");
|
||||
|
||||
DEFINE_int32(subset_cf_checkpoint_one_in, 0,
|
||||
"Each checkpoint covers only a random proper subset of the column "
|
||||
"families with probability 1/N (0 = always all column families, "
|
||||
"1 = always a subset). Ignored when there is only the default "
|
||||
"column family.");
|
||||
|
||||
DEFINE_int32(ingest_external_file_one_in, 0,
|
||||
"If non-zero, then IngestExternalFile() will be called once for "
|
||||
"every N operations on average. 0 indicates IngestExternalFile() "
|
||||
@@ -937,6 +973,11 @@ DEFINE_int32(abort_and_resume_compactions_one_in, 0,
|
||||
"If non-zero, then AbortAllCompactions()+Resume will be called "
|
||||
"once for every N ops on average. 0 disables.");
|
||||
|
||||
DEFINE_int32(abort_and_resume_cf_compactions_one_in, 0,
|
||||
"If non-zero, then AbortCompactions(cf)+ResumeCompactions(cf) "
|
||||
"will be called on a random column family once for every N ops "
|
||||
"on average. 0 disables.");
|
||||
|
||||
DEFINE_int32(compact_range_width, 10000,
|
||||
"The width of the ranges passed to CompactRange().");
|
||||
|
||||
@@ -968,6 +1009,18 @@ DEFINE_bool(use_get_entity, false, "If set, use the GetEntity API for reads.");
|
||||
DEFINE_bool(use_multi_get_entity, false,
|
||||
"If set, use the MultiGetEntity API for reads.");
|
||||
|
||||
DEFINE_int32(
|
||||
lazy_entity_read_one_in, 0,
|
||||
"If greater than 0, then on roughly 1/N of GetEntity / MultiGetEntity "
|
||||
"operations, additionally read the same key(s) via the lazy wide-column "
|
||||
"API "
|
||||
"(GetEntityLazy / MultiGetEntityLazy) under a pinned snapshot and verify "
|
||||
"the "
|
||||
"lazily-resolved columns (a random subset, possibly none) match the eager "
|
||||
"GetEntity result. Only takes effect when open_files == -1 (required by "
|
||||
"the "
|
||||
"lazy API), user_timestamp_size == 0, and not using transactions.");
|
||||
|
||||
DEFINE_int32(test_ingest_standalone_range_deletion_one_in, 0,
|
||||
"If non-zero, file ingestion flow will test standalone range "
|
||||
"deletion file once every N file ingestion operations.");
|
||||
@@ -1079,6 +1132,15 @@ DEFINE_uint64(compression_max_dict_buffer_bytes, 0,
|
||||
"Buffering limit for SST file data to sample for dictionary "
|
||||
"compression.");
|
||||
|
||||
DEFINE_bool(compression_auto_skip, false,
|
||||
"Enable AutoSkip compression: stop attempting compression on data "
|
||||
"blocks once it is not paying off (reuses max_compressed_bytes_per_"
|
||||
"kb as the bar).");
|
||||
|
||||
DEFINE_int32(compression_auto_skip_min_sample_every, 0,
|
||||
"AutoSkip nominal sampling interval (skipped data blocks between "
|
||||
"forced compression samples). 0 selects an internal default.");
|
||||
|
||||
DEFINE_bool(
|
||||
compression_use_zstd_dict_trainer, true,
|
||||
"Use zstd's trainer to generate dictionary. If the options is false, "
|
||||
|
||||
@@ -48,11 +48,7 @@ UniqueIdVerifier::UniqueIdVerifier(const std::string& dir)
|
||||
IOOptions opts;
|
||||
|
||||
Status st = fs->CreateDirIfMissing(dir, opts, nullptr);
|
||||
if (!st.ok()) {
|
||||
fprintf(stderr, "Failed to create directory %s: %s\n", dir.c_str(),
|
||||
st.ToString().c_str());
|
||||
exit(1);
|
||||
}
|
||||
DB_STRESS_ASSERT_OK_MSG(st, "Failed to create directory %s", dir.c_str());
|
||||
|
||||
// Avoid relying on ReopenWritableFile which is not supported by all
|
||||
// file systems. Create a new file and copy the old file contents to it.
|
||||
@@ -99,7 +95,7 @@ UniqueIdVerifier::UniqueIdVerifier(const std::string& dir)
|
||||
id_set_.clear();
|
||||
reader.reset();
|
||||
s = fs->DeleteFile(tmp_path, opts, /*dbg*/ nullptr);
|
||||
assert(s.ok());
|
||||
DB_STRESS_ASSERT_OK(s);
|
||||
size = 0;
|
||||
}
|
||||
break;
|
||||
@@ -113,9 +109,9 @@ UniqueIdVerifier::UniqueIdVerifier(const std::string& dir)
|
||||
// the failure. (Issue #9021)
|
||||
Status s2 = fs->FileExists(tmp_path, opts, /*dbg*/ nullptr);
|
||||
if (!s2.IsNotFound()) {
|
||||
fprintf(stderr, "Error opening unique id file: %s\n",
|
||||
s.ToString().c_str());
|
||||
assert(false);
|
||||
DB_STRESS_ASSERT_OK_MSG(
|
||||
s, "Error opening unique id file; FileExists returned %s",
|
||||
s2.ToString().c_str());
|
||||
}
|
||||
size = 0;
|
||||
}
|
||||
@@ -152,7 +148,7 @@ UniqueIdVerifier::~UniqueIdVerifier() {
|
||||
ThreadStatusUtil::SetThreadOperation(ThreadStatus::OperationType::OP_UNKNOWN);
|
||||
IOStatus s;
|
||||
s = data_file_writer_->Close(IOOptions());
|
||||
assert(s.ok());
|
||||
DB_STRESS_ASSERT_OK(s);
|
||||
ThreadStatusUtil::SetThreadOperation(cur_op_type);
|
||||
}
|
||||
|
||||
|
||||
@@ -231,6 +231,10 @@ class DbStressListener : public EventListener {
|
||||
precommitted_jobs_.erase(it);
|
||||
}
|
||||
|
||||
if (ci.status.ok() && !ci.aborted) {
|
||||
shared_->IncSuccessfulCompactions();
|
||||
}
|
||||
|
||||
// pretending doing some work here
|
||||
RandomSleep();
|
||||
}
|
||||
|
||||
Loaded 100 of 211 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user