Compare commits

..
Author SHA1 Message Date
Richard Barnes fa616f0e79 Fix string-conversion issue in internal_repo_rocksdb/repo/utilities/persistent_cache/volatile_tier_impl.cc +5 (#14296)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14296

This could is triggering `-Wstring-conversion`, which presents as:
```
warning: implicit conversion turns string literal into bool: A to B
```
This is often a bug and what was intended. The most frequent cause is the code was:
```
void foo(bool) { ... }
void foo(std::string) { ... }
foo("this gets interpreted as a bool");
```

It is also possible the issue is innocuous as part of an assert:
```
assert(!"this string is true, so the assertion is false");
EXPECT_FALSE("this string is true, so the expect fails");
```
in these cases the use is to "cute", so we modify the code to make it more obvious.
```
assert(false && "the compiler recognizes and doesn't complain about this pattern");
FAIL() << "much more obvious";
```

Reviewed By: anand1976

Differential Revision: D92013593

fbshipit-source-id: 0b4e00339bef3f76fc5b9ad35e2383c5e4f828f9
2026-02-25 10:30:50 -08:00
Richard Barnes 96db15c6e6 Fix string-conversion issue in internal_repo_rocksdb/repo/table/block_based/index_builder.cc +1
Summary:
This could is triggering `-Wstring-conversion`, which presents as:
```
warning: implicit conversion turns string literal into bool: A to B
```
This is often a bug and what was intended. The most frequent cause is the code was:
```
void foo(bool) { ... }
void foo(std::string) { ... }
foo("this gets interpreted as a bool");
```

It is also possible the issue is innocuous as part of an assert:
```
assert(false && "this string is true, so the assertion is false");
EXPECT_FALSE("this string is true, so the expect fails");
```
in these cases the use is to "cute", so we modify the code to make it more obvious.
```
assert(false && "the compiler recognizes and doesn't complain about this pattern");
FAIL() << "much more obvious";
```

Differential Revision: D92886041

fbshipit-source-id: 6adfaa102f12e293491cc579ec92b48834d1d0a8
2026-02-25 10:30:50 -08:00
Richard Barnes 912596031a Fix string-conversion issue in internal_repo_rocksdb/repo/utilities/persistent_cache/block_cache_tier_file.cc +2 (#14312)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14312

This could is triggering `-Wstring-conversion`, which presents as:
```
warning: implicit conversion turns string literal into bool: A to B
```
This is often a bug and what was intended. The most frequent cause is the code was:
```
void foo(bool) { ... }
void foo(std::string) { ... }
foo("this gets interpreted as a bool");
```

It is also possible the issue is innocuous as part of an assert:
```
assert(!"this string is true, so the assertion is false");
EXPECT_FALSE("this string is true, so the expect fails");
```
in these cases the use is to "cute", so we modify the code to make it more obvious.
```
assert(false && "the compiler recognizes and doesn't complain about this pattern");
FAIL() << "much more obvious";
```

Reviewed By: dmm-fb

Differential Revision: D92528316

fbshipit-source-id: 93fbb624e8731c4cdb559746b44c1aa71d786304
2026-02-25 10:30:50 -08:00
Andrew Chang f4fe40845a Update HISTORY and version to 10.11.2 2026-02-25 10:30:29 -08:00
Andrew Chang 40a5f27f6e Check io_uring probe result in SupportedOps (#14355)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14355

SupportedOps advertised kAsyncIO based only on the IsIOUringEnabled() weak symbol check, without verifying that the constructor's io_uring probe actually succeeded. Add a thread_local_async_read_io_urings_ null check so kAsyncIO is only reported when the probe passed. Also update the constructor to probe with the same IORING_SETUP_SINGLE_ISSUER | IORING_SETUP_DEFER_TASKRUN flags that ReadAsync and MultiRead use at runtime.

Reviewed By: anand1976

Differential Revision: D93780065

fbshipit-source-id: 6f51f544b267cb39d09b49949a9485f55eeae12e
2026-02-24 16:36:14 -08:00
Andrew Chang 9676ce2f84 Propagate file_checksum through FileOptions on NewRandomAccessFile (#14321)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14321

Add file_checksum and file_checksum_func_name fields to FileOptions so that downstream FileSystem implementations can access per-file checksum metadata when SST files are opened. The fields are populated from FileMetaData at all call sites where SST files are opened via NewRandomAccessFile: TableCache::GetTableReader, Version::GetTableProperties, and CompactionJob::ReadTablePropertiesDirectly. Also fixes the fallback path in TableCache::GetTableReader to use the local fopts (with temperature and checksum) instead of the original file_options.

Added a kNoFileChecksumFuncName which is distinct from  kUnknownFileChecksumFuncName:

 - kUnknownFileChecksumFuncName ("Unknown"): We have FileMetaData for this file, and the metadata says no checksum was computed (no factory was configured when the file was written). This is a property of the file itself.
- kNoFileChecksumFuncName ("Unavailable"): We don't even have FileMetaData — we're opening this file in a context where there's no checksum metadata to propagate at all (e.g., SstFileDumper, SstFileReader, checksum generation). It's a property of the call site, not the file.

So the assertion file_checksum.empty() is correct for both, but for different reasons — one says "the file has no checksum," the other says "we have no idea about this file's checksum."

Reviewed By: pdillinger

Differential Revision: D92728944

fbshipit-source-id: 8fd34ea22ca87090b26d0a55c921f354f97f1ffc
2026-02-24 16:36:06 -08:00
Peter Dillinger e506aa7154 Fix format compatibility issues, extend test (#14323)
Summary:
See https://github.com/facebook/rocksdb/issues/14240 which brought this to my attention. Here I've added range deletions and compactions to the format compatible test, and fixed or worked-around compatibility issues (likely longstanding).

The first fix was in Version::MaybeInitializeFileMetaData for an assertion failure simply from adding range deletions from some 5.x version.

The second fix is a broader work-around for older SST files with unreliable num_entries/num_range_deletions/num_deletions statistics in their table properties. We depend on them only for some paranoid checks for compaction, so in my assessment the best way to deal with those files is to exclude the paranoid checks when dealing with the files with unrelaible data. (Details in code comments.) The important part is that compacting old files is exceptionally rare, so we aren't really interefering with the paranoid checks doing thier job on an ongoing basis.

This depends on https://github.com/facebook/rocksdb/issues/14315 (just landed) because there is a remaining undiagnosed problem with some very early releases, but I'm not fixing that because its support is being dropped.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14323

Test Plan: test extended (ran locally excluding some releases)

Reviewed By: xingbowang

Differential Revision: D93032653

Pulled By: pdillinger

fbshipit-source-id: f90b32f30ba4764692e68d23705f42c778e0dc1d
2026-02-16 11:13:06 -08:00
Anand Ananthabhotla 6d4c2c3297 Fix racy assertion in AbortIOPartialHandlesBug test (#14319)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14319

The test asserted h1->is_finished and h2->is_finished immediately after AbortIO({H0}), before calling Poll. This is invalid because AbortIO only guarantees that handles in its abort set are finalized. Non-aborted handles' CQEs may or may not be consumed during AbortIO depending on io_uring completion ordering. If H0's two CQEs (original read + cancel) arrive before H1/H2's CQEs, AbortIO breaks out of its wait loop without processing them. Move the H1/H2 is_finished assertions to after Poll, which correctly handles either case. Also remove the racy req_count checks for non-aborted handles since Poll does not increment req_count.

Reviewed By: jaykorean

Differential Revision: D92848827

fbshipit-source-id: 0c09b44ceada99877e8311cff799fa94f1056545
2026-02-16 11:13:06 -08:00
Anand Ananthabhotla 2ceb3ef4d2 Add a cleanup target to crash_test.mk (#14286)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14286

Add the db_c leanup target which can be used by CI test scripts to delete the db on failure. The db_crashtest.py doesn't automatically delete on error.

Reviewed By: jaykorean

Differential Revision: D91912877

fbshipit-source-id: d36ec0896fba64faaafe055d8673e437e85d0c3a
2026-02-06 12:20:35 -08:00
anand76 edc76202a2 Update HISTORY and version to 10.11.1 2026-02-06 12:20:35 -08:00
Andrew Chang b44c70adcf Fix crash in GetLiveFilesStorageInfo on read-only DB (#14306)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14306

GetLiveFilesStorageInfo crashes when called on a read-only RocksDB because
it calls FlushWAL(), which accesses logs_.back() on an empty deque.

Root cause: DBImplReadOnly overrides SyncWAL() to return NotSupported, but
does NOT override FlushWAL(). Read-only DBs have an empty logs_ deque
because they don't create WAL writers during recovery - there's nothing to
write, so no WAL infrastructure is initialized.

The reason SyncWAL was originally marked NotSupported is that these WAL
operations (SyncWAL syncs buffer to disk, FlushWAL flushes to OS buffer)
require an active WAL writer at logs_.back().writer. Since read-only DBs:
1. Cannot perform writes
2. Don't create WAL files for writing
3. Have an empty logs_ deque

...there's no WAL writer to sync or flush. The operations are semantically
meaningless, not just "forbidden write operations."

The fix adds a FlushWAL override matching the SyncWAL pattern. The caller
in db_filesnapshot.cc:403-405 already handles IsNotSupported() gracefully:
  if (s.IsNotSupported()) { s = Status::OK(); }

Reviewed By: pdillinger

Differential Revision: D92419557

fbshipit-source-id: 7079071209b3c7be41a2c98c9b691e68bc031595
2026-02-06 12:20:35 -08:00
Andrew Chang 71eb5a1646 Propagate Poll errors in FilePrefetchBuffer::PollIfNeeded (#14282)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14282

Previously, `PollIfNeeded` returned `void` and silently ignored errors from
`fs_->Poll()` by calling `PermitUncheckedError()`. This could lead to silent
data corruption or unexpected behavior when Poll operations fail.

This diff changes `PollIfNeeded` to return `Status` and properly propagate
Poll errors to callers. When Poll fails:
1. The IO handle is cleaned up via `DestroyAndClearIOHandle`
2. The error status is returned to the caller
3. Callers (`HandleOverlappingAsyncData` and `PrefetchInternal`) now check
   and propagate this error

Also adds a `TEST_SYNC_POINT_CALLBACK` to allow tests to inject Poll errors.

Reviewed By: anand1976

Differential Revision: D91624185

fbshipit-source-id: 8dd0ee6588ed1ce4bf080bcf857b778c5140ccf5
2026-02-06 12:20:35 -08:00
Anand Ananthabhotla 63a256d768 Fix AbortIO consuming completions for non-aborted handles (#14301)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14301

When AbortIO was called with a subset of outstanding async read handles,
it would consume io_uring completions for handles NOT in the abort set
but fail to finalize them. This caused subsequent Poll calls on those
handles to hang forever waiting for completions that had already been
consumed.

The fix adds an `is_being_aborted` flag to Posix_IOHandle that is set
when submitting the cancel request. When processing completions in
AbortIO, handles with this flag wait for req_count==2 (original + cancel),
while handles without the flag are finalized immediately at req_count==1.

Also refactored the completion finalization logic into a shared
FinalizeAsyncRead() helper function used by both Poll and AbortIO.

Reviewed By: mszeszko-meta, archang19

Differential Revision: D92230883

fbshipit-source-id: e6d11e009a4930e5608459771990f6cf7d46d827
2026-02-06 11:47:45 -08:00
mszeszko-meta 87d4b0d579 Merge pull request #14289 from facebook/cherry_pick_14280_to_10_11
Cherry picks into 10.11.fb
2026-02-02 10:47:06 -08:00
Adam Retter 09bc20b16e Fixes the Windows VS 2022 build (#14280)
Summary:
When building a Release on Windows RTTI is not available, so asserts that use dynamic_cast need to be disabled

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14280

Reviewed By: nmk70

Differential Revision: D91807791

Pulled By: mszeszko-meta

fbshipit-source-id: e29c19c757bcd076a1f09ed40b306bb50ba9e882
2026-01-30 16:36:19 -08:00
Peter Dillinger dd9a806edd Update HISTORY for 10.11.0 2026-01-26 12:42:18 -08:00
603 changed files with 11012 additions and 81155 deletions

No files matched your search

-86
View File
@@ -1,86 +0,0 @@
# When making changes, verify the output of:
# clang-tidy -list-checks
---
Checks: "-*,\
bugprone-argument-comment,\
bugprone-dangling-handle,\
bugprone-fold-init-type,\
bugprone-forward-declaration-namespace,\
bugprone-forwarding-reference-overload,\
bugprone-shadow,\
bugprone-sizeof-*,\
bugprone-string-constructor,\
bugprone-undefined-memory-manipulation,\
bugprone-unused-return-value,\
bugprone-use-after-move,\
cert-env33-c,\
cert-err58-cpp,\
cert-msc30-c,\
cert-msc50-cpp,\
clang-analyzer-*,\
clang-diagnostic-*,\
-clang-diagnostic-missing-designated-field-initializers,\
concurrency-mt-unsafe,\
cppcoreguidelines-avoid-non-const-global-variables,\
cppcoreguidelines-missing-std-forward,\
cppcoreguidelines-pro-type-member-init,\
cppcoreguidelines-special-member-functions,\
cppcoreguidelines-virtual-class-destructor,\
google-build-using-namespace,\
google-explicit-constructor,\
google-readability-avoid-underscore-in-googletest-name,\
misc-definitions-in-headers,\
misc-redundant-expression,\
modernize-make-shared,\
modernize-use-emplace,\
modernize-use-noexcept,\
modernize-use-override,\
modernize-use-using,\
performance-faster-string-find,\
performance-for-range-copy,\
performance-implicit-conversion-in-loop,\
performance-inefficient-algorithm,\
performance-inefficient-string-concatenation,\
performance-inefficient-vector-operation,\
performance-move-const-arg,\
performance-move-constructor-init,\
performance-no-automatic-move,\
performance-no-int-to-ptr,\
performance-noexcept-move-constructor,\
performance-noexcept-swap,\
performance-trivially-destructible,\
performance-type-promotion-in-math-fn,\
performance-unnecessary-copy-initialization,\
performance-unnecessary-value-param,\
readability-braces-around-statements,\
readability-duplicate-include,\
readability-isolate-declaration,\
readability-operators-representation,\
readability-redundant-string-init"
WarningsAsErrors: "bugprone-use-after-move"
CheckOptions:
- key: bugprone-easily-swappable-parameters.MinimumLength
value: 4
- key: cppcoreguidelines-avoid-non-const-global-variables.AllowThreadLocal
value: true
- key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor
value: true
- key: cppcoreguidelines-special-member-functions.AllowImplicitlyDeletedCopyOrMove
value: true
- key: modernize-use-using.IgnoreExternC
value: true
- key: performance-move-const-arg.CheckTriviallyCopyableMove
value: false
- key: performance-unnecessary-value-param.AllowedTypes
value: '[Pp]ointer$;[Pp]tr$;[Rr]ef(erence)?$'
- key: performance-unnecessary-copy-initialization.AllowedTypes
value: '[Pp]ointer$;[Pp]tr$;[Rr]ef(erence)?$'
- key: readability-operators-representation.BinaryOperators
value: '&&;&=;&;|;~;!;!=;||;|=;^;^='
- key: readability-redundant-string-init.StringNames
value: '::std::basic_string'
- key: readability-named-parameter.InsertPlainNamesInForwardDecls
value: true
...
+1 -10
View File
@@ -9,16 +9,7 @@ runs:
steps:
- name: Build folly and dependencies
if: ${{ inputs.cache-hit != 'true' }}
run: |
clean_path=()
IFS=: read -ra path_entries <<< "$PATH"
for entry in "${path_entries[@]}"; do
if [[ "$entry" != "/usr/lib/ccache" ]]; then
clean_path+=("$entry")
fi
done
export PATH="$(IFS=:; echo "${clean_path[*]}")"
make build_folly
run: make build_folly
shell: bash
- name: Skip folly build (using cached version)
if: ${{ inputs.cache-hit == 'true' }}
+1 -1
View File
@@ -30,4 +30,4 @@ runs:
# - The docker image, which may not always be specified/known
# - Hash of folly.mk, which includes the folly repository commit hash
# NOTE: this is still only intended for DEBUG folly builds
key: folly-build-${{ runner.os }}-${{ runner.arch }}-${{ github.job_container.image }}-${{ steps.extract-folly-hash.outputs.hash }}-${{ env.PORTABLE == '1' && 'portable' || 'native' }}
key: folly-build-${{ runner.os }}-${{ runner.arch }}-${{ github.job_container.image }}-${{ steps.extract-folly-hash.outputs.hash }}
-3
View File
@@ -2,9 +2,6 @@ name: pre-steps
runs:
using: composite
steps:
- name: Install lld linker for faster builds
run: apt-get update -y && apt-get install -y lld 2>/dev/null || true
shell: bash
- name: Setup Environment Variables
run: |-
echo "GTEST_THROW_ON_FAILURE=0" >> "$GITHUB_ENV"
-54
View File
@@ -1,54 +0,0 @@
name: setup-ccache
description: Setup ccache for faster C++ compilation caching
inputs:
cache-key-prefix:
description: Unique prefix for the cache key (e.g., 'build-linux')
required: true
portable:
description: Set PORTABLE=1 to disable -march=native (set to "false" for jobs linking pre-built Folly)
required: false
default: "true"
runs:
using: composite
steps:
- name: Set ccache environment variables
run: |
echo "CCACHE_DIR=${{ github.workspace }}/.ccache" >> $GITHUB_ENV
echo "CCACHE_BASEDIR=${{ github.workspace }}" >> $GITHUB_ENV
echo "CCACHE_NOHASHDIR=true" >> $GITHUB_ENV
echo "CCACHE_COMPILERCHECK=content" >> $GITHUB_ENV
echo "CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros" >> $GITHUB_ENV
echo "CCACHE_MAXSIZE=4G" >> $GITHUB_ENV
if [ "${{ inputs.portable }}" = "true" ]; then
echo "PORTABLE=1" >> $GITHUB_ENV
fi
shell: bash
- name: Restore ccache
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/.ccache
key: ccache-${{ inputs.cache-key-prefix }}-${{ github.head_ref || github.ref }}-${{ github.sha }}
restore-keys: |-
ccache-${{ inputs.cache-key-prefix }}-${{ github.head_ref || github.ref }}-
ccache-${{ inputs.cache-key-prefix }}-refs/heads/main-
- name: Install ccache
run: |
if [ "$RUNNER_OS" = "macOS" ]; then
which ccache || brew install ccache
else
which ccache || (apt-get update && apt-get install -y ccache)
fi
shell: bash
- name: Add ccache to PATH
run: |
if [ "$RUNNER_OS" = "macOS" ]; then
echo "$(brew --prefix ccache)/libexec" >> $GITHUB_PATH
else
echo "/usr/lib/ccache" >> $GITHUB_PATH
fi
shell: bash
- name: Zero ccache stats and set build marker
run: |
ccache -z
touch "$CCACHE_DIR/.build_marker"
shell: bash
+2 -2
View File
@@ -6,6 +6,6 @@ runs:
run: |
make checkout_folly
shell: bash
- name: Install patchelf and libaio
run: apt-get update -y && apt-get install -y patchelf libaio-dev
- name: Install patchelf
run: apt-get update -y && apt-get install -y patchelf
shell: bash
@@ -1,15 +0,0 @@
name: teardown-ccache
description: Trim stale ccache entries and print stats
runs:
using: composite
steps:
- name: Trim and print ccache stats
run: |
if [ -z "$CCACHE_DIR" ]; then
echo "teardown-ccache: CCACHE_DIR not set, skipping (setup-ccache may not have run)"
exit 0
fi
.github/scripts/ccache-trim.sh || true
ccache -s || echo "teardown-ccache: ccache not found, skipping stats"
if: always()
shell: bash
+12 -34
View File
@@ -1,31 +1,19 @@
name: windows-build-steps
inputs:
suite-run:
description: Comma-separated list of test suites to run (empty to skip C++ tests)
required: false
default: arena_test,db_basic_test,db_test,db_test2,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test
run-java:
description: Whether to run Java tests
required: false
default: "true"
runs:
using: composite
steps:
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v1.3.1
- name: Cache ccache directory
id: ccache-cache
uses: actions/cache@v4
with:
path: C:\a\rocksdb\rocksdb\.ccache
key: rocksdb-build-${{ runner.os }}-${{ runner.arch }}-ccache-${{ hashFiles('CMakeLists.txt', 'cmake/**/*.cmake') }}-v1
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
max-size: "10GB"
key: ccache-windows-${{ github.workflow }}
restore-keys: |
ccache-windows-
- name: Configure ccache
shell: pwsh
run: |
ccache --set-config=base_dir=C:\a\rocksdb\rocksdb
ccache --set-config=hash_dir=false
ccache --set-config=compiler_check=content
- name: Custom steps
env:
THIRDPARTY_HOME: ${{ github.workspace }}/thirdparty
@@ -68,22 +56,12 @@ runs:
msbuild build/rocksdb.sln /m:32 /p:LinkIncremental=false -property:Configuration=Debug -property:Platform=x64
if(!$?) { Exit $LASTEXITCODE }
echo ========================= Test RocksDB =========================
$suiteRun = "${{ inputs.suite-run }}"
if ($suiteRun -ne "") {
$suiteArray = $suiteRun -split ','
build_tools\run_ci_db_test.ps1 -SuiteRun $suiteArray -Concurrency 16
if(!$?) { Exit $LASTEXITCODE }
} else {
echo "Skipping C++ tests (suite-run is empty)"
}
if ("${{ inputs.run-java }}" -eq "true") {
echo ======================== Test RocksJava ========================
cd build\java
& ctest -C Debug -j 16
if(!$?) { Exit $LASTEXITCODE }
} else {
echo "Skipping Java tests"
}
build_tools\run_ci_db_test.ps1 -SuiteRun arena_test,db_basic_test,db_test,db_test2,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test -Concurrency 16
if(!$?) { Exit $LASTEXITCODE }
echo ======================== Test RocksJava ========================
cd build\java
& ctest -C Debug -j 16
if(!$?) { Exit $LASTEXITCODE }
shell: pwsh
- name: Show ccache stats
shell: pwsh
@@ -1,27 +0,0 @@
// Shared markdown builder for AI review comment bodies.
//
// Usage:
// const build = require('./build-ai-review-comment.js');
// return build({ icon, headerTitle, triggerLine, responseBody, footerLines
// });
module.exports = function buildAiReviewComment(
{icon, headerTitle, triggerLine, responseBody, footerLines}) {
return [
`## ${icon} ${headerTitle}`,
'',
triggerLine,
'',
'---',
'',
responseBody,
'',
'---',
'',
'<details>',
'<summary>ℹ️ About this response</summary>',
'',
...footerLines,
'</details>',
].join('\n');
};
-39
View File
@@ -1,39 +0,0 @@
#!/bin/bash
# Trim ccache to keep only entries accessed during the current build.
#
# Usage:
# 1. Before build: touch "$CCACHE_DIR/.build_marker"
# 2. Run build (ccache updates mtime on hits, creates new files for misses)
# 3. After build: .github/scripts/ccache-trim.sh
#
# This removes cache files not accessed during the build (stale entries from
# previous commits). Only intended for CI where each run builds one commit.
# Do NOT use on local builds where multiple worktrees may share the cache.
set -e
CCACHE_DIR="${CCACHE_DIR:?CCACHE_DIR must be set}"
MARKER="$CCACHE_DIR/.build_marker"
if [ ! -f "$MARKER" ]; then
echo "ccache-trim: No .build_marker found, skipping (was the marker created before build?)"
exit 0
fi
# Count files before cleanup
before=$(find "$CCACHE_DIR" -type f \( -name '*R' -o -name '*M' \) | wc -l)
# Delete cache files (results and manifests) older than the marker
find "$CCACHE_DIR" -type f \( -name '*R' -o -name '*M' \) ! -newer "$MARKER" -delete
# Clean up empty directories
find "$CCACHE_DIR" -mindepth 2 -type d -empty -delete 2>/dev/null || true
# Recalculate size counters
ccache -c 2>/dev/null || true
after=$(find "$CCACHE_DIR" -type f \( -name '*R' -o -name '*M' \) | wc -l)
echo "ccache-trim: $before -> $after cache files (removed $((before - after)) stale entries)"
# Clean up marker
rm -f "$MARKER"
-32
View File
@@ -1,32 +0,0 @@
#!/bin/bash
# Compute test shard for parallel CI execution.
# Distributes tests round-robin across N shards for balanced load.
# Outputs ROCKSDBTESTS_SUBSET — the list of test binaries for this shard.
# The Makefile uses this to build and run only the assigned tests.
#
# Usage: compute-test-shard.sh <shard_index> <num_shards>
set -euo pipefail
shard=${1:?Usage: compute-test-shard.sh <shard_index> <num_shards>}
nshards=${2:?Usage: compute-test-shard.sh <shard_index> <num_shards>}
# Get sorted test list (db_test first since it's the heaviest, then alpha)
make -s list_all_tests 2>/dev/null | tr ' ' '\n' | grep '_test$' | sort -u > /tmp/sorted.txt
(echo db_test; grep -v '^db_test$' /tmp/sorted.txt) > /tmp/all_tests.txt
total=$(wc -l < /tmp/all_tests.txt)
# Round-robin: assign test i to shard (i % nshards).
# This spreads heavy tests (which are scattered alphabetically) evenly.
awk -v s="$shard" -v n="$nshards" 'NR > 0 && (NR - 1) % n == s' /tmp/all_tests.txt > /tmp/include.txt
included=$(wc -l < /tmp/include.txt)
first=$(head -1 /tmp/include.txt)
last=$(tail -1 /tmp/include.txt)
# Output space-separated list for ROCKSDBTESTS_SUBSET
subset=$(tr '\n' ' ' < /tmp/include.txt)
echo "subset=${subset}" >> "$GITHUB_OUTPUT"
echo "Shard $shard/$nshards: $included tests (round-robin), first=$first last=$last (total $total)"
-118
View File
@@ -1,118 +0,0 @@
// Parse Claude Code execution log and produce a markdown review comment.
//
// Usage from actions/github-script:
// const parse = require('./.github/scripts/parse-claude-review.js');
// const markdown = parse({ executionFile, conclusion, meta });
//
// Parameters:
// executionFile - path to the JSON execution log from claude-code-base-action
// conclusion - 'success' or 'failure' from the action output
// meta - { trigger, autoMode, headSha, reviewer, isQuery, isPartial
// }
const fs = require('fs');
const buildComment = require('./build-ai-review-comment.js');
module.exports = function parseClaude({executionFile, conclusion, meta}) {
function getTriggerLine() {
if (meta.trigger !== 'auto') {
return `*Requested by @${meta.reviewer}*`;
}
const shortSha = meta.headSha ? meta.headSha.substring(0, 7) : 'unknown';
if (meta.autoMode === 'early') {
return `*Auto-triggered after CI reached the early-review threshold — reviewing commit ${
shortSha}*`;
}
return `*Auto-triggered after CI passed — reviewing commit ${shortSha}*`;
}
let responseBody = '';
try {
const executionLog = JSON.parse(fs.readFileSync(executionFile, 'utf8'));
if (!Array.isArray(executionLog)) {
throw new Error('Expected array format from claude-code-base-action');
}
const resultMessage = executionLog.find(m => m.type === 'result');
// Helper: extract the last substantial assistant text from the log.
// Used as a fallback when Claude ran out of turns and the recovery
// session also failed to produce a result.
function getLastAssistantText(log, minLength = 200) {
for (let i = log.length - 1; i >= 0; i--) {
const m = log[i];
if (m.type !== 'assistant') continue;
const content = m.message && m.message.content;
if (!Array.isArray(content)) continue;
for (let j = content.length - 1; j >= 0; j--) {
if (content[j].type === 'text' && content[j].text &&
content[j].text.trim().length >= minLength) {
const text = content[j].text.trim();
// Truncate to avoid enormous PR comments
return text.length > 50000 ? text.substring(0, 50000) +
'\n\n*[Truncated — full output in execution log artifact]*' :
text;
}
}
}
return null;
}
if (!resultMessage) {
responseBody = '⚠️ No result message found in execution log.';
} else if (resultMessage.subtype === 'success' && resultMessage.result) {
responseBody = resultMessage.result;
} else if (resultMessage.is_error || resultMessage.subtype === 'error') {
const errorInfo =
resultMessage.result || resultMessage.error || 'Unknown error';
responseBody = `❌ **Claude encountered an error:**\n\n${errorInfo}`;
} else if (resultMessage.subtype === 'error_max_turns') {
// The workflow runs a recovery session when this happens, so this
// branch is typically only hit if recovery wasn't attempted (e.g.,
// no findings file was written). Extract what we can.
const partial = getLastAssistantText(executionLog);
if (partial) {
responseBody =
`⚠️ **Review incomplete — Claude hit the turn limit.**\n\nBelow is the last partial output. You can request a fresh review with \`/claude-review\`.\n\n---\n\n${
partial}`;
} else {
responseBody =
'⚠️ **Review incomplete — Claude hit the turn limit before producing output.** You can request a fresh review with `/claude-review`.';
}
} else if (resultMessage.result) {
responseBody = `⚠️ **Completed with status: ${
resultMessage.subtype}**\n\n${resultMessage.result}`;
} else {
responseBody = '⚠️ Claude completed but produced no output.';
}
} catch (error) {
responseBody = `❌ Error parsing Claude response: ${error.message}`;
}
const isPartial = !!meta.isPartial;
const icon = isPartial ? '🟡' : (conclusion === 'success' ? '✅' : '⚠️');
const headerTitle = meta.isQuery ? 'Claude Response' : 'Claude Code Review';
const triggerLine = getTriggerLine();
return buildComment({
icon,
headerTitle,
triggerLine,
responseBody,
footerLines: [
'Generated by [Claude Code](https://github.com/anthropics/claude-code-action).',
'Review methodology: `claude_md/code_review.md`',
'',
'**Limitations:**',
'- Claude may miss context from files not in the diff',
'- Large PRs may be truncated',
'- Always apply human judgment to AI suggestions',
'',
'**Commands:**',
'- `/claude-review [context]` — Request a code review',
'- `/claude-query <question>` — Ask about the PR or codebase',
],
});
};
-98
View File
@@ -1,98 +0,0 @@
// Parse Codex review artifacts and produce a markdown review comment.
//
// Usage from actions/github-script:
// const parse = require('./.github/scripts/parse-codex-review.js');
// const markdown = parse({ responseFile, recoveryFile, findingsFile, logFile,
// exitCode, meta });
//
// Parameters:
// responseFile - path to Codex final response output
// recoveryFile - path to recovery output formatted from review-findings.md
// findingsFile - path to incremental findings file written during review
// logFile - path to Codex stdout/stderr log
// exitCode - Codex process exit code
// meta - { trigger, autoMode, headSha, reviewer, isQuery, isPartial }
const fs = require('fs');
const buildComment = require('./build-ai-review-comment.js');
module.exports = function parseCodex(
{responseFile, recoveryFile, findingsFile, logFile, exitCode, meta}) {
function getTriggerLine() {
if (meta.trigger !== 'auto') {
return `*Requested by @${meta.reviewer}*`;
}
const shortSha = meta.headSha ? meta.headSha.substring(0, 7) : 'unknown';
if (meta.autoMode === 'early') {
return `*Auto-triggered after CI reached the early-review threshold — reviewing commit ${
shortSha}*`;
}
return `*Auto-triggered after CI passed — reviewing commit ${shortSha}*`;
}
function readIfPresent(path) {
if (!path || !fs.existsSync(path)) {
return '';
}
const text = fs.readFileSync(path, 'utf8').trim();
return text;
}
function tailFile(path, maxChars = 12000) {
const text = readIfPresent(path);
if (!text) {
return '';
}
if (text.length <= maxChars) {
return text;
}
return text.slice(text.length - maxChars);
}
let responseBody = '';
const recovered = readIfPresent(recoveryFile);
const direct = readIfPresent(responseFile);
const findings = readIfPresent(findingsFile);
if (recovered) {
responseBody = recovered;
} else if (direct) {
responseBody = direct;
} else if (findings) {
responseBody =
'⚠️ **Review incomplete — Codex did not produce a final response.**\n\n' +
'Below are the incremental findings recovered from `review-findings.md`.\n\n---\n\n' +
findings;
} else {
const logTail = tailFile(logFile);
responseBody = logTail ?
`❌ **Codex review failed before producing findings.**\n\n\`\`\`\n${
logTail}\n\`\`\`` :
'❌ Codex review failed before producing any output.';
}
const isPartial = !!meta.isPartial;
const code = Number.parseInt(exitCode || '1', 10);
const icon = isPartial ? '🟡' : (code === 0 ? '✅' : '⚠️');
const triggerLine = getTriggerLine();
return buildComment({
icon,
headerTitle: meta.isQuery ? 'Codex Response' : 'Codex Code Review',
triggerLine,
responseBody,
footerLines: [
'Generated by Codex CLI.',
'Review methodology: `claude_md/code_review.md`',
'',
'**Limitations:**',
'- Codex may miss context from files not in the diff',
'- Large PRs may be truncated',
'- Always apply human judgment to AI suggestions',
'',
'**Commands:**',
'- `/codex-review [context]` — Request a code review',
'- `/codex-query <question>` — Ask about the PR or codebase',
],
});
};
-204
View File
@@ -1,204 +0,0 @@
// Shared PR comment posting utility.
// Used by both clang-tidy-comment.yml and claude-review-comment.yml.
//
// Usage from actions/github-script:
// const post = require('./.github/scripts/post-pr-comment.js');
// await post({ github, context, core, prNumber, body, marker });
//
// Parameters:
// github - octokit instance from actions/github-script
// context - GitHub Actions context
// core - @actions/core for logging
// prNumber - PR number to comment on
// body - comment body (markdown string)
// marker - HTML comment marker for dedup (e.g. '<!-- claude-review -->')
// If an existing comment with this marker is found, it is updated.
// If not found, a new comment is created.
// legacyMarkers - optional list of legacy markers that should be migrated by
// being considered part of the same comment family.
// prunePrefix - optional marker prefix whose older comments should be
// superseded. If obsoleteTitle is set, older comments are
// collapsed instead of deleted.
// preserveLatest - optional count of active comments to keep when
// prunePrefix is set.
// obsoleteMarker - optional HTML marker used to detect already-obsolete
// comments.
// obsoleteTitle - optional heading to use when collapsing superseded
// comments into a details block.
module.exports = async function postPrComment({
github,
context,
core,
prNumber,
body,
marker,
legacyMarkers = [],
prunePrefix = '',
preserveLatest = 0,
obsoleteMarker = '',
obsoleteTitle = '',
}) {
if (!prNumber || !body) {
core.warning('Missing prNumber or body; skipping comment.');
return;
}
const owner = context.repo.owner;
const repo = context.repo.repo;
// Ensure marker is embedded in the body
const markedBody = body.includes(marker) ? body : `${marker}\n${body}`;
async function listComments() {
return await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
}
function commentActivityTime(comment) {
const timestamp =
Date.parse(comment.updated_at || comment.created_at || '');
return Number.isNaN(timestamp) ? 0 : timestamp;
}
function commentSortDescending(left, right) {
const timeDelta = commentActivityTime(right) - commentActivityTime(left);
if (timeDelta !== 0) {
return timeDelta;
}
return Number(right.id || 0) - Number(left.id || 0);
}
function isObsoleteComment(comment) {
return !!obsoleteMarker && typeof comment.body === 'string' &&
comment.body.includes(obsoleteMarker);
}
function buildObsoleteBody(comment) {
const originalBody =
typeof comment.body === 'string' && comment.body.trim() ?
comment.body :
'*No original review body preserved.*';
const title = obsoleteTitle || 'AI Review - OBSOLETE';
return `${obsoleteMarker}\n## ${
title}\n\n<details>\n<summary>Superseded by a newer AI review. Expand to see the original review.</summary>\n\n${
originalBody}\n\n</details>`;
}
async function deleteCommentIfPresent(comment, reason) {
try {
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: comment.id,
});
core.info(`${reason} ${comment.id}`);
} catch (error) {
if (error.status === 404) {
core.info(`Comment ${comment.id} was already deleted by another run.`);
return;
}
throw error;
}
}
async function supersedeCommentIfPresent(comment, reason) {
if (!obsoleteTitle) {
await deleteCommentIfPresent(comment, reason);
return;
}
if (isObsoleteComment(comment)) {
return;
}
try {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: comment.id,
body: buildObsoleteBody(comment),
});
core.info(`${reason} ${comment.id}`);
} catch (error) {
if (error.status === 404) {
core.info(`Comment ${comment.id} was already deleted by another run.`);
return;
}
throw error;
}
}
let comments = await listComments();
const existing = comments.find(
comment =>
typeof comment.body === 'string' && comment.body.includes(marker));
let currentCommentId = null;
if (existing) {
try {
const response = await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body: markedBody,
});
currentCommentId = existing.id;
core.info(`Updated existing comment ${existing.id}`);
} catch (error) {
if (error.status !== 404) {
throw error;
}
core.info(
`Comment ${existing.id} disappeared before update; ` +
'creating a fresh comment instead.');
}
}
if (!currentCommentId) {
const response = await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: markedBody,
});
currentCommentId = response.data.id;
core.info('Created new PR comment');
}
if (prunePrefix || legacyMarkers.length > 0) {
comments = await listComments();
const relatedComments = comments.filter(
comment => comment.id !== currentCommentId &&
typeof comment.body === 'string' &&
((prunePrefix && comment.body.includes(prunePrefix)) ||
legacyMarkers.some(
legacyMarker => comment.body.includes(legacyMarker))));
const activeRelatedComments =
comments
.filter(
comment => typeof comment.body === 'string' &&
!isObsoleteComment(comment) &&
(comment.id === currentCommentId ||
relatedComments.some(
relatedComment => relatedComment.id === comment.id)))
.sort(commentSortDescending);
const keep =
new Set(activeRelatedComments.slice(0, Math.max(preserveLatest, 1))
.map(comment => comment.id));
const supersedeCandidates = obsoleteTitle ?
activeRelatedComments.filter(comment => !keep.has(comment.id)) :
relatedComments.filter(comment => !keep.has(comment.id));
for (const comment of supersedeCandidates) {
if (keep.has(comment.id)) {
continue;
}
await supersedeCommentIfPresent(comment, 'Superseded old AI review');
}
}
};
-347
View File
@@ -1,347 +0,0 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const postPrComment = require('./post-pr-comment.js');
const OBSOLETE_MARKER = '<!-- claude-review-obsolete -->';
const OBSOLETE_TITLE = 'Claude Code Review - OBSOLETE';
function makeComment(id, body, createdAt, updatedAt) {
return {
id,
body,
created_at: createdAt,
updated_at: updatedAt || createdAt,
};
}
function createHarness(initialComments, options = {}) {
let comments = initialComments.map(comment => ({...comment}));
let nextCommentId = options.nextCommentId || 1000;
let paginateCount = 0;
const calls = {
update: [],
create: [],
delete: [],
};
const github = {
paginate: async () => {
paginateCount++;
if (options.onPaginate) {
const updated = options.onPaginate({
paginateCount,
comments: comments.map(comment => ({...comment})),
});
if (updated) {
comments = updated.map(comment => ({...comment}));
}
}
return comments.map(comment => ({...comment}));
},
rest: {
issues: {
listComments: () => {
throw new Error('listComments should only be used through paginate');
},
updateComment: async ({comment_id, body}) => {
calls.update.push({comment_id, body});
const error =
options.updateErrors && options.updateErrors[comment_id];
if (error) {
throw error;
}
const index =
comments.findIndex(comment => comment.id === comment_id);
if (index === -1) {
const notFound = new Error(`Comment ${comment_id} not found`);
notFound.status = 404;
throw notFound;
}
const updated = {
...comments[index],
body,
updated_at: options.updateTimestamp || '2026-04-24T00:00:00Z',
};
comments[index] = updated;
return {data: {...updated}};
},
createComment: async ({issue_number, body}) => {
calls.create.push({issue_number, body});
const created = makeComment(
nextCommentId++, body,
options.createTimestamp || '2026-04-24T00:00:00Z');
comments.push(created);
return {data: {id: created.id}};
},
deleteComment: async ({comment_id}) => {
calls.delete.push({comment_id});
const error =
options.deleteErrors && options.deleteErrors[comment_id];
if (error) {
throw error;
}
comments = comments.filter(comment => comment.id !== comment_id);
},
},
},
};
return {
github,
calls,
getComments: () => comments.map(comment => ({...comment})),
};
}
function createCore() {
return {
info: () => {},
warning: () => {},
};
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function assertObsoleteComment(comment, originalBody) {
assert.ok(comment);
assert.match(comment.body, new RegExp(escapeRegExp(OBSOLETE_MARKER)));
assert.match(comment.body, new RegExp(`## ${escapeRegExp(OBSOLETE_TITLE)}`));
assert.match(comment.body, /<details>/);
assert.match(comment.body, /Superseded by a newer AI review/);
assert.match(comment.body, new RegExp(escapeRegExp(originalBody)));
}
const context = {
repo: {
owner: 'facebook',
repo: 'rocksdb',
},
};
test(
'creates a fresh review comment and supersedes legacy comments',
async () => {
const harness = createHarness(
[
makeComment(
1, '<!-- claude-review-auto -->\nold legacy', 'not-a-date'),
makeComment(
2, '<!-- claude-review-auto -->\nnew legacy',
'2026-04-21T00:00:00Z'),
],
{
updateTimestamp: '2026-04-24T00:00:00Z',
});
await postPrComment({
github: harness.github,
context,
core: createCore(),
prNumber: 14659,
body: 'review body',
marker: '<!-- claude-review-auto-run-500 -->',
legacyMarkers: ['<!-- claude-review-auto -->'],
prunePrefix: '<!-- claude-review-auto-',
preserveLatest: 1,
obsoleteMarker: OBSOLETE_MARKER,
obsoleteTitle: OBSOLETE_TITLE,
});
assert.equal(harness.calls.create.length, 1);
assert.deepEqual(
harness.calls.update.map(call => call.comment_id), [2, 1]);
assert.equal(harness.calls.delete.length, 0);
const comments = harness.getComments();
assert.equal(comments.length, 3);
assert.match(
comments.find(comment => comment.id === 1000).body,
/<!-- claude-review-auto-run-500 -->/);
assertObsoleteComment(
comments.find(comment => comment.id === 2),
'<!-- claude-review-auto -->\nnew legacy');
assertObsoleteComment(
comments.find(comment => comment.id === 1),
'<!-- claude-review-auto -->\nold legacy');
});
test(
'updates an exact-match comment and supersedes leftover legacy comments',
async () => {
const harness = createHarness(
[
makeComment(
3, '<!-- claude-review-auto-abcdef0 -->\ncurrent body',
'2026-04-24T00:00:00Z'),
makeComment(
4, '<!-- claude-review-auto -->\nlegacy body',
'2026-04-20T00:00:00Z'),
],
{
updateTimestamp: '2026-04-24T01:00:00Z',
});
await postPrComment({
github: harness.github,
context,
core: createCore(),
prNumber: 14659,
body: 'refreshed body',
marker: '<!-- claude-review-auto-abcdef0 -->',
legacyMarkers: ['<!-- claude-review-auto -->'],
prunePrefix: '<!-- claude-review-auto-',
preserveLatest: 1,
obsoleteMarker: OBSOLETE_MARKER,
obsoleteTitle: OBSOLETE_TITLE,
});
assert.deepEqual(
harness.calls.update.map(call => call.comment_id), [3, 4]);
assert.equal(harness.calls.create.length, 0);
assert.equal(harness.calls.delete.length, 0);
const comments = harness.getComments();
assert.match(
comments.find(comment => comment.id === 3).body,
/<!-- claude-review-auto-abcdef0 -->\nrefreshed body/);
assertObsoleteComment(
comments.find(comment => comment.id === 4),
'<!-- claude-review-auto -->\nlegacy body');
});
test(
'creates a new comment if the target disappears before update',
async () => {
const missing = new Error('gone');
missing.status = 404;
const harness = createHarness(
[
makeComment(
10, '<!-- claude-review-auto-abcdef0 -->\nold body',
'2026-04-20T00:00:00Z'),
],
{
updateErrors: {
10: missing,
},
});
await postPrComment({
github: harness.github,
context,
core: createCore(),
prNumber: 14659,
body: 'replacement body',
marker: '<!-- claude-review-auto-abcdef0 -->',
});
assert.equal(harness.calls.update.length, 1);
assert.equal(harness.calls.create.length, 1);
assert.match(
harness.calls.create[0].body, /<!-- claude-review-auto-abcdef0 -->/);
});
test(
'ignores 404 when superseding a comment already deleted by another run',
async () => {
const missing = new Error('gone');
missing.status = 404;
const harness = createHarness(
[
makeComment(
20, '<!-- claude-review-auto-oldest -->\noldest',
'2026-04-20T00:00:00Z'),
makeComment(
21, '<!-- claude-review-auto-newer -->\nnewer',
'2026-04-21T00:00:00Z'),
],
{
nextCommentId: 30,
createTimestamp: '2026-04-24T00:00:00Z',
deleteErrors: {
20: missing,
},
});
await postPrComment({
github: harness.github,
context,
core: createCore(),
prNumber: 14659,
body: 'fresh body',
marker: '<!-- claude-review-auto-latest -->',
prunePrefix: '<!-- claude-review-auto-',
preserveLatest: 1,
obsoleteMarker: OBSOLETE_MARKER,
obsoleteTitle: OBSOLETE_TITLE,
});
assert.equal(harness.calls.create.length, 1);
assert.equal(harness.calls.delete.length, 0);
assert.deepEqual(
harness.calls.update.map(call => call.comment_id), [21, 20]);
const comments = harness.getComments();
assertObsoleteComment(
comments.find(comment => comment.id === 21),
'<!-- claude-review-auto-newer -->\nnewer');
});
test(
'supersedes the current review when a newer concurrent one appears',
async () => {
const harness = createHarness(
[
makeComment(
40, '<!-- claude-review-auto-current -->\ncurrent',
'2026-04-20T00:00:00Z'),
makeComment(
41, '<!-- claude-review-auto-older -->\nolder',
'2026-04-19T00:00:00Z'),
],
{
updateTimestamp: '2026-04-24T00:00:00Z',
onPaginate: ({paginateCount, comments}) => {
if (paginateCount !== 2) {
return comments;
}
return comments.concat([
makeComment(
42, '<!-- claude-review-auto-newer -->\nnewer',
'2026-04-25T00:00:00Z'),
]);
},
});
await postPrComment({
github: harness.github,
context,
core: createCore(),
prNumber: 14659,
body: 'updated current body',
marker: '<!-- claude-review-auto-current -->',
prunePrefix: '<!-- claude-review-auto-',
preserveLatest: 1,
obsoleteMarker: OBSOLETE_MARKER,
obsoleteTitle: OBSOLETE_TITLE,
});
assert.deepEqual(
harness.calls.update.map(call => call.comment_id), [40, 40, 41]);
assert.equal(harness.calls.delete.length, 0);
const comments = harness.getComments();
assert.match(
comments.find(comment => comment.id === 42).body,
/<!-- claude-review-auto-newer -->\nnewer/);
assertObsoleteComment(
comments.find(comment => comment.id === 40),
'<!-- claude-review-auto-current -->\nupdated current body');
assertObsoleteComment(
comments.find(comment => comment.id === 41),
'<!-- claude-review-auto-older -->\nolder');
});
File diff suppressed because it is too large. Load diff
-185
View File
@@ -1,185 +0,0 @@
# Shared comment-posting workflow for AI reviews.
name: AI Review Comment
on:
workflow_call:
inputs:
provider:
description: Provider key, for example "claude" or "codex"
required: true
type: string
result_artifact_name:
description: Artifact name produced by the analysis workflow
required: true
type: string
comment_file:
description: Markdown comment file produced by the analysis workflow
required: true
type: string
permissions:
pull-requests: write
issues: write
jobs:
comment:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- name: Checkout scripts
uses: actions/checkout@v4
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: true
- name: Download review artifact
id: download
uses: actions/download-artifact@v4
with:
name: ${{ inputs.result_artifact_name }}
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Post or update PR comment
if: steps.download.outcome == 'success'
env:
PROVIDER: ${{ inputs.provider }}
COMMENT_FILE: ${{ inputs.comment_file }}
RUN_ID: ${{ github.event.workflow_run.id }}
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (!fs.existsSync(process.env.COMMENT_FILE) ||
!fs.existsSync('pr_number.txt')) {
core.info('No review results found; skipping.');
return;
}
const post = require('./.github/scripts/post-pr-comment.js');
const provider = process.env.PROVIDER;
const providerTitle = provider === 'codex' ? 'Codex' : 'Claude';
const body = fs.readFileSync(process.env.COMMENT_FILE, 'utf8');
const prNumber = parseInt(
fs.readFileSync('pr_number.txt', 'utf8').trim(),
10
);
const trigger = fs.existsSync('trigger_type.txt') ?
fs.readFileSync('trigger_type.txt', 'utf8').trim() :
'auto';
const headSha = fs.existsSync('head_sha.txt') ?
fs.readFileSync('head_sha.txt', 'utf8').trim() :
'';
const shortSha = headSha ? headSha.substring(0, 7) : '';
const runId = process.env.RUN_ID;
let marker = '';
let legacyMarkers = [];
let prunePrefix = '';
let preserveLatest = 0;
let obsoleteMarker = '';
let obsoleteTitle = '';
if (trigger === 'auto') {
if (!shortSha) {
core.warning(
'Missing head_sha.txt for auto review; skipping comment.'
);
return;
}
marker = `<!-- ${provider}-review-auto-run-${runId} -->`;
legacyMarkers = [`<!-- ${provider}-review-auto -->`];
prunePrefix = `<!-- ${provider}-review-auto-`;
preserveLatest = 1;
obsoleteMarker = `<!-- ${provider}-review-obsolete -->`;
obsoleteTitle = `${providerTitle} Code Review - OBSOLETE`;
} else {
marker = `<!-- ${provider}-review-manual-run-${runId} -->`;
}
await post({
github,
context,
core,
prNumber,
body,
marker,
legacyMarkers,
prunePrefix,
preserveLatest,
obsoleteMarker,
obsoleteTitle,
});
- name: Add reaction to trigger comment
if: steps.download.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (!fs.existsSync('trigger_type.txt')) return;
const trigger = fs.readFileSync('trigger_type.txt', 'utf8').trim();
if (trigger !== 'manual') return;
if (!fs.existsSync('comment_id.txt')) return;
const commentId = parseInt(
fs.readFileSync('comment_id.txt', 'utf8').trim(),
10
);
if (!commentId) return;
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId,
content: 'rocket'
});
failure-notice:
if: github.event.workflow_run.conclusion == 'failure'
runs-on: ubuntu-latest
steps:
- name: Download artifact for PR number
id: download
uses: actions/download-artifact@v4
with:
name: ${{ inputs.result_artifact_name }}
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: React with failure
if: steps.download.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (!fs.existsSync('comment_id.txt')) return;
const commentId = parseInt(
fs.readFileSync('comment_id.txt', 'utf8').trim(),
10
);
if (!commentId) return;
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId,
content: 'confused'
});
unauthorized-notice:
runs-on: ubuntu-latest
if: >-
github.event.workflow_run.event == 'issue_comment' &&
github.event.workflow_run.conclusion == 'skipped'
steps:
- name: Log skipped unauthorized request
uses: actions/github-script@v7
with:
script: |
core.info(
'Analysis workflow was skipped, which usually means the ' +
'issue_comment requester was not in the authorized list.'
);
-51
View File
@@ -1,51 +0,0 @@
name: Post clang-tidy PR comment
on:
workflow_run:
workflows: ["clang-tidy"]
types: [completed]
permissions:
pull-requests: write
jobs:
comment:
if: github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout scripts
uses: actions/checkout@v4
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: true
- name: Download clang-tidy results
id: download
uses: actions/download-artifact@v4.1.3
with:
name: clang-tidy-result
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Post or update PR comment
if: steps.download.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (!fs.existsSync('clang-tidy-comment.md') || !fs.existsSync('pr_number.txt')) {
core.info('No clang-tidy results found; skipping.');
return;
}
const body = fs.readFileSync('clang-tidy-comment.md', 'utf8');
const prNumber = parseInt(fs.readFileSync('pr_number.txt', 'utf8').trim());
const post = require('./.github/scripts/post-pr-comment.js');
await post({
github,
context,
core,
prNumber,
body,
marker: '<!-- clang-tidy-bot -->',
});
-74
View File
@@ -1,74 +0,0 @@
name: clang-tidy
on:
push:
pull_request:
permissions: {}
jobs:
clang-tidy:
if: github.repository_owner == 'facebook'
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
steps:
- uses: actions/checkout@v4.1.0
with:
fetch-depth: 2
- name: Mark workspace as safe for git
run: git config --global --add safe.directory $GITHUB_WORKSPACE
- name: Determine diff base
id: diff-base
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="${{ github.event.pull_request.base.sha }}"
git fetch --depth=1 origin "$BASE"
else
BASE="${{ github.event.before }}"
if echo "$BASE" | grep -q '^0\{40\}$'; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "New branch push; skipping clang-tidy."
exit 0
fi
fi
echo "ref=$BASE" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
- name: Install clang-tidy
if: steps.diff-base.outputs.skip != 'true'
run: apt-get update && apt-get install -y clang-tidy-21 && ln -sf /usr/bin/clang-tidy-21 /usr/local/bin/clang-tidy
- name: Generate compile_commands.json
if: steps.diff-base.outputs.skip != 'true'
run: |
mkdir build && cd build
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DCMAKE_C_COMPILER=clang-21 \
-DCMAKE_CXX_COMPILER=clang++-21 ..
cd ..
ln -sf build/compile_commands.json compile_commands.json
- name: Run clang-tidy on changed files
id: clang-tidy
if: steps.diff-base.outputs.skip != 'true'
run: |
python3 tools/run_clang_tidy.py \
-j 4 \
--diff-base ${{ steps.diff-base.outputs.ref }} \
--github-annotations \
--github-step-summary \
--comment-output clang-tidy-comment.md
continue-on-error: true
- name: Save PR number
if: github.event_name == 'pull_request' && always()
run: echo "${{ github.event.pull_request.number }}" > pr_number.txt
- name: Upload clang-tidy results
if: always()
uses: actions/upload-artifact@v4.0.0
with:
name: clang-tidy-result
path: |
clang-tidy-comment.md
pr_number.txt
if-no-files-found: ignore
- name: Fail if clang-tidy found issues
if: steps.clang-tidy.outcome == 'failure'
run: exit 1
@@ -1,25 +0,0 @@
# Claude Code Review — Comment Posting Workflow
#
# Thin wrapper around .github/workflows/ai-review-comment.yml so the provider
# specific workflow name stays stable while the shared implementation lives in
# one reusable workflow.
name: Post Claude Review Comment
on:
workflow_run:
workflows: ["Claude Code Review"]
types: [completed]
permissions:
pull-requests: write
issues: write
jobs:
review-comment:
uses: ./.github/workflows/ai-review-comment.yml
with:
provider: claude
result_artifact_name: claude-review-result
comment_file: claude-review-comment.md
secrets: inherit
-69
View File
@@ -1,69 +0,0 @@
# Claude Code Review — Analysis Workflow
#
# Thin wrapper around .github/workflows/ai-review-analysis.yml so provider
# specific triggers and workflow_dispatch inputs stay stable while the shared
# implementation lives in one reusable workflow.
name: Claude Code Review
on:
workflow_run:
workflows: ["facebook/rocksdb/pr-jobs"]
types: [completed]
# The early pull_request_target path is limited to same-repo PRs by the job
# condition below. Fork PRs skip this path and rely on workflow_run instead.
pull_request_target:
types: [opened, reopened, synchronize]
issue_comment:
types: [created]
workflow_dispatch:
inputs:
pr_number:
description: PR number to review
required: true
type: number
model:
description: Claude model to use (defaults to latest Opus)
required: false
type: choice
options:
- claude-opus-4-6
- claude-sonnet-4-6
default: claude-opus-4-6
thinking_budget:
description: Override MAX_THINKING_TOKENS (blank = auto-classify, capped at 24000)
required: false
type: string
default: ""
permissions:
contents: read
# The shared analysis workflow polls check state, inspects prior runs, and
# reads PR metadata. Keep those permissions read-only in the caller.
actions: read
checks: read
pull-requests: read
jobs:
review:
if: >-
github.event_name != 'pull_request_target' ||
github.event.pull_request.head.repo.full_name == github.repository
uses: ./.github/workflows/ai-review-analysis.yml
with:
provider: claude
display_name: Claude
review_command: /claude-review
query_command: /claude-query
result_artifact_name: claude-review-result
comment_file: claude-review-comment.md
default_model: claude-opus-4-6
selected_model: ${{ github.event_name == 'workflow_dispatch' && inputs.model || '' }}
classifier_model: claude-sonnet-4-6
recovery_model: claude-sonnet-4-6
thinking_budget: ${{ github.event_name == 'workflow_dispatch' && inputs.thinking_budget || '' }}
dispatch_pr_number: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || '' }}
secrets: inherit
@@ -1,25 +0,0 @@
# Codex Code Review — Comment Posting Workflow
#
# Thin wrapper around .github/workflows/ai-review-comment.yml so the provider
# specific workflow name stays stable while the shared implementation lives in
# one reusable workflow.
name: Post Codex Review Comment
on:
workflow_run:
workflows: ["Codex Code Review"]
types: [completed]
permissions:
pull-requests: write
issues: write
jobs:
review-comment:
uses: ./.github/workflows/ai-review-comment.yml
with:
provider: codex
result_artifact_name: codex-review-result
comment_file: codex-review-comment.md
secrets: inherit
-68
View File
@@ -1,68 +0,0 @@
# Codex Code Review — Analysis Workflow
#
# Thin wrapper around .github/workflows/ai-review-analysis.yml so provider
# specific triggers and workflow_dispatch inputs stay stable while the shared
# implementation lives in one reusable workflow.
name: Codex Code Review
on:
workflow_run:
workflows: ["facebook/rocksdb/pr-jobs"]
types: [completed]
# The early pull_request_target path is limited to same-repo PRs by the job
# condition below. Fork PRs skip this path and rely on workflow_run instead.
pull_request_target:
types: [opened, reopened, synchronize]
issue_comment:
types: [created]
workflow_dispatch:
inputs:
pr_number:
description: PR number to review
required: true
type: number
model:
description: Codex model to use
required: false
type: choice
options:
- gpt-5.5
- gpt-5.3-codex
- gpt-5.2-codex
default: gpt-5.5
thinking_budget:
description: Override MAX_THINKING_TOKENS (blank = auto-classify)
required: false
type: string
default: ""
permissions:
contents: read
# The shared analysis workflow polls check state, inspects prior runs, and
# reads PR metadata. Keep those permissions read-only in the caller.
actions: read
checks: read
pull-requests: read
jobs:
review:
if: >-
github.event_name != 'pull_request_target' ||
github.event.pull_request.head.repo.full_name == github.repository
uses: ./.github/workflows/ai-review-analysis.yml
with:
provider: codex
display_name: Codex
review_command: /codex-review
query_command: /codex-query
result_artifact_name: codex-review-result
comment_file: codex-review-comment.md
default_model: gpt-5.5
selected_model: ${{ github.event_name == 'workflow_dispatch' && inputs.model || '' }}
thinking_budget: ${{ github.event_name == 'workflow_dispatch' && inputs.thinking_budget || '' }}
dispatch_pr_number: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || '' }}
secrets: inherit
+7 -26
View File
@@ -41,41 +41,22 @@ jobs:
- uses: "./.github/actions/pre-steps"
- run: make V=1 -j32 check
- uses: "./.github/actions/post-steps"
build-linux-clang-21-asan-ubsan-with-folly:
build-linux-clang-18-asan-ubsan-with-folly:
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
env:
CC: clang-21
CXX: clang++-21
CC: clang-18
CXX: clang++-18
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- name: Build folly and dependencies
run: |
clean_path=()
IFS=: read -ra path_entries <<< "$PATH"
for entry in "${path_entries[@]}"; do
if [[ "$entry" != "/usr/lib/ccache" ]]; then
clean_path+=("$entry")
fi
done
export PATH="$(IFS=:; echo "${clean_path[*]}")"
ccache_bin="$(command -v ccache || true)"
if [[ -n "$ccache_bin" && -x "$ccache_bin" ]]; then
mv "$ccache_bin" "${ccache_bin}.disabled"
fi
export CC=gcc
export CXX=g++
export USE_CCACHE=0
export CCACHE_DISABLE=1
make build_folly
shell: bash
- uses: "./.github/actions/build-folly"
- run: LIB_MODE=static USE_CLANG=1 USE_FOLLY=1 COMPILE_WITH_UBSAN=1 COMPILE_WITH_ASAN=1 make -j32 check
- uses: "./.github/actions/post-steps"
build-linux-cmake-with-folly:
@@ -164,13 +145,13 @@ jobs:
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- name: Build rocksdb lib
run: CC=clang-21 CXX=clang++-21 USE_CLANG=1 make -j4 static_lib
run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 make -j4 static_lib
- name: Build fuzzers
run: cd fuzz && make sst_file_writer_fuzzer db_fuzzer db_map_fuzzer
- uses: "./.github/actions/post-steps"
+95 -272
View File
@@ -1,18 +1,7 @@
name: facebook/rocksdb/pr-jobs
on: [push, pull_request]
permissions: {}
env:
# Set to a job name to run only that job (on any repo), or leave empty for
# normal behavior (all jobs on facebook repo only).
ONLY_JOB: ''
jobs:
config:
runs-on: ubuntu-latest
outputs:
only_job: ${{ steps.set.outputs.only_job }}
steps:
- id: set
run: echo "only_job=$ONLY_JOB" >> "$GITHUB_OUTPUT"
# NOTE: multiple workflows would be recommended, but the current GHA UI in
# PRs doesn't make it clear when there's an overall error with a workflow,
# making it easy to overlook something broken. Grouping everything into one
@@ -30,10 +19,6 @@ jobs:
# increasing the risk of misconfiguration, especially on forks that might
# want to run with this GHA setup.
#
# SELECTIVE JOB EXECUTION: Set the ONLY_JOB env var at the top of this file
# to a job name (e.g. "build-linux-clang-tidy") to run only that job,
# bypassing the repository owner check. Leave it empty for normal behavior.
#
# DEBUGGING WITH SSH: Temporarily add this as a job step, either before the
# step of interest without the "if:" line or after the failing step with the
# "if:" line. Then use ssh command printed in CI output.
@@ -45,8 +30,7 @@ jobs:
# ======================== Fast Initial Checks ====================== #
check-format-and-targets:
if: needs.config.outputs.only_job == 'check-format-and-targets' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4.1.0
@@ -60,10 +44,6 @@ jobs:
run: python -m pip install --upgrade pip
- name: Install argparse
run: pip install argparse
- name: Install clang-format
run: |
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
- name: Check format
@@ -72,8 +52,6 @@ jobs:
run: make check-buck-targets
- name: Simple source code checks
run: make check-sources
- name: Validate GitHub Actions YAML
run: make check-workflow-yaml
- name: Sanity check check_format_compatible.sh
run: |-
export TEST_TMPDIR=/dev/shm/rocksdb
@@ -84,8 +62,7 @@ jobs:
SANITY_CHECK=1 LONG_TEST=1 tools/check_format_compatible.sh
# ========================= Linux With Tests ======================== #
build-linux:
if: needs.config.outputs.only_job == 'build-linux' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 16-core-ubuntu
container:
@@ -94,27 +71,18 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: build-linux
- run: make V=1 J=32 -j32 check
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-cmake-mingw:
if: needs.config.outputs.only_job == 'build-linux-cmake-mingw' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: cmake-mingw
- run: update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix
- name: Build cmake-mingw
run: |-
@@ -123,14 +91,11 @@ jobs:
which java && java -version
which javac && javac -version
mkdir build && cd build && cmake -DJNI=1 -DWITH_GFLAGS=OFF .. -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ -DCMAKE_SYSTEM_NAME=Windows && make -j4 rocksdb rocksdbjni
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-make-with-folly:
if: needs.config.outputs.only_job == 'build-linux-make-with-folly' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 32-core-ubuntu
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
@@ -139,21 +104,15 @@ jobs:
- 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: make-with-folly
- uses: "./.github/actions/cache-folly"
id: cache-folly
- uses: "./.github/actions/build-folly"
with:
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
- run: USE_FOLLY=1 LIB_MODE=static V=1 make -j64 check
- uses: "./.github/actions/teardown-ccache"
if: always()
- run: USE_FOLLY=1 LIB_MODE=static V=1 make -j32 check
- uses: "./.github/actions/post-steps"
build-linux-make-with-folly-lite-no-test:
if: needs.config.outputs.only_job == 'build-linux-make-with-folly-lite-no-test' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 16-core-ubuntu
container:
@@ -164,18 +123,12 @@ jobs:
- 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: make-folly-lite
- run: USE_FOLLY_LITE=1 EXTRA_CXXFLAGS=-DGLOG_USE_GLOG_EXPORT V=1 make -j32 all
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-cmake-with-folly-coroutines:
if: needs.config.outputs.only_job == 'build-linux-cmake-with-folly-coroutines' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 32-core-ubuntu
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
@@ -184,48 +137,27 @@ jobs:
- 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: cmake-folly-coroutines
- uses: "./.github/actions/cache-folly"
id: cache-folly
- uses: "./.github/actions/build-folly"
with:
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
- run: "(mkdir build && cd build && cmake -DUSE_COROUTINES=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 -DPORTABLE=ON .. && make VERBOSE=1 -j64 && ctest -j64)"
- uses: "./.github/actions/teardown-ccache"
if: always()
- run: "(mkdir build && cd build && cmake -DUSE_COROUTINES=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 .. && make VERBOSE=1 -j20 && ctest -j20)"
- uses: "./.github/actions/post-steps"
build-linux-cmake-with-benchmark-no-thread-status:
if: needs.config.outputs.only_job == 'build-linux-cmake-with-benchmark-no-thread-status' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 16-core-ubuntu
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3]
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: cmake-benchmark
- name: Build
run: mkdir build && cd build && cmake -DWITH_GFLAGS=1 -DWITH_BENCHMARK=1 -DPORTABLE=ON -DCMAKE_CXX_FLAGS=-DNROCKSDB_THREAD_STATUS .. && make VERBOSE=1 -j20
- name: Test shard ${{ matrix.shard }} of 4
run: cd build && ctest -j20 -I ${{ matrix.shard }},,4
- uses: "./.github/actions/teardown-ccache"
if: always()
- run: mkdir build && cd build && cmake -DWITH_GFLAGS=1 -DWITH_BENCHMARK=1 -DCMAKE_CXX_FLAGS=-DNROCKSDB_THREAD_STATUS .. && make VERBOSE=1 -j20 && ctest -j20
- uses: "./.github/actions/post-steps"
with:
artifact-prefix: "${{ github.job }}-shard${{ matrix.shard }}"
build-linux-encrypted_env-no_compression:
if: needs.config.outputs.only_job == 'build-linux-encrypted_env-no_compression' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 16-core-ubuntu
container:
@@ -234,18 +166,12 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: encrypted-env-no-compression
- run: ENCRYPTED_ENV=1 ROCKSDB_DISABLE_SNAPPY=1 ROCKSDB_DISABLE_ZLIB=1 ROCKSDB_DISABLE_BZIP=1 ROCKSDB_DISABLE_LZ4=1 ROCKSDB_DISABLE_ZSTD=1 make V=1 J=32 -j32 check
- run: "./sst_dump --help | grep -E -q 'Supported built-in compression types: kNoCompression$' # Verify no compiled in compression\n"
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
# ======================== Linux No Test Runs ======================= #
build-linux-release:
if: needs.config.outputs.only_job == 'build-linux-release' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 16-core-ubuntu
container:
@@ -253,9 +179,6 @@ jobs:
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: release
- run: make V=1 -j32 LIB_MODE=shared release
- run: ls librocksdb.so
- run: "./trace_analyzer --version" # A tool dependent on gflags that can run in release build
@@ -272,12 +195,9 @@ jobs:
- run: USE_RTTI=1 make V=1 -j32 release
- run: ls librocksdb.a
- run: if ./trace_analyzer --version; then false; else true; fi
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-clang-13-no_test_run:
if: needs.config.outputs.only_job == 'build-linux-clang-13-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 8-core-ubuntu
container:
@@ -286,61 +206,61 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang-13
# FIXME: get back to "all microbench" targets
- run: CC=clang-13 CXX=clang++-13 USE_CLANG=1 EXTRA_CXXFLAGS=-stdlib=libc++ EXTRA_LDFLAGS=-stdlib=libc++ make -j32 shared_lib
- run: make clean
# FIXME: get back to "release" target
- run: CC=clang-13 CXX=clang++-13 USE_CLANG=1 EXTRA_CXXFLAGS=-stdlib=libc++ EXTRA_LDFLAGS=-stdlib=libc++ DEBUG_LEVEL=0 make -j32 shared_lib
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-clang-21-no_test_run:
if: needs.config.outputs.only_job == 'build-linux-clang-21-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
build-linux-clang-18-no_test_run:
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang-21
- run: CC=clang-21 CXX=clang++-21 USE_CLANG=1 make -j32 all microbench
- run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 make -j32 all microbench
- run: make clean
- run: CC=clang-21 CXX=clang++-21 USE_CLANG=1 DEBUG_LEVEL=0 make -j32 release
- uses: "./.github/actions/teardown-ccache"
if: always()
- run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 DEBUG_LEVEL=0 make -j32 release
- uses: "./.github/actions/post-steps"
build-linux-gcc-14-no_test_run:
if: needs.config.outputs.only_job == 'build-linux-gcc-14-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: gcc-14
- run: CC=gcc-14 CXX=g++-14 V=1 make -j32 all microbench
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
# ======================== Linux Other Checks ======================= #
build-linux-clang18-clang-analyze:
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- run: CC=clang-18 CXX=clang++-18 ROCKSDB_DISABLE_ALIGNED_NEW=1 CLANG_ANALYZER="/usr/bin/clang++-18" CLANG_SCAN_BUILD=scan-build-18 USE_CLANG=1 make V=1 -j32 analyze
- uses: "./.github/actions/post-steps"
- name: compress test report
run: tar -cvzf scan_build_report.tar.gz scan_build_report
if: failure()
- uses: actions/upload-artifact@v4.0.0
with:
name: scan-build-report
path: scan_build_report.tar.gz
build-linux-unity-and-headers:
if: needs.config.outputs.only_job == 'build-linux-unity-and-headers' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 4-core-ubuntu
container:
@@ -349,101 +269,49 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- run: apt-get update -y && apt-get install -y libgflags-dev
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: unity-headers
- name: Unity build
run: make V=1 -j8 unity_test
- run: make V=1 -j8 -k check-headers
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-mini-crashtest:
if: needs.config.outputs.only_job == 'build-linux-mini-crashtest' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 4-core-ubuntu
strategy:
fail-fast: false
matrix:
include:
- crash_test_target: blackbox_crash_test_with_atomic_flush
crash_duration: 480
- crash_test_target: blackbox_crash_test
crash_duration: 240
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: mini-crashtest
- run: ulimit -S -n `ulimit -H -n` && 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()
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=960 --max_key=2500000' blackbox_crash_test_with_atomic_flush
- uses: "./.github/actions/post-steps"
with:
artifact-prefix: "${{ github.job }}-${{ matrix.crash_test_target }}"
# ======================= Linux with Sanitizers ===================== #
build-linux-clang21-asan-ubsan:
if: needs.config.outputs.only_job == 'build-linux-clang21-asan-ubsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
build-linux-clang18-asan-ubsan:
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 32-core-ubuntu
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2]
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang21-asan-ubsan
- run: COMPILE_WITH_ASAN=1 COMPILE_WITH_UBSAN=1 CC=clang-21 CXX=clang++-21 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j40 check
env:
CI_SHARD_INDEX: ${{ matrix.shard }}
CI_TOTAL_SHARDS: 3
- uses: "./.github/actions/teardown-ccache"
if: always()
- run: COMPILE_WITH_ASAN=1 COMPILE_WITH_UBSAN=1 CC=clang-18 CXX=clang++-18 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j40 check
- uses: "./.github/actions/post-steps"
with:
artifact-prefix: "${{ github.job }}-shard${{ matrix.shard }}"
build-linux-clang21-mini-tsan:
if: needs.config.outputs.only_job == 'build-linux-clang21-mini-tsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
build-linux-clang18-mini-tsan:
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 32-core-ubuntu
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2]
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang21-tsan
- run: COMPILE_WITH_TSAN=1 CC=clang-21 CXX=clang++-21 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j32 check
env:
CI_SHARD_INDEX: ${{ matrix.shard }}
CI_TOTAL_SHARDS: 3
- uses: "./.github/actions/teardown-ccache"
if: always()
- run: COMPILE_WITH_TSAN=1 CC=clang-18 CXX=clang++-18 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j32 check
- uses: "./.github/actions/post-steps"
with:
artifact-prefix: "${{ github.job }}-shard${{ matrix.shard }}"
build-linux-static_lib-alt_namespace-status_checked:
if: needs.config.outputs.only_job == 'build-linux-static_lib-alt_namespace-status_checked' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 16-core-ubuntu
container:
@@ -452,17 +320,11 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: static-alt-namespace
- run: ASSERT_STATUS_CHECKED=1 TEST_UINT128_COMPAT=1 ROCKSDB_MODIFY_NPHASH=1 LIB_MODE=static OPT="-DROCKSDB_USE_STD_SEMAPHORES -DROCKSDB_NAMESPACE=alternative_rocksdb_ns" make V=1 -j24 check
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
# ========================= MacOS build only ======================== #
build-macos:
if: needs.config.outputs.only_job == 'build-macos' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on: macos-15-xlarge
env:
ROCKSDB_DISABLE_JEMALLOC: 1
@@ -471,21 +333,15 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/pre-steps-macos"
- name: Build
run: ulimit -S -n `ulimit -H -n` && make V=1 J=16 -j8 all
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
# ========================= MacOS with Tests ======================== #
build-macos-cmake:
if: needs.config.outputs.only_job == 'build-macos-cmake' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on: macos-15-xlarge
strategy:
matrix:
@@ -495,9 +351,6 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-cmake
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/pre-steps-macos"
@@ -517,87 +370,83 @@ jobs:
- name: Run shard 3 out of 4 test shards
run: ulimit -S -n `ulimit -H -n` && cd build && ctest -j8 -I 3,,4
if: ${{ matrix.run_sharded_tests == 3 }}
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
# ======================== Windows with Tests ======================= #
# NOTE: some windows jobs are in "nightly" to save resources
build-windows-vs2022:
if: needs.config.outputs.only_job == 'build-windows-vs2022' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on: windows-8-core
strategy:
fail-fast: false
matrix:
include:
- test_shard: db_test
suite_run: db_test
run_java: "false"
- test_shard: other
suite_run: arena_test,db_basic_test,db_test2,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test
run_java: "false"
- test_shard: java
suite_run: ""
run_java: "true"
env:
CMAKE_GENERATOR: Visual Studio 17 2022
CMAKE_PORTABLE: 1
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/windows-build-steps"
with:
suite-run: ${{ matrix.suite_run }}
run-java: ${{ matrix.run_java }}
# ============================ Java Jobs ============================ #
build-linux-java:
if: needs.config.outputs.only_job == 'build-linux-java' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
# The docker image is intentionally based on an OS that has an older GLIBC version.
# That GLIBC is incompatibile with GitHub's actions/checkout. Thus we implement a manual checkout step.
# NOTE: replaced evolvedbinary/rocksjava:centos7_x64-be with ghcr.io/facebook/rocksdb_ubuntu:22.1
# until a more appropriate docker image with C++20 support is made.
- name: Checkout
env:
GH_TOKEN: ${{ github.token }}
run: |
chown `whoami` . || true
git clone --no-checkout https://oath2:$GH_TOKEN@github.com/${{ github.repository }}.git .
git -c protocol.version=2 fetch --update-head-ok --no-tags --prune --no-recurse-submodules --depth=1 origin +${{ github.sha }}:${{ github.ref }}
git checkout --progress --force ${{ github.ref }}
git log -1 --format='%H'
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: java
- name: Set Java Environment
run: |-
echo "JAVA_HOME=${JAVA_HOME}"
which java && java -version
which javac && javac -version
- name: Test RocksDBJava
# NOTE: replaced scl enable devtoolset-7 'make V=1 J=8 -j8 jtest'
run: make V=1 J=8 -j8 jtest
- uses: "./.github/actions/teardown-ccache"
if: always()
# post-steps skipped because of compatibility issues with docker image
build-linux-java-static:
if: needs.config.outputs.only_job == 'build-linux-java-static' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
# The docker image is intentionally based on an OS that has an older GLIBC version.
# That GLIBC is incompatibile with GitHub's actions/checkout. Thus we implement a manual checkout step.
# NOTE: replaced evolvedbinary/rocksjava:centos7_x64-be with ghcr.io/facebook/rocksdb_ubuntu:22.1
# until a more appropriate docker image with C++20 support is made.
- name: Checkout
env:
GH_TOKEN: ${{ github.token }}
run: |
chown `whoami` . || true
git clone --no-checkout https://oath2:$GH_TOKEN@github.com/${{ github.repository }}.git .
git -c protocol.version=2 fetch --update-head-ok --no-tags --prune --no-recurse-submodules --depth=1 origin +${{ github.sha }}:${{ github.ref }}
git checkout --progress --force ${{ github.ref }}
git log -1 --format='%H'
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: java-static
- name: Set Java Environment
run: |-
echo "JAVA_HOME=${JAVA_HOME}"
which java && java -version
which javac && javac -version
- name: Build RocksDBJava Static Library
# NOTE: replaced scl enable devtoolset-7 'make V=1 J=8 -j8 rocksdbjavastatic'
run: make V=1 J=8 -j8 rocksdbjavastatic
- uses: "./.github/actions/teardown-ccache"
if: always()
# post-steps skipped because of compatibility issues with docker image
build-macos-java:
if: needs.config.outputs.only_job == 'build-macos-java' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on: macos-15-xlarge
env:
JAVA_HOME: "/Library/Java/JavaVirtualMachines/liberica-jdk-8.jdk/Contents/Home"
@@ -607,9 +456,6 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-java
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/install-jdk8-on-macos"
@@ -621,12 +467,9 @@ jobs:
which javac && javac -version
- name: Test RocksDBJava
run: make V=1 J=16 -j16 jtest
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-macos-java-static:
if: needs.config.outputs.only_job == 'build-macos-java-static' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on: macos-15-xlarge
env:
JAVA_HOME: "/Library/Java/JavaVirtualMachines/liberica-jdk-8.jdk/Contents/Home"
@@ -635,9 +478,6 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-java-static
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/install-jdk8-on-macos"
@@ -649,12 +489,9 @@ jobs:
which javac && javac -version
- name: Build RocksDBJava x86 and ARM Static Libraries
run: make V=1 J=16 -j16 rocksdbjavastaticosx
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-macos-java-static-universal:
if: needs.config.outputs.only_job == 'build-macos-java-static-universal' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on: macos-15-xlarge
env:
JAVA_HOME: "/Library/Java/JavaVirtualMachines/liberica-jdk-8.jdk/Contents/Home"
@@ -663,9 +500,6 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-java-static-universal
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/install-jdk8-on-macos"
@@ -677,12 +511,9 @@ jobs:
which javac && javac -version
- name: Build RocksDBJava Universal Binary Static Library
run: make V=1 J=16 -j16 rocksdbjavastaticosx_ub
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-java-pmd:
if: needs.config.outputs.only_job == 'build-linux-java-pmd' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 4-core-ubuntu
container:
@@ -708,20 +539,12 @@ jobs:
name: maven-site
path: "${{ github.workspace }}/java/target/site"
build-linux-arm:
if: needs.config.outputs.only_job == 'build-linux-arm' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 4-core-ubuntu-arm
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- run: sudo apt-get update && sudo apt-get install -y build-essential ccache
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: arm
- run: sudo apt-get update && sudo apt-get install -y build-essential
- run: ROCKSDBTESTS_PLATFORM_DEPENDENT=only make V=1 J=4 -j4 all_but_some_tests check_some
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
-8
View File
@@ -102,11 +102,3 @@ cmake-build-*
third-party/folly/
.cache
*.sublime-*
# Claude Code local settings
.claude/settings.local.json
tools/__pycache__/
# Keep documentation trackable even if broader ignores match names like "*_test".
!docs/
!docs/**
-9
View File
@@ -1,9 +0,0 @@
# Agent Instructions
This repository's authoritative agent instructions live in `CLAUDE.md`.
Read and follow [`CLAUDE.md`](./CLAUDE.md) in full before making changes or
reviewing code in this checkout.
If there is any ambiguity between this file and `CLAUDE.md`, `CLAUDE.md` takes
precedence.
+4 -67
View File
@@ -32,14 +32,12 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"db/blob/blob_file_cache.cc",
"db/blob/blob_file_garbage.cc",
"db/blob/blob_file_meta.cc",
"db/blob/blob_file_partition_manager.cc",
"db/blob/blob_file_reader.cc",
"db/blob/blob_garbage_meter.cc",
"db/blob/blob_log_format.cc",
"db/blob/blob_log_sequential_reader.cc",
"db/blob/blob_log_writer.cc",
"db/blob/blob_source.cc",
"db/blob/blob_write_batch_transformer.cc",
"db/blob/prefetch_buffer_collection.cc",
"db/builder.cc",
"db/c.cc",
@@ -108,10 +106,8 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"db/version_edit.cc",
"db/version_edit_handler.cc",
"db/version_set.cc",
"db/version_util.cc",
"db/wal_edit.cc",
"db/wal_manager.cc",
"db/wide/read_path_blob_resolver.cc",
"db/wide/wide_column_serialization.cc",
"db/wide/wide_columns.cc",
"db/wide/wide_columns_helper.cc",
@@ -211,7 +207,6 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"table/block_based/hash_index_reader.cc",
"table/block_based/index_builder.cc",
"table/block_based/index_reader_common.cc",
"table/block_based/multi_scan_index_iterator.cc",
"table/block_based/parsed_full_filter_block.cc",
"table/block_based/partitioned_filter_block.cc",
"table/block_based/partitioned_index_iterator.cc",
@@ -334,7 +329,6 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"utilities/secondary_index/simple_secondary_index.cc",
"utilities/simulator_cache/cache_simulator.cc",
"utilities/simulator_cache/sim_cache.cc",
"utilities/sorted_run_builder/sorted_run_builder.cc",
"utilities/table_properties_collectors/compact_for_tiering_collector.cc",
"utilities/table_properties_collectors/compact_on_deletion_collector.cc",
"utilities/trace/file_trace_reader_writer.cc",
@@ -368,9 +362,6 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"utilities/transactions/write_prepared_txn_db.cc",
"utilities/transactions/write_unprepared_txn.cc",
"utilities/transactions/write_unprepared_txn_db.cc",
"utilities/trie_index/bitvector.cc",
"utilities/trie_index/louds_trie.cc",
"utilities/trie_index/trie_index_factory.cc",
"utilities/ttl/db_ttl_impl.cc",
"utilities/types_util.cc",
"utilities/wal_filter.cc",
@@ -378,10 +369,10 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"utilities/write_batch_with_index/write_batch_with_index_internal.cc",
], deps=[
"//folly/container:f14_hash",
"//folly/coro:blocking_wait",
"//folly/coro:collect",
"//folly/coro:coroutine",
"//folly/coro:task",
"//folly/experimental/coro:blocking_wait",
"//folly/experimental/coro:collect",
"//folly/experimental/coro:coroutine",
"//folly/experimental/coro:task",
"//folly/synchronization:distributed_mutex",
], headers=glob(["**/*.h"]), link_whole=False, extra_test_libs=False)
@@ -4808,12 +4799,6 @@ cpp_unittest_wrapper(name="db_blob_corruption_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_blob_direct_write_test",
srcs=["db/blob/db_blob_direct_write_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_blob_index_test",
srcs=["db/blob/db_blob_index_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -4838,12 +4823,6 @@ cpp_unittest_wrapper(name="db_clip_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_compaction_abort_test",
srcs=["db/db_compaction_abort_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_compaction_filter_test",
srcs=["db/db_compaction_filter_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -4952,12 +4931,6 @@ cpp_unittest_wrapper(name="db_merge_operator_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_open_with_config_test",
srcs=["db/db_open_with_config_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_options_test",
srcs=["db/db_options_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5048,12 +5021,6 @@ cpp_unittest_wrapper(name="db_wide_basic_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_wide_blob_direct_write_test",
srcs=["db/wide/db_wide_blob_direct_write_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_with_timestamp_basic_test",
srcs=["db/db_with_timestamp_basic_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5164,12 +5131,6 @@ cpp_unittest_wrapper(name="faiss_ivf_index_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="fault_injection_fs_test",
srcs=["utilities/fault_injection_fs_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="fault_injection_test",
srcs=["db/fault_injection_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5554,12 +5515,6 @@ cpp_unittest_wrapper(name="slice_transform_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="sorted_run_builder_test",
srcs=["utilities/sorted_run_builder/sorted_run_builder_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="sst_dump_test",
srcs=["tools/sst_dump_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5662,18 +5617,6 @@ cpp_unittest_wrapper(name="transaction_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="trie_index_db_test",
srcs=["utilities/trie_index/trie_index_db_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="trie_index_test",
srcs=["utilities/trie_index/trie_index_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="ttl_test",
srcs=["utilities/ttl/ttl_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5776,12 +5719,6 @@ cpp_unittest_wrapper(name="write_controller_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="write_prepared_transaction_seqno_test",
srcs=["utilities/transactions/write_prepared_transaction_seqno_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="write_prepared_transaction_test",
srcs=["utilities/transactions/write_prepared_transaction_test.cc"],
deps=[":rocksdb_test_lib"],
-331
View File
@@ -1,331 +0,0 @@
# RocksDB Code Generation and Review Guidance
This document provides guidance for generating and reviewing code in the RocksDB project, derived from analysis of code review feedback across hundreds of complex merged Pull Requests. Use this as a reference when writing code with AI assistants or conducting code reviews.
---
## General Best Practices
### Code Quality and Maintainability
**Clarity and Readability:** Write clear, self-documenting code. Use meaningful variable names, add comments for complex logic, and structure code to minimize cognitive load. Avoid clever tricks that sacrifice readability for marginal performance gains unless absolutely necessary.
**Consistent Style:** Follow existing code style conventions. RocksDB uses `.clang-format` for formatting, specific naming conventions, and structural patterns. Deviations from these patterns are frequently flagged in reviews.
**Error Handling:** Ensure robust error handling throughout the codebase. Use RocksDB's `Status` type consistently, propagate errors appropriately, and avoid silently ignoring failures. Reviewers pay close attention to edge cases and failure modes.
### Testing Philosophy
**Comprehensive Coverage:** Every change should include appropriate test coverage. This includes unit tests for isolated functionality, integration tests for component interactions, and stress tests for concurrency and performance validation. Reviewers will ask for additional tests if coverage is insufficient.
**Edge Cases and Failure Modes:** Tests should explicitly cover edge cases, boundary conditions, and potential failure scenarios. This is especially important for changes affecting core database operations, compaction, or recovery logic.
**Platform-Specific Testing:** RocksDB supports multiple platforms (Linux, Windows, macOS) and compilers (GCC, Clang, MSVC). Changes should be tested across relevant platforms, particularly when touching platform-specific code or using compiler-specific features.
### Performance Considerations
**⚠️ PERFORMANCE IS CRITICAL:** RocksDB is a high-performance storage engine where every CPU cycle and memory access matters. When writing code, always evaluate from a performance perspective. This is not optional—performance-aware coding is a fundamental requirement for all contributions.
**Benchmarking and Profiling:** Performance claims should be backed by empirical evidence. Use RocksDB's benchmarking tools (e.g., `db_bench`) to validate improvements. Reviewers will request benchmark results for changes that could impact performance.
**Memory Allocation:** Minimize dynamic memory allocations, especially in hot paths. Prefer stack allocation over heap allocation. Reuse buffers when possible. Consider using arena allocators or memory pools for frequent small allocations. Every `new`, `malloc`, or container resize has a cost.
**Memory Copy:** Avoid unnecessary memory copies. Use move semantics, `std::string_view`, `Slice`, and pass-by-reference where appropriate. Be aware of implicit copies in STL containers and function returns. Prefer in-place operations over copy-and-modify patterns.
**CPU Cache Efficiency:** Design data structures and access patterns to be cache-friendly. Keep frequently accessed data together (data locality). Prefer sequential memory access over random access. Be mindful of cache line sizes (typically 64 bytes) and avoid false sharing in concurrent code. Consider struct packing and field ordering to improve cache utilization.
**Loop Optimization:** Look for opportunities to collapse nested loops, reduce loop overhead, and minimize branch mispredictions. Hoist invariant computations out of loops. Consider loop unrolling for tight inner loops. Batch operations when possible to amortize per-operation overhead.
**SIMD and Vectorization:** Leverage SIMD instructions (SSE, AVX) for data-parallel operations when appropriate. Structure data to enable auto-vectorization by the compiler. Consider explicit SIMD intrinsics for critical hot paths like checksum computation, encoding/decoding, and bulk data processing.
**Branch Prediction:** Minimize unpredictable branches in hot paths. Use `LIKELY`/`UNLIKELY` macros to hint branch prediction. Consider branchless alternatives for simple conditionals. Order switch cases and if-else chains by frequency.
**Memory and Resource Management:** Be mindful of memory allocations, especially in hot paths. Use RAII patterns, smart pointers, and RocksDB's memory management utilities appropriately.
**Hot Path Analysis:** When deciding how aggressively to optimize code, consider whether it's on a hot path:
- **Hot path** (executed thousands+ times, e.g., data access, iteration, compaction loops): Performance is paramount. Apply all optimization techniques—loop collapsing, SIMD, cache optimization, pre-allocation, etc. The cost of each operation is multiplied by execution frequency.
- **Cold path** (executed rarely, e.g., DB open, configuration parsing, error handling): Maintainability and clarity are more important. Prefer readable code over micro-optimizations. Complex optimizations here add maintenance burden with negligible performance benefit.
- **Warm path** (moderate frequency): Balance both concerns. Use profiling data to guide optimization decisions.
**Avoid Premature Optimization:** While performance is critical, focus on correctness first, then optimize based on profiling data. However, be performance-aware from the start—choosing the right algorithm and data structure upfront is not premature optimization. Use the hot path analysis above to decide how much optimization effort is warranted.
### API Design and Compatibility
**Backwards Compatibility:** RocksDB maintains strong backwards compatibility guarantees. Breaking changes are rare and require extensive justification. When deprecating features, follow the project's deprecation policy (typically spanning multiple releases).
**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.
---
## Component-Specific Guidance
### Database Core (`db`)
The database core handles write-ahead logging (WAL), memtables, compaction, and recovery. This component receives the most scrutiny in code reviews.
**Concurrency and Thread Safety:** Database operations are highly concurrent. Reviewers carefully examine locking strategies, atomic operations, and memory ordering. Document synchronization assumptions clearly. Use appropriate memory ordering semantics (`acquire`/`release` vs. `seq_cst`).
**Compaction Logic:** Changes to compaction are complex and high-risk. Ensure that compaction logic respects configured parameters, handles edge cases (empty databases, single-file compactions), and maintains correctness under concurrent operations.
**Error Propagation:** Database operations can fail in many ways (I/O errors, corruption, resource exhaustion). Ensure that errors are properly propagated, logged, and handled. Avoid assertions in production code paths.
**Testing:** Database core changes require extensive testing, including unit tests, integration tests, and stress tests. Test with various configurations, compaction styles, and concurrent workloads.
### Public Headers (`include`)
Public headers define RocksDB's API surface. Changes here have the highest compatibility impact.
**API Design:** New APIs should be intuitive, consistent with existing patterns, and well-documented. Consider how the API will be used in practice and avoid adding unnecessary complexity.
**Backwards Compatibility:** Breaking changes to public APIs require extensive justification and a deprecation plan. Maintain ABI compatibility for bug fixes and patch releases.
**Documentation:** Every public API must be thoroughly documented with usage examples, parameter descriptions, and notes on thread safety and performance characteristics.
**Deprecation:** When deprecating APIs, follow the project's policy. Mark deprecated APIs clearly, provide migration guidance, and maintain support for at least one major release.
### Internal Utilities (`util`)
Internal utilities provide common functionality used throughout the codebase.
**Code Reuse:** Utilities should be general-purpose and reusable. Avoid duplicating functionality that already exists elsewhere in the codebase.
**Error Handling:** Utility functions should handle errors robustly and propagate them appropriately. Consider edge cases like overflow, underflow, and invalid inputs.
**Testing:** Utility functions should have comprehensive test coverage, including edge cases and failure modes. Consider adding death tests for assertions.
**Performance:** Utilities are often used in hot paths. Ensure that implementations are efficient and avoid unnecessary allocations or copies.
### Table Management (`table`)
Table management handles SST file format, block-based tables, and table readers/writers.
**Block Format and Checksums:** Changes to block format require extreme care. Ensure that checksums are computed and verified correctly. Test with various compression algorithms and block sizes.
**Iterator Correctness:** Table iterators are used throughout the codebase. Ensure that iterator semantics (Seek, Next, Prev) are correct, especially at boundaries and with deletions.
**Caching and Prefetching:** Table readers interact with the block cache and prefetching logic. Ensure that cache keys are unique and that prefetching respects configured limits.
**Performance:** Table operations are performance-critical. Benchmark changes that could impact read or write performance.
### Utilities (`utilities`)
Utilities include optional features like transactions, backup engine, and checkpoint.
**Feature Isolation:** Utilities should be self-contained and not introduce unnecessary dependencies on core database internals.
**Deprecation and Cleanup:** Legacy features are being phased out. When removing deprecated code, ensure that migration paths are documented and that users have sufficient warning.
**Cross-Platform Compatibility:** Utilities often interact with OS-specific APIs. Ensure that code works on all supported platforms.
### Options and Configuration (`options`)
Options define RocksDB's configuration system.
**Type Safety:** Use appropriate types for options (e.g., `uint32_t` for flags, scoped enums for enumerated values).
**Deprecation Policy:** When deprecating options, follow the project's policy. Document the deprecation, provide migration guidance, and maintain support for at least one major release.
**Dynamic Configuration:** Some options can be changed dynamically. Ensure that dynamic changes are thread-safe and take effect correctly.
**Validation:** Validate option values and provide clear error messages for invalid configurations.
### Cache (`cache`)
Cache management is critical for RocksDB's performance.
**Concurrency:** Cache operations are highly concurrent. Ensure that implementations are thread-safe and use appropriate synchronization primitives.
**Performance:** Cache operations are in the hot path. Optimize for low latency and high throughput. Benchmark changes carefully.
**Memory Management:** Cache implementations must manage memory carefully to avoid leaks and excessive allocations.
**Eviction Policies:** Changes to eviction policies should be well-tested and benchmarked to ensure they improve overall performance.
---
## Code Review Checklist
When reviewing RocksDB code (or preparing code for review), use this checklist:
### Correctness
- [ ] Does the change preserve database semantics (e.g., snapshot isolation, key ordering)?
- [ ] Are all error cases handled appropriately?
- [ ] Is the change thread-safe? Are synchronization primitives used correctly?
- [ ] Are there any potential data races or deadlocks?
### Testing
- [ ] Does the change include appropriate test coverage?
- [ ] Are edge cases and failure modes tested?
- [ ] Have the tests been run on all supported platforms?
- [ ] Are stress tests passing?
### Performance
- [ ] Are there benchmark results for performance-sensitive changes?
- [ ] Does the change avoid unnecessary allocations or copies?
- [ ] Are hot paths optimized appropriately?
### API and Compatibility
- [ ] Is the change backwards compatible?
- [ ] Are new APIs consistent with existing patterns?
- [ ] Is the public API documented?
- [ ] Are deprecated features handled according to policy?
### Code Quality
- [ ] Does the code follow RocksDB's style conventions?
- [ ] Is the code clear and maintainable?
- [ ] Are comments and documentation sufficient?
- [ ] Are there any code smells or anti-patterns?
---
## Common Review Feedback Patterns
The following patterns emerged as frequent sources of review feedback:
1. **Test Coverage:** Reviewers frequently request additional tests for edge cases, platform-specific behavior, and failure modes. Complex changes require comprehensive test coverage including unit tests, integration tests, and stress tests.
2. **Error Handling:** Ensure proper error propagation using RocksDB's `Status` type. Avoid silent failures and provide clear error messages that include context about what failed and why.
3. **API Design:** New APIs should be consistent with existing patterns. Use descriptive names that follow established conventions. Avoid breaking changes without strong justification and a clear deprecation plan.
4. **Documentation:** Public APIs must be documented with usage examples and notes on thread safety, performance characteristics, and compatibility considerations. Complex internal logic should also be well-commented.
5. **Performance:** Performance-sensitive changes require benchmark results to validate improvements. Use `db_bench` and other profiling tools to measure impact. Avoid premature optimization that adds complexity without measurable benefit.
6. **Concurrency:** Thread safety is critical in RocksDB. Document synchronization assumptions clearly. Use appropriate memory ordering semantics. Consider potential race conditions and deadlocks.
7. **Code Style:** Follow existing conventions for naming, formatting, and structure. Use `.clang-format` for consistent formatting. Prefer scoped enums (`enum class`) over unscoped enums.
8. **Backwards Compatibility:** RocksDB maintains strong compatibility guarantees. Breaking changes require extensive justification. When deprecating features, provide migration guidance and maintain support across multiple releases.
9. **Refactoring:** Reviewers appreciate refactoring that improves code readability and maintainability. Look for opportunities to deduplicate code and simplify complex logic.
10. **Platform Compatibility:** Ensure changes work correctly on all supported platforms (Linux, Windows, macOS) and with all supported compilers (GCC, Clang, MSVC).
---
## Important tips
### Build system
* There are 3 build system. Make, CMake, BUCK(meta internal).
* When a new .cc file is added, update Makefile, CMakeLists.txt, src.mk, BUCK.
* Don't manually edit BUCK file, after updating src.mk, run
/usr/local/bin/python3 buckifier/buckify_rocksdb.py to update it
* Use make to build and run the test. CMake and BUCK are not used locally.
* Use `make dbg` command to build all of the unit test in debug mode.
* For -j in make command, use the number of CPU cores to decide it.
### Unit Test
* After all of the unit tests are added, review them and try to extract common
reusable utility functions to reduce code duplication due to copy past between
unit tests. This should be done every time unit test is updated.
* Don't use sleep to wait for certain events to happen. This will cause test to
be flaky. Instead, use sync point to synchronize thread progress.
* Cap unit test execution with 60 seconds timeout.
* When there are multiple unit tests need to be executed, try to use
gtest_parallel.py if available. E.g.
python3 ${GTEST_PARALLEL}/gtest_parallel.py ./table_test
* After writing a test, stress-test for flakiness:
```bash
COERCE_CONTEXT_SWITCH=1 make {test_binary}
./{test_binary} --gtest_filter="*YourTestName*" --gtest_repeat=5
```
### Unit test dedup guidelines
* Extract helper functions for repeated patterns such as object
construction, round-trip (encode → decode → verify), and common
assertion sequences.
* Use table-driven tests (struct array + loop) when multiple test cases
share the same logic but differ only in input/expected data.
* Prefer randomized tests over exhaustive parameter permutations. Use
`Random` from `util/random.h` (not `std::mt19937`). Use a time-based
seed with `SCOPED_TRACE("seed=" + std::to_string(seed))` so failures
are reproducible.
* Keep deterministic edge-case tests separate from randomized tests
(error paths, boundary conditions, format verification).
* Methods only used in tests should be private with `friend class` +
`TEST_F` fixture wrappers. In wrappers, always fully qualify the
target method to avoid infinite recursion.
### Adding new public API
Refer to claude_md/add_public_api.md
### Adding new option
Refer to claude_md/add_option.md
### Removing deprecated option
Refer to claude_md/remove_option.md
### Metrics
* When adding a new feature, evaluate whether there is opportunity to add
metrics. Try to avoid causing performance regression on hot path when adding
metrics.
### Stress test
* When adding a new feature, make sure stress test covers the new option.
### Component docs
* For component-level design notes and implementation walkthroughs, start with
`docs/components/index.md`.
* Documentation under `docs/components/` is organized by subsystem in
`docs/components/<area>/`.
* Each subsystem directory should have an `index.md` entry point plus focused
chapter files for deeper topics.
### DB bench update
* When adding a performance related feature, support it in db_bench
### Adding release note
* Release note should be kept short at high level for external user consumption.
### Blog posts (docs/_posts)
* Blog post authors must be defined in `docs/_data/authors.yml` to be displayed
### Final verification of the change
* Execute make clean to clean all of the changes.
* Execute make check to build all of the changes and execute all of the tests.
Note that executing all of the tests could take multiple minutes.
* Run `ASSERT_STATUS_CHECKED=1 make check` to verify all Status objects are
properly checked. This catches missing error handling that can lead to
silent data corruption.
### Monitoring make check progress
* Use `make check-progress` to get machine-parseable JSON progress while
`make check` is running. This is useful for Claude Code to monitor long
builds without timeout issues.
* Run `make check` in background, then poll progress:
```bash
make check &
# Poll periodically:
make check-progress
```
* The output shows current phase and progress:
```json
{"status":"running","phase":"compiling","completed":300,"total":919,...}
{"status":"running","phase":"testing","completed":1500,"total":29962,"failed":0,"percent":5,...}
{"status":"completed","phase":"testing","completed":29962,"total":29962,"failed":0,"percent":100,...}
```
* Phases: `compiling` -> `linking` -> `generating` -> `testing` -> `completed`
* Key fields: `status`, `phase`, `completed`, `total`, `failed`, `percent`
* When tests fail, `failed_tests` array shows details (up to 10 failures):
```json
{"status":"running",...,"failed":3,"failed_tests":[
{"test":"cache_test-CacheTest.Usage","exit_code":1,"signal":0,"output":"...test log..."},
{"test":"env_test-EnvTest.Open","exit_code":0,"signal":11,"output":"...Segmentation fault..."}
]}
```
* `exit_code`: non-zero means test assertion failed
* `signal`: non-zero means test was killed (e.g., 9=SIGKILL, 6=SIGABRT, 11=SIGSEGV)
* `output`: last 50 lines of test log including error messages and stack traces
### Executing benchmark using db_bench
* Since the goal is to measure performance, we need to build a release binary
using `make clean && DEBUG_LEVEL=0 make db_bench`. If there is an engine
crash due to bug, we need to switch back to debug build. Make sure to run
`make clean` before running `make dbg`.
### Formatting code
* After making change, use `make format-auto` to auto-apply formatting without
interactive prompts (Claude Code friendly).
+4 -80
View File
@@ -82,15 +82,10 @@ if(NOT CMAKE_BUILD_TYPE)
endif()
message(STATUS "CMAKE_BUILD_TYPE is set to ${CMAKE_BUILD_TYPE}")
# Use ccache for compilation if available. Use CMAKE_C/CXX_COMPILER_LAUNCHER
# instead of RULE_LAUNCH_COMPILE to avoid double-wrapping when ccache is also
# injected via PATH (e.g., /usr/lib/ccache or brew --prefix ccache/libexec).
# Note: we intentionally do NOT set RULE_LAUNCH_LINK because ccache cannot
# cache link operations -- it only adds overhead.
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
set(CMAKE_C_COMPILER_LAUNCHER ccache)
set(CMAKE_CXX_COMPILER_LAUNCHER ccache)
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache)
endif(CCACHE_FOUND)
option(WITH_JEMALLOC "build with JeMalloc" OFF)
@@ -520,15 +515,6 @@ elseif(CMAKE_SYSTEM_NAME MATCHES "iOS")
add_definitions(-DOS_MACOSX -DIOS_CROSS_COMPILE)
elseif(CMAKE_SYSTEM_NAME MATCHES "Linux")
add_definitions(-DOS_LINUX)
# Use lld linker if available for faster linking (12x faster than ld.bfd)
if(NOT ROCKSDB_NO_FAST_LINKER)
execute_process(COMMAND ${CMAKE_CXX_COMPILER} -fuse-ld=lld -Wl,--version
OUTPUT_VARIABLE LLD_VERSION_OUTPUT ERROR_QUIET RESULT_VARIABLE LLD_RESULT)
if(LLD_RESULT EQUAL 0 AND LLD_VERSION_OUTPUT MATCHES "LLD")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=lld")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fuse-ld=lld")
endif()
endif()
elseif(CMAKE_SYSTEM_NAME MATCHES "SunOS")
add_definitions(-DOS_SOLARIS)
elseif(CMAKE_SYSTEM_NAME MATCHES "kFreeBSD")
@@ -674,38 +660,10 @@ if(USE_FOLLY)
endif()
endif()
# Folly itself uses gflags transitively, but RocksDB tools and benchmarks
# also call gflags APIs directly, so they need an explicit link dependency.
if(WITH_GFLAGS)
if(NOT TARGET gflags::gflags AND NOT TARGET gflags_shared AND
NOT gflags_LIBRARIES)
find_package(gflags CONFIG QUIET)
if(NOT gflags_FOUND)
find_package(gflags REQUIRED)
endif()
endif()
if(TARGET gflags::gflags)
set(GFLAGS_LIB gflags::gflags)
elseif(TARGET gflags_shared)
set(GFLAGS_LIB gflags_shared)
elseif(DEFINED GFLAGS_TARGET AND TARGET ${GFLAGS_TARGET})
set(GFLAGS_LIB ${GFLAGS_TARGET})
elseif(gflags_LIBRARIES)
set(GFLAGS_LIB ${gflags_LIBRARIES})
else()
message(FATAL_ERROR
"WITH_GFLAGS is enabled, but no gflags library could be resolved")
endif()
list(APPEND THIRDPARTY_LIBS ${GFLAGS_LIB})
endif()
add_compile_definitions(USE_FOLLY FOLLY_NO_CONFIG HAVE_CXX11_ATOMIC)
list(APPEND THIRDPARTY_LIBS Folly::folly)
set(FOLLY_LIBS Folly::folly)
# --copy-dt-needed-entries is ld.bfd-specific; lld handles DT_NEEDED transitively
if(NOT CMAKE_EXE_LINKER_FLAGS MATCHES "fuse-ld=lld")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--copy-dt-needed-entries")
endif()
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--copy-dt-needed-entries")
endif()
find_package(Threads REQUIRED)
@@ -732,7 +690,6 @@ set(SOURCES
db/blob/blob_file_addition.cc
db/blob/blob_file_builder.cc
db/blob/blob_file_cache.cc
db/blob/blob_file_partition_manager.cc
db/blob/blob_file_garbage.cc
db/blob/blob_file_meta.cc
db/blob/blob_file_reader.cc
@@ -741,7 +698,6 @@ set(SOURCES
db/blob/blob_log_sequential_reader.cc
db/blob/blob_log_writer.cc
db/blob/blob_source.cc
db/blob/blob_write_batch_transformer.cc
db/blob/prefetch_buffer_collection.cc
db/builder.cc
db/c.cc
@@ -810,10 +766,8 @@ set(SOURCES
db/version_edit.cc
db/version_edit_handler.cc
db/version_set.cc
db/version_util.cc
db/wal_edit.cc
db/wal_manager.cc
db/wide/read_path_blob_resolver.cc
db/wide/wide_column_serialization.cc
db/wide/wide_columns.cc
db/wide/wide_columns_helper.cc
@@ -888,7 +842,6 @@ set(SOURCES
table/block_based/block_based_table_builder.cc
table/block_based/block_based_table_factory.cc
table/block_based/block_based_table_iterator.cc
table/block_based/multi_scan_index_iterator.cc
table/block_based/block_based_table_reader.cc
table/block_based/block_builder.cc
table/block_based/block_cache.cc
@@ -965,7 +918,6 @@ set(SOURCES
util/data_structure.cc
util/dynamic_bloom.cc
util/hash.cc
util/io_dispatcher_imp.cc
util/murmurhash.cc
util/random.cc
util/rate_limiter.cc
@@ -995,7 +947,6 @@ set(SOURCES
utilities/cassandra/merge_operator.cc
utilities/checkpoint/checkpoint_impl.cc
utilities/compaction_filters.cc
utilities/sorted_run_builder/sorted_run_builder.cc
utilities/compaction_filters/remove_emptyvalue_compactionfilter.cc
utilities/counted_fs.cc
utilities/debug.cc
@@ -1047,9 +998,6 @@ set(SOURCES
utilities/transactions/write_prepared_txn_db.cc
utilities/transactions/write_unprepared_txn.cc
utilities/transactions/write_unprepared_txn_db.cc
utilities/trie_index/bitvector.cc
utilities/trie_index/louds_trie.cc
utilities/trie_index/trie_index_factory.cc
utilities/types_util.cc
utilities/ttl/db_ttl_impl.cc
utilities/wal_filter.cc
@@ -1165,23 +1113,8 @@ if(USE_FOLLY_LITE)
FMT_INCLUDE_DIR)
include_directories(${FMT_INCLUDE_DIR})
exec_program(python3 ${PROJECT_SOURCE_DIR}/third-party/folly ARGS
build/fbcode_builder/getdeps.py show-inst-dir glog OUTPUT_VARIABLE
GLOG_INST_PATH)
if(EXISTS ${GLOG_INST_PATH}/lib64)
set(GLOG_LIB_DIR ${GLOG_INST_PATH}/lib64)
else()
set(GLOG_LIB_DIR ${GLOG_INST_PATH}/lib)
endif()
add_definitions(-DUSE_FOLLY -DFOLLY_NO_CONFIG)
find_library(GLOG_LIBRARY NAMES glog PATHS ${GLOG_LIB_DIR} NO_DEFAULT_PATH)
if(NOT GLOG_LIBRARY)
find_library(GLOG_LIBRARY NAMES glog)
endif()
if(GLOG_LIBRARY)
list(APPEND THIRDPARTY_LIBS ${GLOG_LIBRARY})
endif()
list(APPEND THIRDPARTY_LIBS glog)
endif()
set(ROCKSDB_STATIC_LIB rocksdb${ARTIFACT_SUFFIX})
@@ -1427,7 +1360,6 @@ if(WITH_TESTS)
db/blob/blob_garbage_meter_test.cc
db/blob/blob_source_test.cc
db/blob/db_blob_basic_test.cc
db/blob/db_blob_direct_write_test.cc
db/blob/db_blob_compaction_test.cc
db/blob/db_blob_corruption_test.cc
db/blob/db_blob_index_test.cc
@@ -1449,7 +1381,6 @@ if(WITH_TESTS)
db/db_bloom_filter_test.cc
db/db_compaction_filter_test.cc
db/db_compaction_test.cc
db/db_compaction_abort_test.cc
db/db_clip_test.cc
db/db_dynamic_level_test.cc
db/db_encryption_test.cc
@@ -1465,7 +1396,6 @@ if(WITH_TESTS)
db/db_memtable_test.cc
db/db_merge_operator_test.cc
db/db_merge_operand_test.cc
db/db_open_with_config_test.cc
db/db_options_test.cc
db/db_properties_test.cc
db/db_range_del_test.cc
@@ -1518,7 +1448,6 @@ if(WITH_TESTS)
db/wal_manager_test.cc
db/wal_edit_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
db/wide/wide_columns_helper_test.cc
db/write_batch_test.cc
@@ -1599,9 +1528,7 @@ if(WITH_TESTS)
utilities/cassandra/cassandra_row_merge_test.cc
utilities/cassandra/cassandra_serialize_test.cc
utilities/checkpoint/checkpoint_test.cc
utilities/sorted_run_builder/sorted_run_builder_test.cc
utilities/env_timed_test.cc
utilities/fault_injection_fs_test.cc
utilities/memory/memory_test.cc
utilities/merge_operators/string_append/stringappend_test.cc
utilities/object_registry_test.cc
@@ -1619,12 +1546,9 @@ if(WITH_TESTS)
utilities/transactions/lock/point/point_lock_manager_stress_test.cc
utilities/transactions/write_committed_transaction_ts_test.cc
utilities/transactions/write_prepared_transaction_test.cc
utilities/transactions/write_prepared_transaction_seqno_test.cc
utilities/transactions/write_unprepared_transaction_test.cc
utilities/transactions/lock/range/range_locking_test.cc
utilities/transactions/timestamped_snapshot_test.cc
utilities/trie_index/trie_index_db_test.cc
utilities/trie_index/trie_index_test.cc
utilities/ttl/ttl_test.cc
utilities/types_util_test.cc
utilities/util_merge_operators_test.cc
+4 -60
View File
@@ -1,69 +1,13 @@
# Rocksdb Change Log
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
## 11.2.0 (04/18/2026)
### New Features
* Added experimental `DBOptions::fast_sst_open` option. When enabled, RocksDB retrieves opaque file system metadata for SST files after flush, compaction, and external file ingestion, persists it in the MANIFEST, and passes it back to the file system on subsequent file opens to accelerate DB open time.
* Added new option `min_tombstones_for_range_conversion` in `AdvancedColumnFamilyOptions`. When set to a non-zero value N, forward or reverse iteration will convert N or more contiguous point tombstones into a range tombstone in the mutable memtable. Future read operations will then be able to benefit from range tombstone optimizations. There are some limitations when it comes to table_filters, prefix_filters, and UDTs. See header comments for more details.
* Added read-triggered compaction: a new column family option `read_triggered_compaction_threshold` (default 0, disabled) that marks SST files for compaction when their read frequency (`num_collapsible_entry_reads_sampled / file_size`) exceeds the threshold. This helps reduce read amplification for frequently-read ("hot") keys. A new DB option `max_compaction_trigger_wakeup_seconds` (default 43200s / 12 hours) controls the maximum interval for periodic compaction score re-evaluation, which is necessary for this feature to work on quiet (no-write) databases.
### Public API Changes
* Added new `WideColumnBlobResolver` interface and `CompactionFilter::FilterV4()` method, including `ResolveColumn()` / `ResolveColumns()` helpers for lazy loading blob column values in wide-column entities during compaction. This allows compaction filters to resolve blob values on-demand, avoiding unnecessary I/O for blob columns they don't need to access.
* Changed experimental feature `ExternalTableReader::Get` and `ExternalTableReader::MultiGet` to use `PinnableSlice` instead of `std::string` for output values, enabling zero-copy pinning. This will break existing implementations.
* Added `SstFileReader::Get` and `SstFileReader::MultiGet` overloads that accept `PinnableSlice`/`std::vector<PinnableSlice>*`, enabling zero-copy reads when the underlying `TableReader` supports pinning.
### Behavior Changes
* Prefix filter changes - when seeking to a key that is out of domain, and total_order_seek is false, total_order_seek is treated as if it were true. When prefix_same_as_start = true, now iterating past a key that is out of domain invalidates the iterator. The existing behavior does not check for InDomain in the DBIter, so Transform() can produce undefined behavior (e.g. key of size 3 on FixedPrefixTransform(4)).
* Wide-column entities with blob-backed columns now use a new V2 on-disk encoding; older RocksDB versions that do not support wide-column blob separation will reject DBs or SSTs containing those entities.
### Bug Fixes
* Fix blob garbage accounting for blob direct-write flushes so flush-time filtering and overwrite elision correctly register obsolete blob bytes in blob metadata.
* Fix a memory accounting leak in IODispatcher where ReadIndex() moved block values out of ReadSet without releasing the associated prefetch memory, causing subsequent prefetches to be blocked when max_prefetch_memory_bytes was set.
* Fix MultiScan to fall back to synchronous coalesced reads when async I/O is unsupported at runtime.
## 11.1.0 (03/23/2026)
### New Features
* Add a new option `open_files_async`. The existing behavior is on DB open, we open all sst files and do basic validations. For very large DBs on remote filesystems with many ssts, this may take very long. This option performs these validations instead in the background. Open errors found by this async background task are surfaced as a new background error kAsyncFileOpen.
* Added `BlockBasedTableOptions::kAuto` index block search type that automatically selects between binary and interpolation search on a per-index-block basis. During SST construction, each index block's key distribution uniformity is analyzed using the coefficient of variation of key gaps, and index blocks with uniform keys use interpolation search while others fall back to binary search. The uniformity threshold is configurable via `BlockBasedTableOptions::uniform_cv_threshold` (default: 0.2).
* Introduced enforce_write_buffer_manager_during_recovery option to allow WriteBufferManager to be enforced during WAL recovery. (Default: true)
* Add `memtable_batch_lookup_optimization` option to use batch lookup optimization for memtable MultiGet. For skip list memtables, after each key lookup, the search path is cached and reused for the next key, reducing per-key cost from O(log N) to O(log d) where d is the distance between consecutive keys. Benchmarks show ~7% improvement in memtable-resident MultiGet throughput.
* Added `BlockBasedTableOptions::PrepopulateBlockCache::kFlushAndCompaction` to prepopulate the block cache during both flush and compaction. Compaction-warmed blocks are inserted at `BOTTOM` priority (vs `LOW` for flush) so they are evicted first under cache pressure. Recommended only for use cases where most or all of the database is expected to reside in cache (e.g., tiered or remote storage where the working set fits in cache).
* Added new mutable DB option `verify_manifest_content_on_close` (default: false). When enabled, on DB close the MANIFEST file is read back and all records are validated (CRC checksums and logical content). If corruption is detected, a fresh MANIFEST is written from in-memory state.
### Behavior Changes
* num_reads_sampled now factors in re-seeks and next/prev() on file iterators for files in L1+. next/prev() is discounted by 64x compared to seek due to being a much cheaper call.
* Remote compaction workers now skip WAL recovery when opening the secondary DB instance, since only the LSM state from MANIFEST is needed for compaction. This reduces I/O and speeds up remote compaction startup.
### Bug Fixes
* Fix a bug in round-robin compaction that missed selecting input files that are needed to guarantee data correctness and cause crashing in debug builds or silent data corruption in release builds for Get().
## 11.0.0 (02/23/2026)
### New Features
* Added support for storing wide-column entity column values in blob files. When `min_blob_size` is configured, large column values in wide-column entities will be stored in blob files, reducing SST file size and improving read performance.
* Added `CompactionOptionsFIFO::max_data_files_size` to support FIFO compaction trimming based on combined SST and blob file sizes. Added `CompactionOptionsFIFO::use_kv_ratio_compaction` to enable a capacity-derived intra-L0 compaction strategy optimized for BlobDB workloads, producing uniform-sized compacted files for predictable FIFO trimming.
* Include interpolation search as an alternative to binary search, which typically performs better when keys are uniformly distributed. This is exposed as a new table option `index_block_search_type`. The default is `binary_search`.
### Public API Changes
* Added new virtual methods `AbortAllCompactions()` and `ResumeAllCompactions()` to the `DB` class. Added new `Status::SubCode::kCompactionAborted` to indicate a compaction was aborted. Added `Status::IsCompactionAborted()` helper method to check if a status represents an aborted compaction.
* Drop support for reading (and writing) SST files using `BlockBasedTableOptions.format_version` < 2, which hasn't been the default format for about 10 years. An upgrade path is still possible with full compaction using a RocksDB version >= 4.6.0 and < 11.0.0 and then using the newer version.
* Remove deprecated raw `DB*` variants of `DB::Open` and related functions. Some other minor public APIs were updated as a result
* Remove deprecated `DB::MaxMemCompactionLevel()`
* Remove useless option `CompressedSecondaryCacheOptions::compress_format_version`
* Remove deprecated DB option `skip_checking_sst_file_sizes_on_db_open`. The option was deprecated in 10.5.0 and has been a no-op since then. File size validation is now always performed in parallel during DB open.
* Remove deprecated `SliceTransform::InRange()` virtual method and the `in_range` callback parameter from `rocksdb_slicetransform_create()` in the C API. `InRange()` was never called by RocksDB and existed only for backward compatibility.
* Remove deprecated, unused APIs and options: `ReadOptions::managed` and `ColumnFamilyOptions::snap_refresh_nanos`. Corresponding C and Java APIs are also removed.
* Remove deprecated `SstFileWriter::Add()` method (use `Put()` instead) and the deprecated `skip_filters` parameter from `SstFileWriter` constructors (use `BlockBasedTableOptions::filter_policy` set to `nullptr` to skip filter generation instead).
### Behavior Changes
* Change the default value of `CompactionOptionsUniversal::reduce_file_locking` from `false` to `true` to improve write stall and reduce read regression
## 10.11.2 (02/25/2026)
### Bug Fixes
* Fix longstanding failures that can arise from reading and/or compacting old DB dirs with range deletions (likely from version < 5.19.0) in many newer versions.
* Fix a bug where WritePrepared/WriteUnprepared TransactionDB with two_write_queues=true could experience "sequence number going backwards" corruption during recovery from a background error, due to allocated-but-not-published sequence numbers not being synced before creating new WAL files.
### Performance Improvements
* Add a new table option `separate_key_value_in_data_block`. When set to true keys and values will be stored separately in the data block, which can result in higher cpu cache hit rate and better compression. Works best with data blocks with sufficient restart intervals and large values. Previous versions of RocksDB will reject files written using this option.
## 10.11.1 (02/06/2026)
### Bug Fixes
* Fixed a bug in PosixFS AbortIO that could cause it to ignore an IO completion and a subsequent Poll to hang
## 10.11.0 (01/23/2026)
### Public API Changes
+23 -154
View File
@@ -384,10 +384,6 @@ endif
# TSAN doesn't work well with jemalloc. If we're compiling with TSAN, we should use regular malloc.
ifdef COMPILE_WITH_TSAN
DISABLE_JEMALLOC=1
# Use a suppressions file instead of the process-wide TSAN default
# suppressions hook, which belongs to the final application.
TSAN_OPTIONS?=suppressions=$(CURDIR)/tools/tsan_suppressions.txt
export TSAN_OPTIONS
EXEC_LDFLAGS += -fsanitize=thread
PLATFORM_CCFLAGS += -fsanitize=thread -fPIC -DFOLLY_SANITIZE_THREAD
PLATFORM_CXXFLAGS += -fsanitize=thread -fPIC -DFOLLY_SANITIZE_THREAD
@@ -454,7 +450,7 @@ ifndef USE_FOLLY
endif
ifndef GTEST_THROW_ON_FAILURE
export GTEST_THROW_ON_FAILURE=0
export GTEST_THROW_ON_FAILURE=1
endif
ifndef GTEST_HAS_EXCEPTIONS
export GTEST_HAS_EXCEPTIONS=1
@@ -625,24 +621,25 @@ endif
ROCKSDBTESTS_SUBSET ?= $(TESTS)
# c_test - doesn't use gtest, can't be sharded
# Other tests previously listed here (backup_engine_test, db_bloom_filter_test,
# perf_context_test, etc.) were NON_PARALLEL due to /dev/shm memory concerns
# when the old per-test-case sharding spawned thousands of processes. With the
# new gtest-based sharding (GTEST_SHARD_SIZE=10, max NCORES*8 shards), at most
# ~4 shards run concurrently on CI (4 cores), so peak memory is manageable
# (4 * 1GB = 4GB << 16GB shm).
# c_test - doesn't use gtest
# env_test - suspicious use of test::TmpDir
# deletefile_test - serial because it generates giant temporary files in
# its various tests. Parallel can fill up your /dev/shm
# db_bloom_filter_test - serial because excessive space usage by instances
# of DBFilterConstructionReserveMemoryTestWithParam can fill up /dev/shm
NON_PARALLEL_TEST = \
c_test \
env_test \
deletefile_test \
db_bloom_filter_test \
$(PLUGIN_TESTS) \
PARALLEL_TEST = $(filter-out $(NON_PARALLEL_TEST), $(ROCKSDBTESTS_SUBSET))
PARALLEL_TEST = $(filter-out $(NON_PARALLEL_TEST), $(TESTS))
# Not necessarily well thought out or up-to-date, but matches old list
TESTS_PLATFORM_DEPENDENT := \
db_basic_test \
db_blob_basic_test \
db_blob_direct_write_test \
db_encryption_test \
external_sst_file_basic_test \
auto_roll_logger_test \
@@ -650,7 +647,6 @@ TESTS_PLATFORM_DEPENDENT := \
dynamic_bloom_test \
c_test \
checkpoint_test \
sorted_run_builder_test \
crc32c_test \
coding_test \
inlineskiplist_test \
@@ -809,20 +805,9 @@ endif # PLATFORM_SHARED_EXT
.PHONY: check clean coverage ldb_tests package dbg gen-pc build_size \
release tags tags0 valgrind_check format static_lib shared_lib all \
rocksdbjavastatic rocksdbjava install install-static install-shared \
uninstall analyze tools tools_lib check-headers checkout_folly clang-tidy
uninstall analyze tools tools_lib check-headers checkout_folly
# Auto-configure git hooks on first build so developers do not need to run
# "make install-hooks" manually. This is a no-op if already set.
setup-hooks:
@if [ -d .git ] && [ -d githooks ]; then \
cur=$$(git config core.hooksPath 2>/dev/null); \
if [ "$$cur" != "githooks" ]; then \
git config core.hooksPath githooks; \
echo "git hooks: configured core.hooksPath = githooks"; \
fi; \
fi
all: setup-hooks $(LIBRARY) $(BENCHMARKS) tools tools_lib test_libs $(TESTS)
all: $(LIBRARY) $(BENCHMARKS) tools tools_lib test_libs $(TESTS)
all_but_some_tests: $(LIBRARY) $(BENCHMARKS) tools tools_lib test_libs $(ROCKSDBTESTS_SUBSET)
@@ -886,42 +871,21 @@ coverage: clean
parallel_tests = $(patsubst %,parallel_%,$(PARALLEL_TEST))
.PHONY: gen_parallel_tests $(parallel_tests)
# Shard size controls how many test cases run per process. The actual number
# of shards per binary is: min(ceil(test_count / GTEST_SHARD_SIZE), NCORES * 8)
# This adapts to machine size: many small shards on beefy machines, fewer
# larger shards on CI (typically 2-4 cores).
GTEST_SHARD_SIZE ?= 10
NCORES ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
$(parallel_tests):
$(AM_V_at)TEST_BINARY=$(patsubst parallel_%,%,$@); \
TEST_COUNT=` \
(./$$TEST_BINARY --gtest_list_tests 2>/dev/null || echo " list_failure") \
| grep -c '^ '`; \
if [ "$$TEST_COUNT" -le 0 ]; then TEST_COUNT=1; fi; \
MAX_SHARDS=$$(( $(NCORES) * 8 )); \
NUM_SHARDS=$$(( (TEST_COUNT + $(GTEST_SHARD_SIZE) - 1) / $(GTEST_SHARD_SIZE) )); \
if [ "$$NUM_SHARDS" -gt "$$MAX_SHARDS" ]; then NUM_SHARDS=$$MAX_SHARDS; fi; \
if [ "$$NUM_SHARDS" -le 0 ]; then NUM_SHARDS=1; fi; \
echo " Generating $$NUM_SHARDS shards for $$TEST_BINARY ($$TEST_COUNT tests)"; \
SHARD_IDX=0; \
while [ "$$SHARD_IDX" -lt "$$NUM_SHARDS" ]; do \
if [ -n "$(CI_TOTAL_SHARDS)" ] && [ $$(( $$SHARD_IDX % $(CI_TOTAL_SHARDS) )) -ne $(CI_SHARD_INDEX) ]; then \
SHARD_IDX=$$((SHARD_IDX + 1)); \
continue; \
fi; \
TEST_SCRIPT=t/run-$$TEST_BINARY-shard-$$SHARD_IDX; \
TEST_NAMES=` \
(./$$TEST_BINARY --gtest_list_tests || echo " $${TEST_BINARY}__list_tests_failure") \
| awk '/^[^ ]/ { prefix = $$1 } /^[ ]/ { print prefix $$1 }'`; \
echo " Generating parallel test scripts for $$TEST_BINARY"; \
for TEST_NAME in $$TEST_NAMES; do \
TEST_SCRIPT=t/run-$$TEST_BINARY-$${TEST_NAME//\//-}; \
printf '%s\n' \
'#!/bin/sh' \
"d=\$(TEST_TMPDIR)$$TEST_SCRIPT" \
'mkdir -p $$d' \
"TEST_TMPDIR=\$$d GTEST_TOTAL_SHARDS=$$NUM_SHARDS GTEST_SHARD_INDEX=$$SHARD_IDX $(DRIVER) ./$$TEST_BINARY" \
'test_retcode=$$?' \
'[ $$test_retcode -eq 0 ] && rm -rf $$d' \
'exit $$test_retcode' \
"TEST_TMPDIR=\$$d $(DRIVER) ./$$TEST_BINARY --gtest_filter=$$TEST_NAME" \
> $$TEST_SCRIPT; \
chmod a=rx $$TEST_SCRIPT; \
SHARD_IDX=$$((SHARD_IDX + 1)); \
done
gen_parallel_tests:
@@ -945,10 +909,8 @@ gen_parallel_tests:
# 152.120 PASS t/DBTest.FileCreationRandomFailure
# 107.816 PASS t/DBTest.EncodeDecompressedBlockSizeTest
#
# With sharded test execution, prioritize binaries known to be slow.
# These generate many shards and should start early for good load balancing.
slow_test_regexp = \
^.*block_based_table_reader_test.*$$|^.*table_test.*$$|^.*block_test.*$$|^.*write_prepared_transaction_test.*$$|^.*transaction_test.*$$|^.*external_sst_file_test.*$$|^.*db_wal_test.*$$|^.*db_with_timestamp_basic_test.*$$|^.*db_test-.*$$
^.*MySQLStyleTransactionTest.*$$|^.*SnapshotConcurrentAccessTest.*$$|^.*SeqAdvanceConcurrentTest.*$$|^t/run-table_test-HarnessTest.Randomized$$|^t/run-db_test-.*(?:FileCreationRandomFailure|EncodeDecompressedBlockSizeTest)$$|^.*RecoverFromCorruptedWALWithoutFlush$$
prioritize_long_running_tests = \
perl -pe 's,($(slow_test_regexp)),100 $$1,' \
| sort -k1,1gr \
@@ -983,13 +945,7 @@ check_0:
'To monitor subtest <duration,pass/fail,name>,' \
' run "make watch-log" in a separate window' ''; \
{ \
NON_PARALLEL_LIST="$(filter-out $(PARALLEL_TEST),$(ROCKSDBTESTS_SUBSET))"; \
if [ -n "$$NON_PARALLEL_LIST" ]; then \
printf './%s\n' $$NON_PARALLEL_LIST \
| if [ -n "$(CI_TOTAL_SHARDS)" ]; then \
awk -v s=$(CI_SHARD_INDEX) -v n=$(CI_TOTAL_SHARDS) '(NR-1)%n==s'; \
else cat; fi; \
fi; \
printf './%s\n' $(filter-out $(PARALLEL_TEST),$(TESTS)); \
find t -name 'run-*' -print; \
} \
| $(prioritize_long_running_tests) \
@@ -1037,19 +993,9 @@ watch-log:
dump-log:
bash -c '$(quoted_perl_command)' < LOG
# Machine-parseable progress output for automated monitoring (e.g., Claude Code)
# Outputs JSON: {"status":"running","completed":45,"total":100,"failed":0,"percent":45,"eta_seconds":120}
check-progress:
@build_tools/check_progress.sh
# If J != 1 and GNU parallel is installed, run the tests in parallel,
# via the check_0 rule above. Otherwise, run them sequentially.
check: all
$(AM_V_at)echo "Cleaning up stale test directories older than 3 hours..."; \
test_tmpdir_parent=$$(dirname $(TEST_TMPDIR)); \
find $$test_tmpdir_parent -maxdepth 1 -name 'rocksdb.*' -type d \
-mmin +180 -exec rm -rf {} + 2>/dev/null; \
true
$(MAKE) gen_parallel_tests
$(AM_V_GEN)if test "$(J)" != 1 \
&& (build_tools/gnu_parallel --gnu --help 2>/dev/null) | \
@@ -1065,7 +1011,6 @@ 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
@@ -1073,7 +1018,6 @@ ifndef SKIP_FORMAT_BUCK_CHECKS
$(MAKE) check-format
$(MAKE) check-buck-targets
$(MAKE) check-sources
$(MAKE) check-workflow-yaml
endif
# TODO add ldb_tests
@@ -1084,10 +1028,6 @@ check_some: $(ROCKSDBTESTS_SUBSET)
ldb_tests: ldb
$(PYTHON) tools/ldb_test.py
.PHONY: db_crashtest_tests
db_crashtest_tests:
$(PYTHON) tools/db_crashtest_test.py
include crash_test.mk
asan_check: clean
@@ -1264,59 +1204,15 @@ tags0:
format:
build_tools/format-diff.sh
# Non-interactive format (auto-apply without prompts, for CI/automation/Claude Code)
format-auto:
build_tools/format-diff.sh -y
check-format:
build_tools/format-diff.sh -c
# Crude alternative to setup-hooks: copies hooks into .git/hooks/ instead of
# using core.hooksPath. The copies won't track changes to githooks/.
install-hooks:
@echo "Installing git hooks from githooks/..."
@if [ -d githooks ]; then \
for hook in githooks/*; do \
hook_name=$$(basename "$$hook"); \
cp "$$hook" .git/hooks/"$$hook_name"; \
chmod +x .git/hooks/"$$hook_name"; \
echo " Installed $$hook_name"; \
done; \
echo "Done. Hooks installed to .git/hooks/"; \
else \
echo "Error: githooks/ directory not found"; \
exit 1; \
fi
# Reverse of install-hooks (not needed if using setup-hooks / core.hooksPath).
uninstall-hooks:
@echo "Removing installed git hooks..."
@for hook in githooks/*; do \
hook_name=$$(basename "$$hook"); \
rm -f .git/hooks/"$$hook_name"; \
echo " Removed $$hook_name"; \
done
@echo "Done."
check-buck-targets:
buckifier/check_buck_targets.sh
check-sources:
build_tools/check-sources.sh
check-workflow-yaml:
build_tools/check-workflow-yaml.sh
# Run clang-tidy on locally changed files, filtered to changed lines only.
# Requires compile_commands.json (generate with cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON).
# Override CLANG_TIDY_BINARY and CLANG_TIDY_JOBS as needed:
# make clang-tidy CLANG_TIDY_BINARY=/usr/bin/clang-tidy CLANG_TIDY_JOBS=8
CLANG_TIDY_BINARY ?= /opt/homebrew/opt/llvm/bin/clang-tidy
CLANG_TIDY_JOBS ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
clang-tidy:
python3 tools/run_clang_tidy.py --clang-tidy-binary $(CLANG_TIDY_BINARY) -j $(CLANG_TIDY_JOBS)
package:
bash build_tools/make_package.sh $(SHARED_MAJOR).$(SHARED_MINOR)
@@ -1495,9 +1391,6 @@ db_basic_test: $(OBJ_DIR)/db/db_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
db_blob_basic_test: $(OBJ_DIR)/db/blob/db_blob_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_blob_direct_write_test: $(OBJ_DIR)/db/blob/db_blob_direct_write_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_blob_compaction_test: $(OBJ_DIR)/db/blob/db_blob_compaction_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1507,9 +1400,6 @@ 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_wide_blob_direct_write_test: $(OBJ_DIR)/db/wide/db_wide_blob_direct_write_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_with_timestamp_basic_test: $(OBJ_DIR)/db/db_with_timestamp_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1519,9 +1409,6 @@ db_with_timestamp_compaction_test: db/db_with_timestamp_compaction_test.o $(TEST
db_encryption_test: $(OBJ_DIR)/db/db_encryption_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_open_with_config_test: $(OBJ_DIR)/db/db_open_with_config_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_test: $(OBJ_DIR)/db/db_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1555,9 +1442,6 @@ db_compaction_filter_test: $(OBJ_DIR)/db/db_compaction_filter_test.o $(TEST_LIBR
db_compaction_test: $(OBJ_DIR)/db/db_compaction_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_compaction_abort_test: $(OBJ_DIR)/db/db_compaction_abort_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_clip_test: $(OBJ_DIR)/db/db_clip_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1666,9 +1550,6 @@ backup_engine_test: $(OBJ_DIR)/utilities/backup/backup_engine_test.o $(TEST_LIBR
checkpoint_test: $(OBJ_DIR)/utilities/checkpoint/checkpoint_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
sorted_run_builder_test: $(OBJ_DIR)/utilities/sorted_run_builder/sorted_run_builder_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
cache_simulator_test: $(OBJ_DIR)/utilities/simulator_cache/cache_simulator_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1687,12 +1568,6 @@ object_registry_test: $(OBJ_DIR)/utilities/object_registry_test.o $(TEST_LIBRARY
ttl_test: $(OBJ_DIR)/utilities/ttl/ttl_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
trie_index_db_test: $(OBJ_DIR)/utilities/trie_index/trie_index_db_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
trie_index_test: $(OBJ_DIR)/utilities/trie_index/trie_index_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
types_util_test: $(OBJ_DIR)/utilities/types_util_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1744,9 +1619,6 @@ io_posix_test: $(OBJ_DIR)/env/io_posix_test.o $(TEST_LIBRARY) $(LIBRARY)
fault_injection_test: $(OBJ_DIR)/db/fault_injection_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
fault_injection_fs_test: $(OBJ_DIR)/utilities/fault_injection_fs_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
rate_limiter_test: $(OBJ_DIR)/util/rate_limiter_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1945,9 +1817,6 @@ write_committed_transaction_ts_test: $(OBJ_DIR)/utilities/transactions/write_com
write_prepared_transaction_test: $(OBJ_DIR)/utilities/transactions/write_prepared_transaction_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
write_prepared_transaction_seqno_test: $(OBJ_DIR)/utilities/transactions/write_prepared_transaction_seqno_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
write_unprepared_transaction_test: $(OBJ_DIR)/utilities/transactions/write_unprepared_transaction_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -2668,7 +2537,7 @@ list_all_tests:
# Remove the rules for which dependencies should not be generated and see if any are left.
#If so, include the dependencies; if not, do not include the dependency files
ROCKS_DEP_RULES=$(filter-out clean format check-format check-buck-targets check-headers check-sources check-workflow-yaml clang-tidy jclean jtest package analyze tags rocksdbjavastatic% unity.% unity_test checkout_folly, $(MAKECMDGOALS))
ROCKS_DEP_RULES=$(filter-out clean format check-format check-buck-targets check-headers check-sources jclean jtest package analyze tags rocksdbjavastatic% unity.% unity_test checkout_folly, $(MAKECMDGOALS))
ifneq ("$(ROCKS_DEP_RULES)", "")
-include $(DEPFILES)
endif
+4 -4
View File
@@ -146,10 +146,10 @@ def generate_buck(repo_path, deps_map):
src_mk["RANGE_TREE_SOURCES"] + src_mk["TOOL_LIB_SOURCES"],
deps=[
"//folly/container:f14_hash",
"//folly/coro:blocking_wait",
"//folly/coro:collect",
"//folly/coro:coroutine",
"//folly/coro:task",
"//folly/experimental/coro:blocking_wait",
"//folly/experimental/coro:collect",
"//folly/experimental/coro:coroutine",
"//folly/experimental/coro:task",
"//folly/synchronization:distributed_mutex",
],
headers=LiteralValue("glob([\"**/*.h\"])")
-18
View File
@@ -148,24 +148,6 @@ PLATFORM_SHARED_LDFLAGS="-Wl,--no-as-needed -shared -Wl,-soname -Wl,"
PLATFORM_SHARED_CFLAGS="-fPIC"
PLATFORM_SHARED_VERSIONED=true
# Prefer lld linker when available on Linux. lld is typically 5-10x faster
# than the default ld.bfd for large C++ projects. macOS uses ld64 (or
# ld-prime) which is already fast, so we skip lld detection there.
# Set ROCKSDB_NO_FAST_LINKER=1 to disable this auto-detection.
if [ -z "$ROCKSDB_NO_FAST_LINKER" ] && [ "$TARGET_OS" = "Linux" ]; then
if $CXX -fuse-ld=lld -L/usr/local/lib -x c++ - -o /dev/null 2>/dev/null <<EOF
int main() { return 0; }
EOF
then
PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -fuse-ld=lld"
# Ensure lld can find libraries in /usr/local/lib (lld does not
# search there by default, unlike ld.bfd)
if [ -d /usr/local/lib ]; then
PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -L/usr/local/lib"
fi
fi
fi
# generic port files (working on all platform by #ifdef) go directly in /port
GENERIC_PORT_FILES=`cd "$ROCKSDB_ROOT"; find port -name '*.cc' | tr "\n" " "`
+1 -1
View File
@@ -37,7 +37,7 @@ if [ "$?" != "1" ]; then
BAD=1
fi
LC_ALL=C git grep -n $'[\x80-\xff]' -- ':!docs' ':!*.md' ':!.github'
git grep -n -P "[\x80-\xFF]" -- ':!docs' ':!*.md'
if [ "$?" != "1" ]; then
echo '^^^^ Use only ASCII characters in source files'
BAD=1
-42
View File
@@ -1,42 +0,0 @@
#!/usr/bin/env bash
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
#
# Validate GitHub Actions workflow YAML before it reaches CI runtime.
set -euo pipefail
if ! command -v ruby >/dev/null 2>&1; then
echo "ruby is required to validate GitHub Actions workflow YAML"
echo "On CentOS Stream: sudo dnf install ruby rubygems rubygem-psych"
exit 1
fi
if ! ruby -e 'require "psych"' 2>/dev/null; then
echo "ruby is installed but cannot load required library 'psych'"
echo "On CentOS Stream: sudo dnf install rubygems rubygem-psych"
exit 1
fi
ruby <<'RUBY'
require "psych"
bad = false
workflow_files = Dir[".github/workflows/*.{yml,yaml}"].sort
if workflow_files.empty?
warn "No workflow YAML files found under .github/workflows"
exit 1
end
workflow_files.each do |path|
begin
Psych.parse_file(path)
puts "OK #{path}"
rescue Psych::Exception => e
warn "Invalid YAML in #{path}: #{e.message}"
bad = true
end
end
exit(bad ? 1 : 0)
RUBY
-231
View File
@@ -1,231 +0,0 @@
#!/bin/bash
# Output test progress in JSON format for machine parsing
# Usage: build_tools/check_progress.sh
LOG_FILE="LOG"
T_DIR="t"
SRC_MK="src.mk"
# Maximum lines of test output to include per failed test
MAX_OUTPUT_LINES=50
# Helper to escape string for JSON (handles newlines, quotes, backslashes, tabs)
json_escape() {
local str="$1"
# Use python for reliable JSON escaping if available, otherwise use sed
if command -v python3 &>/dev/null; then
printf '%s' "$str" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read())[1:-1], end="")'
else
printf '%s' "$str" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\r/\\r/g' | awk '{printf "%s\\n", $0}' | sed 's/\\n$//'
fi
}
# Helper to output JSON and exit
output_json() {
local status="$1"
local completed="${2:-0}"
local total="${3:-0}"
local failed="${4:-0}"
local percent="${5:-0}"
local eta="${6:-0}"
local avg_time="${7:-0}"
local last_item="${8:-}"
local phase="${9:-}"
local failed_tests="${10:-}"
# Build JSON output
local json="{\"status\":\"$status\""
if [[ -n "$phase" ]]; then
json="$json,\"phase\":\"$phase\""
fi
json="$json,\"completed\":$completed,\"total\":$total,\"failed\":$failed,\"percent\":$percent"
json="$json,\"eta_seconds\":$eta,\"avg_time\":\"$avg_time\",\"last_item\":\"$(json_escape "$last_item")\""
if [[ -n "$failed_tests" ]]; then
json="$json,\"failed_tests\":[$failed_tests]"
fi
json="$json}"
echo "$json"
}
# Get failed test info with log output
get_failed_tests_json() {
local log_file="$1"
local t_dir="$2"
local max_failures=10
local count=0
local first=true
# Get failed tests from LOG file
while IFS=$'\t' read -r seq host starttime runtime send recv exitval signal cmd; do
# Skip header line
[[ "$seq" == "Seq" ]] && continue
# Check if failed (exitval != 0 or signal != 0)
if [[ "$exitval" != "0" || "$signal" != "0" ]]; then
# Extract test name from command
test_name=$(echo "$cmd" | sed 's,.*/run-,,;s, .*,,')
# Get log file path
log_path="$t_dir/log-run-$test_name"
# Read test output (last N lines)
if [[ -f "$log_path" ]]; then
output=$(tail -n "$MAX_OUTPUT_LINES" "$log_path" 2>/dev/null)
else
output="(log file not found: $log_path)"
fi
# Escape output for JSON
escaped_output=$(json_escape "$output")
# Build JSON object for this failure
if [[ "$first" == "true" ]]; then
first=false
else
printf ","
fi
printf '{"test":"%s","exit_code":%d,"signal":%d,"output":"%s"}' \
"$test_name" "$exitval" "$signal" "$escaped_output"
((count++))
if [[ $count -ge $max_failures ]]; then
break
fi
fi
done < "$log_file"
}
# Check if tests are running (LOG file exists)
if [[ -f "$LOG_FILE" ]]; then
# Count total tests from t/run-* files
if [[ -d "$T_DIR" ]]; then
total=$(find "$T_DIR" -name 'run-*' -type f 2>/dev/null | wc -l)
else
total=0
fi
# If no parallel tests generated yet
if [[ "$total" -eq 0 ]]; then
output_json "running" 0 0 0 0 0 "0" "" "generating"
exit 0
fi
# Parse LOG file (skip header line)
# LOG format: Seq Host Starttime JobRuntime Send Receive Exitval Signal Command
completed=$(tail -n +2 "$LOG_FILE" 2>/dev/null | wc -l)
# Count failures
failed=$(awk -F'\t' 'NR>1 && ($7 != 0 || $8 != 0) {count++} END {print count+0}' "$LOG_FILE" 2>/dev/null)
# Get failed tests JSON with output (only if there are failures)
if [[ "$failed" -gt 0 ]]; then
failed_tests=$(get_failed_tests_json "$LOG_FILE" "$T_DIR")
else
failed_tests=""
fi
# Calculate percentage
if [[ "$total" -gt 0 ]]; then
percent=$((completed * 100 / total))
else
percent=0
fi
# Get last completed test name (extract from command column)
last_test=$(tail -1 "$LOG_FILE" 2>/dev/null | awk -F'\t' '{print $9}' | sed 's,.*/run-,,;s, .*,,;s,^./,,')
# Calculate ETA based on average time
if [[ "$completed" -gt 0 ]]; then
avg_time=$(awk -F'\t' 'NR>1 {sum+=$4; count++} END {if(count>0) printf "%.1f", sum/count; else print "0"}' "$LOG_FILE")
remaining=$((total - completed))
eta=$(awk "BEGIN {printf \"%.0f\", $avg_time * $remaining}")
else
avg_time="0"
eta="0"
fi
# Determine status
if [[ "$completed" -ge "$total" ]]; then
status="completed"
elif [[ "$completed" -gt 0 ]]; then
status="running"
else
status="starting"
fi
output_json "$status" "$completed" "$total" "$failed" "$percent" "$eta" "$avg_time" "$last_test" "testing" "$failed_tests"
exit 0
fi
# No LOG file - check if we're in compilation/linking phase
# Count expected source files from src.mk
if [[ -f "$SRC_MK" ]]; then
# Count LIB_SOURCES (library object files to compile)
expected_lib_objects=$(grep -E '\.cc\s*\\?$' "$SRC_MK" | grep -v '^#' | wc -l)
# Count TEST_MAIN_SOURCES (test binaries to link)
expected_test_binaries=$(sed -n '/^TEST_MAIN_SOURCES =/,/^[^ ]/p' "$SRC_MK" | grep -cE '\.cc\s*\\?$' 2>/dev/null || echo 0)
else
expected_lib_objects=0
expected_test_binaries=0
fi
# Check for test generation phase (t/ directory being created)
if [[ -d "$T_DIR" ]]; then
total=$(find "$T_DIR" -name 'run-*' -type f 2>/dev/null | wc -l)
if [[ "$total" -gt 0 ]]; then
output_json "running" 0 "$total" 0 0 0 "0" "" "generating"
exit 0
fi
fi
# Count compiled object files (in subdirectories matching source structure)
# Object files are created as dir/file.o (e.g., cache/cache.o, db/db_impl.o)
compiled_objects=0
if [[ "$expected_lib_objects" -gt 0 ]]; then
# Count .o files in source directories
compiled_objects=$(find cache db env file logging memory memtable monitoring options port table test_util trace_replay util utilities -name '*.o' -type f 2>/dev/null | wc -l)
fi
# Count linked test binaries (test binaries are in current directory with _test suffix)
linked_tests=0
if [[ "$expected_test_binaries" -gt 0 ]]; then
linked_tests=$(find . -maxdepth 1 -name '*_test' -type f -executable 2>/dev/null | wc -l)
fi
# Determine phase based on what exists
if [[ "$compiled_objects" -eq 0 && "$linked_tests" -eq 0 ]]; then
# Nothing compiled yet - not started or just beginning
output_json "not_started" 0 0 0 0 0 "0" ""
exit 0
fi
# Calculate total work units: compiling + linking
total_work=$((expected_lib_objects + expected_test_binaries))
completed_work=$((compiled_objects + linked_tests))
if [[ "$total_work" -gt 0 ]]; then
percent=$((completed_work * 100 / total_work))
else
percent=0
fi
# Determine phase
if [[ "$compiled_objects" -lt "$expected_lib_objects" ]]; then
phase="compiling"
# Get most recently modified .o file as last_item
last_item=$(find cache db env file logging memory memtable monitoring options port table test_util trace_replay util utilities -name '*.o' -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2- | sed 's,^\./,,;s,\.o$,,')
elif [[ "$linked_tests" -lt "$expected_test_binaries" ]]; then
phase="linking"
# Get most recently modified test binary as last_item
last_item=$(find . -maxdepth 1 -name '*_test' -type f -executable -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2- | sed 's,^\./,,')
else
phase="generating"
last_item=""
fi
output_json "running" "$completed_work" "$total_work" 0 "$percent" 0 "0" "$last_item" "$phase"
+6 -16
View File
@@ -7,18 +7,14 @@ print_usage () {
echo "Usage:"
echo "format-diff.sh [OPTIONS]"
echo "-c: check only."
echo "-y: auto-apply formatting without prompts (non-interactive mode)."
echo "-h: print this message."
}
while getopts ':cyh' OPTION; do
while getopts ':ch' OPTION; do
case "$OPTION" in
c)
CHECK_ONLY=1
;;
y)
AUTO_APPLY=1
;;
h)
print_usage
exit 1
@@ -244,16 +240,11 @@ echo "$diffs" |
sed -e "s/\(^-.*$\)/`echo -e \"$COLOR_RED\1$COLOR_END\"`/" |
sed -e "s/\(^+.*$\)/`echo -e \"$COLOR_GREEN\1$COLOR_END\"`/"
# Handle auto-apply mode (non-interactive)
if [ "$AUTO_APPLY" ]; then
to_fix="y"
else
echo -e "Would you like to fix the format automatically (y/n): \c"
echo -e "Would you like to fix the format automatically (y/n): \c"
# Make sure under any mode, we can read user input.
exec < /dev/tty
read to_fix
fi
# Make sure under any mode, we can read user input.
exec < /dev/tty
read to_fix
if [ "$to_fix" != "y" ]
then
@@ -270,8 +261,7 @@ fi
echo "Files reformatted!"
# Amend to last commit if user do the post-commit format check
# Skip amend prompt in auto-apply mode (user can amend manually if desired)
if [ -z "$uncommitted_code" ] && [ -z "$AUTO_APPLY" ]; then
if [ -z "$uncommitted_code" ]; then
echo -e "Would you like to amend the changes to last commit (`git log HEAD --oneline | head -1`)? (y/n): \c"
read to_amend
+1 -1
View File
@@ -1913,7 +1913,7 @@ sub drain_job_queue {
}
$last_progress_time = time();
$ps_reported = 0;
} elsif (not $ps_reported and (time() - $last_progress_time) >= 300) {
} elsif (not $ps_reported and (time() - $last_progress_time) >= 60) {
# No progress in at least 60 seconds: run ps
print $Global::original_stderr "\n";
my $script_dir = ::dirname($0);
+3 -5
View File
@@ -5,8 +5,6 @@
# the docker and docker-registry groups, and logging out and back in to pick
# those up.)
#
# Meta employees: see https://fburl.com/rocksdb-docker to build this on a devvm.
#
# Follow https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic
# to login with your GitHub credentials, as in
#
@@ -17,8 +15,8 @@
# Then in the build_tools/ubuntu22_image directory, (bump minor version for
# random docker file updates, major version tracks Ubuntu release)
#
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:22.2
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:22.2
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:22.0
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:22.0
#
# Might need to change visibility to public through
# https://github.com/orgs/facebook/packages/container/rocksdb_ubuntu/settings
@@ -30,7 +28,7 @@ FROM ubuntu:22.04
RUN apt-get update
RUN apt-get upgrade -y
# install basic tools
RUN apt-get install -y vim wget curl ccache
RUN apt-get install -y vim wget curl
# install tzdata noninteractive
RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install tzdata
# install git and default compilers
+3 -9
View File
@@ -5,8 +5,6 @@
# the docker and docker-registry groups, and logging out and back in to pick
# those up.)
#
# Meta employees: see https://fburl.com/rocksdb-docker to build this on a devvm.
#
# Follow https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic
# to login with your GitHub credentials, as in
#
@@ -17,8 +15,8 @@
# Then in the build_tools/ubuntu24_image directory, (bump minor version for
# random docker file updates, major version tracks Ubuntu release)
#
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:24.1
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:24.1
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:24.0
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:24.0
#
# Might need to change visibility to public through
# https://github.com/orgs/facebook/packages/container/rocksdb_ubuntu/settings
@@ -30,15 +28,11 @@ FROM ubuntu:24.04
RUN apt-get update
RUN apt-get upgrade -y
# install basic tools
RUN apt-get install -y vim wget curl ccache
RUN apt-get install -y vim wget curl
# install tzdata noninteractive
RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install tzdata
# install git and default compilers
RUN apt-get install -y git gcc g++ clang clang-tools
# install clang-21 from LLVM snapshot repo
RUN curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/llvm.gpg && \
echo "deb https://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" > /etc/apt/sources.list.d/llvm-21.list && \
apt-get update && apt-get install -y clang-21
# install basic package
RUN apt-get install -y lsb-release software-properties-common gnupg
# install gflags, tbb
+5
View File
@@ -54,6 +54,11 @@ static std::unordered_map<std::string, OptionTypeInfo>
{offsetof(struct CompressedSecondaryCacheOptions, compression_type),
OptionType::kCompressionType, OptionVerificationType::kNormal,
OptionTypeFlags::kMutable}},
{"compress_format_version",
{offsetof(struct CompressedSecondaryCacheOptions,
compress_format_version),
OptionType::kUInt32T, OptionVerificationType::kNormal,
OptionTypeFlags::kMutable}},
{"enable_custom_split_merge",
{offsetof(struct CompressedSecondaryCacheOptions,
enable_custom_split_merge),
+9 -9
View File
@@ -101,23 +101,23 @@ class CacheEntryStatsCollector {
}
// Gets saved stats, regardless of age
void GetStats(Stats* stats) {
void GetStats(Stats *stats) {
std::lock_guard<std::mutex> lock(saved_mutex_);
*stats = saved_stats_;
}
Cache* GetCache() const { return cache_; }
Cache *GetCache() const { return cache_; }
// Gets or creates a shared instance of CacheEntryStatsCollector in the
// cache itself, and saves into `ptr`. This shared_ptr will hold the
// entry in cache until all refs are destroyed.
static Status GetShared(Cache* raw_cache, SystemClock* clock,
std::shared_ptr<CacheEntryStatsCollector>* ptr) {
static Status GetShared(Cache *raw_cache, SystemClock *clock,
std::shared_ptr<CacheEntryStatsCollector> *ptr) {
assert(raw_cache);
BasicTypedCacheInterface<CacheEntryStatsCollector, CacheEntryRole::kMisc>
cache{raw_cache};
const Slice& cache_key = GetCacheKey();
const Slice &cache_key = GetCacheKey();
auto h = cache.Lookup(cache_key);
if (h == nullptr) {
// Not yet in cache, but Cache doesn't provide a built-in way to
@@ -152,7 +152,7 @@ class CacheEntryStatsCollector {
}
private:
explicit CacheEntryStatsCollector(Cache* cache, SystemClock* clock)
explicit CacheEntryStatsCollector(Cache *cache, SystemClock *clock)
: saved_stats_(),
working_stats_(),
last_start_time_micros_(0),
@@ -160,7 +160,7 @@ class CacheEntryStatsCollector {
cache_(cache),
clock_(clock) {}
static const Slice& GetCacheKey() {
static const Slice &GetCacheKey() {
// For each template instantiation
static CacheKey ckey = CacheKey::CreateUniqueForProcessLifetime();
static Slice ckey_slice = ckey.AsSlice();
@@ -175,8 +175,8 @@ class CacheEntryStatsCollector {
uint64_t last_start_time_micros_;
uint64_t last_end_time_micros_;
Cache* const cache_;
SystemClock* const clock_;
Cache *const cache_;
SystemClock *const clock_;
};
} // namespace ROCKSDB_NAMESPACE
+3 -3
View File
@@ -24,7 +24,7 @@ namespace ROCKSDB_NAMESPACE {
// 0 | >= 1<<63 | CreateUniqueForProcessLifetime
// > 0 | any | OffsetableCacheKey.WithOffset
CacheKey CacheKey::CreateUniqueForCacheLifetime(Cache* cache) {
CacheKey CacheKey::CreateUniqueForCacheLifetime(Cache *cache) {
// +1 so that we can reserve all zeros for "unset" cache key
uint64_t id = cache->NewId() + 1;
// Ensure we don't collide with CreateUniqueForProcessLifetime
@@ -297,8 +297,8 @@ CacheKey CacheKey::CreateUniqueForProcessLifetime() {
//
// TODO: Nevertheless / regardless, an efficient way to detect (and thus
// quantify) block cache corruptions, including collisions, should be added.
OffsetableCacheKey::OffsetableCacheKey(const std::string& db_id,
const std::string& db_session_id,
OffsetableCacheKey::OffsetableCacheKey(const std::string &db_id,
const std::string &db_session_id,
uint64_t file_number) {
UniqueId64x2 internal_id;
Status s = GetSstInternalUniqueId(db_id, db_session_id, file_number,
+5 -5
View File
@@ -44,13 +44,13 @@ class CacheKey {
inline Slice AsSlice() const {
static_assert(sizeof(*this) == 16, "Standardized on 16-byte cache key");
assert(!IsEmpty());
return Slice(reinterpret_cast<const char*>(this), sizeof(*this));
return Slice(reinterpret_cast<const char *>(this), sizeof(*this));
}
// Create a CacheKey that is unique among others associated with this Cache
// instance. Depends on Cache::NewId. This is useful for block cache
// "reservations".
static CacheKey CreateUniqueForCacheLifetime(Cache* cache);
static CacheKey CreateUniqueForCacheLifetime(Cache *cache);
// Create a CacheKey that is unique among others for the lifetime of this
// process. This is useful for saving in a static data member so that
@@ -87,7 +87,7 @@ class OffsetableCacheKey : private CacheKey {
// Constructs an OffsetableCacheKey with the given information about a file.
// This constructor never generates an "empty" base key.
OffsetableCacheKey(const std::string& db_id, const std::string& db_session_id,
OffsetableCacheKey(const std::string &db_id, const std::string &db_session_id,
uint64_t file_number);
// Creates an OffsetableCacheKey from an SST unique ID, so that cache keys
@@ -134,9 +134,9 @@ class OffsetableCacheKey : private CacheKey {
static_assert(sizeof(file_num_etc64_) == kCommonPrefixSize,
"8 byte common prefix expected");
assert(!IsEmpty());
assert(&this->file_num_etc64_ == static_cast<const void*>(this));
assert(&this->file_num_etc64_ == static_cast<const void *>(this));
return Slice(reinterpret_cast<const char*>(this), kCommonPrefixSize);
return Slice(reinterpret_cast<const char *>(this), kCommonPrefixSize);
}
};
+16 -16
View File
@@ -44,8 +44,8 @@ class CacheReservationManager {
bool increase) = 0;
virtual Status MakeCacheReservation(
std::size_t incremental_memory_used,
std::unique_ptr<CacheReservationManager::CacheReservationHandle>*
handle) = 0;
std::unique_ptr<CacheReservationManager::CacheReservationHandle>
*handle) = 0;
virtual std::size_t GetTotalReservedCacheSize() = 0;
virtual std::size_t GetTotalMemoryUsed() = 0;
};
@@ -90,11 +90,11 @@ class CacheReservationManagerImpl
bool delayed_decrease = false);
// no copy constructor, copy assignment, move constructor, move assignment
CacheReservationManagerImpl(const CacheReservationManagerImpl&) = delete;
CacheReservationManagerImpl& operator=(const CacheReservationManagerImpl&) =
CacheReservationManagerImpl(const CacheReservationManagerImpl &) = delete;
CacheReservationManagerImpl &operator=(const CacheReservationManagerImpl &) =
delete;
CacheReservationManagerImpl(CacheReservationManagerImpl&&) = delete;
CacheReservationManagerImpl& operator=(CacheReservationManagerImpl&&) =
CacheReservationManagerImpl(CacheReservationManagerImpl &&) = delete;
CacheReservationManagerImpl &operator=(CacheReservationManagerImpl &&) =
delete;
~CacheReservationManagerImpl() override;
@@ -178,7 +178,7 @@ class CacheReservationManagerImpl
// REQUIRES: handle != nullptr
Status MakeCacheReservation(
std::size_t incremental_memory_used,
std::unique_ptr<CacheReservationManager::CacheReservationHandle>* handle)
std::unique_ptr<CacheReservationManager::CacheReservationHandle> *handle)
override;
// Return the size of the cache (which is a multiple of kSizeDummyEntry)
@@ -200,7 +200,7 @@ class CacheReservationManagerImpl
// For testing only - it is to help ensure the CacheItemHelperForRole<R>
// accessed from CacheReservationManagerImpl and the one accessed from the
// test are from the same translation units
static const Cache::CacheItemHelper* TEST_GetCacheItemHelperForRole();
static const Cache::CacheItemHelper *TEST_GetCacheItemHelperForRole();
private:
static constexpr std::size_t kSizeDummyEntry = 256 * 1024;
@@ -216,7 +216,7 @@ class CacheReservationManagerImpl
bool delayed_decrease_;
std::atomic<std::size_t> cache_allocated_size_;
std::size_t memory_used_;
std::vector<Cache::Handle*> dummy_handles_;
std::vector<Cache::Handle *> dummy_handles_;
CacheKey cache_key_;
};
@@ -251,14 +251,14 @@ class ConcurrentCacheReservationManager
std::shared_ptr<CacheReservationManager> cache_res_mgr) {
cache_res_mgr_ = std::move(cache_res_mgr);
}
ConcurrentCacheReservationManager(const ConcurrentCacheReservationManager&) =
ConcurrentCacheReservationManager(const ConcurrentCacheReservationManager &) =
delete;
ConcurrentCacheReservationManager& operator=(
const ConcurrentCacheReservationManager&) = delete;
ConcurrentCacheReservationManager(ConcurrentCacheReservationManager&&) =
ConcurrentCacheReservationManager &operator=(
const ConcurrentCacheReservationManager &) = delete;
ConcurrentCacheReservationManager(ConcurrentCacheReservationManager &&) =
delete;
ConcurrentCacheReservationManager& operator=(
ConcurrentCacheReservationManager&&) = delete;
ConcurrentCacheReservationManager &operator=(
ConcurrentCacheReservationManager &&) = delete;
~ConcurrentCacheReservationManager() override {}
@@ -286,7 +286,7 @@ class ConcurrentCacheReservationManager
inline Status MakeCacheReservation(
std::size_t incremental_memory_used,
std::unique_ptr<CacheReservationManager::CacheReservationHandle>* handle)
std::unique_ptr<CacheReservationManager::CacheReservationHandle> *handle)
override {
std::unique_ptr<CacheReservationManager::CacheReservationHandle>
wrapped_handle;
+5 -1
View File
@@ -50,7 +50,8 @@ CompressedSecondaryCache::CompressedSecondaryCache(
std::make_shared<CacheReservationManagerImpl<CacheEntryRole::kMisc>>(
cache_))),
disable_cache_(opts.capacity == 0) {
auto mgr = GetBuiltinV2CompressionManager();
auto mgr =
GetBuiltinCompressionManager(cache_options_.compress_format_version);
compressor_ = mgr->GetCompressor(cache_options_.compression_opts,
cache_options_.compression_type);
decompressor_ =
@@ -355,6 +356,9 @@ std::string CompressedSecondaryCache::GetPrintableOptions() const {
const_cast<CompressionOptions&>(cache_options_.compression_opts))
.c_str());
ret.append(buffer);
snprintf(buffer, kBufferSize, " compress_format_version : %d\n",
cache_options_.compress_format_version);
ret.append(buffer);
return ret;
}
+3 -2
View File
@@ -856,7 +856,8 @@ TEST_P(CompressedSecondaryCacheTestWithCompressionParam, BasicTestFromString) {
if (LZ4_Supported()) {
sec_cache_uri =
"compressed_secondary_cache://"
"capacity=2048;num_shard_bits=0;compression_type=kLZ4Compression";
"capacity=2048;num_shard_bits=0;compression_type=kLZ4Compression;"
"compress_format_version=2";
} else {
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
sec_cache_uri =
@@ -887,7 +888,7 @@ TEST_P(CompressedSecondaryCacheTestWithCompressionParam,
sec_cache_uri =
"compressed_secondary_cache://"
"capacity=2048;num_shard_bits=0;compression_type=kLZ4Compression;"
"enable_custom_split_merge=true";
"compress_format_version=2;enable_custom_split_merge=true";
} else {
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
sec_cache_uri =
+11 -11
View File
@@ -2179,7 +2179,7 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadBasic) {
&cache_dumper);
ASSERT_OK(s);
std::vector<DB*> db_list;
db_list.push_back(db_.get());
db_list.push_back(db_);
s = cache_dumper->SetDumpFilter(db_list);
ASSERT_OK(s);
s = cache_dumper->DumpCacheEntriesToWriter();
@@ -2263,11 +2263,11 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadWithFilter) {
options.env = fault_env_.get();
std::string dbname1 = test::PerThreadDBPath("db_1");
ASSERT_OK(DestroyDB(dbname1, options));
std::unique_ptr<DB> db1;
DB* db1 = nullptr;
ASSERT_OK(DB::Open(options, dbname1, &db1));
std::string dbname2 = test::PerThreadDBPath("db_2");
ASSERT_OK(DestroyDB(dbname2, options));
std::unique_ptr<DB> db2;
DB* db2 = nullptr;
ASSERT_OK(DB::Open(options, dbname2, &db2));
fault_fs_->SetFailGetUniqueId(true);
@@ -2335,7 +2335,7 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadWithFilter) {
&cache_dumper);
ASSERT_OK(s);
std::vector<DB*> db_list;
db_list.push_back(db1.get());
db_list.push_back(db1);
s = cache_dumper->SetDumpFilter(db_list);
ASSERT_OK(s);
s = cache_dumper->DumpCacheEntriesToWriter();
@@ -2377,7 +2377,7 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadWithFilter) {
ASSERT_OK(s);
ASSERT_OK(db1->Close());
db1.reset();
delete db1;
ASSERT_OK(DB::Open(options, dbname1, &db1));
// After load, we do the Get again. To validate the cache, we do not allow any
@@ -2406,8 +2406,8 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadWithFilter) {
ASSERT_EQ(256, static_cast<int>(block_lookup));
fault_fs_->SetFailGetUniqueId(false);
fault_fs_->SetFilesystemActive(true);
db1.reset();
db2.reset();
delete db1;
delete db2;
ASSERT_OK(DestroyDB(dbname1, options));
ASSERT_OK(DestroyDB(dbname2, options));
}
@@ -2619,11 +2619,11 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionTwoDB) {
options.paranoid_file_checks = true;
std::string dbname1 = test::PerThreadDBPath("db_t_1");
ASSERT_OK(DestroyDB(dbname1, options));
std::unique_ptr<DB> db1;
DB* db1 = nullptr;
ASSERT_OK(DB::Open(options, dbname1, &db1));
std::string dbname2 = test::PerThreadDBPath("db_t_2");
ASSERT_OK(DestroyDB(dbname2, options));
std::unique_ptr<DB> db2;
DB* db2 = nullptr;
Options options2 = options;
options2.lowest_used_cache_tier = CacheTier::kVolatileTier;
ASSERT_OK(DB::Open(options2, dbname2, &db2));
@@ -2700,8 +2700,8 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionTwoDB) {
fault_fs_->SetFailGetUniqueId(false);
fault_fs_->SetFilesystemActive(true);
db1.reset();
db2.reset();
delete db1;
delete db2;
ASSERT_OK(DestroyDB(dbname1, options));
ASSERT_OK(DestroyDB(dbname2, options));
}
-512
View File
@@ -1,512 +0,0 @@
# Adding New Options to RocksDB Public API
This document provides guidance on how to add new options to RocksDB's public API. There are two main categories of options:
1. **Standard Column Family Options** (Options/DBOptions/AdvancedColumnFamilyOptions)
2. **BlockBasedTableOptions** (options specific to block-based table format)
## Overview of Files to Modify
### For Standard Column Family Options
| File | Purpose |
|------|---------|
| `include/rocksdb/advanced_options.h` | Define the option with documentation |
| `include/rocksdb/options.h` | Add reference in related option groups if needed |
| `options/cf_options.h` | Add to `MutableCFOptions` or `ImmutableCFOptions` struct |
| `options/cf_options.cc` | Register option for serialization/deserialization and logging |
| `options/options_helper.cc` | Add to `UpdateColumnFamilyOptions()` for mutable options |
| `options/options_settable_test.cc` | Add to test string for option parsing |
| `db_stress_tool/db_stress_common.h` | Declare gflag |
| `db_stress_tool/db_stress_gflags.cc` | Define gflag with default value |
| `db_stress_tool/db_stress_test_base.cc` | Apply flag to options |
| `tools/db_bench_tool.cc` | Add flag definition and apply to options |
| `tools/db_crashtest.py` | Add randomized values for stress testing |
| `unreleased_history/new_features/` | Add release note markdown file |
### For BlockBasedTableOptions
| File | Purpose |
|------|---------|
| `include/rocksdb/table.h` | Define the option in `BlockBasedTableOptions` struct |
| `table/block_based/block_based_table_factory.cc` | Register for serialization, validation, and printing |
| `options/options_settable_test.cc` | Add to `BlockBasedTableOptionsAllFieldsSettable` test |
| `options/options_test.cc` | Add to `MutableCFOptions` test if applicable |
| `db_stress_tool/db_stress_common.h` | Declare gflag |
| `db_stress_tool/db_stress_gflags.cc` | Define gflag |
| `db_stress_tool/db_stress_test_base.cc` | Apply flag to `block_based_options` |
| `tools/db_bench_tool.cc` | Add flag definition and apply to `block_based_options` |
| `tools/db_crashtest.py` | Add randomized values |
| `java/src/main/java/org/rocksdb/BlockBasedTableConfig.java` | Java API |
| `java/rocksjni/portal.h` | JNI portal for Java bindings |
| `java/rocksjni/table.cc` | JNI implementation |
| `java/src/test/java/org/rocksdb/BlockBasedTableConfigTest.java` | Java unit test |
---
## Pattern 1: Adding a Standard Column Family Option
Example reference: commit `94e65a2e0b4f817aa4bfa4c96cdf867e7980d7bc` (memtable_veirfy_per_key_checksum_on_seek)
### Step 1: Define the Option in Public Header
**File: `include/rocksdb/advanced_options.h`**
Add the option with documentation in `AdvancedColumnFamilyOptions` struct:
```cpp
// Enables additional integrity checks during seek.
// Specifically, for skiplist-based memtables, key checksum validation could
// be enabled during seek optionally. This is helpful to detect corrupted
// memtable keys during reads. Enabling this feature incurs a performance
// overhead due to additional key checksum validation during memtable seek
// operation.
// This option depends on memtable_protection_bytes_per_key to be non zero.
// If memtable_protection_bytes_per_key is zero, no validation is performed.
bool memtable_veirfy_per_key_checksum_on_seek = false;
```
### Step 2: Add to Internal Options Structs
**File: `options/cf_options.h`**
Add to `MutableCFOptions` struct (or `ImmutableCFOptions` for immutable options):
```cpp
// In MutableCFOptions constructor from Options:
memtable_veirfy_per_key_checksum_on_seek(
options.memtable_veirfy_per_key_checksum_on_seek),
// In MutableCFOptions default constructor:
memtable_veirfy_per_key_checksum_on_seek(false),
// In MutableCFOptions struct member declarations:
bool memtable_veirfy_per_key_checksum_on_seek;
```
### Step 3: Register for Serialization/Deserialization
**File: `options/cf_options.cc`**
Add to the options type info map for serialization:
```cpp
{"memtable_veirfy_per_key_checksum_on_seek",
{offsetof(struct MutableCFOptions,
memtable_veirfy_per_key_checksum_on_seek),
OptionType::kBoolean, OptionVerificationType::kNormal,
OptionTypeFlags::kMutable}},
```
Add logging in `MutableCFOptions::Dump()`:
```cpp
ROCKS_LOG_INFO(log, "memtable_veirfy_per_key_checksum_on_seek: %d",
memtable_veirfy_per_key_checksum_on_seek);
```
### Step 4: Update Options Helper
**File: `options/options_helper.cc`**
Add to `UpdateColumnFamilyOptions()`:
```cpp
cf_opts->memtable_veirfy_per_key_checksum_on_seek =
moptions.memtable_veirfy_per_key_checksum_on_seek;
```
### Step 5: Add to Options Settable Test
**File: `options/options_settable_test.cc`**
Add to the test string in `ColumnFamilyOptionsAllFieldsSettable`:
```cpp
"memtable_veirfy_per_key_checksum_on_seek=1;"
```
### Step 6: Add db_stress Support
**File: `db_stress_tool/db_stress_common.h`**
```cpp
DECLARE_bool(memtable_veirfy_per_key_checksum_on_seek);
```
**File: `db_stress_tool/db_stress_gflags.cc`**
```cpp
DEFINE_bool(
memtable_veirfy_per_key_checksum_on_seek,
ROCKSDB_NAMESPACE::Options().memtable_veirfy_per_key_checksum_on_seek,
"Sets CF option memtable_veirfy_per_key_checksum_on_seek.");
```
**File: `db_stress_tool/db_stress_test_base.cc`**
```cpp
options.memtable_veirfy_per_key_checksum_on_seek =
FLAGS_memtable_veirfy_per_key_checksum_on_seek;
```
### Step 7: Add db_bench Support
**File: `tools/db_bench_tool.cc`**
```cpp
// Flag definition (near related flags):
DEFINE_bool(memtable_veirfy_per_key_checksum_on_seek, false,
"Sets CF option memtable_veirfy_per_key_checksum_on_seek");
// Apply flag to options (in InitializeOptionsFromFlags or similar):
options.memtable_veirfy_per_key_checksum_on_seek =
FLAGS_memtable_veirfy_per_key_checksum_on_seek;
```
### Step 8: Add Crash Test Support
**File: `tools/db_crashtest.py`**
```python
"memtable_veirfy_per_key_checksum_on_seek": lambda: random.choice([0] * 7 + [1]),
```
Also add constraint handling in `finalize_and_sanitize()` if needed:
```python
# only skip list memtable representation supports paranoid memory checks
if dest_params.get("memtablerep") != "skip_list":
dest_params["memtable_veirfy_per_key_checksum_on_seek"] = 0
```
### Step 9: Add Release Note
**File: `unreleased_history/new_features/<descriptive_name>.md`**
```markdown
A new flag memtable_veirfy_per_key_checksum_on_seek is added to AdvancedColumnFamilyOptions. When it is enabled, it will validate key checksum along the binary search path on skiplist based memtable during seek operation.
```
---
## Pattern 2: Adding a BlockBasedTableOptions Option
Example reference: commit `742741b175c5f238374c1714f9db3340d49de569` (super_block_alignment_size)
### Step 1: Define the Option in Public Header
**File: `include/rocksdb/table.h`**
Add to `BlockBasedTableOptions` struct with documentation:
```cpp
// Align data blocks on super block alignment. Avoid a data block split across
// super block boundaries. Works with/without compression.
//
// Here a "super block" refers to an aligned unit of underlying Filesystem
// storage for which there is an extra cost when a random read involves two
// such super blocks instead of just one. Configuring that size here suggests
// inserting padding in the SST file to avoid a single SST block splitting
// across two super blocks. Only power-of-two sizes are supported. See also
// super_block_alignment_space_overhead_ratio. Default to 0, which means super
// block alignment is disabled.
size_t super_block_alignment_size = 0;
// This option controls the storage space overhead of super block alignment.
// It is used to calculate the max padding size allowed for super block
// alignment. It is calculated in this way. If super_block_alignment_size is
// 2MB, and super_block_alignment_overhead_ratio is 128, then the max padding
// size allowed for super block alignment is 2MB / 128 = 16KB.
// Note that, when it is set to 0, super block alignment is disabled.
size_t super_block_alignment_space_overhead_ratio = 128;
```
### Step 2: Register for Serialization in Table Factory
**File: `table/block_based/block_based_table_factory.cc`**
Add to the type info map:
```cpp
{"super_block_alignment_size",
{offsetof(struct BlockBasedTableOptions, super_block_alignment_size),
OptionType::kSizeT, OptionVerificationType::kNormal}},
{"super_block_alignment_space_overhead_ratio",
{offsetof(struct BlockBasedTableOptions,
super_block_alignment_space_overhead_ratio),
OptionType::kSizeT, OptionVerificationType::kNormal}},
```
Add validation in `ValidateOptions()`:
```cpp
if ((table_options_.super_block_alignment_size &
(table_options_.super_block_alignment_size - 1))) {
return Status::InvalidArgument(
"Super Block alignment requested but super block alignment size is not "
"a power of 2");
}
if (table_options_.super_block_alignment_size >
std::numeric_limits<uint32_t>::max()) {
return Status::InvalidArgument(
"Super block alignment size exceeds maximum number (4GiB) allowed");
}
```
Add printing in `GetPrintableOptions()`:
```cpp
snprintf(buffer, kBufferSize,
" super_block_alignment_size: %" ROCKSDB_PRIszt "\n",
table_options_.super_block_alignment_size);
ret.append(buffer);
```
### Step 3: Add to Options Settable Test
**File: `options/options_settable_test.cc`**
Add to `BlockBasedTableOptionsAllFieldsSettable` test:
```cpp
"super_block_alignment_size=65536;"
"super_block_alignment_space_overhead_ratio=4096;"
```
### Step 4: Add to Options Test
**File: `options/options_test.cc`**
```cpp
ASSERT_OK(GetColumnFamilyOptionsFromString(
config_options, cf_opts,
"block_based_table_factory.super_block_alignment_size=65536; "
"block_based_table_factory.super_block_alignment_space_overhead_ratio=4096;",
&cf_opts));
ASSERT_EQ(bbto->super_block_alignment_size, 65536);
ASSERT_EQ(bbto->super_block_alignment_space_overhead_ratio, 4096);
```
### Step 5: Add db_stress Support
**File: `db_stress_tool/db_stress_common.h`**
```cpp
DECLARE_uint64(super_block_alignment_size);
DECLARE_uint64(super_block_alignment_space_overhead_ratio);
```
**File: `db_stress_tool/db_stress_gflags.cc`**
```cpp
DEFINE_uint64(
super_block_alignment_size,
ROCKSDB_NAMESPACE::BlockBasedTableOptions().super_block_alignment_size,
"BlockBasedTableOptions.super_block_alignment_size");
DEFINE_uint64(
super_block_alignment_space_overhead_ratio,
ROCKSDB_NAMESPACE::BlockBasedTableOptions()
.super_block_alignment_space_overhead_ratio,
"BlockBasedTableOptions.super_block_alignment_space_overhead_ratio");
```
**File: `db_stress_tool/db_stress_test_base.cc`**
```cpp
block_based_options.super_block_alignment_size =
fLU64::FLAGS_super_block_alignment_size;
block_based_options.super_block_alignment_space_overhead_ratio =
fLU64::FLAGS_super_block_alignment_space_overhead_ratio;
```
### Step 6: Add db_bench Support
**File: `tools/db_bench_tool.cc`**
```cpp
// Flag definitions:
DEFINE_uint64(
super_block_alignment_size,
ROCKSDB_NAMESPACE::BlockBasedTableOptions().super_block_alignment_size,
"Configure super block size");
DEFINE_uint64(super_block_alignment_space_overhead_ratio,
ROCKSDB_NAMESPACE::BlockBasedTableOptions()
.super_block_alignment_space_overhead_ratio,
"Configure space overhead for super block alignment");
// Apply to block_based_options (in the block where other options are set):
block_based_options.super_block_alignment_size = FLAGS_super_block_alignment_size;
block_based_options.super_block_alignment_space_overhead_ratio =
FLAGS_super_block_alignment_space_overhead_ratio;
```
### Step 7: Add Crash Test Support
**File: `tools/db_crashtest.py`**
```python
"super_block_alignment_size": lambda: random.choice(
[0, 128 * 1024, 512 * 1024, 2 * 1024 * 1024]
),
"super_block_alignment_space_overhead_ratio": lambda: random.choice([0, 32, 4096]),
```
### Step 8: Add Java API Support
**File: `java/src/main/java/org/rocksdb/BlockBasedTableConfig.java`**
Add getter and setter methods:
```java
/**
* Get the super block alignment size.
*
* @return the super block alignment size.
*/
public long superBlockAlignmentSize() {
return superBlockAlignmentSize;
}
/**
* Set the super block alignment size.
* When set to 0, super block alignment is disabled.
*
* @param superBlockAlignmentSize the super block alignment size.
*
* @return the reference to the current option.
*/
public BlockBasedTableConfig setSuperBlockAlignmentSize(final long superBlockAlignmentSize) {
this.superBlockAlignmentSize = superBlockAlignmentSize;
return this;
}
```
Add member variable:
```java
private long superBlockAlignmentSize;
```
Update constructor and native method signature.
**File: `java/rocksjni/portal.h`**
Update `GetMethodID` signature and add fields to Java object construction.
**File: `java/rocksjni/table.cc`**
Add parameters to JNI function and apply to options.
**File: `java/src/test/java/org/rocksdb/BlockBasedTableConfigTest.java`**
Add unit tests:
```java
@Test
public void superBlockAlignmentSize() {
final BlockBasedTableConfig blockBasedTableConfig = new BlockBasedTableConfig();
blockBasedTableConfig.setSuperBlockAlignmentSize(1024 * 1024);
assertThat(blockBasedTableConfig.superBlockAlignmentSize()).isEqualTo(1024 * 1024);
}
```
---
## Pattern 3: Adding C API for Existing Option
Example reference: commit `429b36c22d76403d275dd0e6877b08d4cea2bc90` (block_align C API)
If an option already exists but needs C API support:
**File: `db/c.cc`**
```cpp
void rocksdb_block_based_options_set_block_align(
rocksdb_block_based_table_options_t* options, unsigned char v) {
options->rep.block_align = v;
}
```
**File: `include/rocksdb/c.h`**
```cpp
extern ROCKSDB_LIBRARY_API void rocksdb_block_based_options_set_block_align(
rocksdb_block_based_table_options_t*, unsigned char);
```
---
## Unit Testing Guidelines
### For Standard Options
Add tests in appropriate test files (e.g., `db/db_memtable_test.cc`, `db/db_options_test.cc`):
```cpp
TEST_F(DBMemTableTest, YourOptionTest) {
Options options;
options.your_new_option = true;
Reopen(options);
// Test the behavior
}
```
### For BlockBasedTableOptions
Add tests in `db/db_flush_test.cc`, `table/block_based/block_based_table_reader_test.cc`, or `table/table_test.cc`:
```cpp
TEST_P(DBFlushYourFeatureTest, YourFeature) {
Options options;
BlockBasedTableOptions block_options;
block_options.your_new_option = some_value;
options.table_factory.reset(NewBlockBasedTableFactory(block_options));
ASSERT_OK(options.table_factory->ValidateOptions(
DBOptions(options), ColumnFamilyOptions(options)));
Reopen(options);
// Test the behavior
}
```
---
## Option Type Reference
Common option types used in serialization:
| OptionType | C++ Type | Example |
|------------|----------|---------|
| `kBoolean` | `bool` | `paranoid_memory_checks` |
| `kInt` | `int` | `max_write_buffer_number` |
| `kInt32T` | `int32_t` | `level0_file_num_compaction_trigger` |
| `kUInt32T` | `uint32_t` | `memtable_protection_bytes_per_key` |
| `kUInt64T` | `uint64_t` | `target_file_size_base` |
| `kSizeT` | `size_t` | `block_size` |
| `kDouble` | `double` | `compression_ratio` |
| `kString` | `std::string` | `db_log_dir` |
---
## Checklist Summary
- [ ] Public header file with option definition and documentation
- [ ] Internal options struct (MutableCFOptions or ImmutableCFOptions)
- [ ] Options serialization/deserialization registration
- [ ] Options logging in Dump() method
- [ ] UpdateColumnFamilyOptions() for mutable options
- [ ] options_settable_test.cc
- [ ] db_stress_common.h (DECLARE)
- [ ] db_stress_gflags.cc (DEFINE)
- [ ] db_stress_test_base.cc (apply flag)
- [ ] db_bench_tool.cc (DEFINE and apply)
- [ ] db_crashtest.py (randomized values)
- [ ] Unit tests
- [ ] unreleased_history markdown file
- [ ] Java API (for BlockBasedTableOptions)
- [ ] C API (if needed)
-504
View File
@@ -1,504 +0,0 @@
# RocksDB API Development Guide
This document provides guidance for adding new public APIs to RocksDB, following the established patterns used by existing APIs like `CompactRange`.
## API Layer Architecture
RocksDB exposes public APIs through multiple layers. Users can access RocksDB through any of the three public APIs: C++ headers, C headers, or Java bindings.
Here is an example for public header db.h:
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Level 1: Public APIs (User Entry Points) │
├───────────────────────┬─────────────────────────┬───────────────────────────┤
│ C++ Public API │ C API Bindings │ Java/JNI API │
│ include/rocksdb/db.h │ include/rocksdb/c.h │ java/src/.../RocksDB.java │
│ include/rocksdb/*.h │ │ java/src/.../*.java │
└───────────────────────┴────────────┬────────────┴───────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────┐
│ Level 2: C++ Implementation (Internal Core) │
│ db/db_impl/db_impl*.cc, db/c.cc, java/rocksjni/*.cc │
└─────────────────────────────────────────────────────────────────────────────┘
```
## Step-by-Step Guide: Adding a New Public API
### Step 1: Define the C++ Public Interface
**File:** `include/rocksdb/db.h`
Add the virtual method declaration in the `DB` class:
\`\`\`cpp
// Pure virtual - must be implemented by DBImpl
virtual Status YourNewAPI(const YourAPIOptions& options,
ColumnFamilyHandle* column_family,
/* other params */) = 0;
// Convenience overload for default column family
virtual Status YourNewAPI(const YourAPIOptions& options,
/* other params */) {
return YourNewAPI(options, DefaultColumnFamily(), /* other params */);
}
\`\`\`
**Key Patterns:**
- Use `Status` return type for error handling
- Use `OptSlice` to avoid unnecessary levels of indirection and use of raw pointers.
- Use `ColumnFamilyHandle*` for column family support
- Provide convenience overloads for the default column family
### Step 2: Define Options Struct (If Needed)
**File:** `include/rocksdb/options.h`
If your API has multiple configuration options, define an options struct:
\`\`\`cpp
struct YourAPIOptions {
// Document each option with clear comments
bool some_boolean_option = false;
// Default value explanation
int some_int_option = -1;
// Pointer options require careful lifetime management
std::atomic<bool>* canceled = nullptr;
// Enum options for multi-choice settings
YourEnumType some_enum = YourEnumType::kDefault;
};
\`\`\`
**Key Patterns:**
- Use sensible default values specified inline (e.g., `= false`, `= -1`)
- Do NOT redundantly document the default value in comments; instead, document the rationale (why this default), historical context, and how different values are interpreted
- Group related options logically
- Consider thread-safety for pointer options
### Step 3: Implement in DBImpl
**Header:** `db/db_impl/db_impl.h`
\`\`\`cpp
using DB::YourNewAPI;
Status YourNewAPI(const YourAPIOptions& options,
ColumnFamilyHandle* column_family,
/* other params */) override;
// Private internal implementation if needed
Status YourNewAPIInternal(const YourAPIOptions& options,
ColumnFamilyHandle* column_family,
/* other params */);
\`\`\`
**Implementation:** `db/db_impl/db_impl_<category>.cc`
Choose the appropriate implementation file based on functionality:
- `db_impl_compaction_flush.cc` - Compaction and flush operations
- `db_impl_write.cc` - Write operations
- `db_impl_open.cc` - DB opening/closing
- `db_impl_files.cc` - File operations
- `db_impl.cc` - General operations
\`\`\`cpp
Status DBImpl::YourNewAPI(const YourAPIOptions& options,
ColumnFamilyHandle* column_family,
/* other params */) {
// 1. Input validation
if (/* invalid input */) {
return Status::InvalidArgument("Error message");
}
// 2. Check for cancellation/abort conditions
if (options.canceled && options.canceled->load(std::memory_order_acquire)) {
return Status::Incomplete(Status::SubCode::kManualCompactionPaused);
}
// 3. Get column family data
auto cfh = static_cast<ColumnFamilyHandleImpl*>(column_family);
auto cfd = cfh->cfd();
// 4. Core implementation logic
// ...
return Status::OK();
}
\`\`\`
### Step 4: Handle Special DB Types
**StackableDB (Wrapper DBs):**
**File:** `include/rocksdb/utilities/stackable_db.h`
\`\`\`cpp
using DB::YourNewAPI;
Status YourNewAPI(const YourAPIOptions& options,
ColumnFamilyHandle* column_family,
/* other params */) override {
return db_->YourNewAPI(options, column_family, /* other params */);
}
\`\`\`
**Secondary DB (Read-Only):**
**File:** `db/db_impl/db_impl_secondary.h`
\`\`\`cpp
using DBImpl::YourNewAPI;
Status YourNewAPI(const YourAPIOptions& /*options*/,
ColumnFamilyHandle* /*column_family*/,
/* other params */) override {
return Status::NotSupported("Not supported in secondary DB");
}
\`\`\`
**CompactedDB (Read-Only):**
**File:** `db/db_impl/compacted_db_impl.h`
\`\`\`cpp
using DBImpl::YourNewAPI;
Status YourNewAPI(const YourAPIOptions& /*options*/,
ColumnFamilyHandle* /*column_family*/,
/* other params */) override {
return Status::NotSupported("Not supported for read-only DB");
}
\`\`\`
### Step 5: Add C API Bindings
**Header:** `include/rocksdb/c.h`
\`\`\`c
// Basic version
extern ROCKSDB_LIBRARY_API void rocksdb_your_new_api(
rocksdb_t* db,
const char* start_key, size_t start_key_len,
const char* limit_key, size_t limit_key_len);
// Column family version
extern ROCKSDB_LIBRARY_API void rocksdb_your_new_api_cf(
rocksdb_t* db, rocksdb_column_family_handle_t* column_family,
const char* start_key, size_t start_key_len,
const char* limit_key, size_t limit_key_len);
// With options and error handling
extern ROCKSDB_LIBRARY_API void rocksdb_your_new_api_opt(
rocksdb_t* db, rocksdb_your_api_options_t* opt,
const char* start_key, size_t start_key_len,
const char* limit_key, size_t limit_key_len,
char** errptr);
\`\`\`
**Implementation:** `db/c.cc`
\`\`\`cpp
void rocksdb_your_new_api(rocksdb_t* db, const char* start_key,
size_t start_key_len, const char* limit_key,
size_t limit_key_len) {
Slice a, b;
db->rep->YourNewAPI(
YourAPIOptions(), // Default options
(start_key ? (a = Slice(start_key, start_key_len), &a) : nullptr),
(limit_key ? (b = Slice(limit_key, limit_key_len), &b) : nullptr));
}
void rocksdb_your_new_api_cf(rocksdb_t* db,
rocksdb_column_family_handle_t* column_family,
const char* start_key, size_t start_key_len,
const char* limit_key, size_t limit_key_len) {
Slice a, b;
db->rep->YourNewAPI(
YourAPIOptions(),
column_family->rep,
(start_key ? (a = Slice(start_key, start_key_len), &a) : nullptr),
(limit_key ? (b = Slice(limit_key, limit_key_len), &b) : nullptr));
}
\`\`\`
**If you have options, also add:**
\`\`\`cpp
// Options struct wrapper
struct rocksdb_your_api_options_t {
YourAPIOptions rep;
};
rocksdb_your_api_options_t* rocksdb_your_api_options_create() {
return new rocksdb_your_api_options_t;
}
void rocksdb_your_api_options_destroy(rocksdb_your_api_options_t* opt) {
delete opt;
}
void rocksdb_your_api_options_set_some_option(
rocksdb_your_api_options_t* opt, unsigned char value) {
opt->rep.some_boolean_option = value;
}
\`\`\`
### Step 6: Add Java Bindings
**Java API:** `java/src/main/java/org/rocksdb/RocksDB.java`
\`\`\`java
// Basic version
public void yourNewAPI() throws RocksDBException {
yourNewAPI(null);
}
// Column family version
public void yourNewAPI(ColumnFamilyHandle columnFamilyHandle)
throws RocksDBException {
yourNewAPI(nativeHandle_, null, -1, null, -1, 0,
columnFamilyHandle == null ? 0 : columnFamilyHandle.nativeHandle_);
}
// Range version
public void yourNewAPI(final byte[] begin, final byte[] end)
throws RocksDBException {
yourNewAPI(null, begin, end);
}
// Full-featured version with options
public void yourNewAPI(ColumnFamilyHandle columnFamilyHandle,
final byte[] begin, final byte[] end,
final YourAPIOptions options)
throws RocksDBException {
yourNewAPI(nativeHandle_,
begin, begin == null ? -1 : begin.length,
end, end == null ? -1 : end.length,
options.nativeHandle_,
columnFamilyHandle == null ? 0 : columnFamilyHandle.nativeHandle_);
}
// Native method declaration
private static native void yourNewAPI(final long handle,
/* @Nullable */ final byte[] begin, final int beginLen,
/* @Nullable */ final byte[] end, final int endLen,
final long optionsHandle,
final long cfHandle);
\`\`\`
**Options Class:** `java/src/main/java/org/rocksdb/YourAPIOptions.java`
\`\`\`java
public class YourAPIOptions extends RocksObject {
public YourAPIOptions() {
super(newYourAPIOptions());
}
// Builder pattern setters
public YourAPIOptions setSomeBooleanOption(boolean value) {
setSomeBooleanOption(nativeHandle_, value);
return this;
}
// Getters
public boolean someBooleanOption() {
return someBooleanOption(nativeHandle_);
}
// Native method declarations
private static native long newYourAPIOptions();
private static native void disposeInternalJni(long handle);
private static native void setSomeBooleanOption(long handle, boolean value);
private static native boolean someBooleanOption(long handle);
@Override
protected final void disposeInternal(final long handle) {
disposeInternalJni(handle);
}
}
\`\`\`
**JNI Implementation:** `java/rocksjni/rocksjni.cc`
\`\`\`cpp
void Java_org_rocksdb_RocksDB_yourNewAPI(
JNIEnv* env, jclass,
jlong jdb_handle, jbyteArray jbegin, jint jbegin_len,
jbyteArray jend, jint jend_len,
jlong joptions_handle, jlong jcf_handle) {
// 1. Convert Java byte arrays to C++ strings
jboolean has_exception = JNI_FALSE;
std::string str_begin;
if (jbegin_len > 0) {
str_begin = ROCKSDB_NAMESPACE::JniUtil::byteString<std::string>(
env, jbegin, jbegin_len,
[](const char* str, const size_t len) { return std::string(str, len); },
&has_exception);
if (has_exception == JNI_TRUE) return;
}
std::string str_end;
if (jend_len > 0) {
str_end = ROCKSDB_NAMESPACE::JniUtil::byteString<std::string>(
env, jend, jend_len,
[](const char* str, const size_t len) { return std::string(str, len); },
&has_exception);
if (has_exception == JNI_TRUE) return;
}
// 2. Get or create options
ROCKSDB_NAMESPACE::YourAPIOptions* options = nullptr;
if (joptions_handle == 0) {
options = new ROCKSDB_NAMESPACE::YourAPIOptions();
} else {
options = reinterpret_cast<ROCKSDB_NAMESPACE::YourAPIOptions*>(joptions_handle);
}
// 3. Unwrap handles
auto* db = reinterpret_cast<ROCKSDB_NAMESPACE::DB*>(jdb_handle);
ROCKSDB_NAMESPACE::ColumnFamilyHandle* cf_handle =
jcf_handle == 0 ? db->DefaultColumnFamily()
: reinterpret_cast<ROCKSDB_NAMESPACE::ColumnFamilyHandle*>(jcf_handle);
// 4. Create Slices
std::unique_ptr<ROCKSDB_NAMESPACE::Slice> begin;
std::unique_ptr<ROCKSDB_NAMESPACE::Slice> end;
if (jbegin_len > 0) begin.reset(new ROCKSDB_NAMESPACE::Slice(str_begin));
if (jend_len > 0) end.reset(new ROCKSDB_NAMESPACE::Slice(str_end));
// 5. Call C++ API
ROCKSDB_NAMESPACE::Status s = db->YourNewAPI(*options, cf_handle, begin.get(), end.get());
// 6. Cleanup if we created options
if (joptions_handle == 0) delete options;
// 7. Throw Java exception on error
ROCKSDB_NAMESPACE::RocksDBExceptionJni::ThrowNew(env, s);
}
\`\`\`
**Options JNI:** `java/rocksjni/your_api_options.cc`
\`\`\`cpp
jlong Java_org_rocksdb_YourAPIOptions_newYourAPIOptions(JNIEnv*, jclass) {
auto* options = new ROCKSDB_NAMESPACE::YourAPIOptions();
return GET_CPLUSPLUS_POINTER(options);
}
void Java_org_rocksdb_YourAPIOptions_disposeInternalJni(JNIEnv*, jclass, jlong jhandle) {
auto* options = reinterpret_cast<ROCKSDB_NAMESPACE::YourAPIOptions*>(jhandle);
delete options;
}
void Java_org_rocksdb_YourAPIOptions_setSomeBooleanOption(
JNIEnv*, jclass, jlong jhandle, jboolean value) {
auto* options = reinterpret_cast<ROCKSDB_NAMESPACE::YourAPIOptions*>(jhandle);
options->some_boolean_option = static_cast<bool>(value);
}
jboolean Java_org_rocksdb_YourAPIOptions_someBooleanOption(JNIEnv*, jclass, jlong jhandle) {
auto* options = reinterpret_cast<ROCKSDB_NAMESPACE::YourAPIOptions*>(jhandle);
return static_cast<jboolean>(options->some_boolean_option);
}
\`\`\`
### Step 7: Update Build Files
**Java CMakeLists.txt:** `java/CMakeLists.txt`
Add your new Java source files:
\`\`\`cmake
src/main/java/org/rocksdb/YourAPIOptions.java
src/test/java/org/rocksdb/YourAPIOptionsTest.java
\`\`\`
### Step 8: Add Release Notes
**Directory:** `unreleased_history/`
RocksDB uses individual files in the `unreleased_history/` directory rather than directly editing `HISTORY.md`. This avoids merge conflicts and ensures changes are attributed to the correct release version.
Add a file to the appropriate subdirectory:
- `unreleased_history/new_features/` - For new functionality
- `unreleased_history/public_api_changes/` - For API changes
- `unreleased_history/behavior_changes/` - For behavior modifications
- `unreleased_history/bug_fixes/` - For bug fixes
**Example:** `unreleased_history/new_features/your_new_api.md`
\`\`\`markdown
Added `YourNewAPI()` to support [describe functionality]. See `YourAPIOptions` for configuration.
\`\`\`
**Example:** `unreleased_history/public_api_changes/your_api_options.md`
**Note:** Files should contain one line of markdown. The "* " prefix is automatically added if not included. These files are compiled into `HISTORY.md` during the release process.
### Step 9: Add Tests
**C++ Unit Tests:** `db/db_your_api_test.cc` or add to existing test file
\`\`\`cpp
TEST_F(DBTest, YourNewAPIBasic) {
Options options = CurrentOptions();
CreateAndReopenWithCF({"pikachu"}, options);
// Setup test data
ASSERT_OK(Put(1, "key1", "value1"));
ASSERT_OK(Put(1, "key2", "value2"));
// Test your API
YourAPIOptions api_options;
api_options.some_boolean_option = true;
ASSERT_OK(db_->YourNewAPI(api_options, handles_[1], nullptr, nullptr));
// Verify results
// ...
}
\`\`\`
**Java Tests:** `java/src/test/java/org/rocksdb/YourAPIOptionsTest.java`
\`\`\`java
public class YourAPIOptionsTest {
@Test
public void yourAPIOptions() {
try (final YourAPIOptions options = new YourAPIOptions()) {
assertFalse(options.someBooleanOption());
options.setSomeBooleanOption(true);
assertTrue(options.someBooleanOption());
}
}
}
\`\`\`
## File Summary Checklist
| Component | File(s) | Required |
|-----------|---------|----------|
| C++ Public Interface | `include/rocksdb/db.h` | ✓ |
| Options Struct | `include/rocksdb/options.h` | If needed |
| DBImpl Declaration | `db/db_impl/db_impl.h` | ✓ |
| DBImpl Implementation | `db/db_impl/db_impl_*.cc` | ✓ |
| StackableDB | `include/rocksdb/utilities/stackable_db.h` | ✓ |
| Secondary DB | `db/db_impl/db_impl_secondary.h` | If not supported |
| Compacted DB | `db/db_impl/compacted_db_impl.h` | If not supported |
| C API Header | `include/rocksdb/c.h` | ✓ |
| C API Implementation | `db/c.cc` | ✓ |
| Java API | `java/src/main/java/org/rocksdb/RocksDB.java` | ✓ |
| Java Options | `java/src/main/java/org/rocksdb/YourAPIOptions.java` | If needed |
| JNI Implementation | `java/rocksjni/rocksjni.cc` | ✓ |
| JNI Options | `java/rocksjni/your_api_options.cc` | If needed |
| Java CMake | `java/CMakeLists.txt` | If new files |
| Changelog | `unreleased_history/*.md` | ✓ |
| C++ Tests | `db/db_*_test.cc` | ✓ |
| Java Tests | `java/src/test/java/org/rocksdb/*Test.java` | ✓ |
## Best Practices
1. **Error Handling**: Always return `Status` objects in C++, throw exceptions in Java
2. **Default Values**: Provide sensible defaults for all options
3. **Documentation**: Add clear comments for all public methods and options
4. **Column Family Support**: Always support column family operations
5. **Thread Safety**: Document thread-safety guarantees
6. **Backward Compatibility**: Avoid breaking existing API contracts
7. **Testing**: Add comprehensive unit tests for all code paths
-61
View File
@@ -1,61 +0,0 @@
# PR Complexity Classifier — CI Triage Prompt
## Purpose
Quickly classify a RocksDB pull request as **simple** or **complex** so the
downstream reviewer can pick an appropriate thinking budget.
## Output Contract (STRICT)
Your final response MUST be exactly one of these two lowercase tokens, on a
line by itself, with no other text:
```
simple
```
or
```
complex
```
If you are unsure, output `complex`. The downstream review will use this token
to set its thinking budget — when in doubt, prefer the more thorough budget.
## Classification Heuristics
Treat a PR as **simple** when ALL of the following hold:
- Diff touches < ~200 lines of non-test code
- No changes to public headers under `include/rocksdb/`
- No changes to on-disk format (SST block layout, WAL record layout,
manifest, options file format, version edit encoding)
- No changes to compaction picker, write-path locking, recovery, or
WAL/memtable lifecycle
- No new public option, no new statistics counter, no new RPC/serialized
message
- Tests-only or docs-only changes
- Trivial refactors (rename, comment, dead-code removal, lint fixes)
- One-line bug fix with obvious root cause and a focused regression test
Treat a PR as **complex** when ANY of the following hold:
- Touches files in `db/`, `db/compaction/`, `db/blob/`, `cache/`,
`table/block_based/`, `file/`, `env/io_*` with > ~50 lines of logic change
- Modifies thread-safety/synchronization (mutexes, atomics, memory ordering,
refcounting, condvars)
- Changes serialization or on-disk format
- Changes public API surface in `include/rocksdb/`
- Adds a new feature flag, option, or compaction style
- Touches transactions (`utilities/transactions/`), backup/checkpoint,
user-defined timestamps, or remote compaction
- Cross-cutting refactor spanning > 5 directories
## How To Decide
1. Read the diff summary and changed-file list provided below.
2. Skim the diff hunks — focus on which subsystems are touched and whether
the change is mechanical vs. semantic.
3. Apply the heuristics above. Default to `complex` if the rules conflict.
4. Output the single token. No preamble. No explanation.
## Tool Use
You may use `View`, `GlobTool`, and `GrepTool` to peek at one or two files
if the diff snippet alone is ambiguous, but keep this fast — this is a
triage step, not a review. Do NOT spawn sub-agents.
-3
View File
@@ -1,3 +0,0 @@
You are an expert C++ engineer for RocksDB.
Read CLAUDE.md in the repo root and claude_md/ files for project context.
Answer thoroughly with exact file paths and line numbers.
-54
View File
@@ -1,54 +0,0 @@
Read review-findings.md. This file contains partial code review findings
from a session that ran out of turns before finishing. Reformat them into
a clean final review comment.
## Required Output Structure
Use this exact structure so the PR page stays scrollable:
```markdown
## Summary
> **Partial review** — the analysis was interrupted before all perspectives
> were completed. Findings below cover only the perspectives that finished.
> You can request a fresh full review with `/claude-review`.
<!-- One or two sentences of overall assessment. -->
**High-severity findings (N):**
- **[file.cc:123]** One-line description. <!-- repeat per HIGH finding -->
<!-- If no HIGH findings were salvaged, write: -->
<!-- _No high-severity findings recovered._ -->
<details>
<summary>Recovered findings (click to expand)</summary>
### Findings
#### :red_circle: HIGH
... (H1, H2, ...)
#### :yellow_circle: MEDIUM
... (M1, M2, ...)
#### :green_circle: LOW / NIT
... (L1, L2, ...)
### Cross-Component Analysis
<!-- Whatever cross-component results were captured. -->
### Positive Observations
<!-- Optional. -->
</details>
```
## Rules
- The `## Summary`, the partial-review notice, and the HIGH bullet list MUST
stay outside the `<details>` block.
- All per-finding detail and analysis MUST live inside the `<details>` block.
- Do NOT nest `<details>` blocks.
- Do NOT add any text after the closing `</details>` — the comment-builder
appends its own footer.
- Output ONLY the formatted review. No commentary, no explanation.
-661
View File
@@ -1,661 +0,0 @@
# Code Review Workflow — CI Prompt
## Purpose
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.
## Workflow Steps
### 1. Setup Phase
- Read CLAUDE.md to understand review guidelines
- Identify the commit/diff/PR to review
- The PR diff is provided below in this prompt. The repository is already
checked out — use local file search tools (Grep, Glob, Read) to explore.
- Parse the changed files and categorize them (API, core logic, tests, etc.)
- Read all changed files to build full context
### 2. Codebase Context Phase (CRITICAL — Do Not Skip)
**Why this matters**: Review agents that only see the diff miss systemic bugs.
For example, a change to an iterator's return value might look correct in
isolation but break multi-SST iteration 5 layers up the call stack. A change
to error handling might silently drop errors that a caller 3 levels up relies
on for recovery. This phase builds the context that prevents those blind spots.
Go deep. Surface-level reading leads to reviews that miss caching layers,
existing helper functions, or concurrency requirements. The most expensive
failure mode is findings that look correct in isolation but miss how the
change breaks the surrounding system.
The team lead spawns one or more research agents to perform the following
analyses using local file search tools (Grep, Glob, Read). The research
must be written to `context.md` BEFORE any review agent is spawned.
#### 2a. Subsystem Deep-Read
Read the changed files AND their surrounding subsystem **in depth** — not just
signatures, but actual logic, edge cases, data flows, locking protocols,
concurrency patterns, and interactions between components.
For each changed file, also read:
- The header that declares its interface (if the change is in a .cc file)
- The sibling implementation that does the same thing the "standard" way
(e.g., for a UDI iterator change, read how `IndexBlockIter` handles the
same scenario — it's the reference implementation)
- The wrapper/adapter layer that sits between this code and its callers
- Any existing tests to understand what behaviors are currently guaranteed
#### 2b. Caller-Chain Analysis (upstream impact)
For each changed function/method, trace the call chain UPWARD 3-5 levels to
understand who consumes the changed behavior and what invariants they rely on.
**Focus on control flow decisions** — if/else branches, while loop conditions,
early returns — that depend on the changed return values or state.
**How to do this**:
- Search for callers of each changed function (search for the function name, the
class method, virtual overrides)
- For each caller, search for ITS callers, repeating 3-5 levels up
- At each level, read the actual code to understand the control flow decision
- Stop when you reach a "terminal" caller (e.g., user-facing API, top-level
iterator) or when the propagation is clearly safe
#### 2c. Callee-Chain Analysis (downstream dependencies AND side effects)
For each changed function, trace what it calls and whether those calls have
new preconditions or changed semantics. **Critically, trace SIDE EFFECTS —
not just return values.** Many bugs hide in side effects on shared state
(counters, sequence numbers, flags) that are invisible if you only check
return values.
**For each callee, ask:**
1. What does it RETURN? (the obvious check)
2. What shared state does it MUTATE? (the non-obvious check)
3. Under what PRECONDITIONS does it operate correctly?
4. Are those preconditions met in the new calling context?
#### 2d. Sibling Implementation Comparison
Identify the "reference" or "standard" implementation that does the same thing
the changed code does, and compare their behavior. This is critical for plugin/
extension APIs where the implementation must match an implicit contract.
#### 2e. Invariant Documentation
Document the key invariants that the changed code must maintain. Read existing
comments, assertions, and test expectations to discover these invariants.
#### 2f. Related Functionality and Existing Conventions
Identify related functionality that already exists in the codebase:
- Are there existing helper functions the change should use (or is duplicating)?
- Are there existing patterns for the same kind of change (e.g., how other
Customizable subclasses handle deprecation)?
- Are there existing tests that test similar scenarios (e.g., multi-SST
iteration tests that could be extended)?
#### 2g. Cross-Component Data Consumer Analysis (CRITICAL — common blind spot)
**Why this is separate from caller-chain analysis:** Sections 2b-2c trace
who CALLS the changed functions. This section traces who CONSUMES the data
that the changed code PRODUCES. These are often completely different
subsystems.
When the change writes data to a shared structure (memtable, SST block, cache
entry, statistics counter), ask: **"Who reads this data, under what rules,
and do those rules match the writer's assumptions?"**
**How to do this:**
1. Identify every piece of data the change WRITES (e.g., a range tombstone
entry, a counter update, a metadata field)
2. For each, search the codebase for ALL readers of that data structure
3. For each reader, check:
- Does the reader apply visibility rules (seqno, read_callback, snapshot)?
- Are those rules compatible with the writer's assumptions?
- Does the reader assume invariants about the data (e.g., seqno ordering,
monotonicity) that the writer might violate?
#### 2h. Alternative Execution Contexts
The same code path may run in different contexts with different assumptions.
**Enumerate all contexts and check each one.** This is where "works in the
common case but breaks in edge configurations" bugs hide.
For RocksDB, always check whether the changed code interacts differently with:
| Context | Key difference | Common failure mode |
|---------|---------------|-------------------|
| WritePreparedTxnDB / WriteUnpreparedTxnDB | `read_callback_` controls visibility, not just seqno | Visibility bypass |
| ReadOnly DB / SecondaryInstance | No mutable memtable, writes not allowed | Null pointer or no-op needed |
| CompactionService / Remote compaction | Different process, serialized state | Serialization mismatch |
| User-defined timestamps | Extra dimension in key comparison and visibility | Wrong ordering |
| MemPurge | Memtable-to-memtable, not memtable-to-SST | Missing data |
| Column family with BlobDB | Values may be in blob files, not inline | Missing dereference |
| Snapshots (old, held long-term) | Snapshot seqno may be far behind current state | Metadata corruption |
| Concurrent writers (allow_concurrent_memtable_write) | Lock-free paths vs locked paths | Lost updates |
| FIFO / Universal compaction | Different compaction invariants than Level | Wrong assumptions |
| Prefix seek / total order seek | Different iterator behavior | Wrong iteration results |
**For each context, ask:**
1. Does the code execute in this context? (Check constructor, feature flags)
2. If yes, do the assumptions still hold?
3. If not, should the feature be disabled in this context?
#### 2i. Assumption Stress-Testing (the "logically X" audit)
When the change claims a property (e.g., "logically redundant," "no-op in
this case," "safe because X"), **systematically break it:**
1. **State the claim precisely.** E.g., "The inserted range tombstone is
logically redundant — it covers only keys already deleted by point
tombstones."
2. **List the preconditions** that must be true for the claim to hold.
3. **For each precondition, construct a counterexample** — a concrete scenario
where the precondition is violated.
4. **Verify each counterexample against the code.** Does the code guard
against it? If not, it's a bug.
**Anti-pattern: treating asserts as proofs.** When you see an assert, do NOT
conclude "the invariant holds." Instead ask: "Can I construct an input where
this assert fires?" If yes, it's a bug (the assert catches it in debug but
release builds silently corrupt). If you cannot construct a counterexample
after trying, document WHY it's impossible.
#### 2j. Write Context Document
Write the complete analysis to `context.md` in the review folder. This document
is provided to ALL review agents as part of their prompt.
**Context document template**:
```markdown
# Codebase Context for Review
## How the Relevant Subsystem Works Today
[Detailed description of the subsystem architecture, data flows,
and component interactions. Not just "what" but "how" and "why."]
## Changed Functions and Their Call Chains
### function_name() (file:line)
**What changed**: [brief description of the behavioral change]
**Upstream callers** (who depends on this):
1. CallerA::method() (file:line) — uses return value to decide X
2. CallerB::method() (file:line) — passes result to CallerC
3. CallerC::method() (file:line) — THE CRITICAL DECISION POINT: ...
**Downstream callees** (what this depends on):
1. calleeA() — behavior unchanged
2. calleeB() — NEW dependency, requires X
**Sibling implementation** (how the "standard" version handles this):
- StandardImpl::method() does Y at file:line, which ensures invariant Z
- The changed code must match or document any deviation
**Key invariants**:
- Must never return X when Y is true because CallerC will...
- Must always set Z before returning because CallerB assumes...
## System Architecture Context
[How the changed components fit into the overall system]
## Known Invariants That Must Be Preserved
1. ...
2. ...
## Existing Conventions and Related Code
- [helper functions, patterns, existing tests that are relevant]
## Cross-Component Data Consumers
For each piece of data the change WRITES to a shared structure:
### [data item] written to [structure]
**Writer assumptions**: [what the writer assumes about how this data is consumed]
**All readers**:
1. ReaderA::method() (file:line) — reads under [visibility rules]
- Compatible with writer? YES/NO. If NO, explain the mismatch.
2. ReaderB::method() (file:line) — reads under [different rules]
- Compatible with writer? YES/NO.
## Alternative Execution Contexts
| Context | Does code execute? | Assumptions hold? | Action needed? |
|---------|-------------------|-------------------|----------------|
| WritePreparedTxnDB | YES/NO | YES/NO | [disable/guard/safe] |
| Old snapshots | YES/NO | YES/NO | ... |
| User-defined timestamps | YES/NO | YES/NO | ... |
| ReadOnly DB | YES/NO | YES/NO | ... |
| ... | ... | ... | ... |
## Assumption Stress Test
### Claim: "[the design claim, e.g., logically redundant]"
**Preconditions for claim to hold:**
1. [precondition] — counterexample: [scenario]. Guarded? YES/NO.
2. [precondition] — counterexample: [scenario]. Guarded? YES/NO.
## Potential Pitfalls
- [specific scenarios where the change could interact badly with
the surrounding system, identified from the caller-chain analysis]
```
### 3. Initial Review Phase (Parallel)
Create a team and spawn 5 review agents in parallel. **Include the context
document from Phase 2 in each agent's prompt.** Each agent writes findings
to its own file and sends a summary message to the team lead.
Each agent's prompt should include:
```
## Codebase Context (READ THIS FIRST)
[paste or reference the context.md file]
You MUST consider how your findings interact with the upstream callers
and system invariants documented above. A finding that looks correct
in isolation may be a critical bug when you consider the full call chain.
```
#### Agent: Design & Approach Reviewer
- Read the commit message deeply to understand the problem being solved
- Analyze whether this is the right approach to the problem
- Consider alternative designs and trade-offs:
- Are there simpler solutions that achieve the same goal?
- Does the approach introduce unnecessary complexity?
- Would a different data structure or algorithm be more appropriate?
- Evaluate whether the change follows existing architectural patterns or
introduces new patterns that may be inconsistent
- Assess the scope: is the change too large? Should it be split into
smaller, independently reviewable pieces?
- Check if the approach has precedent in the codebase or in the academic
literature it references (e.g., SuRF paper)
#### Agent: Correctness Reviewer
- Thread safety and concurrency analysis
- Error handling and propagation via Status type
- Edge cases (empty inputs, overflow, boundary conditions)
- Data corruption scenarios
- Logic correctness of new algorithms
- **Behavioral contract changes**: Do return value semantics match what
upstream callers expect? (Use the caller-chain analysis from context.md)
- **Callee side effects**: Does the change call functions that mutate shared
state (counters, seqnos, metadata)? Are the mutations correct for the new
calling context? (Use the callee-chain analysis from context.md)
#### Agent: Cross-Component & Adversarial Reviewer
This agent exists because the most critical bugs hide at component boundaries,
not within a single component. It uses a fundamentally different methodology
than the correctness reviewer: instead of verifying "does the code do what it
intends?", it asks "what breaks when we change the assumptions?"
**Data-flow analysis:** Perform the cross-component data consumer analysis
described in section 2g. For every piece of data the change WRITES to a shared
structure, trace ALL READERS and verify their visibility rules are compatible
with the writer's assumptions. This is a DATA-FLOW question, not a
CONTROL-FLOW question — readers may be in completely different subsystems.
**Alternative execution contexts:** Use the canonical table from section 2h.
Enumerate all contexts where the changed code executes and verify assumptions
hold in each.
**Assumption stress-testing and assert-breaking:** Follow the methodology from
section 2i. Identify every design claim, enumerate preconditions, construct
counterexamples. Treat asserts as hypotheses to break, not proofs.
**Red flag words**: "logically redundant", "safe because", "no-op",
"always true", "cannot happen", "invariant holds"
**Callee side-effect audit:** Perform the callee side-effect audit described
in section 2c. For every callee, ask what it RETURNS and what it MUTATES.
Write findings to `findings-cross-component.md`.
#### Agent: Invariant Adversary
This agent does NOT read the diff in detail. It takes the feature summary and
systematically tries to break it. While other agents verify "does the code do
what it says?", this agent asks "what existing system invariants does this
feature violate?" and "under what inputs do the design claims fail?"
**Steps 1-4: Assumption stress-testing.** Follow the methodology from section
2i: extract design claims, enumerate preconditions, construct counterexamples,
and attempt to break every assert. Be exhaustive about input parameter ranges,
shared state configurations, and concurrent interleaving.
**Step 5: Callee side-effect audit.** Perform the callee side-effect audit
described in section 2c. For every function the changed code calls, list ALL
mutations to shared state (atomic CAS loops, counter increments, flag/metadata
updates, cache invalidation). A function returning `Status::OK()` does NOT
mean its side effects are correct.
Write findings to `findings-invariant-adversary.md`.
#### Agent: Caller-Context Auditor
This agent traces BACKWARD from every entry point the changed code hooks into.
While other agents read the changed code forward ("what does it do?"), this
agent enumerates WHO invokes it and WITH WHAT parameter ranges.
The key insight: a function that is correct for all *typical* inputs may be
catastrophically wrong for *atypical-but-reachable* inputs. This agent's job
is to find the atypical inputs.
**Step 1: Identify entry points.** List every function/constructor/method in
the changed code that receives external input (parameters, config values,
pointers to shared state). Include:
- Constructor parameters (e.g., `active_mem`, `read_callback`, `sequence`)
- Virtual method overrides that callers invoke polymorphically
- Functions called from multiple subsystems
**Step 2: Enumerate all callers (3-5 levels up).** For each entry point,
search the codebase for ALL callers. Do NOT stop at the first caller — trace
the full call chain. Use `Grep` and `Glob` to find:
- Direct callers of the function
- Callers of wrapper/adapter functions that forward to it
- Factory functions that construct the object
- Virtual dispatch sites (search for the base class method name)
**Step 3: Parameter range analysis.** For each caller, determine:
- What values does it pass for each parameter?
- Under what conditions does it call this function?
- Are there callers that pass unusual values? (e.g., `kMaxSequenceNumber`,
`nullptr`, old snapshot seqnos, non-null `read_callback_`)
Build a parameter range table for each entry point, listing all callers
and the values they pass for each parameter.
**Step 4: Configuration matrix.** Enumerate option/config combinations that
affect the changed code path:
- Which options enable/disable the feature?
- Which options change the execution context? (e.g.,
`allow_concurrent_memtable_write`, `use_trie_index`)
- Are there option combinations that create contradictions?
**Step 5: Cross-reference with context.md.** Compare your caller analysis
with the invariants and execution contexts documented in context.md. Flag
any caller that violates an assumed precondition.
Write findings to `findings-caller-audit.md`.
#### Agent: Performance Reviewer
- Memory allocation on hot paths
- Unnecessary copies (string, buffer)
- Cache efficiency and data locality
- Loop optimization opportunities
- Branch prediction (LIKELY/UNLIKELY)
- Zero-overhead design verification
#### Agent: API & Compatibility Reviewer
- Public API backwards compatibility
- API consistency with existing patterns
- Documentation completeness
- Deprecation policy compliance
- Forward compatibility (struct extensibility)
- **Behavioral compatibility**: Do changed semantics (e.g., kOutOfBound vs
kUnknown) break implicit contracts with callers?
#### Agent: Serialization & Deserialization Reviewer
- Format correctness and versioning
- Alignment requirements
- Integer overflow in size computations
- Bounds checking on untrusted data
- Crafted input attack vectors
#### Agent: Test Coverage Reviewer
- Edge case coverage
- Failure mode testing (corruption, truncation)
- Round-trip testing (build → serialize → deserialize → verify)
- Integration test coverage
- **System-level test coverage**: Are the caller-chain interactions tested?
(e.g., multi-SST iteration with UDI, level compaction with new behavior)
- Test quality (no flaky patterns, good assertions)
### 4. Round-Robin Debate Phase
After all 5 agents complete their initial review, the team lead orchestrates
a structured debate:
#### Step 4a: Share findings
- Team lead sends each agent a summary of ALL other agents' findings
(or instructs each agent to read the other agents' findings files)
#### Step 4b: Critique round (2 rounds)
Each agent reviews the findings from the other agents and sends messages to
challenge, support, or refine them:
- **Challenge**: "I disagree with [agent] [finding] because [evidence]."
- **Support**: "I independently found the same issue — this confirms it."
- **Refine**: "[Finding A] is actually a consequence of [Finding B].
The root cause is the same."
The debate assignment follows a round-robin pattern:
```
correctness-reviewer → critiques → invariant-adversary, serialization
cross-component-reviewer → critiques → correctness, caller-audit
invariant-adversary → critiques → correctness, cross-component
caller-audit → critiques → invariant-adversary, cross-component
performance-reviewer → critiques → api, cross-component
api-reviewer → critiques → correctness, performance
serialization-reviewer → critiques → correctness, invariant-adversary
test-reviewer → critiques → serialization, caller-audit
design-reviewer → critiques → cross-component, invariant-adversary
```
**Note:** The invariant-adversary and caller-audit agents are deliberately
cross-linked with correctness and cross-component because their findings
often reveal the same underlying bug from different angles (invariant
violation vs reachable bad input vs data-flow mismatch).
Each agent should:
1. Read the assigned agents' findings files
2. Send a critique message to each assigned agent
3. Respond to critiques received from other agents
4. Update their own findings file with any revisions (upgraded/downgraded severity,
withdrawn findings, new findings inspired by others)
#### Step 4c: Team lead collects debate results
- Read all updated findings files
- Review the debate messages
- Build consensus: findings supported by 2+ agents → HIGH confidence
- Write consensus document with final severity classifications
### 5. Consensus Phase
Team lead synthesizes the debate into a consensus document:
- Cross-reference overlapping findings across agents
- Count agreement (majority = 2+ agents independently flagging or supporting)
- Note disagreements and how they were resolved
- Classify findings as HIGH/MEDIUM/LOW severity
- Write consensus document
### 6. Summary Phase
- Compile all validated findings ordered by severity
- 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
**Final report quality rules:**
- The final report must be CLEAN and POLISHED. No stream-of-consciousness,
no "Wait, actually...", no "on closer re-examination". Those belong in
the working notes, not the output.
- If a finding was raised during review but later disproven during debate
or deeper analysis, REMOVE IT entirely from the final report. Do not
include it with a retraction — just drop it.
- If a finding was downgraded (e.g., Critical → Suggestion), present it
at the final severity only. Do not narrate the severity change.
- Each finding in the final report should read as a confident, verified
conclusion — not a record of the analysis process.
- The reader should never see the reviewer arguing with itself.
**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.
```markdown
## Summary
<!-- One or two sentences of overall assessment. -->
**High-severity findings (N):**
- **[file.cc:123]** One-line description of the issue. <!-- repeat per HIGH finding -->
<!-- If there are NO high-severity findings, write exactly: -->
<!-- _No high-severity findings._ -->
<details>
<summary>Full review (click to expand)</summary>
### Findings
#### :red_circle: HIGH
##### H1. <Title> — `file.cc:123`
- **Issue:** ...
- **Root cause:** ...
- **Suggested fix:** ...
#### :yellow_circle: MEDIUM
... (same structure: M1, M2, ...)
#### :green_circle: LOW / NIT
... (same structure: L1, L2, ...)
### Cross-Component Analysis
<!-- Execution-context table and assumption stress-test results. -->
### Positive Observations
<!-- Optional: good patterns, clever optimizations. -->
</details>
```
Rules for this structure:
- The top-level `## Summary` and the bullet list of HIGH findings MUST stay
outside the `<details>` block — they are always visible.
- Every detail (per-finding root cause, fix, debate outcomes, cross-component
analysis, positive observations) MUST live inside the `<details>` block.
- Do NOT nest a `<details>` inside another `<details>`.
- Do NOT add any text after the closing `</details>` — the comment-builder
appends its own footer.
- If the review is partial (recovery path), still produce the summary block
first, and put whatever was salvaged inside the `<details>` block.
## Review Checklist
### Context Phase (must be completed before agents spawn)
- [ ] Subsystem deep-read — read changed files AND surrounding subsystem
in depth (logic, edge cases, data flows, locking, concurrency)
- [ ] Caller chain for every changed public/virtual method (3-5 levels up)
- [ ] Critical decision points where callers branch on changed behavior
- [ ] Callee chain with SIDE EFFECTS — downstream dependencies, new
preconditions, AND mutations to shared state (section 2c)
- [ ] Sibling implementations — how does the standard version handle same scenarios?
- [ ] Invariants that callers rely on
- [ ] Related functionality — existing helpers, patterns, conventions
- [ ] Cross-component data consumers — for every data WRITTEN, find ALL readers
and verify visibility rules match (section 2g)
- [ ] Alternative execution contexts verified (section 2h table)
- [ ] Assumption stress-test — for every design claim, list preconditions
and construct counterexamples (section 2i)
- [ ] Multi-component interactions (compaction, recovery, snapshots, iterators)
- [ ] Configuration dependencies and unexpected option combinations
- [ ] Potential pitfalls from caller-chain analysis
### Review Phase (verified by agents)
- [ ] Database semantics preserved (snapshot isolation, key ordering)
- [ ] All error cases handled
- [ ] Thread-safe with correct synchronization
- [ ] No data races or deadlocks
- [ ] Appropriate test coverage (edge cases, failure modes, system-level)
- [ ] No unnecessary allocations or copies in hot paths
- [ ] Backwards compatible
- [ ] New APIs consistent with existing patterns and documented
- [ ] Code follows RocksDB style conventions
- [ ] Behavioral contracts with upstream callers preserved
- [ ] 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
- `context.md` — Codebase context (call chains, invariants)
- `findings-*.md` — Per-agent findings (design, correctness, cross-component,
invariant-adversary, caller-audit, performance, api, serialization, tests)
- `consensus.md` — Cross-review consensus (post-debate)
## Team Structure
```
Team Lead (you)
│
├── Phase 2: Codebase Context (team lead or dedicated research agent)
│ └── context-researcher (general-purpose agent)
│ ├── Trace caller chains (3-5 levels up)
│ ├── Trace callee chains (dependencies AND side effects)
│ ├── Trace data consumers (who reads what the change writes?)
│ └── Document invariants
│
├── Phase 3: Initial Review (parallel, run_in_background)
│ │ (all agents receive context.md in their prompt)
│ ├── design-reviewer (general-purpose agent)
│ ├── correctness-reviewer (general-purpose agent)
│ ├── cross-component-reviewer (general-purpose agent)
│ ├── invariant-adversary (general-purpose agent) ← NEW
│ ├── caller-audit (general-purpose agent) ← NEW
│ ├── performance-reviewer (general-purpose agent)
│ ├── api-reviewer (general-purpose agent)
│ ├── serialization-reviewer (general-purpose agent)
│ └── test-reviewer (general-purpose agent)
│
├── Phase 4: Debate (agents message each other)
│ ├── correctness ↔ invariant-adversary, serialization
│ ├── cross-component ↔ correctness, caller-audit
│ ├── invariant-adversary ↔ correctness, cross-component
│ ├── caller-audit ↔ invariant-adversary, cross-component
│ ├── performance ↔ api, cross-component
│ ├── api ↔ correctness, performance
│ ├── serialization ↔ correctness, invariant-adversary
│ ├── test-coverage ↔ serialization, caller-audit
│ └── design ↔ cross-component, invariant-adversary
│
```
## Agent Communication Protocol
### During Initial Review (Phase 3)
- Each agent writes findings to its own file
- Each agent sends a summary message to team lead when done
- Agents do NOT communicate with each other yet
### During Debate (Phase 4)
- Team lead sends each agent a message with instructions to:
1. Read the assigned agents' findings files
2. Send critique messages to those agents
3. Respond to incoming critiques
4. Update their own findings file with revisions
- Each critique message should include:
- Which finding they're addressing (e.g., "Correctness F1")
- Whether they AGREE, DISAGREE, or want to REFINE
- Their reasoning with code evidence
- Suggested severity adjustment (if any)
### Message format example
```
To: correctness-reviewer
Re: Your Finding F1
AGREE/DISAGREE/REFINE - [reasoning with code evidence].
[Suggested severity adjustment if any.]
```
## Review Anti-Patterns
These recurring failure modes lead to missed bugs. Each is detailed in the
referenced section; this table is a quick-reference checklist.
| Anti-Pattern | Fix | Reference |
|---|---|---|
| Return-Value Tunnel Vision | Trace callee MUTATIONS, not just returns | Section 2c |
| Default-Configuration Bias | Enumerate all execution contexts | Section 2h |
| Assert-as-Proof | Try to BREAK every assert | Section 2i |
| Write-Path-Only Analysis | Trace data readers, not just writers | Section 2g |
| Confirmation-Seeking Research | Use adversarial prompts ("find where X fails") | Invariant Adversary agent |
| Data-Flow vs Control-Flow Confusion | Separate who CALLS from who READS the data | Section 2g |
-104
View File
@@ -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.
-7
View File
@@ -1,7 +0,0 @@
# Removing Deprecated Options from RocksDB
### Keep in these files:
- [ ] **KEEP** entry in type_info (`options/cf_options.cc` or `options/db_options.cc`) with `OptionVerificationType::kDeprecated` for loading old option files
- [ ] **KEEP** or add test in `options/options_test.cc` `OptionsOldApiTest::GetOptionsFromMapTest` for loading old option files
### Documentation:
- [ ] Add release note to `HISTORY.md` and `unreleased_history/`
-7
View File
@@ -7,13 +7,6 @@ DB_STRESS_CMD?=./db_stress
include common.mk
ifdef COMPILE_WITH_TSAN
# Keep direct `make -f crash_test.mk COMPILE_WITH_TSAN=1 ...` runs
# aligned with the main Makefile's TSAN runtime options.
TSAN_OPTIONS?=suppressions=$(CURDIR)/tools/tsan_suppressions.txt
export TSAN_OPTIONS
endif
CRASHTEST_MAKE=$(MAKE) -f crash_test.mk
CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --cleanup_cmd='$(DB_CLEANUP_CMD)' --destroy_db_initially=1
+7 -18
View File
@@ -5,8 +5,6 @@
#include "db/blob/blob_fetcher.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_index.h"
#include "db/version_set.h"
namespace ROCKSDB_NAMESPACE {
@@ -16,13 +14,10 @@ Status BlobFetcher::FetchBlob(const Slice& user_key,
FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value,
uint64_t* bytes_read) const {
BlobIndex blob_index;
Status status = blob_index.DecodeFrom(blob_index_slice);
if (status.ok()) {
status = FetchBlob(user_key, blob_index, prefetch_buffer, blob_value,
bytes_read);
}
return status;
assert(version_);
return version_->GetBlob(read_options_, user_key, blob_index_slice,
prefetch_buffer, blob_value, bytes_read);
}
Status BlobFetcher::FetchBlob(const Slice& user_key,
@@ -30,16 +25,10 @@ Status BlobFetcher::FetchBlob(const Slice& user_key,
FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value,
uint64_t* bytes_read) const {
if (!allow_write_path_fallback_) {
assert(version_);
assert(version_);
return version_->GetBlob(read_options_, user_key, blob_index,
prefetch_buffer, blob_value, bytes_read);
}
return BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
read_options_, user_key, blob_index, version_, blob_file_cache_,
prefetch_buffer, blob_value, bytes_read);
return version_->GetBlob(read_options_, user_key, blob_index, prefetch_buffer,
blob_value, bytes_read);
}
} // namespace ROCKSDB_NAMESPACE
+3 -13
View File
@@ -10,25 +10,17 @@
namespace ROCKSDB_NAMESPACE {
class BlobFileCache;
class Version;
class Slice;
class FilePrefetchBuffer;
class PinnableSlice;
class BlobIndex;
// A thin wrapper around blob retrieval. By default it reads through Version,
// and it can optionally fall back to direct-write blob files that are not yet
// manifest-visible.
// A thin wrapper around the blob retrieval functionality of Version.
class BlobFetcher {
public:
BlobFetcher(const Version* version, const ReadOptions& read_options,
BlobFileCache* blob_file_cache = nullptr,
bool allow_write_path_fallback = false)
: version_(version),
read_options_(read_options),
blob_file_cache_(blob_file_cache),
allow_write_path_fallback_(allow_write_path_fallback) {}
BlobFetcher(const Version* version, const ReadOptions& read_options)
: version_(version), read_options_(read_options) {}
Status FetchBlob(const Slice& user_key, const Slice& blob_index_slice,
FilePrefetchBuffer* prefetch_buffer,
@@ -41,7 +33,5 @@ class BlobFetcher {
private:
const Version* version_;
ReadOptions read_options_;
BlobFileCache* blob_file_cache_;
bool allow_write_path_fallback_;
};
} // namespace ROCKSDB_NAMESPACE
+4 -2
View File
@@ -67,11 +67,13 @@ BlobFileBuilder::BlobFileBuilder(
min_blob_size_(mutable_cf_options->min_blob_size),
blob_file_size_(mutable_cf_options->blob_file_size),
blob_compression_type_(mutable_cf_options->blob_compression_type),
// TODO: support most CompressionOptions with a new CF option
// blob_compression_opts
// TODO with schema change: support custom compression manager and options
// such as max_compressed_bytes_per_kb
// NOTE: returns nullptr for kNoCompression
blob_compressor_(GetBuiltinV2CompressionManager()->GetCompressor(
mutable_cf_options->blob_compression_opts, blob_compression_type_)),
CompressionOptions{}, blob_compression_type_)),
blob_compressor_wa_(blob_compressor_
? blob_compressor_->ObtainWorkingArea()
: Compressor::ManagedWorkingArea{}),
@@ -109,7 +111,7 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
assert(blob_index);
assert(blob_index->empty());
if (value.empty() || value.size() < min_blob_size_) {
if (value.size() < min_blob_size_) {
return Status::OK();
}
+10 -7
View File
@@ -403,19 +403,22 @@ TEST_F(BlobFileBuilderTest, Compression) {
ASSERT_EQ(blob_file_addition.GetBlobFileNumber(), blob_file_number);
ASSERT_EQ(blob_file_addition.GetTotalBlobCount(), 1);
auto compressor =
GetBuiltinV2CompressionManager()->GetCompressor({}, kSnappyCompression);
GrowableBuffer compressed_value;
ASSERT_OK(LegacyForceBuiltinCompression(*compressor, /*working_area=*/nullptr,
uncompressed_value,
&compressed_value));
CompressionOptions opts;
CompressionContext context(kSnappyCompression, opts);
CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
kSnappyCompression);
std::string compressed_value;
ASSERT_TRUE(Snappy_Compress(info, uncompressed_value.data(),
uncompressed_value.size(), &compressed_value));
ASSERT_EQ(blob_file_addition.GetTotalBlobBytes(),
BlobLogRecord::kHeaderSize + key_size + compressed_value.size());
// Verify the contents of the new blob file as well as the blob reference
std::vector<std::pair<std::string, std::string>> expected_key_value_pairs{
{key, compressed_value.AsSlice().ToString()}};
{key, compressed_value}};
std::vector<std::string> blob_indexes{blob_index};
VerifyBlobFile(blob_file_number, blob_file_path, column_family_id,
+3 -125
View File
@@ -9,7 +9,6 @@
#include <memory>
#include "db/blob/blob_file_reader.h"
#include "logging/logging.h"
#include "options/cf_options.h"
#include "rocksdb/cache.h"
#include "rocksdb/slice.h"
@@ -39,8 +38,7 @@ BlobFileCache::BlobFileCache(Cache* cache,
Status BlobFileCache::GetBlobFileReader(
const ReadOptions& read_options, uint64_t blob_file_number,
CacheHandleGuard<BlobFileReader>* blob_file_reader,
bool allow_footer_skip_retry) {
CacheHandleGuard<BlobFileReader>* blob_file_reader) {
assert(blob_file_reader);
assert(blob_file_reader->IsEmpty());
@@ -75,22 +73,10 @@ Status BlobFileCache::GetBlobFileReader(
{
assert(file_options_);
Status s = BlobFileReader::Create(
const Status s = BlobFileReader::Create(
*immutable_options_, read_options, *file_options_, column_family_id_,
blob_file_read_hist_, blob_file_number, io_tracer_,
/*skip_footer_validation=*/false, &reader);
if (!s.ok() && s.IsCorruption() && allow_footer_skip_retry) {
reader.reset();
s = BlobFileReader::Create(
*immutable_options_, read_options, *file_options_, column_family_id_,
blob_file_read_hist_, blob_file_number, io_tracer_,
/*skip_footer_validation=*/true, &reader);
}
blob_file_read_hist_, blob_file_number, io_tracer_, &reader);
if (!s.ok()) {
ROCKS_LOG_WARN(immutable_options_->logger,
"BlobFileCache open failed for blob file %" PRIu64
" in CF %u: %s",
blob_file_number, column_family_id_, s.ToString().c_str());
RecordTick(statistics, NO_FILE_ERRORS);
return s;
}
@@ -113,120 +99,12 @@ Status BlobFileCache::GetBlobFileReader(
return Status::OK();
}
Status BlobFileCache::OpenBlobFileReaderUncached(
const ReadOptions& read_options, uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
bool allow_footer_skip_retry) {
assert(blob_file_reader);
assert(!*blob_file_reader);
Statistics* const statistics = immutable_options_->stats;
RecordTick(statistics, NO_FILE_OPENS);
Status s = BlobFileReader::Create(
*immutable_options_, read_options, *file_options_, column_family_id_,
blob_file_read_hist_, blob_file_number, io_tracer_,
/*skip_footer_validation=*/false, blob_file_reader);
if (!s.ok() && s.IsCorruption() && allow_footer_skip_retry) {
blob_file_reader->reset();
s = BlobFileReader::Create(
*immutable_options_, read_options, *file_options_, column_family_id_,
blob_file_read_hist_, blob_file_number, io_tracer_,
/*skip_footer_validation=*/true, blob_file_reader);
}
if (!s.ok()) {
RecordTick(statistics, NO_FILE_ERRORS);
}
return s;
}
Status BlobFileCache::InsertBlobFileReader(
uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader) {
assert(blob_file_reader);
assert(*blob_file_reader);
assert(cached_blob_file_reader);
assert(cached_blob_file_reader->IsEmpty());
const Slice key = GetSliceForKey(&blob_file_number);
// Serialize refreshes for the same blob file. The cache API does not expose
// a conditional replace, so refresh is intentionally modeled as
// Lookup/optional Erase/Insert under this per-key mutex.
MutexLock lock(&mutex_.Get(key));
TypedHandle* handle = cache_.Lookup(key);
if (handle) {
*cached_blob_file_reader = cache_.Guard(handle);
blob_file_reader->reset();
return Status::OK();
}
constexpr size_t charge = 1;
Status s = cache_.Insert(key, blob_file_reader->get(), charge, &handle);
if (!s.ok()) {
RecordTick(immutable_options_->stats, NO_FILE_ERRORS);
return s;
}
// Ownership transferred to the cache.
[[maybe_unused]] BlobFileReader* released_reader =
blob_file_reader->release();
*cached_blob_file_reader = cache_.Guard(handle);
return Status::OK();
}
Status BlobFileCache::RefreshBlobFileReader(
uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader) {
assert(blob_file_reader);
assert(*blob_file_reader);
assert(cached_blob_file_reader);
assert(cached_blob_file_reader->IsEmpty());
const Slice key = GetSliceForKey(&blob_file_number);
MutexLock lock(&mutex_.Get(key));
TypedHandle* handle = cache_.Lookup(key);
if (handle) {
BlobFileReader* const cached_reader = cache_.Value(handle);
assert(cached_reader != nullptr);
// Active direct-write blob files can grow between refresh attempts. Keep
// whichever reader observed the larger on-disk size so an older refresh
// cannot overwrite a newer one that another thread already installed.
if (cached_reader->GetFileSize() >= (*blob_file_reader)->GetFileSize()) {
*cached_blob_file_reader = cache_.Guard(handle);
blob_file_reader->reset();
return Status::OK();
}
cache_.Release(handle);
cache_.get()->Erase(key);
}
constexpr size_t charge = 1;
Status s = cache_.Insert(key, blob_file_reader->get(), charge, &handle);
if (!s.ok()) {
RecordTick(immutable_options_->stats, NO_FILE_ERRORS);
return s;
}
// Ownership transferred to the cache.
[[maybe_unused]] BlobFileReader* released_reader =
blob_file_reader->release();
*cached_blob_file_reader = cache_.Guard(handle);
return Status::OK();
}
void BlobFileCache::Evict(uint64_t blob_file_number) {
// NOTE: sharing same Cache with table_cache
const Slice key = GetSliceForKey(&blob_file_number);
assert(cache_);
MutexLock lock(&mutex_.Get(key));
cache_.get()->Erase(key);
}
+1 -27
View File
@@ -32,35 +32,9 @@ class BlobFileCache {
BlobFileCache(const BlobFileCache&) = delete;
BlobFileCache& operator=(const BlobFileCache&) = delete;
// Returns a cached reader for `blob_file_number`, opening and caching it on
// miss. If `allow_footer_skip_retry` is true, a footer-validation corruption
// retries once without requiring a footer.
Status GetBlobFileReader(const ReadOptions& read_options,
uint64_t blob_file_number,
CacheHandleGuard<BlobFileReader>* blob_file_reader,
bool allow_footer_skip_retry = false);
// Opens a blob file reader without inserting it into the cache.
Status OpenBlobFileReaderUncached(
const ReadOptions& read_options, uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
bool allow_footer_skip_retry = false);
// Inserts a freshly opened uncached reader unless another thread already
// cached the same blob file.
Status InsertBlobFileReader(
uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader);
// Installs a refresh-opened reader into the cache. If another thread has
// already cached a reader opened on at least as large a file size, keep that
// reader instead so a racing refresh cannot reintroduce an older active-file
// view after the blob file grows.
Status RefreshBlobFileReader(
uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader);
CacheHandleGuard<BlobFileReader>* blob_file_reader);
// Called when a blob file is obsolete to ensure it is removed from the cache
// to avoid effectively leaking the open file and assicated memory
-96
View File
@@ -192,102 +192,6 @@ TEST_F(BlobFileCacheTest, GetBlobFileReader_Race) {
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(BlobFileCacheTest, RefreshBlobFileReaderPrefersLargestObservedFileSize) {
Options options;
options.env = mock_env_.get();
options.statistics = CreateDBStatistics();
options.cf_paths.emplace_back(
test::PerThreadDBPath(
mock_env_.get(),
"BlobFileCacheTest_"
"RefreshBlobFileReaderPrefersLargestObservedFileSize"),
0);
options.enable_blob_files = true;
constexpr uint32_t column_family_id = 1;
ImmutableOptions immutable_options(options);
constexpr uint64_t blob_file_number = 123;
const std::string blob_file_path =
BlobFileName(immutable_options.cf_paths.front().path, blob_file_number);
std::unique_ptr<FSWritableFile> file;
ASSERT_OK(NewWritableFile(immutable_options.fs.get(), blob_file_path, &file,
FileOptions()));
std::unique_ptr<WritableFileWriter> file_writer(new WritableFileWriter(
std::move(file), blob_file_path, FileOptions(), immutable_options.clock));
constexpr Statistics* statistics = nullptr;
constexpr bool use_fsync = false;
constexpr bool do_flush = false;
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
statistics, blob_file_number, use_fsync,
do_flush);
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range;
BlobLogHeader header(column_family_id, kNoCompression, has_ttl,
expiration_range);
ASSERT_OK(blob_log_writer.WriteHeader(WriteOptions(), header));
uint64_t key_offset = 0;
uint64_t blob_offset = 0;
ASSERT_OK(blob_log_writer.AddRecord(WriteOptions(), "key0", "blob0",
&key_offset, &blob_offset));
ASSERT_OK(blob_log_writer.Sync(WriteOptions()));
constexpr size_t capacity = 10;
std::shared_ptr<Cache> backing_cache = NewLRUCache(capacity);
FileOptions file_options;
constexpr HistogramImpl* blob_file_read_hist = nullptr;
BlobFileCache blob_file_cache(backing_cache.get(), &immutable_options,
&file_options, column_family_id,
blob_file_read_hist, nullptr /*IOTracer*/);
const ReadOptions read_options;
std::unique_ptr<BlobFileReader> stale_reader;
ASSERT_OK(blob_file_cache.OpenBlobFileReaderUncached(
read_options, blob_file_number, &stale_reader,
/*allow_footer_skip_retry=*/true));
std::unique_ptr<BlobFileReader> initial_cached_reader;
ASSERT_OK(blob_file_cache.OpenBlobFileReaderUncached(
read_options, blob_file_number, &initial_cached_reader,
/*allow_footer_skip_retry=*/true));
CacheHandleGuard<BlobFileReader> cached_reader;
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(
blob_file_number, &initial_cached_reader, &cached_reader));
ASSERT_NE(cached_reader.GetValue(), nullptr);
const uint64_t initial_file_size = cached_reader.GetValue()->GetFileSize();
ASSERT_OK(blob_log_writer.AddRecord(WriteOptions(), "key1", "blob1",
&key_offset, &blob_offset));
ASSERT_OK(blob_log_writer.Sync(WriteOptions()));
std::unique_ptr<BlobFileReader> fresh_reader;
ASSERT_OK(blob_file_cache.OpenBlobFileReaderUncached(
read_options, blob_file_number, &fresh_reader,
/*allow_footer_skip_retry=*/true));
ASSERT_GT(fresh_reader->GetFileSize(), initial_file_size);
CacheHandleGuard<BlobFileReader> refreshed_reader;
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(
blob_file_number, &fresh_reader, &refreshed_reader));
ASSERT_NE(refreshed_reader.GetValue(), nullptr);
ASSERT_GT(refreshed_reader.GetValue()->GetFileSize(), initial_file_size);
BlobFileReader* const largest_reader = refreshed_reader.GetValue();
CacheHandleGuard<BlobFileReader> preserved_reader;
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(
blob_file_number, &stale_reader, &preserved_reader));
ASSERT_NE(preserved_reader.GetValue(), nullptr);
ASSERT_EQ(preserved_reader.GetValue(), largest_reader);
ASSERT_EQ(stale_reader.get(), nullptr);
}
TEST_F(BlobFileCacheTest, GetBlobFileReader_IOError) {
Options options;
options.env = mock_env_.get();
-851
View File
@@ -1,851 +0,0 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "db/blob/blob_file_partition_manager.h"
#include <array>
#include <atomic>
#include <cinttypes>
#include <memory>
#include <utility>
#include "cache/cache_key.h"
#include "cache/typed_cache.h"
#include "db/blob/blob_contents.h"
#include "db/blob/blob_file_cache.h"
#include "db/blob/blob_file_completion_callback.h"
#include "db/blob/blob_file_reader.h"
#include "db/blob/blob_index.h"
#include "db/blob/blob_log_writer.h"
#include "db/version_set.h"
#include "file/filename.h"
#include "file/read_write_util.h"
#include "file/writable_file_writer.h"
#include "logging/logging.h"
#include "util/aligned_buffer.h"
#include "util/compression.h"
#include "util/mutexlock.h"
namespace ROCKSDB_NAMESPACE {
namespace {
class RoundRobinBlobFilePartitionStrategy : public BlobFilePartitionStrategy {
public:
using BlobFilePartitionStrategy::SelectPartition;
const char* Name() const override {
return "RoundRobinBlobFilePartitionStrategy";
}
uint32_t SelectPartition(uint32_t num_partitions,
uint32_t /*column_family_id*/, const Slice& /*key*/,
const Slice& /*value*/) override {
assert(num_partitions > 0);
return static_cast<uint32_t>(
next_partition_.fetch_add(1, std::memory_order_relaxed) %
static_cast<uint64_t>(num_partitions));
}
private:
std::atomic<uint64_t> next_partition_{0};
};
struct DirectWriteCompressionState {
CompressionOptions compression_opts;
// `working_area` must be released before its owning compressor.
std::unique_ptr<Compressor> compressor;
Compressor::ManagedWorkingArea working_area;
};
DirectWriteCompressionState& GetDirectWriteCompressionState(
CompressionType compression, const CompressionOptions& compression_opts) {
assert(compression <= kLastBuiltinCompression);
static thread_local std::array<DirectWriteCompressionState,
static_cast<size_t>(kLastBuiltinCompression) +
1>
compression_states;
auto& compression_state =
compression_states[static_cast<size_t>(compression)];
if (compression != kNoCompression &&
(compression_state.compressor == nullptr ||
compression_state.compression_opts != compression_opts)) {
// BDW compression settings are mutable, so rebuild the per-thread cached
// compressor when the latest published opts for this type change.
compression_state.working_area = Compressor::ManagedWorkingArea{};
compression_state.compressor.reset();
compression_state.compression_opts = compression_opts;
compression_state.compressor =
GetBuiltinV2CompressionManager()->GetCompressor(compression_opts,
compression);
if (compression_state.compressor != nullptr) {
compression_state.working_area =
compression_state.compressor->ObtainWorkingArea();
}
}
return compression_state;
}
} // namespace
BlobFilePartitionManager::BlobFilePartitionManager(
uint32_t num_partitions,
std::shared_ptr<BlobFilePartitionStrategy> strategy,
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,
BlobFileCompletionCallback* blob_callback,
const std::vector<std::shared_ptr<EventListener>>& listeners,
FileChecksumGenFactory* file_checksum_gen_factory,
const FileTypeSet& checksum_handoff_file_types,
const std::shared_ptr<IOTracer>& io_tracer, std::string db_id,
std::string db_session_id, Logger* info_log)
: num_partitions_(num_partitions == 0 ? 1 : num_partitions),
strategy_(strategy != nullptr
? std::move(strategy)
: std::make_shared<RoundRobinBlobFilePartitionStrategy>()),
file_number_allocator_(std::move(file_number_allocator)),
fs_(fs),
clock_(clock),
statistics_(statistics),
file_options_(file_options),
db_path_(std::move(db_path)),
column_family_name_(std::move(column_family_name)),
blob_file_size_(blob_file_size),
use_fsync_(use_fsync),
blob_file_cache_(blob_file_cache),
blob_callback_(blob_callback),
listeners_(listeners),
file_checksum_gen_factory_(file_checksum_gen_factory),
checksum_handoff_file_types_(checksum_handoff_file_types),
io_tracer_(io_tracer),
db_id_(std::move(db_id)),
db_session_id_(std::move(db_session_id)),
info_log_(info_log) {
partitions_.reserve(num_partitions_);
for (uint32_t i = 0; i < num_partitions_; ++i) {
partitions_.emplace_back(new Partition());
}
}
BlobFilePartitionManager::~BlobFilePartitionManager() {
if (blob_file_cache_ != nullptr) {
std::vector<uint64_t> tracked_file_numbers;
{
ReadLock lock(&file_partition_mutex_);
tracked_file_numbers.reserve(file_to_partition_.size());
for (const auto& entry : file_to_partition_) {
tracked_file_numbers.push_back(entry.first);
}
}
for (uint64_t file_number : tracked_file_numbers) {
// Dropping the CF or closing the DB can destroy the manager while active
// direct-write readers are still cached. Evict them before the CF-owned
// blob cache goes away so Close() does not retain obsolete footer-less
// readers for files that are no longer live.
blob_file_cache_->Evict(file_number);
}
}
}
void BlobFilePartitionManager::ResetPartitionState(Partition* partition) {
partition->writer.reset();
partition->file_number = 0;
partition->file_size = 0;
partition->blob_count = 0;
partition->total_blob_bytes = 0;
partition->garbage_blob_count = 0;
partition->garbage_blob_bytes = 0;
partition->column_family_id = 0;
partition->compression = kNoCompression;
partition->sync_required = false;
}
void BlobFilePartitionManager::AddFilePartitionMapping(uint64_t file_number,
uint32_t partition_idx) {
WriteLock lock(&file_partition_mutex_);
file_to_partition_[file_number] = partition_idx;
}
void BlobFilePartitionManager::RemoveFilePartitionMapping(
uint64_t file_number) {
WriteLock lock(&file_partition_mutex_);
file_to_partition_.erase(file_number);
}
Status BlobFilePartitionManager::OpenNewBlobFile(Partition* partition,
uint32_t column_family_id,
CompressionType compression,
uint32_t partition_idx) {
assert(partition != nullptr);
assert(!partition->writer);
const uint64_t blob_file_number = file_number_allocator_();
const std::string blob_file_path = BlobFileName(db_path_, blob_file_number);
AddFilePartitionMapping(blob_file_number, partition_idx);
if (blob_callback_ != nullptr) {
blob_callback_->OnBlobFileCreationStarted(
blob_file_path, column_family_name_,
/*job_id=*/0, BlobFileCreationReason::kFlush);
}
std::unique_ptr<FSWritableFile> file;
Status s = NewWritableFile(fs_, blob_file_path, &file, file_options_);
if (!s.ok()) {
RemoveFilePartitionMapping(blob_file_number);
return s;
}
const bool perform_data_verification =
checksum_handoff_file_types_.Contains(FileType::kBlobFile);
auto file_writer = std::make_unique<WritableFileWriter>(
std::move(file), blob_file_path, file_options_, clock_, io_tracer_,
statistics_, Histograms::BLOB_DB_BLOB_FILE_WRITE_MICROS, listeners_,
file_checksum_gen_factory_, perform_data_verification);
// This only drains WritableFileWriter's buffered bytes so readers can see
// each appended record promptly. Durability still comes from SyncAllOpenFiles
// or AppendFooter(), both of which call Sync().
constexpr bool kDoFlushEachRecord = true;
auto blob_log_writer = std::make_unique<BlobLogWriter>(
std::move(file_writer), clock_, statistics_, blob_file_number, use_fsync_,
kDoFlushEachRecord);
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range{};
BlobLogHeader header(column_family_id, compression, has_ttl,
expiration_range);
s = blob_log_writer->WriteHeader(WriteOptions(), header);
if (!s.ok()) {
RemoveFilePartitionMapping(blob_file_number);
return s;
}
partition->writer = std::move(blob_log_writer);
partition->file_number = blob_file_number;
partition->file_size = BlobLogHeader::kSize;
partition->blob_count = 0;
partition->total_blob_bytes = 0;
partition->garbage_blob_count = 0;
partition->garbage_blob_bytes = 0;
partition->column_family_id = column_family_id;
partition->compression = compression;
partition->sync_required = false;
return Status::OK();
}
Status BlobFilePartitionManager::SealActiveBlobFile(
const WriteOptions& write_options, Partition* partition,
SealedFile* sealed_file) {
assert(partition != nullptr);
assert(partition->writer);
assert(sealed_file != nullptr);
const uint64_t file_number = partition->file_number;
Status s =
FinalizeBlobFile(write_options, partition->writer.get(), file_number,
partition->blob_count, partition->total_blob_bytes,
&sealed_file->addition);
if (!s.ok()) {
RemoveFilePartitionMapping(file_number);
ResetPartitionState(partition);
return s;
}
sealed_file->garbage_blob_count = partition->garbage_blob_count;
sealed_file->garbage_blob_bytes = partition->garbage_blob_bytes;
ResetPartitionState(partition);
return Status::OK();
}
Status BlobFilePartitionManager::SealDeferredFile(
const WriteOptions& write_options, DeferredFile* deferred,
SealedFile* sealed_file) {
assert(deferred != nullptr);
assert(deferred->writer);
assert(sealed_file != nullptr);
Status s = FinalizeBlobFile(
write_options, deferred->writer.get(), deferred->file_number,
deferred->blob_count, deferred->total_blob_bytes, &sealed_file->addition);
if (!s.ok()) {
RemoveFilePartitionMapping(deferred->file_number);
return s;
}
sealed_file->garbage_blob_count = deferred->garbage_blob_count;
sealed_file->garbage_blob_bytes = deferred->garbage_blob_bytes;
deferred->writer.reset();
return Status::OK();
}
Status BlobFilePartitionManager::FinalizeBlobFile(
const WriteOptions& write_options, BlobLogWriter* writer,
uint64_t file_number, uint64_t blob_count, uint64_t total_blob_bytes,
BlobFileAddition* addition) {
assert(writer != nullptr);
assert(addition != nullptr);
BlobLogFooter footer;
footer.blob_count = blob_count;
std::string checksum_method;
std::string checksum_value;
Status s = writer->AppendFooter(write_options, footer, &checksum_method,
&checksum_value);
if (!s.ok()) {
return s;
}
if (blob_callback_ != nullptr) {
const std::string blob_file_path = BlobFileName(db_path_, file_number);
s = blob_callback_->OnBlobFileCompleted(
blob_file_path, column_family_name_, /*job_id=*/0, file_number,
BlobFileCreationReason::kFlush, s, checksum_value, checksum_method,
blob_count, total_blob_bytes);
if (!s.ok()) {
return s;
}
}
*addition =
BlobFileAddition(file_number, blob_count, total_blob_bytes,
std::move(checksum_method), std::move(checksum_value));
return Status::OK();
}
void BlobFilePartitionManager::AddSealedFileGarbage(
const SealedFile& sealed_file, std::vector<BlobFileGarbage>* garbages) {
assert(garbages != nullptr);
if (sealed_file.garbage_blob_count == 0) {
assert(sealed_file.garbage_blob_bytes == 0);
return;
}
garbages->emplace_back(sealed_file.addition.GetBlobFileNumber(),
sealed_file.garbage_blob_count,
sealed_file.garbage_blob_bytes);
}
bool BlobFilePartitionManager::MarkPartitionGarbage(Partition* partition,
uint64_t file_number,
uint64_t blob_count,
uint64_t blob_bytes) {
assert(partition != nullptr);
if (!partition->writer || partition->file_number != file_number) {
return false;
}
partition->garbage_blob_count += blob_count;
partition->garbage_blob_bytes += blob_bytes;
assert(partition->garbage_blob_count <= partition->blob_count);
assert(partition->garbage_blob_bytes <= partition->total_blob_bytes);
return true;
}
bool BlobFilePartitionManager::MarkDeferredFileGarbage(DeferredFile* deferred,
uint64_t file_number,
uint64_t blob_count,
uint64_t blob_bytes) {
assert(deferred != nullptr);
if (deferred->file_number != file_number) {
return false;
}
deferred->garbage_blob_count += blob_count;
deferred->garbage_blob_bytes += blob_bytes;
assert(deferred->garbage_blob_count <= deferred->blob_count);
assert(deferred->garbage_blob_bytes <= deferred->total_blob_bytes);
return true;
}
bool BlobFilePartitionManager::MarkSealedFileGarbage(
std::vector<SealedFile>* sealed_files, uint64_t file_number,
uint64_t blob_count, uint64_t blob_bytes) {
assert(sealed_files != nullptr);
for (auto& sealed_file : *sealed_files) {
if (sealed_file.addition.GetBlobFileNumber() != file_number) {
continue;
}
sealed_file.garbage_blob_count += blob_count;
sealed_file.garbage_blob_bytes += blob_bytes;
assert(sealed_file.garbage_blob_count <=
sealed_file.addition.GetTotalBlobCount());
assert(sealed_file.garbage_blob_bytes <=
sealed_file.addition.GetTotalBlobBytes());
return true;
}
return false;
}
Status BlobFilePartitionManager::MaybePrepopulateBlobCache(
const BlobDirectWriteSettings& settings, const Slice& original_value,
uint64_t blob_file_number, uint64_t blob_offset) {
if (settings.blob_cache == nullptr ||
settings.prepopulate_blob_cache != PrepopulateBlobCache::kFlushOnly) {
return Status::OK();
}
FullTypedCacheInterface<BlobContents, BlobContentsCreator> blob_cache{
settings.blob_cache};
const OffsetableCacheKey base_cache_key(db_id_, db_session_id_,
blob_file_number);
const CacheKey cache_key = base_cache_key.WithOffset(blob_offset);
return blob_cache.InsertSaved(cache_key.AsSlice(), original_value, nullptr,
Cache::Priority::BOTTOM,
CacheTier::kVolatileTier);
}
uint32_t BlobFilePartitionManager::SelectWideColumnPartition(
uint32_t column_family_id, const Slice& key,
const WideColumns& columns) const {
return strategy_->SelectPartition(num_partitions_, column_family_id, key,
columns) %
num_partitions_;
}
Status BlobFilePartitionManager::WriteBlob(
const WriteOptions& write_options, uint32_t column_family_id,
CompressionType compression, const Slice& key, const Slice& value,
uint64_t* blob_file_number, uint64_t* blob_offset, uint64_t* blob_size,
const BlobDirectWriteSettings& settings, const uint32_t* partition_idx) {
assert(blob_file_number != nullptr);
assert(blob_offset != nullptr);
assert(blob_size != nullptr);
// Do compression before taking the partition mutex so large-value CPU work
// does not serialize writers. A concurrent file rollover can still cause
// this compressed buffer to be discarded below, which is acceptable on this
// non-hot failure/configuration-change path.
GrowableBuffer compressed_value;
Slice write_value = value;
if (compression != kNoCompression) {
auto& compression_state =
GetDirectWriteCompressionState(compression, settings.compression_opts);
if (compression_state.compressor == nullptr) {
return Status::NotSupported(
"Blob direct write compression type not supported");
}
Status s = LegacyForceBuiltinCompression(*compression_state.compressor,
&compression_state.working_area,
value, &compressed_value);
if (!s.ok()) {
return s;
}
write_value = Slice(compressed_value);
}
// Partition selection is based on the logical write inputs. In particular,
// strategies that inspect value contents or size see the original
// uncompressed value rather than `write_value`. The modulo here is
// intentional so custom strategies can return arbitrary hashed or sentinel
// values without violating the partition bounds.
const uint32_t selected_partition_idx =
partition_idx != nullptr
? (*partition_idx % num_partitions_)
: (strategy_->SelectPartition(num_partitions_, column_family_id, key,
value) %
num_partitions_);
{
MutexLock lock(&mutex_);
Partition* partition = partitions_[selected_partition_idx].get();
auto seal_current_file = [&]() -> Status {
if (!partition->writer) {
return Status::OK();
}
SealedFile sealed_file;
Status s = SealActiveBlobFile(write_options, partition, &sealed_file);
if (s.ok()) {
current_generation_sealed_files_.push_back(std::move(sealed_file));
}
return s;
};
const uint64_t record_size =
BlobLogRecord::kHeaderSize + key.size() + write_value.size();
const uint64_t future_file_size =
partition->file_size + record_size + BlobLogFooter::kSize;
if (partition->writer &&
(partition->column_family_id != column_family_id ||
partition->compression != compression ||
(partition->blob_count > 0 && future_file_size > blob_file_size_))) {
Status s = seal_current_file();
if (!s.ok()) {
return s;
}
}
if (!partition->writer) {
Status s = OpenNewBlobFile(partition, column_family_id, compression,
selected_partition_idx);
if (!s.ok()) {
return s;
}
}
uint64_t key_offset = 0;
Status s = partition->writer->AddRecord(write_options, key, write_value,
&key_offset, blob_offset);
if (!s.ok()) {
return s;
}
partition->sync_required = true;
partition->blob_count += 1;
partition->total_blob_bytes += record_size;
partition->file_size = BlobLogHeader::kSize + partition->total_blob_bytes;
*blob_file_number = partition->file_number;
*blob_size = write_value.size();
}
Status prepopulate_s = MaybePrepopulateBlobCache(
settings, value, *blob_file_number, *blob_offset);
if (!prepopulate_s.ok() && info_log_ != nullptr) {
ROCKS_LOG_WARN(info_log_,
"Failed to pre-populate direct-write blob cache entry: %s",
prepopulate_s.ToString().c_str());
}
return Status::OK();
}
void BlobFilePartitionManager::RotateCurrentGeneration() {
MutexLock lock(&mutex_);
GenerationBatch batch;
batch.sealed_files = std::move(current_generation_sealed_files_);
current_generation_sealed_files_.clear();
for (auto& partition : partitions_) {
if (!partition->writer) {
continue;
}
DeferredFile deferred;
deferred.writer = std::move(partition->writer);
deferred.file_number = partition->file_number;
deferred.blob_count = partition->blob_count;
deferred.total_blob_bytes = partition->total_blob_bytes;
deferred.garbage_blob_count = partition->garbage_blob_count;
deferred.garbage_blob_bytes = partition->garbage_blob_bytes;
ResetPartitionState(partition.get());
batch.deferred_files.emplace_back(std::move(deferred));
}
pending_generations_.emplace_back(std::move(batch));
}
Status BlobFilePartitionManager::PrepareFlushAdditions(
const WriteOptions& write_options, size_t num_generations,
std::vector<BlobFileAddition>* additions,
std::vector<BlobFileGarbage>* garbages,
std::vector<std::vector<uint64_t>>* generation_blob_file_numbers) {
assert(additions != nullptr);
assert(garbages != nullptr);
additions->clear();
garbages->clear();
if (generation_blob_file_numbers != nullptr) {
generation_blob_file_numbers->clear();
}
MutexLock lock(&mutex_);
if (num_generations > pending_generations_.size()) {
return Status::Corruption(
"Missing blob direct write generation metadata for flush");
}
for (size_t i = 0; i < num_generations; ++i) {
GenerationBatch& batch = pending_generations_[i];
while (!batch.deferred_files.empty()) {
DeferredFile deferred = std::move(batch.deferred_files.front());
batch.deferred_files.pop_front();
SealedFile sealed_file;
Status s = SealDeferredFile(write_options, &deferred, &sealed_file);
if (!s.ok()) {
return s;
}
// Keep each successfully sealed file attached to the generation
// immediately. If a later seal or the flush job itself fails, retry must
// reuse these exact on-disk files instead of finalizing replacements.
batch.sealed_files.push_back(std::move(sealed_file));
}
std::vector<uint64_t>* generation_file_numbers = nullptr;
if (generation_blob_file_numbers != nullptr) {
generation_blob_file_numbers->emplace_back();
generation_file_numbers = &generation_blob_file_numbers->back();
generation_file_numbers->reserve(batch.sealed_files.size());
}
for (const auto& sealed_file : batch.sealed_files) {
additions->push_back(sealed_file.addition);
AddSealedFileGarbage(sealed_file, garbages);
if (generation_file_numbers != nullptr) {
generation_file_numbers->push_back(
sealed_file.addition.GetBlobFileNumber());
}
}
}
return Status::OK();
}
Status BlobFilePartitionManager::MarkBlobWriteAsGarbage(uint64_t file_number,
uint64_t blob_count,
uint64_t blob_bytes) {
if (blob_count == 0) {
assert(blob_bytes == 0);
return Status::OK();
}
MutexLock lock(&mutex_);
for (auto& partition : partitions_) {
if (MarkPartitionGarbage(partition.get(), file_number, blob_count,
blob_bytes)) {
return Status::OK();
}
}
if (MarkSealedFileGarbage(&current_generation_sealed_files_, file_number,
blob_count, blob_bytes)) {
return Status::OK();
}
for (auto& batch : pending_generations_) {
for (auto& deferred : batch.deferred_files) {
if (MarkDeferredFileGarbage(&deferred, file_number, blob_count,
blob_bytes)) {
return Status::OK();
}
}
if (MarkSealedFileGarbage(&batch.sealed_files, file_number, blob_count,
blob_bytes)) {
return Status::OK();
}
}
const std::string message =
"Could not match failed blob direct-write rollback for file #" +
std::to_string(file_number);
if (info_log_ != nullptr) {
ROCKS_LOG_ERROR(info_log_, "%s", message.c_str());
}
return Status::Corruption(message);
}
void BlobFilePartitionManager::CommitPreparedGenerations(
size_t num_generations) {
MutexLock lock(&mutex_);
while (num_generations-- > 0 && !pending_generations_.empty()) {
pending_generations_.pop_front();
}
}
Status BlobFilePartitionManager::SyncAllOpenFiles(
const WriteOptions& write_options) {
MutexLock lock(&mutex_);
for (const auto& partition : partitions_) {
if (!partition->writer || !partition->sync_required) {
continue;
}
Status s = partition->writer->Sync(write_options);
if (!s.ok()) {
return s;
}
partition->sync_required = false;
}
return Status::OK();
}
void BlobFilePartitionManager::GetActiveBlobFileNumbers(
UnorderedSet<uint64_t>* file_numbers) const {
assert(file_numbers != nullptr);
ReadLock lock(&file_partition_mutex_);
for (const auto& entry : file_to_partition_) {
file_numbers->insert(entry.first);
}
}
void BlobFilePartitionManager::GetProtectedBlobFileNumbers(
UnorderedSet<uint64_t>* file_numbers) const {
assert(file_numbers != nullptr);
ReadLock lock(&file_partition_mutex_);
for (const auto& entry : protected_blob_file_refs_) {
file_numbers->insert(entry.first);
}
}
bool BlobFilePartitionManager::IsTrackedBlobFileNumber(
uint64_t file_number) const {
ReadLock lock(&file_partition_mutex_);
return file_to_partition_.find(file_number) != file_to_partition_.end() ||
protected_blob_file_refs_.find(file_number) !=
protected_blob_file_refs_.end();
}
void BlobFilePartitionManager::ProtectSealedBlobFileNumbers(
const std::vector<uint64_t>& file_numbers) {
if (file_numbers.empty()) {
return;
}
WriteLock lock(&file_partition_mutex_);
for (uint64_t file_number : file_numbers) {
++protected_blob_file_refs_[file_number];
}
}
void BlobFilePartitionManager::UnprotectSealedBlobFileNumbers(
const std::vector<uint64_t>& file_numbers) {
if (file_numbers.empty()) {
return;
}
WriteLock lock(&file_partition_mutex_);
for (uint64_t file_number : file_numbers) {
auto it = protected_blob_file_refs_.find(file_number);
if (it == protected_blob_file_refs_.end()) {
if (info_log_ != nullptr) {
ROCKS_LOG_ERROR(info_log_,
"Memtable blob protection underflow for file #%" PRIu64,
file_number);
}
continue;
}
assert(it->second > 0);
--it->second;
if (it->second == 0) {
protected_blob_file_refs_.erase(it);
if (blob_file_cache_ != nullptr) {
// Once the last memtable-backed reference goes away, any cached reader
// for this file is either obsolete or can be reopened through the
// manifest-visible path. Evict it here so delayed protection does not
// leave an obsolete blob reader behind until DB close.
blob_file_cache_->Evict(file_number);
}
}
}
}
void BlobFilePartitionManager::RemoveFilePartitionMappings(
const std::vector<uint64_t>& file_numbers) {
if (file_numbers.empty()) {
return;
}
WriteLock lock(&file_partition_mutex_);
for (uint64_t file_number : file_numbers) {
file_to_partition_.erase(file_number);
if (blob_file_cache_ != nullptr) {
// In-flight direct-write reads may have cached a footer-less reader for
// this file before it was sealed. Drop it now so future manifest-visible
// reads reopen against the finalized on-disk size and footer state.
blob_file_cache_->Evict(file_number);
}
}
}
Status BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
const ReadOptions& read_options, const Slice& user_key,
const BlobIndex& blob_idx, const Version* version,
BlobFileCache* blob_file_cache, FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value, uint64_t* bytes_read) {
assert(blob_value != nullptr);
if (version != nullptr) {
// Only fall back when the blob file is still owned exclusively by the
// write path and therefore absent from Version metadata. Once Version
// knows the file number, every result from Version::GetBlob(), including
// corruption, must propagate directly.
if (blob_idx.HasTTL() || blob_idx.IsInlined() ||
version->storage_info()->GetBlobFileMetaData(blob_idx.file_number()) !=
nullptr) {
return version->GetBlob(read_options, user_key, blob_idx, prefetch_buffer,
blob_value, bytes_read);
}
}
if (blob_file_cache == nullptr) {
return version != nullptr ? Status::Corruption("Invalid blob file number")
: Status::NotFound();
}
if (read_options.read_tier == kBlockCacheTier) {
// The direct-write fallback below may need to open the blob file reader,
// which `kBlockCacheTier` forbids. Keep the normal Version-backed path
// above eligible for cache-only hits.
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
}
Status s;
CacheHandleGuard<BlobFileReader> reader;
s = blob_file_cache->GetBlobFileReader(read_options, blob_idx.file_number(),
&reader,
/*allow_footer_skip_retry=*/true);
if (!s.ok()) {
return s;
}
std::unique_ptr<BlobContents> blob_contents;
s = reader.GetValue()->GetBlob(read_options, user_key, blob_idx.offset(),
blob_idx.size(), blob_idx.compression(),
prefetch_buffer, nullptr, &blob_contents,
bytes_read);
if (s.ok()) {
blob_value->PinSelf(blob_contents->data());
return s;
}
if (!s.IsCorruption()) {
return s;
}
reader.Reset();
blob_file_cache->Evict(blob_idx.file_number());
std::unique_ptr<BlobFileReader> fresh_reader;
s = blob_file_cache->OpenBlobFileReaderUncached(
read_options, blob_idx.file_number(), &fresh_reader,
/*allow_footer_skip_retry=*/true);
if (!s.ok()) {
return s;
}
std::unique_ptr<BlobContents> fresh_contents;
s = fresh_reader->GetBlob(read_options, user_key, blob_idx.offset(),
blob_idx.size(), blob_idx.compression(),
prefetch_buffer, nullptr, &fresh_contents,
bytes_read);
if (s.ok()) {
blob_value->PinSelf(fresh_contents->data());
CacheHandleGuard<BlobFileReader> ignored;
blob_file_cache
->RefreshBlobFileReader(blob_idx.file_number(), &fresh_reader, &ignored)
.PermitUncheckedError();
}
return s;
}
} // namespace ROCKSDB_NAMESPACE
-294
View File
@@ -1,294 +0,0 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#pragma once
#include <cstdint>
#include <deque>
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "db/blob/blob_file_addition.h"
#include "db/blob/blob_file_garbage.h"
#include "db/blob/blob_log_format.h"
#include "db/blob/blob_write_batch_transformer.h"
#include "port/port.h"
#include "rocksdb/compression_type.h"
#include "rocksdb/env.h"
#include "rocksdb/file_system.h"
#include "rocksdb/options.h"
#include "rocksdb/rocksdb_namespace.h"
#include "rocksdb/slice.h"
#include "rocksdb/status.h"
#include "util/hash_containers.h"
namespace ROCKSDB_NAMESPACE {
class BlobFileCache;
class BlobFileCompletionCallback;
class BlobIndex;
class BlobLogWriter;
class FilePrefetchBuffer;
class IOTracer;
class Logger;
class PinnableSlice;
class Version;
class WritableFileWriter;
struct FileOptions;
struct ReadOptions;
// Manages per-partition blob files for write-path blob direct write.
//
// The v1 design keeps each memtable switch as one FIFO generation batch.
// Direct-write files for that memtable are sealed and registered when the
// matching flush commits. The current implementation still serializes blob
// appends through one manager mutex; follow-up PRs can add independent
// per-partition locks to allow concurrent blob writes without changing the
// generation/flush contract. Follow-up PRs can also broaden compatibility as
// the feature matures.
class BlobFilePartitionManager {
public:
using FileNumberAllocator = std::function<uint64_t()>;
// Creates the per-column-family manager for write-path blob files.
BlobFilePartitionManager(
uint32_t num_partitions,
std::shared_ptr<BlobFilePartitionStrategy> strategy,
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, BlobFileCompletionCallback* blob_callback,
const std::vector<std::shared_ptr<EventListener>>& listeners,
FileChecksumGenFactory* file_checksum_gen_factory,
const FileTypeSet& checksum_handoff_file_types,
const std::shared_ptr<IOTracer>& io_tracer, std::string db_id,
std::string db_session_id, Logger* info_log);
// Evicts cached readers for manager-owned files during shutdown.
~BlobFilePartitionManager();
// Appends one blob record to a partition file and returns the resulting
// BlobIndex location metadata.
Status WriteBlob(const WriteOptions& write_options, uint32_t column_family_id,
CompressionType compression, const Slice& key,
const Slice& value, uint64_t* blob_file_number,
uint64_t* blob_offset, uint64_t* blob_size,
const BlobDirectWriteSettings& settings,
const uint32_t* partition_idx = nullptr);
// Selects the partition to use for all blob-backed columns of one PutEntity
// operation. The return value is already normalized to [0, num_partitions_).
uint32_t SelectWideColumnPartition(uint32_t column_family_id,
const Slice& key,
const WideColumns& columns) const;
// Move the current active partition files into the next immutable
// memtable-generation batch. Called from SwitchMemtable() while DB mutex is
// held. No I/O is performed here.
void RotateCurrentGeneration();
// Seal the first `num_generations` queued immutable generations and return
// all blob additions and initial-garbage updates that must be registered
// with the matching flush. Generations stay queued until
// CommitPreparedGenerations() is called. If `generation_blob_file_numbers`
// is non-null, it receives the sealed blob file numbers for each prepared
// generation in the same FIFO order.
Status PrepareFlushAdditions(const WriteOptions& write_options,
size_t num_generations,
std::vector<BlobFileAddition>* additions,
std::vector<BlobFileGarbage>* garbages,
std::vector<std::vector<uint64_t>>*
generation_blob_file_numbers = nullptr);
// Remove the first `num_generations` prepared immutable generations after
// their blob-file additions and garbage edits were committed to MANIFEST.
void CommitPreparedGenerations(size_t num_generations);
// In v1 flush-on-write mode each AddRecord flushes to the OS immediately, so
// there is nothing extra to do for the non-sync write path.
Status FlushAllOpenFiles(const WriteOptions& /*write_options*/) {
return Status::OK();
}
// Sync the active open blob files before a sync WAL write.
Status SyncAllOpenFiles(const WriteOptions& write_options);
// Returns blob files still owned by the manager and therefore not yet safe
// to consider obsolete.
void GetActiveBlobFileNumbers(UnorderedSet<uint64_t>* file_numbers) const;
// Returns sealed blob files that are still reachable through live memtables
// or old SuperVersions and therefore must not be purged yet.
void GetProtectedBlobFileNumbers(UnorderedSet<uint64_t>* file_numbers) const;
// Returns true when the blob file is still owned by the write path or
// protected by a live memtable / old SuperVersion.
bool IsTrackedBlobFileNumber(uint64_t file_number) const;
// Increments / decrements memtable-held protection on sealed blob files.
void ProtectSealedBlobFileNumbers(const std::vector<uint64_t>& file_numbers);
void UnprotectSealedBlobFileNumbers(
const std::vector<uint64_t>& file_numbers);
// Stops protecting file numbers that are now registered in MANIFEST.
void RemoveFilePartitionMappings(const std::vector<uint64_t>& file_numbers);
// Marks blob records from a failed transformed write as initial garbage for
// the target blob file while keeping the physical bytes in place.
// Returns Corruption if the manager can no longer match the blob file.
Status MarkBlobWriteAsGarbage(uint64_t file_number, uint64_t blob_count,
uint64_t blob_bytes);
// Resolves a direct-write BlobIndex by consulting manifest-visible state
// first, then falling back to direct blob-file reads only when the target
// blob file is still write-path-owned and therefore not yet tracked by
// Version. Existing manifest-visible read results, including I/O failures,
// are returned directly rather than masked by fallback logic.
static Status ResolveBlobDirectWriteIndex(
const ReadOptions& read_options, const Slice& user_key,
const BlobIndex& blob_idx, const Version* version,
BlobFileCache* blob_file_cache, FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value, uint64_t* bytes_read);
private:
struct Partition {
// Active writer for this partition, or null when the partition is idle.
std::unique_ptr<BlobLogWriter> writer;
// Metadata tracked while the active file remains open.
uint64_t file_number = 0;
uint64_t file_size = 0;
uint64_t blob_count = 0;
uint64_t total_blob_bytes = 0;
// Failed transformed writes already appended to this physical file.
uint64_t garbage_blob_count = 0;
uint64_t garbage_blob_bytes = 0;
uint32_t column_family_id = 0;
CompressionType compression = kNoCompression;
bool sync_required = false;
};
struct DeferredFile {
// Blob writer moved out of an active partition at memtable rotation.
std::unique_ptr<BlobLogWriter> writer;
// Metadata preserved until flush preparation seals the file.
uint64_t file_number = 0;
uint64_t blob_count = 0;
uint64_t total_blob_bytes = 0;
// Failed transformed writes already appended before the file is sealed.
uint64_t garbage_blob_count = 0;
uint64_t garbage_blob_bytes = 0;
};
struct SealedFile {
// Blob-file metadata ready to be added to MANIFEST.
BlobFileAddition addition;
// Initial unreachable bytes that should be registered as garbage
// alongside the file addition.
uint64_t garbage_blob_count = 0;
uint64_t garbage_blob_bytes = 0;
};
struct GenerationBatch {
// Files that were still open when the memtable became immutable.
std::deque<DeferredFile> deferred_files;
// Files already sealed for this memtable generation.
std::vector<SealedFile> sealed_files;
};
// Clears all active-file bookkeeping from a partition after sealing.
void ResetPartitionState(Partition* partition);
// Marks a blob file as manager-owned until it is committed or abandoned.
void AddFilePartitionMapping(uint64_t file_number, uint32_t partition_idx);
// Stops tracking a blob file after open/seal failure.
void RemoveFilePartitionMapping(uint64_t file_number);
// Opens a new active blob file for one partition.
Status OpenNewBlobFile(Partition* partition, uint32_t column_family_id,
CompressionType compression, uint32_t partition_idx);
// Finalizes one active partition file and returns the MANIFEST addition.
Status SealActiveBlobFile(const WriteOptions& write_options,
Partition* partition, SealedFile* sealed_file);
// Finalizes one deferred file that was carried over to flush preparation.
Status SealDeferredFile(const WriteOptions& write_options,
DeferredFile* deferred, SealedFile* sealed_file);
// Appends the footer, reports file completion, and builds the resulting
// BlobFileAddition for one sealed blob file.
Status FinalizeBlobFile(const WriteOptions& write_options,
BlobLogWriter* writer, uint64_t file_number,
uint64_t blob_count, uint64_t total_blob_bytes,
BlobFileAddition* addition);
// Appends one initial-garbage record to the flush output if any failed
// transformed writes targeted the sealed blob file.
static void AddSealedFileGarbage(const SealedFile& sealed_file,
std::vector<BlobFileGarbage>* garbages);
// Returns true if the active open file matches `file_number` and consumes
// the failed-write garbage accounting.
static bool MarkPartitionGarbage(Partition* partition, uint64_t file_number,
uint64_t blob_count, uint64_t blob_bytes);
// Returns true if the deferred flush-preparation file matches `file_number`
// and consumes the failed-write garbage accounting.
static bool MarkDeferredFileGarbage(DeferredFile* deferred,
uint64_t file_number, uint64_t blob_count,
uint64_t blob_bytes);
// Returns true if one already-sealed file matches `file_number` and
// consumes the failed-write garbage accounting.
static bool MarkSealedFileGarbage(std::vector<SealedFile>* sealed_files,
uint64_t file_number, uint64_t blob_count,
uint64_t blob_bytes);
// Seeds the blob cache with the original uncompressed value when configured.
Status MaybePrepopulateBlobCache(const BlobDirectWriteSettings& settings,
const Slice& original_value,
uint64_t blob_file_number,
uint64_t blob_offset);
// Configured partition fanout and write-selection strategy.
const uint32_t num_partitions_;
std::shared_ptr<BlobFilePartitionStrategy> strategy_;
// Shared services needed to create, track, and cache blob files.
FileNumberAllocator file_number_allocator_;
FileSystem* fs_;
SystemClock* clock_;
Statistics* statistics_;
FileOptions file_options_;
// Column-family context used for file naming and callbacks.
std::string db_path_;
std::string column_family_name_;
// File creation policy and shared blob-file infrastructure.
uint64_t blob_file_size_;
bool use_fsync_;
BlobFileCache* blob_file_cache_;
BlobFileCompletionCallback* blob_callback_;
std::vector<std::shared_ptr<EventListener>> listeners_;
FileChecksumGenFactory* file_checksum_gen_factory_;
FileTypeSet checksum_handoff_file_types_;
std::shared_ptr<IOTracer> io_tracer_;
// DB identity used when constructing cache keys.
std::string db_id_;
std::string db_session_id_;
Logger* info_log_;
// One active writer slot per configured partition.
std::vector<std::unique_ptr<Partition>> partitions_;
// Protects partition state and generation queues.
mutable port::Mutex mutex_;
// Files already sealed while the current mutable memtable was active.
std::vector<SealedFile> current_generation_sealed_files_;
// FIFO immutable memtable generations waiting to be flushed.
std::deque<GenerationBatch> pending_generations_;
// Tracks blob files still owned by active or deferred write-path state until
// MANIFEST commit publishes them.
std::unordered_map<uint64_t, uint32_t> file_to_partition_;
// Sealed direct-write files that remain reachable from live memtables, such
// as old SuperVersions serving lazy iterator reads after flush commit.
std::unordered_map<uint64_t, uint32_t> protected_blob_file_refs_;
// Protects file_to_partition_ and protected_blob_file_refs_.
mutable port::RWMutex file_partition_mutex_;
};
} // namespace ROCKSDB_NAMESPACE
+14 -36
View File
@@ -29,7 +29,7 @@ Status BlobFileReader::Create(
const ImmutableOptions& immutable_options, const ReadOptions& read_options,
const FileOptions& file_options, uint32_t column_family_id,
HistogramImpl* blob_file_read_hist, uint64_t blob_file_number,
const std::shared_ptr<IOTracer>& io_tracer, bool skip_footer_validation,
const std::shared_ptr<IOTracer>& io_tracer,
std::unique_ptr<BlobFileReader>* blob_file_reader) {
assert(blob_file_reader);
assert(!*blob_file_reader);
@@ -40,8 +40,7 @@ Status BlobFileReader::Create(
{
const Status s =
OpenFile(immutable_options, file_options, blob_file_read_hist,
blob_file_number, io_tracer, &file_size, &file_reader,
/*skip_footer_size_check=*/skip_footer_validation);
blob_file_number, io_tracer, &file_size, &file_reader);
if (!s.ok()) {
return s;
}
@@ -62,7 +61,7 @@ Status BlobFileReader::Create(
}
}
if (!skip_footer_validation) {
{
const Status s =
ReadFooter(file_reader.get(), read_options, file_size, statistics);
if (!s.ok()) {
@@ -77,10 +76,9 @@ Status BlobFileReader::Create(
compression_type);
}
blob_file_reader->reset(
new BlobFileReader(std::move(file_reader), file_size, compression_type,
std::move(decompressor), immutable_options.clock,
statistics, !skip_footer_validation));
blob_file_reader->reset(new BlobFileReader(
std::move(file_reader), file_size, compression_type,
std::move(decompressor), immutable_options.clock, statistics));
return Status::OK();
}
@@ -89,8 +87,7 @@ Status BlobFileReader::OpenFile(
const ImmutableOptions& immutable_options, const FileOptions& file_opts,
HistogramImpl* blob_file_read_hist, uint64_t blob_file_number,
const std::shared_ptr<IOTracer>& io_tracer, uint64_t* file_size,
std::unique_ptr<RandomAccessFileReader>* file_reader,
bool skip_footer_size_check) {
std::unique_ptr<RandomAccessFileReader>* file_reader) {
assert(file_size);
assert(file_reader);
@@ -115,26 +112,17 @@ Status BlobFileReader::OpenFile(
}
}
if (!skip_footer_size_check &&
*file_size < BlobLogHeader::kSize + BlobLogFooter::kSize) {
return Status::Corruption("Malformed blob file");
}
if (skip_footer_size_check && *file_size < BlobLogHeader::kSize) {
if (*file_size < BlobLogHeader::kSize + BlobLogFooter::kSize) {
return Status::Corruption("Malformed blob file");
}
std::unique_ptr<FSRandomAccessFile> file;
FileOptions reader_file_opts = file_opts;
if (skip_footer_size_check && reader_file_opts.use_direct_reads) {
reader_file_opts.use_direct_reads = false;
}
{
TEST_SYNC_POINT("BlobFileReader::OpenFile:NewRandomAccessFile");
const Status s =
fs->NewRandomAccessFile(blob_file_path, reader_file_opts, &file, dbg);
fs->NewRandomAccessFile(blob_file_path, file_opts, &file, dbg);
if (!s.ok()) {
return s;
}
@@ -303,14 +291,13 @@ BlobFileReader::BlobFileReader(
std::unique_ptr<RandomAccessFileReader>&& file_reader, uint64_t file_size,
CompressionType compression_type,
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
Statistics* statistics, bool has_footer)
Statistics* statistics)
: file_reader_(std::move(file_reader)),
file_size_(file_size),
compression_type_(compression_type),
decompressor_(std::move(decompressor)),
clock_(clock),
statistics_(statistics),
has_footer_(has_footer) {
statistics_(statistics) {
assert(file_reader_);
}
@@ -325,8 +312,7 @@ Status BlobFileReader::GetBlob(
const uint64_t key_size = user_key.size();
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
has_footer_)) {
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_)) {
return Status::Corruption("Invalid blob offset");
}
@@ -442,8 +428,7 @@ void BlobFileReader::MultiGetBlob(
const uint64_t offset = req->offset;
const uint64_t value_size = req->len;
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
has_footer_)) {
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_)) {
*req->status = Status::Corruption("Invalid blob offset");
continue;
}
@@ -469,13 +454,6 @@ void BlobFileReader::MultiGetBlob(
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, total_len);
if (read_reqs.empty()) {
if (bytes_read) {
*bytes_read = 0;
}
return;
}
Buffer buf;
AlignedBuf aligned_buf;
@@ -555,7 +533,7 @@ void BlobFileReader::MultiGetBlob(
}
// Uncompress blob if needed
Slice value_slice(record_slice.data() + adjustments[j - 1], req->len);
Slice value_slice(record_slice.data() + adjustments[i], req->len);
*req->status = UncompressBlobIfNeeded(
value_slice, compression_type_, decompressor_.get(), allocator, clock_,
statistics_, &blob_reqs[i].second);
+3 -22
View File
@@ -36,20 +36,7 @@ class BlobFileReader {
HistogramImpl* blob_file_read_hist,
uint64_t blob_file_number,
const std::shared_ptr<IOTracer>& io_tracer,
std::unique_ptr<BlobFileReader>* reader) {
return Create(immutable_options, read_options, file_options,
column_family_id, blob_file_read_hist, blob_file_number,
io_tracer, /*skip_footer_validation=*/false, reader);
}
// Allows opening in-flight direct-write blob files by optionally skipping
// footer validation.
static Status Create(
const ImmutableOptions& immutable_options,
const ReadOptions& read_options, const FileOptions& file_options,
uint32_t column_family_id, HistogramImpl* blob_file_read_hist,
uint64_t blob_file_number, const std::shared_ptr<IOTracer>& io_tracer,
bool skip_footer_validation, std::unique_ptr<BlobFileReader>* reader);
std::unique_ptr<BlobFileReader>* reader);
BlobFileReader(const BlobFileReader&) = delete;
BlobFileReader& operator=(const BlobFileReader&) = delete;
@@ -76,22 +63,18 @@ class BlobFileReader {
uint64_t GetFileSize() const { return file_size_; }
private:
// `has_footer` tracks whether offset validation should reserve footer space.
BlobFileReader(std::unique_ptr<RandomAccessFileReader>&& file_reader,
uint64_t file_size, CompressionType compression_type,
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
Statistics* statistics, bool has_footer);
Statistics* statistics);
// `skip_footer_size_check` is used for direct-write files that are still
// missing their footer at open time.
static Status OpenFile(const ImmutableOptions& immutable_options,
const FileOptions& file_opts,
HistogramImpl* blob_file_read_hist,
uint64_t blob_file_number,
const std::shared_ptr<IOTracer>& io_tracer,
uint64_t* file_size,
std::unique_ptr<RandomAccessFileReader>* file_reader,
bool skip_footer_size_check);
std::unique_ptr<RandomAccessFileReader>* file_reader);
static Status ReadHeader(const RandomAccessFileReader* file_reader,
const ReadOptions& read_options,
@@ -127,8 +110,6 @@ class BlobFileReader {
std::shared_ptr<Decompressor> decompressor_;
SystemClock* clock_;
Statistics* statistics_;
// False when the reader was opened before the blob file footer was written.
bool has_footer_;
};
} // namespace ROCKSDB_NAMESPACE
-86
View File
@@ -1011,92 +1011,6 @@ TEST_P(BlobFileReaderDecodingErrorTest, DecodingError) {
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(BlobFileReaderTest, MultiGetBlobWithFailedValidation) {
// Test that MultiGetBlob correctly handles the case where some requests
// fail validation. The adjustments vector is only populated for requests
// that pass validation, so the indices must track correctly.
Options options;
options.env = mock_env_.get();
options.cf_paths.emplace_back(
test::PerThreadDBPath(
mock_env_.get(),
"BlobFileReaderTest_MultiGetBlobWithFailedValidation"),
0);
options.enable_blob_files = true;
ImmutableOptions immutable_options(options);
constexpr uint32_t column_family_id = 1;
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range;
constexpr uint64_t blob_file_number = 1;
constexpr size_t num_blobs = 3;
const std::vector<std::string> key_strs = {"key1", "key2", "key3"};
const std::vector<std::string> blob_strs = {"blob1", "blob2", "blob3"};
const std::vector<Slice> keys = {key_strs[0], key_strs[1], key_strs[2]};
const std::vector<Slice> blobs = {blob_strs[0], blob_strs[1], blob_strs[2]};
std::vector<uint64_t> blob_offsets(keys.size());
std::vector<uint64_t> blob_sizes(keys.size());
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
expiration_range, blob_file_number, keys, blobs, kNoCompression,
blob_offsets, blob_sizes);
constexpr HistogramImpl* blob_file_read_hist = nullptr;
std::unique_ptr<BlobFileReader> reader;
ReadOptions read_options;
ASSERT_OK(BlobFileReader::Create(
immutable_options, read_options, FileOptions(), column_family_id,
blob_file_read_hist, blob_file_number, nullptr /*IOTracer*/, &reader));
// Enable checksum verification so adjustments are non-zero
read_options.verify_checksums = true;
constexpr MemoryAllocator* allocator = nullptr;
// Fail the first request by giving it an invalid offset (too small).
// This causes the first request to be skipped in the adjustments vector,
// so adjustments has only 2 entries (for blobs 1 and 2), while blob_reqs
// has 3 entries. Without the fix, adjustments[i] would use the wrong
// index for the remaining valid requests.
std::array<Status, num_blobs> statuses_buf;
std::array<BlobReadRequest, num_blobs> requests_buf;
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
blob_reqs;
// First request: invalid offset (too small, will fail validation)
requests_buf[0] = BlobReadRequest(keys[0], /*offset=*/0, blob_sizes[0],
kNoCompression, nullptr, &statuses_buf[0]);
// Second and third requests: valid
requests_buf[1] = BlobReadRequest(keys[1], blob_offsets[1], blob_sizes[1],
kNoCompression, nullptr, &statuses_buf[1]);
requests_buf[2] = BlobReadRequest(keys[2], blob_offsets[2], blob_sizes[2],
kNoCompression, nullptr, &statuses_buf[2]);
for (size_t i = 0; i < num_blobs; ++i) {
blob_reqs.emplace_back(&requests_buf[i], std::unique_ptr<BlobContents>());
}
uint64_t bytes_read = 0;
reader->MultiGetBlob(read_options, allocator, blob_reqs, &bytes_read);
// First request should fail validation
ASSERT_TRUE(statuses_buf[0].IsCorruption());
ASSERT_EQ(blob_reqs[0].second, nullptr);
// Second and third requests should succeed with correct data
ASSERT_OK(statuses_buf[1]);
ASSERT_NE(blob_reqs[1].second, nullptr);
ASSERT_EQ(blob_reqs[1].second->data(), blobs[1]);
ASSERT_OK(statuses_buf[2]);
ASSERT_NE(blob_reqs[2].second, nullptr);
ASSERT_EQ(blob_reqs[2].second->data(), blobs[2]);
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+55 -88
View File
@@ -8,48 +8,70 @@
#include "db/blob/blob_index.h"
#include "db/blob/blob_log_format.h"
#include "db/dbformat.h"
#include "db/wide/wide_column_serialization.h"
namespace ROCKSDB_NAMESPACE {
Status BlobGarbageMeter::ProcessInFlow(const Slice& key, const Slice& value) {
return ProcessFlow(key, value, /*is_inflow=*/true);
}
uint64_t blob_file_number = kInvalidBlobFileNumber;
uint64_t bytes = 0;
Status BlobGarbageMeter::ProcessOutFlow(const Slice& key, const Slice& value) {
return ProcessFlow(key, value, /*is_inflow=*/false);
}
Status BlobGarbageMeter::GetBlobReferenceDetails(const ParsedInternalKey& ikey,
const BlobIndex& blob_index,
uint64_t* blob_file_number,
uint64_t* bytes) {
assert(blob_file_number);
assert(*blob_file_number == kInvalidBlobFileNumber);
assert(bytes);
assert(*bytes == 0);
if (blob_index.IsInlined() || blob_index.HasTTL()) {
return Status::Corruption("Unexpected TTL/inlined blob index");
const Status s = Parse(key, value, &blob_file_number, &bytes);
if (!s.ok()) {
return s;
}
*blob_file_number = blob_index.file_number();
*bytes =
blob_index.size() +
BlobLogRecord::CalculateAdjustmentForRecordHeader(ikey.user_key.size());
if (blob_file_number == kInvalidBlobFileNumber) {
return Status::OK();
}
flows_[blob_file_number].AddInFlow(bytes);
return Status::OK();
}
Status BlobGarbageMeter::ParseBlobIndexReference(const ParsedInternalKey& ikey,
const Slice& value,
uint64_t* blob_file_number,
uint64_t* bytes) {
Status BlobGarbageMeter::ProcessOutFlow(const Slice& key, const Slice& value) {
uint64_t blob_file_number = kInvalidBlobFileNumber;
uint64_t bytes = 0;
const Status s = Parse(key, value, &blob_file_number, &bytes);
if (!s.ok()) {
return s;
}
if (blob_file_number == kInvalidBlobFileNumber) {
return Status::OK();
}
// Note: in order to measure the amount of additional garbage, we only need to
// track the outflow for preexisting files, i.e. those that also had inflow.
// (Newly written files would only have outflow.)
auto it = flows_.find(blob_file_number);
if (it == flows_.end()) {
return Status::OK();
}
it->second.AddOutFlow(bytes);
return Status::OK();
}
Status BlobGarbageMeter::Parse(const Slice& key, const Slice& value,
uint64_t* blob_file_number, uint64_t* bytes) {
assert(blob_file_number);
assert(*blob_file_number == kInvalidBlobFileNumber);
assert(bytes);
assert(*bytes == 0);
ParsedInternalKey ikey;
{
constexpr bool log_err_key = false;
const Status s = ParseInternalKey(key, &ikey, log_err_key);
if (!s.ok()) {
return s;
}
}
if (ikey.type != kTypeBlobIndex) {
return Status::OK();
}
@@ -63,71 +85,16 @@ Status BlobGarbageMeter::ParseBlobIndexReference(const ParsedInternalKey& ikey,
}
}
return GetBlobReferenceDetails(ikey, blob_index, blob_file_number, bytes);
}
void BlobGarbageMeter::AddFlow(uint64_t blob_file_number, uint64_t bytes,
bool is_inflow) {
if (is_inflow) {
flows_[blob_file_number].AddInFlow(bytes);
return;
if (blob_index.IsInlined() || blob_index.HasTTL()) {
return Status::Corruption("Unexpected TTL/inlined blob index");
}
// Note: in order to measure the amount of additional garbage, we only need to
// track the outflow for preexisting files, i.e. those that also had inflow.
// (Newly written files would only have outflow.)
auto it = flows_.find(blob_file_number);
if (it != flows_.end()) {
it->second.AddOutFlow(bytes);
}
}
*blob_file_number = blob_index.file_number();
*bytes =
blob_index.size() +
BlobLogRecord::CalculateAdjustmentForRecordHeader(ikey.user_key.size());
Status BlobGarbageMeter::ProcessFlow(const Slice& key, const Slice& value,
bool is_inflow) {
ParsedInternalKey ikey;
{
constexpr bool log_err_key = false;
const Status s = ParseInternalKey(key, &ikey, log_err_key);
if (!s.ok()) {
return s;
}
}
uint64_t blob_file_number = kInvalidBlobFileNumber;
uint64_t bytes = 0;
if (Status s =
ParseBlobIndexReference(ikey, value, &blob_file_number, &bytes);
!s.ok()) {
return s;
}
if (blob_file_number != kInvalidBlobFileNumber) {
AddFlow(blob_file_number, bytes, is_inflow);
return Status::OK();
}
return ProcessEntityBlobReferences(ikey, value, is_inflow);
}
Status BlobGarbageMeter::ProcessEntityBlobReferences(
const ParsedInternalKey& ikey, const Slice& value, bool is_inflow) {
if (ikey.type != kTypeWideColumnEntity) {
return Status::OK();
}
return WideColumnSerialization::ForEachBlobFileNumber(
value, [&](const BlobIndex& blob_index) -> Status {
uint64_t file_number = kInvalidBlobFileNumber;
uint64_t blob_bytes = 0;
if (const Status s = GetBlobReferenceDetails(ikey, blob_index,
&file_number, &blob_bytes);
!s.ok()) {
return s;
}
AddFlow(file_number, blob_bytes, is_inflow);
return Status::OK();
});
return Status::OK();
}
} // namespace ROCKSDB_NAMESPACE
+2 -15
View File
@@ -15,8 +15,6 @@
namespace ROCKSDB_NAMESPACE {
class BlobIndex;
struct ParsedInternalKey;
class Slice;
// A class that can be used to compute the amount of additional garbage
@@ -95,19 +93,8 @@ class BlobGarbageMeter {
}
private:
static Status GetBlobReferenceDetails(const ParsedInternalKey& ikey,
const BlobIndex& blob_index,
uint64_t* blob_file_number,
uint64_t* bytes);
static Status ParseBlobIndexReference(const ParsedInternalKey& ikey,
const Slice& value,
uint64_t* blob_file_number,
uint64_t* bytes);
void AddFlow(uint64_t blob_file_number, uint64_t bytes, bool is_inflow);
Status ProcessFlow(const Slice& key, const Slice& value, bool is_inflow);
Status ProcessEntityBlobReferences(const ParsedInternalKey& ikey,
const Slice& value, bool is_inflow);
static Status Parse(const Slice& key, const Slice& value,
uint64_t* blob_file_number, uint64_t* bytes);
std::unordered_map<uint64_t, BlobInOutFlow> flows_;
};
-74
View File
@@ -11,23 +11,10 @@
#include "db/blob/blob_index.h"
#include "db/blob/blob_log_format.h"
#include "db/dbformat.h"
#include "db/wide/wide_column_serialization.h"
#include "test_util/testharness.h"
namespace ROCKSDB_NAMESPACE {
namespace {
BlobIndex MakeBlobIndex(uint64_t file_number, uint64_t offset, uint64_t size) {
std::string encoded;
BlobIndex::EncodeBlob(&encoded, file_number, offset, size, kNoCompression);
BlobIndex blob_index;
EXPECT_OK(blob_index.DecodeFrom(encoded));
return blob_index;
}
} // namespace
TEST(BlobGarbageMeterTest, MeasureGarbage) {
BlobGarbageMeter blob_garbage_meter;
@@ -201,67 +188,6 @@ TEST(BlobGarbageMeterTest, InlinedTTLBlobIndex) {
ASSERT_NOK(blob_garbage_meter.ProcessOutFlow(key_slice, value_slice));
}
TEST(BlobGarbageMeterTest, WideColumnEntity) {
constexpr char user_key[] = "entity_key";
constexpr SequenceNumber seq = 123;
const InternalKey key(user_key, seq, kTypeWideColumnEntity);
const Slice key_slice = key.Encode();
const std::vector<std::pair<std::string, std::string>> columns = {
{"", "default_inline"},
{"blob_attr", "blob_inline"},
{"ttl", "00000001"},
};
const BlobIndex default_blob = MakeBlobIndex(4, 123, 555);
const BlobIndex attr_blob = MakeBlobIndex(5, 456, 777);
std::string inflow_value;
ASSERT_OK(WideColumnSerialization::SerializeV2(
columns, {{0, default_blob}, {1, attr_blob}}, inflow_value));
std::string outflow_value;
ASSERT_OK(WideColumnSerialization::SerializeV2(columns, {{0, default_blob}},
outflow_value));
BlobGarbageMeter blob_garbage_meter;
ASSERT_OK(blob_garbage_meter.ProcessInFlow(key_slice, inflow_value));
ASSERT_OK(blob_garbage_meter.ProcessOutFlow(key_slice, outflow_value));
const auto& flows = blob_garbage_meter.flows();
ASSERT_EQ(flows.size(), 2);
constexpr size_t kUserKeySize = sizeof(user_key) - 1;
const uint64_t default_bytes =
default_blob.size() +
BlobLogRecord::CalculateAdjustmentForRecordHeader(kUserKeySize);
const uint64_t attr_bytes =
attr_blob.size() +
BlobLogRecord::CalculateAdjustmentForRecordHeader(kUserKeySize);
{
const auto it = flows.find(default_blob.file_number());
ASSERT_NE(it, flows.end());
ASSERT_EQ(it->second.GetInFlow().GetCount(), 1);
ASSERT_EQ(it->second.GetInFlow().GetBytes(), default_bytes);
ASSERT_EQ(it->second.GetOutFlow().GetCount(), 1);
ASSERT_EQ(it->second.GetOutFlow().GetBytes(), default_bytes);
ASSERT_FALSE(it->second.HasGarbage());
}
{
const auto it = flows.find(attr_blob.file_number());
ASSERT_NE(it, flows.end());
ASSERT_EQ(it->second.GetInFlow().GetCount(), 1);
ASSERT_EQ(it->second.GetInFlow().GetBytes(), attr_bytes);
ASSERT_EQ(it->second.GetOutFlow().GetCount(), 0);
ASSERT_EQ(it->second.GetOutFlow().GetBytes(), 0);
ASSERT_TRUE(it->second.HasGarbage());
ASSERT_EQ(it->second.GetGarbageCount(), 1);
ASSERT_EQ(it->second.GetGarbageBytes(), attr_bytes);
}
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
-12
View File
@@ -137,18 +137,6 @@ class BlobIndex {
return oss.str();
}
// Encode this blob index into dst based on its type.
void EncodeTo(std::string* dst) const {
if (IsInlined()) {
EncodeInlinedTTL(dst, expiration_, value_);
} else if (HasTTL()) {
EncodeBlobTTL(dst, expiration_, file_number_, offset_, size_,
compression_);
} else {
EncodeBlob(dst, file_number_, offset_, size_, compression_);
}
}
static void EncodeInlinedTTL(std::string* dst, uint64_t expiration,
const Slice& value) {
assert(dst != nullptr);
+4 -17
View File
@@ -148,26 +148,13 @@ struct BlobLogRecord {
// Checks whether a blob offset is potentially valid or not.
inline bool IsValidBlobOffset(uint64_t value_offset, uint64_t key_size,
uint64_t value_size, uint64_t file_size,
bool has_footer = true) {
constexpr uint64_t kMinPrefix =
BlobLogHeader::kSize + BlobLogRecord::kHeaderSize;
if (value_offset < kMinPrefix) {
return false;
}
if (value_offset - kMinPrefix < key_size) {
uint64_t value_size, uint64_t file_size) {
if (value_offset <
BlobLogHeader::kSize + BlobLogRecord::kHeaderSize + key_size) {
return false;
}
const uint64_t footer_size = has_footer ? BlobLogFooter::kSize : 0;
if (file_size < footer_size) {
return false;
}
const uint64_t max_value_end = file_size - footer_size;
if (value_size > max_value_end) {
return false;
}
if (value_offset > max_value_end - value_size) {
if (value_offset + value_size + BlobLogFooter::kSize > file_size) {
return false;
}
-130
View File
@@ -20,21 +20,6 @@
namespace ROCKSDB_NAMESPACE {
namespace {
Status AppendBlobRefreshRetryFailure(const Status& stale_status,
const Status& retry_status) {
assert(stale_status.IsCorruption());
assert(!retry_status.ok());
if (retry_status.IsCorruption()) {
return retry_status;
}
return Status::CopyAppendMessage(
stale_status, "; refresh retry failed: ", retry_status.ToString());
}
} // namespace
BlobSource::BlobSource(const ImmutableOptions& immutable_options,
const MutableCFOptions& mutable_cf_options,
const std::string& db_id,
@@ -246,38 +231,6 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
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();
}
}
if (!s.ok()) {
return s;
}
@@ -444,89 +397,6 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
blob_file_reader.GetValue()->MultiGetBlob(read_options, allocator,
_blob_reqs, &_bytes_read);
bool needs_reader_refresh = false;
for (const auto& blob_req : _blob_reqs) {
BlobReadRequest* const req = blob_req.first;
assert(req != nullptr);
assert(req->status != nullptr);
if (req->status->IsCorruption()) {
needs_reader_refresh = true;
break;
}
}
if (needs_reader_refresh) {
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()) {
for (const auto& blob_req : _blob_reqs) {
BlobReadRequest* const req = blob_req.first;
assert(req != nullptr);
assert(req->status != nullptr);
if (req->status->IsCorruption()) {
*req->status = AppendBlobRefreshRetryFailure(*req->status, s);
}
}
return;
}
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
retry_blob_reqs;
autovector<Status> stale_statuses;
for (auto& blob_req : _blob_reqs) {
BlobReadRequest* const req = blob_req.first;
assert(req != nullptr);
assert(req->status != nullptr);
if (!req->status->IsCorruption()) {
continue;
}
stale_statuses.emplace_back(*req->status);
*req->status = Status::OK();
blob_req.second.reset();
retry_blob_reqs.emplace_back(req, std::unique_ptr<BlobContents>());
}
uint64_t refreshed_bytes_read = 0;
fresh_reader->MultiGetBlob(read_options, allocator, retry_blob_reqs,
&refreshed_bytes_read);
_bytes_read += refreshed_bytes_read;
bool install_fresh_reader = false;
for (size_t i = 0; i < retry_blob_reqs.size(); ++i) {
auto& retried_blob_req = retry_blob_reqs[i];
BlobReadRequest* const retried_req = retried_blob_req.first;
assert(retried_req != nullptr);
if (retried_req->status->ok()) {
install_fresh_reader = true;
} else {
*retried_req->status = AppendBlobRefreshRetryFailure(
stale_statuses[i], *retried_req->status);
}
for (auto& blob_req : _blob_reqs) {
if (blob_req.first != retried_req) {
continue;
}
blob_req.second = std::move(retried_blob_req.second);
break;
}
}
if (install_fresh_reader) {
CacheHandleGuard<BlobFileReader> ignored_reader;
blob_file_cache_
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
.PermitUncheckedError();
}
}
if (blob_cache_ && read_options.fill_cache) {
// If filling cache is allowed and a cache is configured, try to put
// the blob(s) to the cache.
-150
View File
@@ -1044,156 +1044,6 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromCache) {
}
}
TEST_F(BlobSourceTest, GetBlobPreservesCorruptionDetailsWhenRefreshOpenFails) {
options_.cf_paths.emplace_back(
test::PerThreadDBPath(env_,
"BlobSourceTest_GetBlobPreservesCorruptionDetails"),
0);
DestroyAndReopen(options_);
ImmutableOptions immutable_options(options_);
MutableCFOptions mutable_cf_options(options_);
constexpr uint32_t column_family_id = 1;
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range;
constexpr uint64_t blob_file_number = 1;
const std::string key_str = "key0";
const std::string blob_str = "blob0";
std::vector<Slice> keys{Slice(key_str)};
std::vector<Slice> blobs{Slice(blob_str)};
std::vector<uint64_t> blob_offsets(1);
std::vector<uint64_t> blob_sizes(1);
const uint64_t file_size = BlobLogHeader::kSize + BlobLogRecord::kHeaderSize +
key_str.size() + blob_str.size() +
BlobLogFooter::kSize;
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
expiration_range, blob_file_number, keys, blobs, kNoCompression,
blob_offsets, blob_sizes);
constexpr size_t capacity = 1024;
std::shared_ptr<Cache> backing_cache = NewLRUCache(capacity);
FileOptions file_options;
std::unique_ptr<BlobFileCache> blob_file_cache =
std::make_unique<BlobFileCache>(
backing_cache.get(), &immutable_options, &file_options,
column_family_id, nullptr /*HistogramImpl*/, nullptr /*IOTracer*/);
BlobSource blob_source(immutable_options, mutable_cf_options, db_id_,
db_session_id_, blob_file_cache.get());
ReadOptions read_options;
read_options.verify_checksums = true;
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
PinnableSlice value;
uint64_t bytes_read = 0;
ASSERT_OK(blob_source.GetBlob(
read_options, keys[0], blob_file_number, blob_offsets[0], file_size,
blob_sizes[0], kNoCompression, prefetch_buffer, &value, &bytes_read));
ASSERT_EQ(value, blobs[0]);
const std::string blob_file_path =
BlobFileName(immutable_options.cf_paths.front().path, blob_file_number);
ASSERT_OK(env_->DeleteFile(blob_file_path));
PinnableSlice invalid_value;
bytes_read = 0;
Status s =
blob_source.GetBlob(read_options, keys[0], blob_file_number, file_size,
file_size, blob_sizes[0], kNoCompression,
prefetch_buffer, &invalid_value, &bytes_read);
ASSERT_TRUE(s.IsCorruption());
ASSERT_EQ(bytes_read, 0);
const std::string status_str = s.ToString();
ASSERT_NE(status_str.find("Invalid blob offset"), std::string::npos);
ASSERT_NE(status_str.find("refresh retry failed"), std::string::npos);
ASSERT_NE(status_str.find("IO error"), std::string::npos);
}
TEST_F(BlobSourceTest,
MultiGetBlobPreservesCorruptionDetailsWhenRefreshOpenFails) {
options_.cf_paths.emplace_back(
test::PerThreadDBPath(
env_, "BlobSourceTest_MultiGetBlobPreservesCorruptionDetails"),
0);
DestroyAndReopen(options_);
ImmutableOptions immutable_options(options_);
MutableCFOptions mutable_cf_options(options_);
constexpr uint32_t column_family_id = 1;
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range;
constexpr uint64_t blob_file_number = 1;
const std::string key_str = "key0";
const std::string blob_str = "blob0";
std::vector<Slice> keys{Slice(key_str)};
std::vector<Slice> blobs{Slice(blob_str)};
std::vector<uint64_t> blob_offsets(1);
std::vector<uint64_t> blob_sizes(1);
const uint64_t file_size = BlobLogHeader::kSize + BlobLogRecord::kHeaderSize +
key_str.size() + blob_str.size() +
BlobLogFooter::kSize;
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
expiration_range, blob_file_number, keys, blobs, kNoCompression,
blob_offsets, blob_sizes);
constexpr size_t capacity = 1024;
std::shared_ptr<Cache> backing_cache = NewLRUCache(capacity);
FileOptions file_options;
std::unique_ptr<BlobFileCache> blob_file_cache =
std::make_unique<BlobFileCache>(
backing_cache.get(), &immutable_options, &file_options,
column_family_id, nullptr /*HistogramImpl*/, nullptr /*IOTracer*/);
BlobSource blob_source(immutable_options, mutable_cf_options, db_id_,
db_session_id_, blob_file_cache.get());
ReadOptions read_options;
read_options.verify_checksums = true;
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
PinnableSlice value;
uint64_t bytes_read = 0;
ASSERT_OK(blob_source.GetBlob(
read_options, keys[0], blob_file_number, blob_offsets[0], file_size,
blob_sizes[0], kNoCompression, prefetch_buffer, &value, &bytes_read));
ASSERT_EQ(value, blobs[0]);
const std::string blob_file_path =
BlobFileName(immutable_options.cf_paths.front().path, blob_file_number);
ASSERT_OK(env_->DeleteFile(blob_file_path));
std::array<Status, 1> statuses;
std::array<PinnableSlice, 1> values;
autovector<BlobReadRequest> blob_reqs;
blob_reqs.emplace_back(keys[0], file_size, blob_sizes[0], kNoCompression,
&values[0], &statuses[0]);
bytes_read = 0;
blob_source.MultiGetBlobFromOneFile(read_options, blob_file_number, file_size,
blob_reqs, &bytes_read);
ASSERT_TRUE(statuses[0].IsCorruption());
ASSERT_EQ(bytes_read, 0);
const std::string status_str = statuses[0].ToString();
ASSERT_NE(status_str.find("Invalid blob offset"), std::string::npos);
ASSERT_NE(status_str.find("refresh retry failed"), std::string::npos);
ASSERT_NE(status_str.find("IO error"), std::string::npos);
}
class BlobSecondaryCacheTest : public DBTestBase {
protected:
public:
-316
View File
@@ -1,316 +0,0 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// 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 "db/blob/blob_write_batch_transformer.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_index.h"
#include "db/blob/blob_log_format.h"
#include "db/wide/wide_column_serialization.h"
#include "db/write_batch_internal.h"
#include "port/likely.h"
namespace ROCKSDB_NAMESPACE {
BlobWriteBatchTransformer::BlobWriteBatchTransformer(
const BlobPartitionManagerProvider& partition_mgr_provider,
WriteBatch* output_batch,
const BlobDirectWriteSettingsProvider& settings_provider,
const WriteOptions& write_options)
: partition_mgr_provider_(partition_mgr_provider),
output_batch_(output_batch),
settings_provider_(settings_provider),
write_options_(write_options) {
assert(partition_mgr_provider_);
assert(output_batch_);
assert(settings_provider_);
}
Status BlobWriteBatchTransformer::TransformBatch(
const WriteOptions& write_options, WriteBatch* input_batch,
WriteBatch* output_batch,
const BlobPartitionManagerProvider& partition_mgr_provider,
const BlobDirectWriteSettingsProvider& settings_provider, bool* transformed,
std::vector<BlobFilePartitionManager*>* used_managers,
std::vector<RollbackInfo>* rollback_infos) {
assert(input_batch);
assert(output_batch);
assert(transformed);
output_batch->Clear();
*transformed = false;
BlobWriteBatchTransformer transformer(partition_mgr_provider, output_batch,
settings_provider, write_options);
Status s = input_batch->Iterate(&transformer);
*transformed = transformer.HasTransformed();
if (used_managers) {
used_managers->assign(transformer.used_managers_.begin(),
transformer.used_managers_.end());
}
if (rollback_infos) {
*rollback_infos = std::move(transformer.rollback_infos_);
}
return s;
}
Status BlobWriteBatchTransformer::MaybePreprocessWideColumns(
const WriteOptions& write_options, uint32_t column_family_id,
const Slice& key, const WideColumns& columns,
BlobFilePartitionManager* partition_mgr,
const BlobDirectWriteSettings& settings, bool serialize_inline_entity,
std::string* entity, bool* transformed,
std::vector<RollbackInfo>* rollback_infos) {
assert(entity != nullptr);
assert(transformed != nullptr);
*transformed = false;
if (partition_mgr == nullptr || !settings.enable_blob_direct_write) {
if (!serialize_inline_entity) {
return Status::OK();
}
entity->clear();
return WideColumnSerialization::Serialize(columns, *entity);
}
std::vector<std::pair<size_t, BlobIndex>> new_blob_columns;
new_blob_columns.reserve(columns.size());
std::string blob_index_buf;
bool has_entity_partition = false;
uint32_t entity_partition = 0;
for (size_t column_idx = 0; column_idx < columns.size(); ++column_idx) {
const Slice& column_value = columns[column_idx].value();
if (column_value.size() < settings.min_blob_size) {
continue;
}
if (!has_entity_partition) {
entity_partition = partition_mgr->SelectWideColumnPartition(
column_family_id, key, columns);
has_entity_partition = true;
}
uint64_t blob_file_number = 0;
uint64_t blob_offset = 0;
uint64_t blob_size = 0;
Status s = partition_mgr->WriteBlob(
write_options, column_family_id, settings.compression_type, key,
column_value, &blob_file_number, &blob_offset, &blob_size, settings,
&entity_partition);
if (!s.ok()) {
return s;
}
if (rollback_infos != nullptr) {
const uint64_t record_bytes =
BlobLogRecord::kHeaderSize + key.size() + blob_size;
rollback_infos->push_back(
{partition_mgr, blob_file_number, /*count=*/1, record_bytes});
}
BlobIndex blob_index;
BlobIndex::EncodeBlob(&blob_index_buf, blob_file_number, blob_offset,
blob_size, settings.compression_type);
s = blob_index.DecodeFrom(blob_index_buf);
if (!s.ok()) {
return s;
}
new_blob_columns.emplace_back(column_idx, blob_index);
}
if (new_blob_columns.empty()) {
if (!serialize_inline_entity) {
return Status::OK();
}
entity->clear();
return WideColumnSerialization::Serialize(columns, *entity);
}
entity->clear();
Status s =
WideColumnSerialization::SerializeV2(columns, new_blob_columns, *entity);
if (s.ok()) {
*transformed = true;
}
return s;
}
Status BlobWriteBatchTransformer::PutCF(uint32_t column_family_id,
const Slice& key, const Slice& value) {
// Use cached settings/manager for the same CF to avoid per-entry lookup.
if (column_family_id != cached_cf_id_) {
cached_settings_ = settings_provider_(column_family_id);
cached_partition_mgr_ = partition_mgr_provider_(column_family_id);
cached_cf_id_ = column_family_id;
}
const auto& settings = cached_settings_;
if (!cached_partition_mgr_ || !settings.enable_blob_direct_write ||
value.size() < settings.min_blob_size) {
return WriteBatchInternal::Put(output_batch_, column_family_id, key, value);
}
uint64_t blob_file_number = 0;
uint64_t blob_offset = 0;
uint64_t blob_size = 0;
Status s = cached_partition_mgr_->WriteBlob(
write_options_, column_family_id, settings.compression_type, key, value,
&blob_file_number, &blob_offset, &blob_size, settings);
if (!s.ok()) {
return s;
}
used_managers_.insert(cached_partition_mgr_);
// Track the exact file so stale transformed attempts can rollback
// per-file rather than smearing bytes across all partitions at seal time.
const uint64_t record_bytes =
BlobLogRecord::kHeaderSize + key.size() + blob_size;
rollback_infos_.push_back(
{cached_partition_mgr_, blob_file_number, /*count=*/1, record_bytes});
BlobIndex::EncodeBlob(&blob_index_buf_, blob_file_number, blob_offset,
blob_size, settings.compression_type);
has_transformed_ = true;
return WriteBatchInternal::PutBlobIndex(output_batch_, column_family_id, key,
blob_index_buf_);
}
Status BlobWriteBatchTransformer::TimedPutCF(uint32_t column_family_id,
const Slice& key,
const Slice& value,
uint64_t write_time) {
// TimedPut: pass through without blob separation for now.
return WriteBatchInternal::TimedPut(output_batch_, column_family_id, key,
value, write_time);
}
Status BlobWriteBatchTransformer::PutEntityCF(uint32_t column_family_id,
const Slice& key,
const Slice& entity) {
if (column_family_id != cached_cf_id_) {
cached_settings_ = settings_provider_(column_family_id);
cached_partition_mgr_ = partition_mgr_provider_(column_family_id);
cached_cf_id_ = column_family_id;
}
const auto& settings = cached_settings_;
if (!cached_partition_mgr_ || !settings.enable_blob_direct_write) {
return WriteBatchInternal::PutEntitySerialized(
output_batch_, column_family_id, key, entity);
}
Slice entity_ref = entity;
std::vector<WideColumn> columns;
std::vector<std::pair<size_t, BlobIndex>> existing_blob_columns;
Status status = WideColumnSerialization::DeserializeV2(entity_ref, columns,
existing_blob_columns);
if (status.ok()) {
// Reject pre-serialized entities that already contain blob references.
// Passing them through would preserve references to blob files outside this
// batch's direct-write lifetime tracking, which can later turn into stale
// reads after blob GC.
if (UNLIKELY(!existing_blob_columns.empty())) {
return Status::NotSupported(
"Blob direct write does not support pre-serialized wide entities "
"with blob references");
}
std::string rewritten_entity;
bool transformed = false;
status = MaybePreprocessWideColumns(
write_options_, column_family_id, key, columns, cached_partition_mgr_,
settings, /*serialize_inline_entity=*/false, &rewritten_entity,
&transformed, &rollback_infos_);
if (status.ok()) {
if (!transformed) {
return WriteBatchInternal::PutEntitySerialized(
output_batch_, column_family_id, key, entity);
}
has_transformed_ = true;
used_managers_.insert(cached_partition_mgr_);
return WriteBatchInternal::PutEntitySerialized(
output_batch_, column_family_id, key, rewritten_entity);
}
}
return status;
}
Status BlobWriteBatchTransformer::DeleteCF(uint32_t column_family_id,
const Slice& key) {
return WriteBatchInternal::Delete(output_batch_, column_family_id, key);
}
Status BlobWriteBatchTransformer::SingleDeleteCF(uint32_t column_family_id,
const Slice& key) {
return WriteBatchInternal::SingleDelete(output_batch_, column_family_id, key);
}
Status BlobWriteBatchTransformer::DeleteRangeCF(uint32_t column_family_id,
const Slice& begin_key,
const Slice& end_key) {
return WriteBatchInternal::DeleteRange(output_batch_, column_family_id,
begin_key, end_key);
}
Status BlobWriteBatchTransformer::MergeCF(uint32_t column_family_id,
const Slice& key,
const Slice& value) {
return WriteBatchInternal::Merge(output_batch_, column_family_id, key, value);
}
Status BlobWriteBatchTransformer::PutBlobIndexCF(uint32_t column_family_id,
const Slice& key,
const Slice& value) {
// Already a blob index -- pass through unchanged.
return WriteBatchInternal::PutBlobIndex(output_batch_, column_family_id, key,
value);
}
void BlobWriteBatchTransformer::LogData(const Slice& blob) {
output_batch_->PutLogData(blob).PermitUncheckedError();
}
Status BlobWriteBatchTransformer::MarkBeginPrepare(bool unprepared) {
return WriteBatchInternal::InsertBeginPrepare(
output_batch_, !unprepared /* write_after_commit */, unprepared);
}
Status BlobWriteBatchTransformer::MarkEndPrepare(const Slice& xid) {
return WriteBatchInternal::InsertEndPrepare(output_batch_, xid);
}
Status BlobWriteBatchTransformer::MarkCommit(const Slice& xid) {
return WriteBatchInternal::MarkCommit(output_batch_, xid);
}
Status BlobWriteBatchTransformer::MarkCommitWithTimestamp(const Slice& xid,
const Slice& ts) {
return WriteBatchInternal::MarkCommitWithTimestamp(output_batch_, xid, ts);
}
Status BlobWriteBatchTransformer::MarkRollback(const Slice& xid) {
return WriteBatchInternal::MarkRollback(output_batch_, xid);
}
Status BlobWriteBatchTransformer::MarkNoop(bool /*empty_batch*/) {
return WriteBatchInternal::InsertNoop(output_batch_);
}
} // namespace ROCKSDB_NAMESPACE
-176
View File
@@ -1,176 +0,0 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#pragma once
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "rocksdb/advanced_options.h"
#include "rocksdb/compression_type.h"
#include "rocksdb/options.h"
#include "rocksdb/rocksdb_namespace.h"
#include "rocksdb/slice.h"
#include "rocksdb/status.h"
#include "rocksdb/write_batch.h"
#include "util/hash_containers.h"
namespace ROCKSDB_NAMESPACE {
class BlobFilePartitionManager;
class Cache;
// Callback to look up per-CF blob settings.
struct BlobDirectWriteSettings {
// Whether this column family should attempt direct-write blob separation.
bool enable_blob_direct_write = false;
// Minimum value size eligible for direct-write blob separation.
uint64_t min_blob_size = 0;
// Compression to use for newly written blob records.
CompressionType compression_type = kNoCompression;
// Compression options for newly written blob records.
CompressionOptions compression_opts;
// Raw pointer -- the Cache is owned by ColumnFamilyOptions and outlives all
// settings snapshots. Using raw avoids 2 atomic ref-count ops per Put().
Cache* blob_cache = nullptr;
// Blob-cache prepopulation policy for direct-write records.
PrepopulateBlobCache prepopulate_blob_cache = PrepopulateBlobCache::kDisable;
};
using BlobDirectWriteSettingsProvider =
std::function<BlobDirectWriteSettings(uint32_t column_family_id)>;
// Callback to look up per-CF partition manager.
using BlobPartitionManagerProvider =
std::function<BlobFilePartitionManager*(uint32_t column_family_id)>;
// Transforms a WriteBatch by writing large values directly to blob files
// and replacing them with BlobIndex entries. Non-qualifying entries
// (small values, deletes, merges, etc.) are passed through unchanged.
class BlobWriteBatchTransformer : public WriteBatch::Handler {
public:
struct RollbackInfo {
// Manager that owns the file updated during transform.
BlobFilePartitionManager* partition_mgr = nullptr;
// Blob file number written during transform.
uint64_t file_number = 0;
// Number of blob records appended to that file.
uint64_t count = 0;
// Number of bytes appended to that file.
uint64_t bytes = 0;
};
// Builds the final serialized entity for one PutEntity write.
//
// `columns` must already be sorted by column name. When direct-write blob
// separation is enabled and one or more columns meet `min_blob_size`, this
// writes those columns to blob files and returns a V2 entity containing
// BlobIndex references. Otherwise, if `serialize_inline_entity` is true, it
// returns a normal V1 entity with all columns inline.
//
// `rollback_infos`, when provided, is appended with the exact blob writes
// performed before the method returns, even if it later fails, so callers
// can account those bytes as initial garbage.
static Status MaybePreprocessWideColumns(
const WriteOptions& write_options, uint32_t column_family_id,
const Slice& key, const WideColumns& columns,
BlobFilePartitionManager* partition_mgr,
const BlobDirectWriteSettings& settings, bool serialize_inline_entity,
std::string* entity, bool* transformed,
std::vector<RollbackInfo>* rollback_infos = nullptr);
// Constructs a per-batch transformer. Most callers should use
// TransformBatch() instead of driving the handler directly.
BlobWriteBatchTransformer(
const BlobPartitionManagerProvider& partition_mgr_provider,
WriteBatch* output_batch,
const BlobDirectWriteSettingsProvider& settings_provider,
const WriteOptions& write_options);
// Transform a WriteBatch. If no values qualify for blob separation,
// output_batch will be empty and the caller should use the original batch.
// If any values are separated, output_batch contains the full transformed
// batch. used_managers (if non-null) receives the set of partition managers
// that had data written to them, so the caller can flush/sync them.
// rollback_infos (if non-null) receives the exact file/count/byte writes so
// a failed transformed attempt can be accounted as initial garbage. Partial
// results are returned even if this method fails after some blob writes.
static Status TransformBatch(
const WriteOptions& write_options, WriteBatch* input_batch,
WriteBatch* output_batch,
const BlobPartitionManagerProvider& partition_mgr_provider,
const BlobDirectWriteSettingsProvider& settings_provider,
bool* transformed,
std::vector<BlobFilePartitionManager*>* used_managers = nullptr,
std::vector<RollbackInfo>* rollback_infos = nullptr);
// WriteBatch::Handler overrides
Status PutCF(uint32_t column_family_id, const Slice& key,
const Slice& value) override;
Status TimedPutCF(uint32_t column_family_id, const Slice& key,
const Slice& value, uint64_t write_time) override;
Status PutEntityCF(uint32_t column_family_id, const Slice& key,
const Slice& entity) override;
Status DeleteCF(uint32_t column_family_id, const Slice& key) override;
Status SingleDeleteCF(uint32_t column_family_id, const Slice& key) override;
Status DeleteRangeCF(uint32_t column_family_id, const Slice& begin_key,
const Slice& end_key) override;
Status MergeCF(uint32_t column_family_id, const Slice& key,
const Slice& value) override;
Status PutBlobIndexCF(uint32_t column_family_id, const Slice& key,
const Slice& value) override;
void LogData(const Slice& blob) override;
Status MarkBeginPrepare(bool unprepared = false) override;
Status MarkEndPrepare(const Slice& xid) override;
Status MarkCommit(const Slice& xid) override;
Status MarkCommitWithTimestamp(const Slice& xid, const Slice& ts) override;
Status MarkRollback(const Slice& xid) override;
Status MarkNoop(bool empty_batch) override;
// Returns true once at least one value has been rewritten to a BlobIndex.
bool HasTransformed() const { return has_transformed_; }
private:
// Callback to look up the partition manager for a given column family ID.
BlobPartitionManagerProvider partition_mgr_provider_;
// Output batch that receives transformed entries (BlobIndex for qualifying
// values, passthrough for everything else).
WriteBatch* output_batch_;
// Callback to look up blob direct write settings for a given CF ID.
BlobDirectWriteSettingsProvider settings_provider_;
// Write options from the caller, forwarded to WriteBlob calls.
const WriteOptions& write_options_;
// True once at least one value has been separated into a blob file.
bool has_transformed_ = false;
// Reusable buffer for encoding BlobIndex entries (avoids per-Put alloc).
std::string blob_index_buf_;
// Per-batch cache of the last CF's settings and manager, avoiding
// redundant provider lookups when consecutive entries share the same CF.
uint32_t cached_cf_id_ = UINT32_MAX;
// Cached settings for `cached_cf_id_`.
BlobDirectWriteSettings cached_settings_;
// Cached partition manager for `cached_cf_id_`.
BlobFilePartitionManager* cached_partition_mgr_ = nullptr;
// Set of partition managers that received data during this batch,
// returned to the caller so it can flush/sync them.
UnorderedSet<BlobFilePartitionManager*> used_managers_;
// Exact blob writes performed during this batch. We only aggregate these
// entries if rollback is needed so the normal path keeps minimal overhead.
std::vector<RollbackInfo> rollback_infos_;
};
} // namespace ROCKSDB_NAMESPACE
-222
View File
@@ -3,29 +3,17 @@
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include <algorithm>
#include <array>
#include <set>
#include <sstream>
#include <string>
#include "cache/compressed_secondary_cache.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_index.h"
#include "db/blob/blob_log_format.h"
#include "db/blob/blob_log_sequential_reader.h"
#include "db/column_family.h"
#include "db/db_test_util.h"
#include "db/db_with_timestamp_test_util.h"
#include "file/filename.h"
#include "file/random_access_file_reader.h"
#include "port/stack_trace.h"
#include "rocksdb/convenience.h"
#include "rocksdb/trace_reader_writer.h"
#include "rocksdb/trace_record.h"
#include "rocksdb/utilities/replayer.h"
#include "test_util/sync_point.h"
#include "util/compression.h"
#include "utilities/fault_injection_env.h"
namespace ROCKSDB_NAMESPACE {
@@ -63,43 +51,6 @@ TEST_F(DBBlobBasicTest, GetBlob) {
.IsIncomplete());
}
TEST_F(DBBlobBasicTest, EmptyValueNotStoredAsBlob) {
// Regression test for crash when empty blob value is evicted to
// CompressedSecondaryCache (T261142690). Empty values should always be
// stored inline in the SST, never as blobs, even with min_blob_size=0.
// A BlobIndex for an empty value is strictly larger than the value itself,
// so storing it as a blob is pure overhead.
Options options = GetDefaultOptions();
options.enable_blob_files = true;
options.min_blob_size = 0;
options.disable_auto_compactions = true;
Reopen(options);
// Write an empty value and a non-empty value.
ASSERT_OK(Put("empty_key", ""));
constexpr char blob_value[] = "blob_value";
ASSERT_OK(Put("nonempty_key", blob_value));
ASSERT_OK(Flush());
// Both values should be readable.
ASSERT_EQ(Get("empty_key"), "");
ASSERT_EQ(Get("nonempty_key"), blob_value);
// The empty value should be stored inline (readable from block cache
// without blob file I/O), while the non-empty value requires blob I/O.
ReadOptions ro;
ro.read_tier = kBlockCacheTier;
PinnableSlice result;
ASSERT_OK(db_->Get(ro, db_->DefaultColumnFamily(), "empty_key", &result));
ASSERT_EQ(result, "");
result.Reset();
ASSERT_TRUE(db_->Get(ro, db_->DefaultColumnFamily(), "nonempty_key", &result)
.IsIncomplete());
}
TEST_F(DBBlobBasicTest, GetBlobFromCache) {
Options options = GetDefaultOptions();
@@ -1620,47 +1571,6 @@ TEST_P(DBBlobBasicIOErrorTest, GetBlob_IOError) {
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_P(DBBlobBasicIOErrorTest, GetEntityMergeWithBlobBaseIOError) {
// Goal: verify GetEntity preserves injected blob-read IOErrors when merge
// reads a blob-backed base value, instead of laundering them into Corruption.
// The test writes a blob-backed base value plus a merge operand, then injects
// an IOError at blob read time and checks both GetEntity and Get see it.
Options options;
options.env = fault_injection_env_.get();
options.enable_blob_files = true;
options.min_blob_size = 0;
options.merge_operator = MergeOperators::CreateStringAppendOperator();
Reopen(options);
constexpr char key[] = "key";
constexpr char base_value[] = "base_value";
ASSERT_OK(Put(key, base_value));
ASSERT_OK(Flush());
ASSERT_OK(Merge(key, "merge_operand"));
ASSERT_OK(Flush());
SyncPoint::GetInstance()->SetCallBack(sync_point_, [this](void* /* arg */) {
fault_injection_env_->SetFilesystemActive(false,
Status::IOError(sync_point_));
});
SyncPoint::GetInstance()->EnableProcessing();
PinnableWideColumns entity_result;
Status s = db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), key,
&entity_result);
ASSERT_TRUE(s.IsIOError()) << "Expected IOError but got: " << s.ToString();
PinnableSlice get_result;
s = db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &get_result);
ASSERT_TRUE(s.IsIOError()) << "Expected IOError but got: " << s.ToString();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_P(DBBlobBasicIOErrorMultiGetTest, MultiGetBlobs_IOError) {
Options options = GetDefaultOptions();
options.env = fault_injection_env_.get();
@@ -2549,138 +2459,6 @@ TEST_F(DBBlobWithTimestampTest, IterateBlobs) {
}
}
TEST_F(DBBlobBasicTest, GetApproximateSizesIncludingBlobFiles) {
Options options = GetDefaultOptions();
options.enable_blob_files = true;
options.min_blob_size = 0;
Reopen(options);
// Write some key-value pairs with blob values and flush to create blob files.
constexpr int kNumKeys = 1000;
constexpr int kValueSize = 1024;
Random rnd(301);
for (int i = 0; i < kNumKeys; ++i) {
ASSERT_OK(Put(Key(i), rnd.RandomString(kValueSize)));
}
ASSERT_OK(Flush());
// Verify blob files exist.
std::vector<std::string> files;
ASSERT_OK(env_->GetChildren(dbname_, &files));
bool has_blob_files = false;
for (const auto& f : files) {
if (f.size() > 5 && f.substr(f.size() - 5) == ".blob") {
has_blob_files = true;
break;
}
}
ASSERT_TRUE(has_blob_files);
// Query the full range - all keys are covered.
std::string start = Key(0);
std::string end = Key(kNumKeys);
Range r(start, end);
// Without include_blob_files (default behavior): should not include blob
// file sizes.
uint64_t size_without_blobs = 0;
{
SizeApproximationOptions size_approx_options;
size_approx_options.include_files = true;
size_approx_options.include_blob_files = false;
ASSERT_OK(db_->GetApproximateSizes(size_approx_options,
db_->DefaultColumnFamily(), &r, 1,
&size_without_blobs));
ASSERT_GT(size_without_blobs, 0);
}
// With include_blob_files: should be strictly larger.
{
SizeApproximationOptions size_approx_options;
size_approx_options.include_files = true;
size_approx_options.include_blob_files = true;
uint64_t size_with_blobs = 0;
ASSERT_OK(db_->GetApproximateSizes(size_approx_options,
db_->DefaultColumnFamily(), &r, 1,
&size_with_blobs));
ASSERT_GT(size_with_blobs, size_without_blobs);
}
// Range that doesn't overlap any data should return 0.
{
std::string no_start = Key(kNumKeys + 100);
std::string no_end = Key(kNumKeys + 200);
Range no_r(no_start, no_end);
SizeApproximationOptions size_approx_options;
size_approx_options.include_files = true;
size_approx_options.include_blob_files = true;
uint64_t no_size = 0;
ASSERT_OK(db_->GetApproximateSizes(
size_approx_options, db_->DefaultColumnFamily(), &no_r, 1, &no_size));
ASSERT_EQ(no_size, 0);
}
// Partial range should return proportionally less blob size than full range.
{
SizeApproximationOptions size_approx_options;
size_approx_options.include_files = true;
size_approx_options.include_blob_files = true;
uint64_t full_size = 0;
ASSERT_OK(db_->GetApproximateSizes(
size_approx_options, db_->DefaultColumnFamily(), &r, 1, &full_size));
// Query roughly the first half of keys.
std::string half_end = Key(kNumKeys / 2);
Range half_r(start, half_end);
uint64_t half_size = 0;
ASSERT_OK(db_->GetApproximateSizes(size_approx_options,
db_->DefaultColumnFamily(), &half_r, 1,
&half_size));
ASSERT_GT(half_size, 0);
ASSERT_LT(half_size, full_size);
}
// Via SizeApproximationFlags API.
{
uint64_t size_flags = 0;
ASSERT_OK(db_->GetApproximateSizes(
db_->DefaultColumnFamily(), &r, 1, &size_flags,
DB::SizeApproximationFlags::INCLUDE_FILES |
DB::SizeApproximationFlags::INCLUDE_BLOB_FILES));
ASSERT_GT(size_flags, size_without_blobs);
}
// Multi-range query: two non-overlapping sub-ranges should sum to
// approximately the full-range result.
{
SizeApproximationOptions size_approx_options;
size_approx_options.include_files = true;
size_approx_options.include_blob_files = true;
std::string mid = Key(kNumKeys / 2);
std::string r1_start = Key(0);
std::string r1_end = mid;
std::string r2_start = mid;
std::string r2_end = Key(kNumKeys);
Range ranges[2] = {Range(r1_start, r1_end), Range(r2_start, r2_end)};
uint64_t sizes[2] = {0, 0};
ASSERT_OK(db_->GetApproximateSizes(
size_approx_options, db_->DefaultColumnFamily(), ranges, 2, sizes));
// Each sub-range should return a positive size.
ASSERT_GT(sizes[0], 0);
ASSERT_GT(sizes[1], 0);
// Sum of sub-ranges should be close to the full-range result.
uint64_t full_size = 0;
ASSERT_OK(db_->GetApproximateSizes(
size_approx_options, db_->DefaultColumnFamily(), &r, 1, &full_size));
ASSERT_NEAR(static_cast<double>(sizes[0] + sizes[1]),
static_cast<double>(full_size), full_size * 0.1);
}
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+8 -74
View File
@@ -13,10 +13,7 @@
#include <deque>
#include <vector>
#include "db/blob/blob_counting_iterator.h"
#include "db/blob/blob_file_builder.h"
#include "db/blob/blob_garbage_meter.h"
#include "db/column_family.h"
#include "db/compaction/compaction_iterator.h"
#include "db/dbformat.h"
#include "db/event_helpers.h"
@@ -90,8 +87,7 @@ Status BuildTable(
Env::WriteLifeTimeHint write_hint, const std::string* full_history_ts_low,
BlobFileCompletionCallback* blob_callback, Version* version,
uint64_t* memtable_payload_bytes, uint64_t* memtable_garbage_bytes,
InternalStats::CompactionStats* flush_stats,
std::vector<BlobFileGarbage>* blob_file_garbages, bool fast_sst_open) {
InternalStats::CompactionStats* flush_stats) {
assert((tboptions.column_family_id ==
TablePropertiesCollectorFactory::Context::kUnknownColumnFamily) ==
tboptions.column_family_name.empty());
@@ -103,24 +99,7 @@ Status BuildTable(
/*enable_hash=*/paranoid_file_checks);
Status s;
meta->fd.file_size = 0;
if (blob_file_garbages != nullptr) {
blob_file_garbages->clear();
}
InternalIterator* input = iter;
std::unique_ptr<BlobGarbageMeter> blob_garbage_meter;
std::unique_ptr<BlobCountingIterator> blob_counting_iter;
if (blob_file_garbages != nullptr) {
// Flush can drop blob-backed records via overwrite elision and flush-time
// compaction filters. Track input and surviving output refs just like the
// compaction path so the manifest learns about the new garbage.
blob_garbage_meter.reset(new BlobGarbageMeter());
blob_counting_iter.reset(
new BlobCountingIterator(iter, blob_garbage_meter.get()));
input = blob_counting_iter.get();
}
input->SeekToFirst();
iter->SeekToFirst();
std::unique_ptr<CompactionRangeDelAggregator> range_del_agg(
new CompactionRangeDelAggregator(&tboptions.internal_comparator,
snapshots, full_history_ts_low));
@@ -149,7 +128,7 @@ Status BuildTable(
TableProperties tp;
bool table_file_created = false;
if (input->Valid() || !range_del_agg->IsEmpty()) {
if (iter->Valid() || !range_del_agg->IsEmpty()) {
std::unique_ptr<CompactionFilter> compaction_filter;
if (ioptions.compaction_filter_factory != nullptr &&
ioptions.compaction_filter_factory->ShouldFilterTableFileCreation(
@@ -232,7 +211,7 @@ Status BuildTable(
const std::atomic<bool> kManualCompactionCanceledFalse{false};
CompactionIterator c_iter(
input, ucmp, &merge, kMaxSequenceNumber, &snapshots, earliest_snapshot,
iter, ucmp, &merge, kMaxSequenceNumber, &snapshots, earliest_snapshot,
earliest_write_conflict_snapshot, job_snapshot, snapshot_checker, env,
ShouldReportDetailedTime(env, ioptions.stats), range_del_agg.get(),
blob_file_builder.get(), ioptions.allow_data_in_errors,
@@ -240,17 +219,7 @@ Status BuildTable(
/*manual_compaction_canceled=*/kManualCompactionCanceledFalse,
true /* must_count_input_entries */,
/*compaction=*/nullptr, compaction_filter.get(),
/*shutting_down=*/nullptr, db_options.info_log, full_history_ts_low,
std::nullopt, version, tboptions.read_options.io_activity);
if (version != nullptr) {
ColumnFamilyData* const cfd = version->cfd();
c_iter.SetBlobFetcher(
version, cfd != nullptr ? cfd->blob_file_cache() : nullptr,
tboptions.read_options.io_activity,
tboptions.reason == TableFileCreationReason::kFlush ||
tboptions.reason == TableFileCreationReason::kRecovery);
}
/*shutting_down=*/nullptr, db_options.info_log, full_history_ts_low);
SequenceNumber smallest_preferred_seqno = kMaxSequenceNumber;
std::string key_after_flush_buf;
@@ -299,14 +268,6 @@ Status BuildTable(
flush_stats->num_output_records++;
}
if (blob_garbage_meter != nullptr) {
s = blob_garbage_meter->ProcessOutFlow(key_after_flush,
value_after_flush);
if (!s.ok()) {
break;
}
}
s = meta->UpdateBoundaries(key_after_flush, value_after_flush,
ikey.sequence, ikey.type);
if (!s.ok()) {
@@ -361,8 +322,6 @@ Status BuildTable(
}
TEST_SYNC_POINT("BuildTable:BeforeFinishBuildTable");
TEST_SYNC_POINT_CALLBACK("BuildTable:BeforeCheckEmpty",
static_cast<TableBuilder*>(builder));
const bool empty = builder->IsEmpty();
if (flush_stats) {
assert(c_iter.HasNumInputEntryScanned());
@@ -371,14 +330,6 @@ Status BuildTable(
}
if (!s.ok() || empty) {
builder->Abandon();
// Propagate the builder's error when the builder is empty due to an
// internal error (e.g., write fault injection causing all Add() calls to
// return early). Without this, `s` would remain OK and the downstream
// key count validation in flush_job.cc would produce a misleading
// Corruption error instead of the actual builder error.
if (s.ok() && !builder->status().ok()) {
s = builder->status();
}
} else {
SeqnoToTimeMapping relevant_mapping;
if (seqno_to_time_mapping) {
@@ -496,8 +447,6 @@ Status BuildTable(
// here because this is a special case after we finish the table building.
// No matter whether use_direct_io_for_flush_and_compaction is true,
// the goal is to cache it here for further user reads.
std::string* file_open_metadata_ptr =
fast_sst_open ? &meta->file_open_metadata : nullptr;
std::unique_ptr<InternalIterator> it(table_cache->NewIterator(
tboptions.read_options, file_options, tboptions.internal_comparator,
*meta, nullptr /* range_del_agg */, mutable_cf_options, nullptr,
@@ -508,10 +457,7 @@ Status BuildTable(
MaxFileSizeForL0MetaPin(mutable_cf_options),
/*smallest_compaction_key=*/nullptr,
/*largest_compaction_key*/ nullptr,
/*allow_unprepared_value*/ false,
/*range_del_read_seqno=*/nullptr,
/*range_del_iter=*/nullptr,
/*maybe_pin_table_handle=*/false, file_open_metadata_ptr));
/*allow_unprepared_value*/ false));
s = it->status();
if (s.ok() && paranoid_file_checks) {
OutputValidator file_validator(tboptions.internal_comparator,
@@ -529,20 +475,8 @@ Status BuildTable(
}
// Check for input iterator errors
if (!input->status().ok()) {
s = input->status();
}
if (s.ok() && blob_file_garbages != nullptr &&
blob_garbage_meter != nullptr) {
for (const auto& pair : blob_garbage_meter->flows()) {
const uint64_t blob_file_number = pair.first;
const BlobGarbageMeter::BlobInOutFlow& flow = pair.second;
if (flow.HasGarbage()) {
blob_file_garbages->emplace_back(
blob_file_number, flow.GetGarbageCount(), flow.GetGarbageBytes());
}
}
if (!iter->status().ok()) {
s = iter->status();
}
if (!s.ok() || meta->fd.GetFileSize() == 0) {
+1 -4
View File
@@ -31,7 +31,6 @@ struct FileMetaData;
class VersionSet;
class BlobFileAddition;
class BlobFileGarbage;
class SnapshotChecker;
class TableCache;
class TableBuilder;
@@ -80,8 +79,6 @@ Status BuildTable(
BlobFileCompletionCallback* blob_callback = nullptr,
Version* version = nullptr, uint64_t* memtable_payload_bytes = nullptr,
uint64_t* memtable_garbage_bytes = nullptr,
InternalStats::CompactionStats* flush_stats = nullptr,
std::vector<BlobFileGarbage>* blob_file_garbages = nullptr,
bool fast_sst_open = false);
InternalStats::CompactionStats* flush_stats = nullptr);
} // namespace ROCKSDB_NAMESPACE
+57 -186
View File
@@ -11,7 +11,6 @@
#include <cstdlib>
#include <map>
#include <memory>
#include <unordered_set>
#include <vector>
@@ -596,6 +595,7 @@ struct rocksdb_slicetransform_t : public SliceTransform {
char* (*transform_)(void*, const char* key, size_t length,
size_t* dst_length);
unsigned char (*in_domain_)(void*, const char* key, size_t length);
unsigned char (*in_range_)(void*, const char* key, size_t length);
~rocksdb_slicetransform_t() override { (*destructor_)(state_); }
@@ -610,6 +610,10 @@ struct rocksdb_slicetransform_t : public SliceTransform {
bool InDomain(const Slice& src) const override {
return (*in_domain_)(state_, src.data(), src.size());
}
bool InRange(const Slice& src) const override {
return (*in_range_)(state_, src.data(), src.size());
}
};
struct rocksdb_universal_compaction_options_t {
@@ -1218,12 +1222,12 @@ char* rocksdb_open_and_compact_with_options(
rocksdb_t* rocksdb_open(const rocksdb_options_t* options, const char* name,
char** errptr) {
std::unique_ptr<DB> dbptr;
if (SaveError(errptr, DB::Open(options->rep, std::string(name), &dbptr))) {
DB* db;
if (SaveError(errptr, DB::Open(options->rep, std::string(name), &db))) {
return nullptr;
}
rocksdb_t* result = new rocksdb_t;
result->rep = dbptr.release();
result->rep = db;
return result;
}
@@ -1243,14 +1247,13 @@ rocksdb_t* rocksdb_open_for_read_only(const rocksdb_options_t* options,
const char* name,
unsigned char error_if_wal_file_exists,
char** errptr) {
std::unique_ptr<DB> dbptr;
if (SaveError(errptr,
DB::OpenForReadOnly(options->rep, std::string(name), &dbptr,
error_if_wal_file_exists))) {
DB* db;
if (SaveError(errptr, DB::OpenForReadOnly(options->rep, std::string(name),
&db, error_if_wal_file_exists))) {
return nullptr;
}
rocksdb_t* result = new rocksdb_t;
result->rep = dbptr.release();
result->rep = db;
return result;
}
@@ -1258,14 +1261,14 @@ rocksdb_t* rocksdb_open_as_secondary(const rocksdb_options_t* options,
const char* name,
const char* secondary_path,
char** errptr) {
std::unique_ptr<DB> dbptr;
DB* db;
if (SaveError(errptr,
DB::OpenAsSecondary(options->rep, std::string(name),
std::string(secondary_path), &dbptr))) {
std::string(secondary_path), &db))) {
return nullptr;
}
rocksdb_t* result = new rocksdb_t;
result->rep = dbptr.release();
result->rep = db;
return result;
}
@@ -1579,11 +1582,11 @@ rocksdb_t* rocksdb_open_and_trim_history(
std::string trim_ts_(trim_ts, trim_tslen);
std::unique_ptr<DB> dbptr;
DB* db;
std::vector<ColumnFamilyHandle*> handles;
if (SaveError(errptr, DB::OpenAndTrimHistory(
DBOptions(db_options->rep), std::string(name),
column_families, &handles, &dbptr, trim_ts_))) {
column_families, &handles, &db, trim_ts_))) {
return nullptr;
}
@@ -1595,7 +1598,7 @@ rocksdb_t* rocksdb_open_and_trim_history(
column_family_handles[i] = c_handle;
}
rocksdb_t* result = new rocksdb_t;
result->rep = dbptr.release();
result->rep = db;
return result;
}
@@ -1611,10 +1614,10 @@ rocksdb_t* rocksdb_open_column_families(
ColumnFamilyOptions(column_family_options[i]->rep));
}
std::unique_ptr<DB> dbptr;
DB* db;
std::vector<ColumnFamilyHandle*> handles;
if (SaveError(errptr, DB::Open(DBOptions(db_options->rep), std::string(name),
column_families, &handles, &dbptr))) {
column_families, &handles, &db))) {
return nullptr;
}
@@ -1626,7 +1629,7 @@ rocksdb_t* rocksdb_open_column_families(
column_family_handles[i] = c_handle;
}
rocksdb_t* result = new rocksdb_t;
result->rep = dbptr.release();
result->rep = db;
return result;
}
@@ -1679,12 +1682,12 @@ rocksdb_t* rocksdb_open_for_read_only_column_families(
ColumnFamilyOptions(column_family_options[i]->rep));
}
std::unique_ptr<DB> dbptr;
DB* db;
std::vector<ColumnFamilyHandle*> handles;
if (SaveError(errptr, DB::OpenForReadOnly(DBOptions(db_options->rep),
std::string(name), column_families,
&handles, &dbptr,
error_if_wal_file_exists))) {
if (SaveError(errptr,
DB::OpenForReadOnly(DBOptions(db_options->rep),
std::string(name), column_families,
&handles, &db, error_if_wal_file_exists))) {
return nullptr;
}
@@ -1696,7 +1699,7 @@ rocksdb_t* rocksdb_open_for_read_only_column_families(
column_family_handles[i] = c_handle;
}
rocksdb_t* result = new rocksdb_t;
result->rep = dbptr.release();
result->rep = db;
return result;
}
@@ -1712,12 +1715,12 @@ rocksdb_t* rocksdb_open_as_secondary_column_families(
std::string(column_family_names[i]),
ColumnFamilyOptions(column_family_options[i]->rep));
}
std::unique_ptr<DB> dbptr;
DB* db;
std::vector<ColumnFamilyHandle*> handles;
if (SaveError(errptr, DB::OpenAsSecondary(
DBOptions(db_options->rep), std::string(name),
std::string(secondary_path), column_families,
&handles, &dbptr))) {
if (SaveError(errptr, DB::OpenAsSecondary(DBOptions(db_options->rep),
std::string(name),
std::string(secondary_path),
column_families, &handles, &db))) {
return nullptr;
}
for (size_t i = 0; i != handles.size(); ++i) {
@@ -1728,7 +1731,7 @@ rocksdb_t* rocksdb_open_as_secondary_column_families(
column_family_handles[i] = c_handle;
}
rocksdb_t* result = new rocksdb_t;
result->rep = dbptr.release();
result->rep = db;
return result;
}
@@ -1759,13 +1762,9 @@ rocksdb_column_family_handle_t* rocksdb_create_column_family(
rocksdb_t* db, const rocksdb_options_t* column_family_options,
const char* column_family_name, char** errptr) {
rocksdb_column_family_handle_t* handle = new rocksdb_column_family_handle_t;
handle->rep = nullptr;
if (SaveError(errptr, db->rep->CreateColumnFamily(
ColumnFamilyOptions(column_family_options->rep),
std::string(column_family_name), &(handle->rep)))) {
delete handle;
return nullptr;
}
SaveError(errptr, db->rep->CreateColumnFamily(
ColumnFamilyOptions(column_family_options->rep),
std::string(column_family_name), &(handle->rep)));
handle->immortal = false;
return handle;
}
@@ -3043,20 +3042,12 @@ class H : public WriteBatch::Handler {
void* state_;
void (*put_)(void*, const char* k, size_t klen, const char* v, size_t vlen);
void (*deleted_)(void*, const char* k, size_t klen);
void (*log_data_)(void*, const char* blob, size_t blob_len);
void Put(const Slice& key, const Slice& value) override {
(*put_)(state_, key.data(), key.size(), value.data(), value.size());
}
void Delete(const Slice& key) override {
(*deleted_)(state_, key.data(), key.size());
}
void LogData(const Slice& blob) override {
if (log_data_) {
(*log_data_)(state_, blob.data(), blob.size());
}
}
Status MarkNoop(bool /* empty_batch */) override { return Status::OK(); }
};
class HCF : public WriteBatch::Handler {
@@ -3067,8 +3058,6 @@ class HCF : public WriteBatch::Handler {
void (*deleted_cf_)(void*, uint32_t cfid, const char* k, size_t klen);
void (*merge_cf_)(void*, uint32_t cfid, const char* k, size_t klen,
const char* v, size_t vlen);
void (*log_data_)(void*, const char* blob, size_t blob_len);
Status PutCF(uint32_t column_family_id, const Slice& key,
const Slice& value) override {
(*put_cf_)(state_, column_family_id, key.data(), key.size(), value.data(),
@@ -3085,12 +3074,6 @@ class HCF : public WriteBatch::Handler {
value.size());
return Status::OK();
}
void LogData(const Slice& blob) override {
if (log_data_) {
(*log_data_)(state_, blob.data(), blob.size());
}
}
Status MarkNoop(bool /* empty_batch */) override { return Status::OK(); }
};
void rocksdb_writebatch_iterate(rocksdb_writebatch_t* b, void* state,
@@ -3102,20 +3085,6 @@ void rocksdb_writebatch_iterate(rocksdb_writebatch_t* b, void* state,
handler.state_ = state;
handler.put_ = put;
handler.deleted_ = deleted;
handler.log_data_ = nullptr;
b->rep.Iterate(&handler);
}
void rocksdb_writebatch_iterate_ld(
rocksdb_writebatch_t* b, void* state,
void (*put)(void*, const char* k, size_t klen, const char* v, size_t vlen),
void (*deleted)(void*, const char* k, size_t klen),
void (*log_data)(void*, const char* blob, size_t blob_len)) {
H handler;
handler.state_ = state;
handler.put_ = put;
handler.deleted_ = deleted;
handler.log_data_ = log_data;
b->rep.Iterate(&handler);
}
@@ -3131,24 +3100,6 @@ void rocksdb_writebatch_iterate_cf(
handler.put_cf_ = put_cf;
handler.deleted_cf_ = deleted_cf;
handler.merge_cf_ = merge_cf;
handler.log_data_ = nullptr;
b->rep.Iterate(&handler);
}
void rocksdb_writebatch_iterate_cf_ld(
rocksdb_writebatch_t* b, void* state,
void (*put_cf)(void*, uint32_t cfid, const char* k, size_t klen,
const char* v, size_t vlen),
void (*deleted_cf)(void*, uint32_t cfid, const char* k, size_t klen),
void (*merge_cf)(void*, uint32_t cfid, const char* k, size_t klen,
const char* v, size_t vlen),
void (*log_data)(void*, const char* blob, size_t blob_len)) {
HCF handler;
handler.state_ = state;
handler.put_cf_ = put_cf;
handler.deleted_cf_ = deleted_cf;
handler.merge_cf_ = merge_cf;
handler.log_data_ = log_data;
b->rep.Iterate(&handler);
}
@@ -3749,11 +3700,6 @@ void rocksdb_block_based_options_set_format_version(
options->rep.format_version = v;
}
void rocksdb_block_based_options_set_separate_key_value_in_data_block(
rocksdb_block_based_table_options_t* options, unsigned char v) {
options->rep.separate_key_value_in_data_block = v;
}
void rocksdb_block_based_options_set_index_type(
rocksdb_block_based_table_options_t* options, int v) {
options->rep.index_type = static_cast<BlockBasedTableOptions::IndexType>(v);
@@ -3765,12 +3711,6 @@ void rocksdb_block_based_options_set_data_block_index_type(
static_cast<BlockBasedTableOptions::DataBlockIndexType>(v);
}
void rocksdb_block_based_options_set_index_block_search_type(
rocksdb_block_based_table_options_t* options, int v) {
options->rep.index_block_search_type =
static_cast<BlockBasedTableOptions::BlockSearchType>(v);
}
void rocksdb_block_based_options_set_data_block_hash_ratio(
rocksdb_block_based_table_options_t* options, double v) {
options->rep.data_block_hash_table_util_ratio = v;
@@ -4357,15 +4297,6 @@ unsigned char rocksdb_options_get_paranoid_checks(rocksdb_options_t* opt) {
return opt->rep.paranoid_checks;
}
void rocksdb_options_set_open_files_async(rocksdb_options_t* opt,
unsigned char v) {
opt->rep.open_files_async = v;
}
unsigned char rocksdb_options_get_open_files_async(rocksdb_options_t* opt) {
return opt->rep.open_files_async;
}
void rocksdb_options_set_db_paths(rocksdb_options_t* opt,
const rocksdb_dbpath_t** dbpath_values,
size_t num_paths) {
@@ -4650,16 +4581,6 @@ uint32_t rocksdb_options_get_memtable_avg_op_scan_flush_trigger(
return opt->rep.memtable_avg_op_scan_flush_trigger;
}
void rocksdb_options_set_min_tombstones_for_range_conversion(
rocksdb_options_t* opt, uint32_t n) {
opt->rep.min_tombstones_for_range_conversion = n;
}
uint32_t rocksdb_options_get_min_tombstones_for_range_conversion(
rocksdb_options_t* opt) {
return opt->rep.min_tombstones_for_range_conversion;
}
void rocksdb_options_enable_statistics(rocksdb_options_t* opt) {
opt->rep.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
}
@@ -4697,6 +4618,16 @@ unsigned char rocksdb_options_get_skip_stats_update_on_db_open(
return opt->rep.skip_stats_update_on_db_open;
}
void rocksdb_options_set_skip_checking_sst_file_sizes_on_db_open(
rocksdb_options_t* opt, unsigned char val) {
opt->rep.skip_checking_sst_file_sizes_on_db_open = val;
}
unsigned char rocksdb_options_get_skip_checking_sst_file_sizes_on_db_open(
rocksdb_options_t* opt) {
return opt->rep.skip_checking_sst_file_sizes_on_db_open;
}
/* Blob Options Settings */
void rocksdb_options_set_enable_blob_files(rocksdb_options_t* opt,
unsigned char val) {
@@ -4759,26 +4690,6 @@ double rocksdb_options_get_blob_gc_force_threshold(rocksdb_options_t* opt) {
return opt->rep.blob_garbage_collection_force_threshold;
}
void rocksdb_options_set_read_triggered_compaction_threshold(
rocksdb_options_t* opt, double val) {
opt->rep.read_triggered_compaction_threshold = val;
}
double rocksdb_options_get_read_triggered_compaction_threshold(
rocksdb_options_t* opt) {
return opt->rep.read_triggered_compaction_threshold;
}
void rocksdb_options_set_max_compaction_trigger_wakeup_seconds(
rocksdb_options_t* opt, uint64_t val) {
opt->rep.max_compaction_trigger_wakeup_seconds = val;
}
uint64_t rocksdb_options_get_max_compaction_trigger_wakeup_seconds(
rocksdb_options_t* opt) {
return opt->rep.max_compaction_trigger_wakeup_seconds;
}
void rocksdb_options_set_blob_compaction_readahead_size(rocksdb_options_t* opt,
uint64_t val) {
opt->rep.blob_compaction_readahead_size = val;
@@ -5834,16 +5745,6 @@ uint64_t rocksdb_perfcontext_metric(rocksdb_perfcontext_t* context,
return rep->internal_range_del_reseek_count;
case rocksdb_block_read_cpu_time:
return rep->block_read_cpu_time;
case rocksdb_data_block_read_byte:
return rep->data_block_read_byte;
case rocksdb_index_block_read_byte:
return rep->index_block_read_byte;
case rocksdb_filter_block_read_byte:
return rep->filter_block_read_byte;
case rocksdb_compression_dict_block_read_byte:
return rep->compression_dict_block_read_byte;
case rocksdb_metadata_block_read_byte:
return rep->metadata_block_read_byte;
default:
break;
}
@@ -6142,6 +6043,11 @@ unsigned char rocksdb_readoptions_get_tailing(rocksdb_readoptions_t* opt) {
return opt->rep.tailing;
}
void rocksdb_readoptions_set_managed(rocksdb_readoptions_t* opt,
unsigned char v) {
opt->rep.managed = v;
}
void rocksdb_readoptions_set_readahead_size(rocksdb_readoptions_t* opt,
size_t v) {
opt->rep.readahead_size = v;
@@ -6942,12 +6848,14 @@ rocksdb_slicetransform_t* rocksdb_slicetransform_create(
char* (*transform)(void*, const char* key, size_t length,
size_t* dst_length),
unsigned char (*in_domain)(void*, const char* key, size_t length),
unsigned char (*in_range)(void*, const char* key, size_t length),
const char* (*name)(void*)) {
rocksdb_slicetransform_t* result = new rocksdb_slicetransform_t;
result->state_ = state;
result->destructor_ = destructor;
result->transform_ = transform;
result->in_domain_ = in_domain;
result->in_range_ = in_range;
result->name_ = name;
return result;
}
@@ -6963,6 +6871,7 @@ struct SliceTransformWrapper : public rocksdb_slicetransform_t {
return rep_->Transform(src);
}
bool InDomain(const Slice& src) const override { return rep_->InDomain(src); }
bool InRange(const Slice& src) const override { return rep_->InRange(src); }
static void DoNothing(void*) {}
};
@@ -7086,27 +6995,6 @@ uint64_t rocksdb_fifo_compaction_options_get_max_table_files_size(
return fifo_opts->rep.max_table_files_size;
}
void rocksdb_fifo_compaction_options_set_max_data_files_size(
rocksdb_fifo_compaction_options_t* fifo_opts, uint64_t size) {
fifo_opts->rep.max_data_files_size = size;
}
uint64_t rocksdb_fifo_compaction_options_get_max_data_files_size(
rocksdb_fifo_compaction_options_t* fifo_opts) {
return fifo_opts->rep.max_data_files_size;
}
void rocksdb_fifo_compaction_options_set_use_kv_ratio_compaction(
rocksdb_fifo_compaction_options_t* fifo_opts,
unsigned char use_kv_ratio_compaction) {
fifo_opts->rep.use_kv_ratio_compaction = use_kv_ratio_compaction;
}
unsigned char rocksdb_fifo_compaction_options_get_use_kv_ratio_compaction(
rocksdb_fifo_compaction_options_t* fifo_opts) {
return fifo_opts->rep.use_kv_ratio_compaction;
}
void rocksdb_fifo_compaction_options_destroy(
rocksdb_fifo_compaction_options_t* fifo_opts) {
delete fifo_opts;
@@ -7580,13 +7468,9 @@ rocksdb_column_family_handle_t* rocksdb_transactiondb_create_column_family(
const rocksdb_options_t* column_family_options,
const char* column_family_name, char** errptr) {
rocksdb_column_family_handle_t* handle = new rocksdb_column_family_handle_t;
handle->rep = nullptr;
if (SaveError(errptr, txn_db->rep->CreateColumnFamily(
ColumnFamilyOptions(column_family_options->rep),
std::string(column_family_name), &(handle->rep)))) {
delete handle;
return nullptr;
}
SaveError(errptr, txn_db->rep->CreateColumnFamily(
ColumnFamilyOptions(column_family_options->rep),
std::string(column_family_name), &(handle->rep)));
handle->immortal = false;
return handle;
}
@@ -8316,11 +8200,6 @@ void rocksdb_transaction_delete_cf(
SaveError(errptr, txn->rep->Delete(column_family->rep, Slice(key, klen)));
}
void rocksdb_transaction_put_log_data(rocksdb_transaction_t* txn,
const char* blob, size_t len) {
txn->rep->PutLogData(Slice(blob, len));
}
// Delete a key outside a transaction
void rocksdb_transactiondb_delete(rocksdb_transactiondb_t* txn_db,
const rocksdb_writeoptions_t* options,
@@ -8691,14 +8570,6 @@ void rocksdb_enable_manual_compaction(rocksdb_t* db) {
db->rep->EnableManualCompaction();
}
void rocksdb_abort_all_compactions(rocksdb_t* db) {
db->rep->AbortAllCompactions();
}
void rocksdb_resume_all_compactions(rocksdb_t* db) {
db->rep->ResumeAllCompactions();
}
rocksdb_statistics_histogram_data_t*
rocksdb_statistics_histogram_data_create() {
return new rocksdb_statistics_histogram_data_t{};
+1 -88
View File
@@ -205,27 +205,6 @@ static void CheckDel(void* ptr, const char* k, size_t klen) {
(*state)++;
}
static void NopPut(void* ptr, const char* k, size_t klen, const char* v,
size_t vlen) {
(void)ptr;
(void)k;
(void)klen;
(void)v;
(void)vlen;
}
static void NopDel(void* ptr, const char* k, size_t klen) {
(void)ptr;
(void)k;
(void)klen;
}
static void CheckLogData(void* ptr, const char* blob, size_t blen) {
CheckEqual("log_blob", blob, blen);
int* found = (int*)ptr;
*found = 1;
}
// Callback from rocksdb_writebatch_iterate_cf()
static void CheckPutCF(void* ptr, uint32_t cfid, const char* k, size_t klen,
const char* v, size_t vlen) {
@@ -2664,10 +2643,6 @@ int main(int argc, char** argv) {
CheckCondition(150 ==
rocksdb_options_get_memtable_avg_op_scan_flush_trigger(o));
rocksdb_options_set_min_tombstones_for_range_conversion(o, 200);
CheckCondition(200 ==
rocksdb_options_get_min_tombstones_for_range_conversion(o));
rocksdb_options_set_ttl(o, 5000);
CheckCondition(5000 == rocksdb_options_get_ttl(o));
@@ -2892,14 +2867,6 @@ int main(int argc, char** argv) {
rocksdb_options_set_blob_gc_force_threshold(o, 0.75);
CheckCondition(0.75 == rocksdb_options_get_blob_gc_force_threshold(o));
rocksdb_options_set_read_triggered_compaction_threshold(o, 0.001);
CheckCondition(0.001 ==
rocksdb_options_get_read_triggered_compaction_threshold(o));
rocksdb_options_set_max_compaction_trigger_wakeup_seconds(o, 3600);
CheckCondition(
3600 == rocksdb_options_get_max_compaction_trigger_wakeup_seconds(o));
rocksdb_options_set_blob_compaction_readahead_size(o, 262144);
CheckCondition(262144 ==
rocksdb_options_get_blob_compaction_readahead_size(o));
@@ -3119,12 +3086,6 @@ int main(int argc, char** argv) {
CheckCondition(150 ==
rocksdb_options_get_memtable_avg_op_scan_flush_trigger(o));
rocksdb_options_set_min_tombstones_for_range_conversion(copy, 1000);
CheckCondition(
1000 == rocksdb_options_get_min_tombstones_for_range_conversion(copy));
CheckCondition(200 ==
rocksdb_options_get_min_tombstones_for_range_conversion(o));
rocksdb_options_set_ttl(copy, 8000);
CheckCondition(8000 == rocksdb_options_get_ttl(copy));
CheckCondition(5000 == rocksdb_options_get_ttl(o));
@@ -3635,14 +3596,6 @@ int main(int argc, char** argv) {
100000 ==
rocksdb_fifo_compaction_options_get_max_table_files_size(fco));
rocksdb_fifo_compaction_options_set_max_data_files_size(fco, 200000);
CheckCondition(
200000 == rocksdb_fifo_compaction_options_get_max_data_files_size(fco));
rocksdb_fifo_compaction_options_set_use_kv_ratio_compaction(fco, 1);
CheckCondition(
1 == rocksdb_fifo_compaction_options_get_use_kv_ratio_compaction(fco));
rocksdb_fifo_compaction_options_destroy(fco);
}
@@ -3889,35 +3842,10 @@ int main(int argc, char** argv) {
CheckMultiGetValues(3, vals, vals_sizes, errs, expected);
}
rocksdb_transaction_put_log_data(txn, "log_blob", 8);
// record sequence number before commit so we can scan the WAL after
rocksdb_t* base_db_ld = rocksdb_transactiondb_get_base_db(txn_db);
uint64_t seq_before = rocksdb_get_latest_sequence_number(base_db_ld);
// commit
rocksdb_transaction_commit(txn, &err);
CheckNoError(err);
// verify log data was written to WAL by scanning batches since seq_before
{
rocksdb_wal_iterator_t* wal_iter =
rocksdb_get_updates_since(base_db_ld, seq_before, NULL, &err);
CheckNoError(err);
int log_found = 0;
for (; rocksdb_wal_iter_valid(wal_iter) && !log_found;
rocksdb_wal_iter_next(wal_iter)) {
uint64_t seq;
rocksdb_writebatch_t* wal_wb =
rocksdb_wal_iter_get_batch(wal_iter, &seq);
rocksdb_writebatch_iterate_ld(wal_wb, &log_found, NopPut, NopDel,
CheckLogData);
rocksdb_writebatch_destroy(wal_wb);
}
CheckCondition(log_found == 1);
rocksdb_wal_iter_destroy(wal_iter);
}
rocksdb_transactiondb_close_base_db(base_db_ld);
// read from outside transaction, after commit
CheckTxnDBGet(txn_db, roptions, "foo", "hello");
CheckTxnDBPinGet(txn_db, roptions, "foo", "hello");
@@ -4519,7 +4447,7 @@ int main(int argc, char** argv) {
StartPhase("statistics");
{
const uint32_t BYTES_WRITTEN_TICKER = 61;
const uint32_t BYTES_WRITTEN_TICKER = 60;
const uint32_t DB_WRITE_HIST = 1;
rocksdb_statistics_histogram_data_t* hist =
@@ -4944,21 +4872,6 @@ int main(int argc, char** argv) {
rocksdb_sst_file_manager_destroy(sst_file_manager);
}
StartPhase("create_column_family_error_returns_null");
{
// Creating a column family with a name that already exists should fail
// and return NULL. Without the fix, the handle is leaked and a non-NULL
// pointer with an indeterminate rep field is returned.
char* cf_err = NULL;
rocksdb_column_family_handle_t* cf_handle =
rocksdb_create_column_family(db, options, "default", &cf_err);
// Should have an error since "default" already exists
CheckCondition(cf_err != NULL);
// The handle should be NULL on error (this is the bug fix)
CheckCondition(cf_handle == NULL);
free(cf_err);
}
StartPhase("cancel_all_background_work");
rocksdb_cancel_all_background_work(db, 1);
+2 -12
View File
@@ -12,7 +12,6 @@ namespace ROCKSDB_NAMESPACE {
void CoalescingIterator::Coalesce(
const autovector<MultiCfIteratorInfo>& items) {
assert(wide_columns_.empty());
assert(owned_columns_.empty());
MinHeap heap;
for (const auto& item : items) {
assert(item.iterator);
@@ -23,22 +22,13 @@ void CoalescingIterator::Coalesce(
if (heap.empty()) {
return;
}
// Own the coalesced bytes so the result stays valid even if a child iterator
// refreshes or reuses its internal buffers.
owned_columns_.reserve(heap.size());
wide_columns_.reserve(heap.size());
auto add_column = [this](const WideColumn& column) {
owned_columns_.emplace_back(column.name().ToString(),
column.value().ToString());
const auto& owned = owned_columns_.back();
wide_columns_.emplace_back(owned.first, owned.second);
};
auto current = heap.top();
heap.pop();
while (!heap.empty()) {
int comparison = current.column->name().compare(heap.top().column->name());
if (comparison < 0) {
add_column(*current.column);
wide_columns_.push_back(*current.column);
} else if (comparison > 0) {
// Shouldn't reach here.
// Current item in the heap is greater than the top item in the min heap
@@ -47,7 +37,7 @@ void CoalescingIterator::Coalesce(
current = heap.top();
heap.pop();
}
add_column(*current.column);
wide_columns_.push_back(*current.column);
if (WideColumnsHelper::HasDefaultColumn(wide_columns_)) {
value_ = WideColumnsHelper::GetDefaultColumn(wide_columns_);
-6
View File
@@ -5,10 +5,6 @@
#pragma once
#include <string>
#include <utility>
#include <vector>
#include "db/multi_cf_iterator_impl.h"
namespace ROCKSDB_NAMESPACE {
@@ -49,7 +45,6 @@ class CoalescingIterator : public Iterator {
void Reset() {
value_.clear();
wide_columns_.clear();
owned_columns_.clear();
}
bool PrepareValue() override { return impl_.PrepareValue(); }
@@ -84,7 +79,6 @@ class CoalescingIterator : public Iterator {
MultiCfIteratorImpl<ResetFunc, PopulateFunc> impl_;
Slice value_;
WideColumns wide_columns_;
std::vector<std::pair<std::string, std::string>> owned_columns_;
struct WideColumnWithOrder {
const WideColumn* column;
+15 -92
View File
@@ -11,14 +11,12 @@
#include <algorithm>
#include <cinttypes>
#include <cmath>
#include <limits>
#include <sstream>
#include <string>
#include <vector>
#include "db/blob/blob_file_cache.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_source.h"
#include "db/compaction/compaction_picker.h"
#include "db/compaction/compaction_picker_fifo.h"
@@ -403,13 +401,7 @@ ColumnFamilyOptions SanitizeCfOptions(const ImmutableDBOptions& db_options,
}
if (result.max_compaction_bytes == 0) {
// For FIFO with use_kv_ratio_compaction, leave max_compaction_bytes as 0
// to signal "auto-calculate target from capacity and SST/blob ratio."
// When explicitly set by the user, it overrides the auto-calculated target.
if (result.compaction_style != kCompactionStyleFIFO ||
!result.compaction_options_fifo.use_kv_ratio_compaction) {
result.max_compaction_bytes = result.target_file_size_base * 25;
}
result.max_compaction_bytes = result.target_file_size_base * 25;
}
bool is_block_based_table = (result.table_factory->IsInstanceOf(
@@ -497,15 +489,6 @@ ColumnFamilyOptions SanitizeCfOptions(const ImmutableDBOptions& db_options,
result.memtable_avg_op_scan_flush_trigger = 0;
}
}
if (result.enable_blob_direct_write && !result.enable_blob_files) {
ROCKS_LOG_WARN(db_options.info_log.get(),
"enable_blob_direct_write requires enable_blob_files=true. "
"Disabling blob direct write.");
result.enable_blob_direct_write = false;
}
if (result.blob_direct_write_partitions == 0) {
result.blob_direct_write_partitions = 1;
}
return result;
}
@@ -612,7 +595,7 @@ ColumnFamilyData::ColumnFamilyData(
const FileOptions* file_options, ColumnFamilySet* column_family_set,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
const std::string& db_session_id, bool read_only, bool fast_sst_open)
const std::string& db_session_id, bool read_only)
: id_(id),
name_(name),
dummy_versions_(_dummy_versions),
@@ -672,7 +655,7 @@ ColumnFamilyData::ColumnFamilyData(
new InternalStats(ioptions_.num_levels, ioptions_.clock, this));
table_cache_.reset(new TableCache(ioptions_, file_options, _table_cache,
block_cache_tracer, io_tracer,
db_session_id, fast_sst_open));
db_session_id));
blob_file_cache_.reset(
new BlobFileCache(_table_cache, &ioptions(), soptions(), id_,
internal_stats_->GetBlobFileReadHist(), io_tracer));
@@ -793,11 +776,6 @@ ColumnFamilyData::~ColumnFamilyData() {
}
}
void ColumnFamilyData::SetBlobPartitionManager(
std::shared_ptr<BlobFilePartitionManager> mgr) {
blob_partition_manager_ = std::move(mgr);
}
bool ColumnFamilyData::UnrefAndTryDelete() {
int old_refs = refs_.fetch_sub(1);
assert(old_refs > 0);
@@ -1539,32 +1517,6 @@ Status ColumnFamilyData::ValidateOptions(
const auto* ucmp = cf_options.comparator;
assert(ucmp);
if (cf_options.enable_blob_direct_write) {
if (db_options.enable_pipelined_write) {
return Status::NotSupported(
"Blob direct write v1 does not support pipelined writes.");
}
if (db_options.allow_concurrent_memtable_write) {
return Status::NotSupported(
"Blob direct write v1 does not support concurrent memtable writes.");
}
if (db_options.unordered_write) {
return Status::NotSupported(
"Blob direct write v1 does not support unordered writes.");
}
if (db_options.two_write_queues) {
return Status::NotSupported(
"Blob direct write v1 does not support two write queues.");
}
if (cf_options.experimental_mempurge_threshold > 0.0) {
return Status::NotSupported(
"Blob direct write does not support MemPurge.");
}
if (ucmp->timestamp_size() > 0) {
return Status::NotSupported(
"Blob direct write does not support user-defined timestamps.");
}
}
if (ucmp->timestamp_size() > 0 &&
!cf_options.persist_user_defined_timestamps) {
if (db_options.atomic_flush) {
@@ -1605,42 +1557,12 @@ Status ColumnFamilyData::ValidateOptions(
}
}
if (cf_options.read_triggered_compaction_threshold < 0.0 ||
std::isnan(cf_options.read_triggered_compaction_threshold) ||
std::isinf(cf_options.read_triggered_compaction_threshold)) {
return Status::InvalidArgument(
"read_triggered_compaction_threshold must be >= 0.0 and finite. "
"Use 0.0 to disable.");
}
if (cf_options.compaction_style == kCompactionStyleFIFO &&
db_options.max_open_files != -1 && cf_options.ttl > 0) {
return Status::NotSupported(
"FIFO compaction only supported with max_open_files = -1.");
}
if (db_options.open_files_async) {
// FIFO TTL picker relies on reading the files table properties inside a
// DB mutex. This can be slow if files are opened asynchronously, so we
// disable it.
//
// TODO: consider blocking fifo compaction until async file open task
// completes.
if (cf_options.compaction_style == kCompactionStyleFIFO) {
return Status::NotSupported(
"FIFO compaction is not supported with open_files_async = true.");
}
// Open files async is not useful if skip_stats_update_on_db_open=true
// because DB open will still block on IO.
//
// TODO: consider moving stats update inside async file open background
// task.
if (!db_options.skip_stats_update_on_db_open) {
return Status::InvalidArgument(
"open_files_async requires skip_stats_update_on_db_open = true.");
}
}
std::vector<uint32_t> supported{0, 1, 2, 4, 8};
if (std::find(supported.begin(), supported.end(),
cf_options.memtable_protection_bytes_per_key) ==
@@ -1809,14 +1731,16 @@ void ColumnFamilyData::RecoverEpochNumbers() {
vstorage->RecoverEpochNumbers(this);
}
ColumnFamilySet::ColumnFamilySet(
const std::string& dbname, const ImmutableDBOptions* db_options,
const FileOptions& file_options, Cache* table_cache,
WriteBufferManager* _write_buffer_manager,
WriteController* _write_controller,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
const std::string& db_session_id, bool fast_sst_open)
ColumnFamilySet::ColumnFamilySet(const std::string& dbname,
const ImmutableDBOptions* db_options,
const FileOptions& file_options,
Cache* table_cache,
WriteBufferManager* _write_buffer_manager,
WriteController* _write_controller,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer,
const std::string& db_id,
const std::string& db_session_id)
: max_column_family_(0),
file_options_(file_options),
dummy_cfd_(new ColumnFamilyData(
@@ -1833,8 +1757,7 @@ ColumnFamilySet::ColumnFamilySet(
block_cache_tracer_(block_cache_tracer),
io_tracer_(io_tracer),
db_id_(db_id),
db_session_id_(db_session_id),
fast_sst_open_(fast_sst_open) {
db_session_id_(db_session_id) {
// initialize linked list
dummy_cfd_->prev_ = dummy_cfd_;
dummy_cfd_->next_ = dummy_cfd_;
@@ -1901,7 +1824,7 @@ ColumnFamilyData* ColumnFamilySet::CreateColumnFamily(
ColumnFamilyData* new_cfd = new ColumnFamilyData(
id, name, dummy_versions, table_cache_, write_buffer_manager_, options,
*db_options_, &file_options_, this, block_cache_tracer_, io_tracer_,
db_id_, db_session_id_, read_only, fast_sst_open_);
db_id_, db_session_id_, read_only);
column_families_.insert({name, id});
column_family_data_.insert({id, new_cfd});
auto ucmp = new_cfd->user_comparator();
+9 -41
View File
@@ -49,7 +49,6 @@ class InstrumentedMutex;
class InstrumentedMutexLock;
struct SuperVersionContext;
class BlobFileCache;
class BlobFilePartitionManager;
class BlobSource;
extern const double kIncSlowdownRatio;
@@ -416,19 +415,6 @@ class ColumnFamilyData {
TableCache* table_cache() const { return table_cache_.get(); }
BlobFileCache* blob_file_cache() const { return blob_file_cache_.get(); }
BlobSource* blob_source() const { return blob_source_.get(); }
// Returns the write-path blob partition manager for this CF, or null if BDW
// is disabled.
BlobFilePartitionManager* blob_partition_manager() const {
return blob_partition_manager_.get();
}
// Returns a shared ownership handle for cold paths that must keep the
// manager alive beyond the lifetime of this ColumnFamilyData.
std::shared_ptr<BlobFilePartitionManager> blob_partition_manager_handle()
const {
return blob_partition_manager_;
}
// Installs the write-path blob partition manager for this CF.
void SetBlobPartitionManager(std::shared_ptr<BlobFilePartitionManager> mgr);
// See documentation in compaction_picker.h
// REQUIRES: DB mutex held
@@ -619,23 +605,16 @@ class ColumnFamilyData {
return ioptions_.cf_allow_ingest_behind || ioptions_.allow_ingest_behind;
}
// Per-CF reader-writer lock that serializes IngestExternalFiles with range
// tombstone conversion.
port::RWMutex& GetIngestSstLock() { return ingest_sst_lock_; }
private:
friend class ColumnFamilySet;
ColumnFamilyData(uint32_t id, const std::string& name,
Version* dummy_versions, Cache* table_cache,
WriteBufferManager* write_buffer_manager,
const ColumnFamilyOptions& options,
const ImmutableDBOptions& db_options,
const FileOptions* file_options,
ColumnFamilySet* column_family_set,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer,
const std::string& db_id, const std::string& db_session_id,
bool read_only, bool fast_sst_open = false);
ColumnFamilyData(
uint32_t id, const std::string& name, Version* dummy_versions,
Cache* table_cache, WriteBufferManager* write_buffer_manager,
const ColumnFamilyOptions& options, const ImmutableDBOptions& db_options,
const FileOptions* file_options, ColumnFamilySet* column_family_set,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
const std::string& db_session_id, bool read_only);
std::vector<std::string> GetDbPaths() const;
@@ -669,8 +648,6 @@ class ColumnFamilyData {
std::unique_ptr<TableCache> table_cache_;
std::unique_ptr<BlobFileCache> blob_file_cache_;
std::unique_ptr<BlobSource> blob_source_;
// Per-CF manager for write-path blob direct-write files.
std::shared_ptr<BlobFilePartitionManager> blob_partition_manager_;
std::unique_ptr<InternalStats> internal_stats_;
@@ -736,10 +713,6 @@ class ColumnFamilyData {
bool mempurge_used_;
std::atomic<uint64_t> next_epoch_number_;
// Used to synchronize IngestExternalFile with range tombstone conversion. See
// also Memtable::ingest_seqno_barrier_.
port::RWMutex ingest_sst_lock_;
};
// ColumnFamilySet has interesting thread-safety requirements
@@ -785,8 +758,7 @@ class ColumnFamilySet {
WriteController* _write_controller,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer,
const std::string& db_id, const std::string& db_session_id,
bool fast_sst_open = false);
const std::string& db_id, const std::string& db_session_id);
~ColumnFamilySet();
ColumnFamilyData* GetDefault() const;
@@ -822,9 +794,6 @@ class ColumnFamilySet {
Cache* get_table_cache() { return table_cache_; }
bool GetFastSstOpen() const { return fast_sst_open_; }
void SetFastSstOpen(bool v) { fast_sst_open_ = v; }
WriteBufferManager* write_buffer_manager() { return write_buffer_manager_; }
WriteController* write_controller() { return write_controller_; }
@@ -873,7 +842,6 @@ class ColumnFamilySet {
std::shared_ptr<IOTracer> io_tracer_;
const std::string& db_id_;
std::string db_session_id_;
bool fast_sst_open_;
};
// A wrapper for ColumnFamilySet that supports releasing DB mutex during each
+13 -46
View File
@@ -118,7 +118,8 @@ class ColumnFamilyTestBase : public testing::Test {
for (int i = 0; i < n; i++) {
if (flush_every != 0 && i != 0 && i % flush_every == 0) {
dbfull()->TEST_FlushMemTable();
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
dbi->TEST_FlushMemTable();
}
int keyi = base + i;
@@ -176,7 +177,8 @@ class ColumnFamilyTestBase : public testing::Test {
}
handles_.clear();
names_.clear();
db_.reset();
delete db_;
db_ = nullptr;
}
Status TryOpen(std::vector<std::string> cf,
@@ -216,7 +218,7 @@ class ColumnFamilyTestBase : public testing::Test {
void Open() { Open({"default"}); }
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_.get()); }
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_); }
int GetProperty(int cf, std::string property) {
std::string value;
@@ -498,7 +500,7 @@ class ColumnFamilyTestBase : public testing::Test {
ColumnFamilyOptions column_family_options_;
DBOptions db_options_;
std::string dbname_;
std::unique_ptr<DB> db_;
DB* db_ = nullptr;
EnvCounter* env_;
std::shared_ptr<Env> env_guard_;
Random rnd_;
@@ -515,7 +517,7 @@ class ColumnFamilyTest
INSTANTIATE_TEST_CASE_P(FormatDef, ColumnFamilyTest,
testing::Values(test::kDefaultFormatVersion));
INSTANTIATE_TEST_CASE_P(FormatLatest, ColumnFamilyTest,
testing::Values(kLatestBbtFormatVersion));
testing::Values(kLatestFormatVersion));
TEST_P(ColumnFamilyTest, DontReuseColumnFamilyID) {
for (int iter = 0; iter < 3; ++iter) {
@@ -705,8 +707,8 @@ INSTANTIATE_TEST_CASE_P(
std::make_tuple(test::kDefaultFormatVersion, false)));
INSTANTIATE_TEST_CASE_P(
FormatLatest, FlushEmptyCFTestWithParam,
testing::Values(std::make_tuple(kLatestBbtFormatVersion, true),
std::make_tuple(kLatestBbtFormatVersion, false)));
testing::Values(std::make_tuple(kLatestFormatVersion, true),
std::make_tuple(kLatestFormatVersion, false)));
TEST_P(ColumnFamilyTest, AddDrop) {
Open();
@@ -734,42 +736,6 @@ TEST_P(ColumnFamilyTest, AddDrop) {
std::vector<std::string>({"default", "four", "three"}));
}
TEST_P(ColumnFamilyTest, EmptyNameRejected) {
// Creating a column family with an empty-string name should be rejected with
// InvalidArgument. Empty strings are reserved by various RocksDB APIs and
// serialization formats to mean "unknown/unspecified column family" (e.g.
// TablePropertiesCollectorFactory::Context::kUnknownColumnFamily, table
// properties).
//
// Prior behavior was silently broken: CreateColumnFamily("") would return
// OK and a usable handle, but the CF was not persisted in the manifest --
// data written to it was lost on DB reopen.
Open();
ColumnFamilyHandle* handle = nullptr;
Status s =
db_->CreateColumnFamily(column_family_options_, std::string(), &handle);
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
ASSERT_EQ(handle, nullptr);
// Same via the bulk-by-names API. The first valid name should still get
// created; the failure should come on the empty one.
std::vector<ColumnFamilyHandle*> handles;
s = db_->CreateColumnFamilies(column_family_options_,
{"ok_name", "", "another"}, &handles);
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
// Clean up the partially-created handle ("ok_name") before the failure.
ASSERT_EQ(handles.size(), 1U);
ASSERT_OK(db_->DropColumnFamily(handles[0]));
ASSERT_OK(db_->DestroyColumnFamilyHandle(handles[0]));
handles.clear();
// Same via the bulk-by-descriptor API.
s = db_->CreateColumnFamilies(
{ColumnFamilyDescriptor("", column_family_options_)}, &handles);
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
ASSERT_TRUE(handles.empty());
}
TEST_P(ColumnFamilyTest, BulkAddDrop) {
constexpr int kNumCF = 1000;
ColumnFamilyOptions cf_options;
@@ -3576,10 +3542,11 @@ TEST_P(ColumnFamilyTest, MultipleCFPathsTest) {
// Re-open and verify the keys.
Reopen({ColumnFamilyOptions(), cf_opt1, cf_opt2});
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
for (int cf = 1; cf != 3; ++cf) {
ReadOptions read_options;
read_options.readahead_size = 0;
auto it = db_->NewIterator(read_options, handles_[cf]);
auto it = dbi->NewIterator(read_options, handles_[cf]);
for (it->SeekToFirst(); it->Valid(); it->Next()) {
ASSERT_OK(it->status());
Slice key(it->key());
@@ -3669,7 +3636,7 @@ TEST(ColumnFamilyTest, ValidateMemtableKVChecksumOption) {
// the behavior of manual flush is that it skips retaining UDTs.
class ColumnFamilyRetainUDTTest : public ColumnFamilyTestBase {
public:
ColumnFamilyRetainUDTTest() : ColumnFamilyTestBase(kLatestBbtFormatVersion) {}
ColumnFamilyRetainUDTTest() : ColumnFamilyTestBase(kLatestFormatVersion) {}
void SetUp() override {
db_options_.allow_concurrent_memtable_write = false;
@@ -3919,7 +3886,7 @@ TEST_F(ManualFlushSkipRetainUDTTest, FlushRemovesStaleEntries) {
static_cast_with_check<ColumnFamilyHandleImpl>(cfh)->cfd();
for (int version = 0; version < 100; version++) {
if (version == 50) {
ASSERT_OK(dbfull()->TEST_SwitchMemtable(cfd));
ASSERT_OK(static_cast_with_check<DBImpl>(db_)->TEST_SwitchMemtable(cfd));
}
ASSERT_OK(
Put(0, "foo", EncodeAsUint64(version), "v" + std::to_string(version)));
+46 -32
View File
@@ -75,9 +75,10 @@ TEST_F(CompactFilesTest, L0ConflictsFiles) {
options.level0_file_num_compaction_trigger = kLevel0Trigger;
options.compression = kNoCompression;
std::unique_ptr<DB> db;
DB* db = nullptr;
ASSERT_OK(DestroyDB(db_name_, options));
ASSERT_OK(DB::Open(options, db_name_, &db));
Status s = DB::Open(options, db_name_, &db);
assert(s.ok());
assert(db);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
@@ -113,6 +114,7 @@ TEST_F(CompactFilesTest, L0ConflictsFiles) {
}
}
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
delete db;
}
TEST_F(CompactFilesTest, MultipleLevel) {
@@ -126,11 +128,11 @@ TEST_F(CompactFilesTest, MultipleLevel) {
FlushedFileCollector* collector = new FlushedFileCollector();
options.listeners.emplace_back(collector);
std::unique_ptr<DB> db;
DB* db = nullptr;
ASSERT_OK(DestroyDB(db_name_, options));
Status s = DB::Open(options, db_name_, &db);
ASSERT_OK(s);
ASSERT_NE(db.get(), nullptr);
ASSERT_NE(db, nullptr);
// create couple files in L0, L3, L4 and L5
for (int i = 5; i > 2; --i) {
@@ -139,8 +141,7 @@ TEST_F(CompactFilesTest, MultipleLevel) {
ASSERT_OK(db->Flush(FlushOptions()));
// Ensure background work is fully finished including listener callbacks
// before accessing listener state.
ASSERT_OK(
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForBackgroundWork());
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForBackgroundWork());
auto l0_files = collector->GetFlushedFiles();
ASSERT_OK(db->CompactFiles(CompactionOptions(), l0_files, i));
@@ -190,6 +191,8 @@ TEST_F(CompactFilesTest, MultipleLevel) {
ASSERT_OK(db->CompactFiles(CompactionOptions(), files, 5));
SyncPoint::GetInstance()->DisableProcessing();
thread.join();
delete db;
}
TEST_F(CompactFilesTest, ObsoleteFiles) {
@@ -209,11 +212,11 @@ TEST_F(CompactFilesTest, ObsoleteFiles) {
FlushedFileCollector* collector = new FlushedFileCollector();
options.listeners.emplace_back(collector);
std::unique_ptr<DB> db;
DB* db = nullptr;
ASSERT_OK(DestroyDB(db_name_, options));
Status s = DB::Open(options, db_name_, &db);
ASSERT_OK(s);
ASSERT_NE(db.get(), nullptr);
ASSERT_NE(db, nullptr);
// create couple files
for (int i = 1000; i < 2000; ++i) {
@@ -223,12 +226,13 @@ TEST_F(CompactFilesTest, ObsoleteFiles) {
auto l0_files = collector->GetFlushedFiles();
ASSERT_OK(db->CompactFiles(CompactionOptions(), l0_files, 1));
ASSERT_OK(static_cast_with_check<DBImpl>(db.get())->TEST_WaitForCompact());
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForCompact());
// verify all compaction input files are deleted
for (const auto& fname : l0_files) {
ASSERT_EQ(Status::NotFound(), env_->FileExists(fname));
}
delete db;
}
TEST_F(CompactFilesTest, NotCutOutputOnLevel0) {
@@ -247,9 +251,10 @@ TEST_F(CompactFilesTest, NotCutOutputOnLevel0) {
FlushedFileCollector* collector = new FlushedFileCollector();
options.listeners.emplace_back(collector);
std::unique_ptr<DB> db;
DB* db = nullptr;
ASSERT_OK(DestroyDB(db_name_, options));
ASSERT_OK(DB::Open(options, db_name_, &db));
Status s = DB::Open(options, db_name_, &db);
assert(s.ok());
assert(db);
// create couple files
@@ -257,20 +262,19 @@ TEST_F(CompactFilesTest, NotCutOutputOnLevel0) {
ASSERT_OK(db->Put(WriteOptions(), std::to_string(i),
std::string(1000, 'a' + (i % 26))));
}
ASSERT_OK(
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForFlushMemTable());
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForFlushMemTable());
auto l0_files_1 = collector->GetFlushedFiles();
collector->ClearFlushedFiles();
for (int i = 0; i < 500; ++i) {
ASSERT_OK(db->Put(WriteOptions(), std::to_string(i),
std::string(1000, 'a' + (i % 26))));
}
ASSERT_OK(
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForFlushMemTable());
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForFlushMemTable());
auto l0_files_2 = collector->GetFlushedFiles();
ASSERT_OK(db->CompactFiles(CompactionOptions(), l0_files_1, 0));
ASSERT_OK(db->CompactFiles(CompactionOptions(), l0_files_2, 0));
// no assertion failure
delete db;
}
TEST_F(CompactFilesTest, CapturingPendingFiles) {
@@ -285,7 +289,7 @@ TEST_F(CompactFilesTest, CapturingPendingFiles) {
FlushedFileCollector* collector = new FlushedFileCollector();
options.listeners.emplace_back(collector);
std::unique_ptr<DB> db;
DB* db = nullptr;
ASSERT_OK(DestroyDB(db_name_, options));
Status s = DB::Open(options, db_name_, &db);
ASSERT_OK(s);
@@ -299,8 +303,7 @@ TEST_F(CompactFilesTest, CapturingPendingFiles) {
// Ensure background work is fully finished including listener callbacks
// before accessing listener state.
ASSERT_OK(
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForBackgroundWork());
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForBackgroundWork());
auto l0_files = collector->GetFlushedFiles();
EXPECT_EQ(5, l0_files.size());
@@ -324,12 +327,13 @@ TEST_F(CompactFilesTest, CapturingPendingFiles) {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
db.reset();
delete db;
// Make sure we can reopen the DB.
s = DB::Open(options, db_name_, &db);
ASSERT_OK(s);
assert(db);
delete db;
}
TEST_F(CompactFilesTest, CompactionFilterWithGetSv) {
@@ -361,12 +365,12 @@ TEST_F(CompactFilesTest, CompactionFilterWithGetSv) {
options.create_if_missing = true;
options.compaction_filter = cf.get();
std::unique_ptr<DB> db;
DB* db = nullptr;
ASSERT_OK(DestroyDB(db_name_, options));
Status s = DB::Open(options, db_name_, &db);
ASSERT_OK(s);
cf->SetDB(db.get());
cf->SetDB(db);
// Write one L0 file
ASSERT_OK(db->Put(WriteOptions(), "K1", "V1"));
@@ -380,6 +384,8 @@ TEST_F(CompactFilesTest, CompactionFilterWithGetSv) {
ASSERT_OK(
db->CompactFiles(ROCKSDB_NAMESPACE::CompactionOptions(), {fname}, 0));
}
delete db;
}
TEST_F(CompactFilesTest, SentinelCompressionType) {
@@ -407,7 +413,7 @@ TEST_F(CompactFilesTest, SentinelCompressionType) {
options.create_if_missing = true;
FlushedFileCollector* collector = new FlushedFileCollector();
options.listeners.emplace_back(collector);
std::unique_ptr<DB> db;
DB* db = nullptr;
ASSERT_OK(DB::Open(options, db_name_, &db));
ASSERT_OK(db->Put(WriteOptions(), "key", "val"));
@@ -415,8 +421,7 @@ TEST_F(CompactFilesTest, SentinelCompressionType) {
// Ensure background work is fully finished including listener callbacks
// before accessing listener state.
ASSERT_OK(
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForBackgroundWork());
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForBackgroundWork());
auto l0_files = collector->GetFlushedFiles();
ASSERT_EQ(1, l0_files.size());
@@ -432,6 +437,7 @@ TEST_F(CompactFilesTest, SentinelCompressionType) {
// compression_name property
ASSERT_EQ("BuiltinV2;02;", name_and_table_props.second->compression_name);
}
delete db;
}
}
@@ -456,7 +462,11 @@ TEST_F(CompactFilesTest, CompressionWithBlockAlign) {
}
std::unique_ptr<DB> db;
ASSERT_OK(DB::Open(options, db_name_, &db));
{
DB* _db = nullptr;
ASSERT_OK(DB::Open(options, db_name_, &_db));
db.reset(_db);
}
ASSERT_OK(db->Put(WriteOptions(), "key", "val"));
ASSERT_OK(db->Flush(FlushOptions()));
@@ -495,7 +505,7 @@ TEST_F(CompactFilesTest, GetCompactionJobInfo) {
FlushedFileCollector* collector = new FlushedFileCollector();
options.listeners.emplace_back(collector);
std::unique_ptr<DB> db;
DB* db = nullptr;
ASSERT_OK(DestroyDB(db_name_, options));
Status s = DB::Open(options, db_name_, &db);
ASSERT_OK(s);
@@ -506,8 +516,7 @@ TEST_F(CompactFilesTest, GetCompactionJobInfo) {
ASSERT_OK(db->Put(WriteOptions(), std::to_string(i),
std::string(1000, 'a' + (i % 26))));
}
ASSERT_OK(
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForFlushMemTable());
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForFlushMemTable());
auto l0_files_1 = collector->GetFlushedFiles();
CompactionOptions co;
co.compression = CompressionType::kLZ4Compression;
@@ -523,6 +532,7 @@ TEST_F(CompactFilesTest, GetCompactionJobInfo) {
ASSERT_EQ(compaction_job_info.output_level, 0);
ASSERT_OK(compaction_job_info.status);
// no assertion failure
delete db;
}
// Helper function to generate zero-padded keys
@@ -538,11 +548,11 @@ TEST_F(CompactFilesTest, TrivialMoveNonOverlappingFiles) {
options.compression = kNoCompression;
options.level_compaction_dynamic_level_bytes = false;
std::unique_ptr<DB> db;
DB* db = nullptr;
ASSERT_OK(DestroyDB(db_name_, options));
Status s = DB::Open(options, db_name_, &db);
ASSERT_OK(s);
ASSERT_NE(db.get(), nullptr);
ASSERT_NE(db, nullptr);
// Create 3 non-overlapping files in L0
// File 1: keys [a00-a99]
@@ -655,6 +665,8 @@ TEST_F(CompactFilesTest, TrivialMoveNonOverlappingFiles) {
ASSERT_OK(db->Get(ReadOptions(), key, &value));
ASSERT_EQ(value, "value_" + key);
}
delete db;
}
TEST_F(CompactFilesTest, TrivialMoveBlockedByOverlap) {
@@ -665,11 +677,11 @@ TEST_F(CompactFilesTest, TrivialMoveBlockedByOverlap) {
options.level_compaction_dynamic_level_bytes = false;
options.num_levels = 7;
std::unique_ptr<DB> db;
DB* db = nullptr;
ASSERT_OK(DestroyDB(db_name_, options));
Status s = DB::Open(options, db_name_, &db);
ASSERT_OK(s);
ASSERT_NE(db.get(), nullptr);
ASSERT_NE(db, nullptr);
// Create a file in L6 with keys [m00-m99] (wide range)
for (int i = 0; i < 100; i++) {
@@ -745,6 +757,8 @@ TEST_F(CompactFilesTest, TrivialMoveBlockedByOverlap) {
ASSERT_OK(db->Get(ReadOptions(), key, &value));
ASSERT_EQ(value, "updated_value_" + key);
}
delete db;
}
} // namespace ROCKSDB_NAMESPACE
+34 -664
View File
@@ -7,14 +7,12 @@
#include <iterator>
#include <limits>
#include <memory>
#include "db/blob/blob_fetcher.h"
#include "db/blob/blob_file_builder.h"
#include "db/blob/blob_index.h"
#include "db/blob/prefetch_buffer_collection.h"
#include "db/snapshot_checker.h"
#include "db/wide/blob_column_resolver_util.h"
#include "db/wide/wide_column_serialization.h"
#include "db/wide/wide_columns_helper.h"
#include "logging/logging.h"
@@ -22,106 +20,8 @@
#include "rocksdb/listener.h"
#include "table/internal_iterator.h"
#include "test_util/sync_point.h"
#include "util/autovector.h"
namespace ROCKSDB_NAMESPACE {
// CompactionBlobResolver implementation
void CompactionBlobResolver::Init(BlobFetcher* blob_fetcher,
PrefetchBufferCollection* prefetch_buffers,
CompactionIterationStats* iter_stats) {
blob_fetcher_ = blob_fetcher;
prefetch_buffers_ = prefetch_buffers;
iter_stats_ = iter_stats;
}
void CompactionBlobResolver::Reset(
const Slice& user_key, const std::vector<WideColumn>* columns,
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns,
bool track_resolve_error) {
user_key_ = user_key;
columns_ = columns;
blob_columns_ = blob_columns;
track_resolve_error_ = track_resolve_error;
resolved_cache_.clear();
resolve_error_.reset();
}
Status CompactionBlobResolver::ResolveColumn(size_t column_index,
Slice* resolved_value) {
Status status = Status::OK();
if (columns_ == nullptr || column_index >= columns_->size()) {
status = Status::InvalidArgument("Column index out of bounds");
} else {
const BlobIndex* blob_index_ptr =
blob_resolver_util::FindBlobColumn(blob_columns_, column_index);
if (blob_index_ptr == nullptr) {
// Not a blob column - return the inline value directly
*resolved_value = (*columns_)[column_index].value();
} else {
// Check if we've already resolved this column
PinnableSlice* cached =
blob_resolver_util::FindInCache(resolved_cache_, column_index);
if (cached != nullptr) {
*resolved_value = Slice(*cached);
} else if (blob_fetcher_ == nullptr) {
status = Status::NotSupported("Blob fetcher not available");
} else {
const BlobIndex& blob_index = *blob_index_ptr;
// Handle inlined blobs (e.g., from legacy stacked BlobDB with TTL)
if (blob_index.IsInlined()) {
*resolved_value = blob_resolver_util::CacheInlinedBlob(
resolved_cache_, column_index, blob_index);
} else {
FilePrefetchBuffer* prefetch_buffer =
prefetch_buffers_ ? prefetch_buffers_->GetOrCreatePrefetchBuffer(
blob_index.file_number())
: nullptr;
uint64_t bytes_read = 0;
resolved_cache_.emplace_back(column_index,
std::make_unique<PinnableSlice>());
auto& new_entry = resolved_cache_.back();
status =
blob_fetcher_->FetchBlob(user_key_, blob_index, prefetch_buffer,
new_entry.second.get(), &bytes_read);
if (!status.ok()) {
resolved_cache_.pop_back();
} else {
if (iter_stats_ != nullptr) {
++iter_stats_->num_blobs_read;
iter_stats_->total_blob_bytes_read += bytes_read;
}
*resolved_value = Slice(*new_entry.second);
}
}
}
}
}
if (!status.ok() && track_resolve_error_ && !resolve_error_.has_value()) {
resolve_error_.emplace(status);
}
return status;
}
bool CompactionBlobResolver::IsBlobColumn(size_t column_index) const {
if (columns_ == nullptr || column_index >= columns_->size()) {
return false;
}
return blob_resolver_util::IsBlobColumnIndex(blob_columns_, column_index);
}
size_t CompactionBlobResolver::NumColumns() const {
if (columns_ == nullptr) {
return 0;
}
return columns_->size();
}
CompactionIterator::CompactionIterator(
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
SequenceNumber last_sequence, std::vector<SequenceNumber>* snapshots,
@@ -138,8 +38,7 @@ CompactionIterator::CompactionIterator(
const std::atomic<bool>* shutting_down,
const std::shared_ptr<Logger> info_log,
const std::string* full_history_ts_low,
std::optional<SequenceNumber> preserve_seqno_min,
const Version* input_version, Env::IOActivity blob_read_io_activity)
std::optional<SequenceNumber> preserve_seqno_min)
: CompactionIterator(
input, cmp, merge_helper, last_sequence, snapshots, earliest_snapshot,
earliest_write_conflict_snapshot, job_snapshot, snapshot_checker, env,
@@ -148,8 +47,7 @@ CompactionIterator::CompactionIterator(
manual_compaction_canceled,
compaction ? std::make_unique<RealCompaction>(compaction) : nullptr,
must_count_input_entries, compaction_filter, shutting_down, info_log,
full_history_ts_low, preserve_seqno_min, input_version,
blob_read_io_activity) {}
full_history_ts_low, preserve_seqno_min) {}
CompactionIterator::CompactionIterator(
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
@@ -167,8 +65,7 @@ CompactionIterator::CompactionIterator(
const std::atomic<bool>* shutting_down,
const std::shared_ptr<Logger> info_log,
const std::string* full_history_ts_low,
std::optional<SequenceNumber> preserve_seqno_min,
const Version* input_version, Env::IOActivity blob_read_io_activity)
std::optional<SequenceNumber> preserve_seqno_min)
: input_(input, cmp, must_count_input_entries),
cmp_(cmp),
merge_helper_(merge_helper),
@@ -183,8 +80,6 @@ CompactionIterator::CompactionIterator(
blob_file_builder_(blob_file_builder),
compaction_(std::move(compaction)),
compaction_filter_(compaction_filter),
filter_supports_v4_(
compaction_filter_ ? compaction_filter_->SupportsFilterV4() : false),
shutting_down_(shutting_down),
manual_compaction_canceled_(manual_compaction_canceled),
bottommost_level_(compaction_ && compaction_->bottommost_level() &&
@@ -203,8 +98,7 @@ CompactionIterator::CompactionIterator(
merge_out_iter_(merge_helper_),
blob_garbage_collection_cutoff_file_number_(
ComputeBlobGarbageCollectionCutoffFileNumber(compaction_.get())),
blob_fetcher_(CreateBlobFetcherIfNeeded(compaction_.get(), input_version,
blob_read_io_activity)),
blob_fetcher_(CreateBlobFetcherIfNeeded(compaction_.get())),
prefetch_buffers_(
CreatePrefetchBufferCollectionIfNeeded(compaction_.get())),
current_key_committed_(false),
@@ -226,8 +120,6 @@ CompactionIterator::CompactionIterator(
timestamp_size_ == full_history_ts_low_->size());
#endif
input_.SetPinnedItersMgr(&pinned_iters_mgr_);
blob_resolver_.Init(blob_fetcher_.get(), prefetch_buffers_.get(),
&iter_stats_);
// The default `merge_until_status_` does not need to be checked since it is
// overwritten as soon as `MergeUntil()` is called
merge_until_status_.PermitUncheckedError();
@@ -239,24 +131,6 @@ CompactionIterator::~CompactionIterator() {
input_.SetPinnedItersMgr(nullptr);
}
void CompactionIterator::SetBlobFetcher(const Version* version,
BlobFileCache* blob_file_cache,
Env::IOActivity io_activity,
bool allow_write_path_fallback) {
if (blob_fetcher_ != nullptr || version == nullptr) {
return;
}
ReadOptions read_options;
read_options.io_activity = io_activity;
read_options.fill_cache = false;
blob_fetcher_ = std::make_unique<BlobFetcher>(
version, read_options, blob_file_cache, allow_write_path_fallback);
blob_resolver_.Init(blob_fetcher_.get(), prefetch_buffers_.get(),
&iter_stats_);
}
void CompactionIterator::ResetRecordCounts() {
iter_stats_.num_record_drop_user = 0;
iter_stats_.num_record_drop_hidden = 0;
@@ -384,10 +258,9 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
compaction_filter_skip_until_.rep());
if (decision == CompactionFilter::Decision::kUndetermined &&
!compaction_filter_->IsStackedBlobDbInternalCompactionFilter()) {
if (!blob_fetcher_) {
status_ = Status::NotSupported(
"Blob-backed value filtering requires blob read support in the "
"current table-file creation context");
if (!compaction_) {
status_ =
Status::Corruption("Unexpected blob index outside of compaction");
validity_info_.Invalidate();
return false;
}
@@ -414,6 +287,8 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
uint64_t bytes_read = 0;
assert(blob_fetcher_);
s = blob_fetcher_->FetchBlob(ikey_.user_key, blob_index,
prefetch_buffer, &blob_value_,
&bytes_read);
@@ -434,19 +309,7 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
const Slice* existing_val = nullptr;
const WideColumns* existing_col = nullptr;
// Reuse member variable to avoid per-key heap allocation.
filter_existing_columns_.clear();
WideColumnBlobResolver* blob_resolver_ptr = nullptr;
// Use member variables for entity column storage to avoid per-key
// heap allocations. Cleared at start of each use.
entity_columns_.clear();
entity_blob_columns_.clear();
// Storage for eagerly resolved blob values used by the FilterV3
// compatibility path. Keep this in the same scope as the FilterV4()
// invocation below because filter_existing_columns_ stores Slices into
// these buffers.
autovector<PinnableSlice, 4> resolved_blobs_storage;
WideColumns existing_columns;
if (ikey_.type != kTypeWideColumnEntity) {
if (!blob_value_.empty()) {
@@ -455,125 +318,23 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
existing_val = &value_;
}
} else {
// Check if entity has blob columns that need resolution
bool has_blob_columns = false;
{
Status s_hbc =
WideColumnSerialization::HasBlobColumns(value_, has_blob_columns);
if (!s_hbc.ok()) {
status_ = s_hbc;
validity_info_.Invalidate();
return false;
}
}
if (UNLIKELY(has_blob_columns)) {
// Entity has blob columns. DeserializeV2() populates
// entity_columns_ with the full column set; blob-backed entries still
// carry the serialized BlobIndex bytes in value(). The companion
// entity_blob_columns_ side list identifies which column indexes are
// blob references and provides the decoded BlobIndex objects.
Slice input_copy = value_;
Status s = WideColumnSerialization::DeserializeV2(
input_copy, entity_columns_, entity_blob_columns_);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return false;
}
// Mark as deserialized so PrepareOutput can skip re-deserialization
// if the filter returns kKeep.
entity_deserialized_ = true;
Slice value_copy = value_;
const Status s =
WideColumnSerialization::Deserialize(value_copy, existing_columns);
// Build filter_existing_columns_ with raw values (blob columns
// have blob index as value, not the actual blob data)
for (size_t i = 0; i < entity_columns_.size(); ++i) {
filter_existing_columns_.emplace_back(entity_columns_[i].name(),
entity_columns_[i].value());
}
if (filter_supports_v4_) {
// Reset member blob resolver for this entity - filter can call
// resolver->ResolveColumn() to fetch blob values on-demand
blob_resolver_.Reset(ikey_.user_key, &entity_columns_,
&entity_blob_columns_,
/*track_resolve_error=*/true);
blob_resolver_ptr = &blob_resolver_;
} else {
// FilterV3 compatibility: eagerly resolve all blob columns so
// the filter sees actual values (not raw BlobIndex bytes).
// resolved_blobs_storage is in the outer scope so the data
// outlives filter_existing_columns_ (which holds Slices into
// it).
resolved_blobs_storage.resize(entity_blob_columns_.size());
for (size_t bi = 0; bi < entity_blob_columns_.size(); ++bi) {
const size_t col_idx = entity_blob_columns_[bi].first;
const BlobIndex& blob_idx = entity_blob_columns_[bi].second;
if (blob_idx.IsInlined()) {
resolved_blobs_storage[bi].PinSelf(blob_idx.value());
} else {
FilePrefetchBuffer* prefetch_buffer =
prefetch_buffers_
? prefetch_buffers_->GetOrCreatePrefetchBuffer(
blob_idx.file_number())
: nullptr;
uint64_t bytes_read = 0;
assert(blob_fetcher_);
Status s_fetch = blob_fetcher_->FetchBlob(
ikey_.user_key, blob_idx, prefetch_buffer,
&resolved_blobs_storage[bi], &bytes_read);
if (!s_fetch.ok()) {
status_ = s_fetch;
validity_info_.Invalidate();
return false;
}
++iter_stats_.num_blobs_read;
iter_stats_.total_blob_bytes_read += bytes_read;
}
// Replace the blob index value in filter_existing_columns_
// with the resolved blob value
filter_existing_columns_[col_idx] =
WideColumn(entity_columns_[col_idx].name(),
Slice(resolved_blobs_storage[bi]));
}
}
} else {
// No blob columns, use fast path
Slice value_copy = value_;
const Status s = WideColumnSerialization::Deserialize(
value_copy, filter_existing_columns_);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return false;
}
}
existing_col = &filter_existing_columns_;
}
// existing_val / existing_col point into value_, blob_value_,
// entity_columns_, and resolved_blobs_storage, all of which stay alive
// until this call returns.
decision = compaction_filter_->FilterV4(
level_, filter_key, value_type, existing_val, existing_col,
&compaction_filter_value_, &new_columns,
compaction_filter_skip_until_.rep(), blob_resolver_ptr);
if (blob_resolver_ptr != nullptr) {
Status resolve_status = blob_resolver_.resolve_status();
if (!resolve_status.ok()) {
// Keep lazy FilterV4 failure semantics aligned with the eager
// FilterV3 compatibility path: if blob resolution fails while the
// filter is inspecting the entry, fail compaction even if the
// filter returned kKeep after noticing the error.
status_ = std::move(resolve_status);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return false;
}
existing_col = &existing_columns;
}
decision = compaction_filter_->FilterV3(
level_, filter_key, value_type, existing_val, existing_col,
&compaction_filter_value_, &new_columns,
compaction_filter_skip_until_.rep());
}
iter_stats_.total_filter_time +=
@@ -581,10 +342,10 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
}
if (decision == CompactionFilter::Decision::kUndetermined) {
// Should not reach here, since FilterV2/V3/V4 should never return
// Should not reach here, since FilterV2/FilterV3 should never return
// kUndetermined.
status_ = Status::NotSupported(
"FilterV2/V3/V4 should never return kUndetermined");
"FilterV2/FilterV3 should never return kUndetermined");
validity_info_.Invalidate();
return false;
}
@@ -593,7 +354,7 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
cmp_->Compare(*compaction_filter_skip_until_.rep(), ikey_.user_key) <=
0) {
// Can't skip to a key smaller than the current one.
// Keep the key as per FilterV2/V3/V4 documentation.
// Keep the key as per FilterV2/FilterV3 documentation.
decision = CompactionFilter::Decision::kKeep;
}
@@ -681,16 +442,6 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
}
value_ = compaction_filter_value_;
// Value changed, force re-deserialization in PrepareOutput
entity_deserialized_ = false;
}
if (UNLIKELY(compaction_filter_value_.size() >
std::numeric_limits<uint32_t>::max())) {
status_ = Status::Corruption(
"CompactionFilter result value size exceeds 4GB limit");
validity_info_.Invalidate();
return false;
}
return true;
@@ -699,14 +450,9 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
void CompactionIterator::NextFromInput() {
at_next_ = false;
validity_info_.Invalidate();
entity_deserialized_ = false;
while (!Valid() && input_.Valid() && !IsPausingManualCompaction() &&
!IsShuttingDown()) {
// A filtered-out key can advance directly to the next input record without
// returning to PrepareOutput(), so clear the per-record deserialization
// cache at the start of each loop iteration.
entity_deserialized_ = false;
key_ = input_.key();
value_ = input_.value();
blob_value_.Reset();
@@ -1520,340 +1266,6 @@ void CompactionIterator::GarbageCollectBlobIfNeeded() {
}
}
// Wide-column entity compaction flow after InvokeFilterIfNeeded():
// 1. PrepareOutput() is the only caller of the helper block below.
// 2. PrepareOutput() ensures entity_columns_/entity_blob_columns_ describe
// the current entity, either by reusing the filter-time deserialization or
// by deserializing once itself.
// 3. PrepareOutput() then picks exactly one follow-up path:
// - inline-only entity:
// ExtractLargeColumnValuesIfNeeded()
// This is the flush/compaction extraction path. It may rewrite large
// inline columns into blob references.
// - entity already containing blob references:
// GarbageCollectEntityBlobsIfNeeded()
// This is the blob-GC path. It only fetches / relocates references
// that fall below the current GC cutoff.
// Both helpers leave value_ unchanged when no rewrite is needed.
void CompactionIterator::ExtractLargeColumnValuesIfNeeded() {
assert(ikey_.type == kTypeWideColumnEntity);
// Check if blob extraction is enabled
if (!blob_file_builder_) {
return;
}
// Deserialize using member variables (already populated by PrepareOutput
// for the combined deserialization path). If not yet populated, deserialize
// here.
if (entity_columns_.empty()) {
Slice entity_slice = value_;
entity_columns_.clear();
entity_blob_columns_.clear();
Status s = WideColumnSerialization::DeserializeV2(
entity_slice, entity_columns_, entity_blob_columns_);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return;
}
}
// IMPORTANT: If there are already blob columns, we skip extraction entirely.
// This means new large inline columns added to such entities (e.g., via
// PutEntity after a previous PutEntity that already had blob columns) will
// NOT be extracted to blob files until the entity undergoes a full rewrite
// (e.g., via compaction that GCs all existing blob refs).
//
// This is a deliberate simplification: supporting partial extraction
// (extracting only new large inline columns while preserving existing
// blob refs) would require merging old and new blob column sets during
// serialization, and the benefit is marginal since full rewrites happen
// naturally during blob GC.
if (UNLIKELY(!entity_blob_columns_.empty())) {
return;
}
// Try to extract large column values to blob files
// Track which columns were extracted and their blob indices
std::vector<std::pair<size_t, BlobIndex>> new_blob_columns;
std::string temp_blob_index;
for (size_t i = 0; i < entity_columns_.size(); ++i) {
const Slice& col_value = entity_columns_[i].value();
// Clear the temporary buffer for each column
temp_blob_index.clear();
// Try to add the column value to blob file
// blob_file_builder_->Add() will check min_blob_size internally
// and only write to blob file if the value is large enough
const Status add_status =
blob_file_builder_->Add(user_key(), col_value, &temp_blob_index);
if (!add_status.ok()) {
status_ = add_status;
validity_info_.Invalidate();
return;
}
// If blob_index is not empty, the value was extracted to a blob file
if (!temp_blob_index.empty()) {
BlobIndex blob_idx;
const Status decode_status = blob_idx.DecodeFrom(temp_blob_index);
if (!decode_status.ok()) {
status_ = decode_status;
validity_info_.Invalidate();
return;
}
new_blob_columns.emplace_back(i, blob_idx);
}
}
// If no columns were extracted, nothing to do
if (new_blob_columns.empty()) {
return;
}
// Re-serialize the entity with blob indices using the Slice-based overload
// to avoid copying column names and values to strings.
entity_wide_columns_.clear();
entity_wide_columns_.reserve(entity_columns_.size());
for (const auto& col : entity_columns_) {
entity_wide_columns_.emplace_back(col.name(), col.value());
}
rewritten_entity_.clear();
const Status serialize_status = WideColumnSerialization::SerializeV2(
entity_wide_columns_, new_blob_columns, rewritten_entity_);
if (!serialize_status.ok()) {
status_ = serialize_status;
validity_info_.Invalidate();
return;
}
// Update value_ to point to the rewritten entity
value_ = rewritten_entity_;
}
bool CompactionIterator::FetchBlobsNeedingGC(
const std::vector<WideColumn>& /*columns*/,
const std::vector<std::pair<size_t, BlobIndex>>& blob_columns,
std::vector<std::pair<size_t, PinnableSlice>>* fetched_blob_values) {
assert(fetched_blob_values != nullptr);
for (const auto& blob_col : blob_columns) {
const size_t col_idx = blob_col.first;
const BlobIndex& blob_index = blob_col.second;
// Skip inlined blobs - they don't need GC
if (blob_index.IsInlined()) {
continue;
}
// Check if this blob file needs garbage collection
if (blob_index.file_number() >=
blob_garbage_collection_cutoff_file_number_) {
continue;
}
// This blob needs to be relocated - fetch its value
FilePrefetchBuffer* prefetch_buffer =
prefetch_buffers_ ? prefetch_buffers_->GetOrCreatePrefetchBuffer(
blob_index.file_number())
: nullptr;
uint64_t bytes_read = 0;
assert(blob_fetcher_);
PinnableSlice blob_value;
Status s = blob_fetcher_->FetchBlob(user_key(), blob_index, prefetch_buffer,
&blob_value, &bytes_read);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return false;
}
++iter_stats_.num_blobs_read;
iter_stats_.total_blob_bytes_read += bytes_read;
fetched_blob_values->emplace_back(col_idx, std::move(blob_value));
}
return !fetched_blob_values->empty();
}
std::vector<std::pair<size_t, BlobIndex>>
CompactionIterator::RelocateBlobValues(
const std::vector<std::pair<size_t, PinnableSlice>>& fetched_blob_values) {
std::vector<std::pair<size_t, BlobIndex>> new_blob_columns;
if (blob_file_builder_) {
for (const auto& fv : fetched_blob_values) {
const size_t col_idx = fv.first;
const Slice col_value(fv.second);
std::string temp_blob_index;
Status status =
blob_file_builder_->Add(user_key(), col_value, &temp_blob_index);
if (!status.ok()) {
status_ = status;
validity_info_.Invalidate();
new_blob_columns.clear();
break;
}
if (!temp_blob_index.empty()) {
BlobIndex blob_idx;
status = blob_idx.DecodeFrom(temp_blob_index);
if (!status.ok()) {
status_ = status;
validity_info_.Invalidate();
new_blob_columns.clear();
break;
}
new_blob_columns.emplace_back(col_idx, blob_idx);
// Track relocation stats here (after successful relocation)
++iter_stats_.num_blobs_relocated;
iter_stats_.total_blob_bytes_relocated += col_value.size();
}
// If temp_blob_index is empty, the value will be inlined
}
}
return new_blob_columns;
}
void CompactionIterator::SerializeEntityAfterGC(
const std::vector<WideColumn>& columns,
const std::vector<std::pair<size_t, BlobIndex>>& original_blob_columns,
const std::vector<std::pair<size_t, PinnableSlice>>& fetched_blob_values,
const std::vector<std::pair<size_t, BlobIndex>>& new_blob_columns) {
// Collect all blob columns for final serialization: include non-GC'd
// originals and newly relocated. Use linear scans since these
// collections are typically small (<5 elements).
std::vector<std::pair<size_t, BlobIndex>> final_blob_columns;
for (const auto& blob_col : original_blob_columns) {
// Check if this column was fetched (GC'd) via linear scan
bool was_fetched = false;
for (const auto& fv : fetched_blob_values) {
if (fv.first == blob_col.first) {
was_fetched = true;
break;
}
}
if (!was_fetched) {
// This blob column wasn't GC'd - keep original
final_blob_columns.push_back(blob_col);
}
}
// Add newly relocated blobs
for (const auto& nb : new_blob_columns) {
final_blob_columns.push_back(nb);
}
// Build WideColumns for serialization, replacing fetched blob values
// with their resolved inline values.
WideColumns wide_columns;
wide_columns.reserve(columns.size());
for (size_t i = 0; i < columns.size(); ++i) {
// Linear scan to find if this column was fetched
const PinnableSlice* fetched_value = nullptr;
for (const auto& fv : fetched_blob_values) {
if (fv.first == i) {
fetched_value = &fv.second;
break;
}
}
if (fetched_value != nullptr) {
// This blob column was fetched for GC; use the resolved inline value.
wide_columns.emplace_back(columns[i].name(), Slice(*fetched_value));
} else {
// For inline columns, this is the actual value. For non-fetched blob
// columns, this contains the raw serialized blob index, which is fine
// because SerializeV2() re-encodes from the BlobIndex
// object in final_blob_columns, ignoring this value for blob-type
// columns.
wide_columns.emplace_back(columns[i].name(), columns[i].value());
}
}
// Serialize the entity
rewritten_entity_.clear();
Status s;
if (final_blob_columns.empty()) {
s = WideColumnSerialization::Serialize(wide_columns, rewritten_entity_);
} else {
s = WideColumnSerialization::SerializeV2(wide_columns, final_blob_columns,
rewritten_entity_);
}
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return;
}
value_ = rewritten_entity_;
}
void CompactionIterator::GarbageCollectEntityBlobsIfNeeded() {
assert(ikey_.type == kTypeWideColumnEntity);
if (!compaction_ || !compaction_->enable_blob_garbage_collection()) {
return;
}
// This is the second branch in the flow above: the entity already has V2
// blob references, so compaction considers only relocating the subset of
// referenced blob files that are old enough for blob GC.
// Use member variables already populated by PrepareOutput's combined
// deserialization path. If not yet populated, deserialize here.
if (entity_columns_.empty()) {
Slice entity_slice = value_;
entity_columns_.clear();
entity_blob_columns_.clear();
Status s = WideColumnSerialization::DeserializeV2(
entity_slice, entity_columns_, entity_blob_columns_);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return;
}
}
// The caller (PrepareOutput) already verified blob columns exist.
assert(!entity_blob_columns_.empty());
if (entity_blob_columns_.empty()) {
return;
}
// Fetch blobs that need GC
std::vector<std::pair<size_t, PinnableSlice>> fetched_blob_values;
if (!FetchBlobsNeedingGC(entity_columns_, entity_blob_columns_,
&fetched_blob_values)) {
// Either no blobs needed GC (status_ is OK) or an error occurred
// (status_ is already set by FetchBlobsNeedingGC). In both cases,
// we return without modifying the entity.
return;
}
// Relocate fetched values to new blob files
std::vector<std::pair<size_t, BlobIndex>> new_blob_columns =
RelocateBlobValues(fetched_blob_values);
if (!status_.ok()) {
return; // Error occurred during relocation
}
// Serialize the final entity
SerializeEntityAfterGC(entity_columns_, entity_blob_columns_,
fetched_blob_values, new_blob_columns);
}
void CompactionIterator::PrepareOutput() {
if (Valid()) {
if (LIKELY(!is_range_del_)) {
@@ -1861,37 +1273,6 @@ void CompactionIterator::PrepareOutput() {
ExtractLargeValueIfNeeded();
} else if (ikey_.type == kTypeBlobIndex) {
GarbageCollectBlobIfNeeded();
} else if (ikey_.type == kTypeWideColumnEntity) {
// If entity was already deserialized (e.g., by InvokeFilterIfNeeded)
// and the filter returned kKeep, skip redundant re-deserialization.
if (!entity_deserialized_) {
TEST_SYNC_POINT(
"CompactionIterator::PrepareOutput:DeserializeEntity");
// Deserialize entity once into member variables, then decide between
// blob GC and extraction based on whether blob columns exist.
// This avoids the double parse of HasBlobColumns() + DeserializeV2().
entity_columns_.clear();
entity_blob_columns_.clear();
Slice entity_slice = value_;
{
Status s_deser = WideColumnSerialization::DeserializeV2(
entity_slice, entity_columns_, entity_blob_columns_);
if (!s_deser.ok()) {
status_ = s_deser;
validity_info_.Invalidate();
return;
}
}
} else {
TEST_SYNC_POINT(
"CompactionIterator::PrepareOutput:SkipDeserializeEntity");
}
entity_deserialized_ = false; // Reset for next iteration
if (UNLIKELY(!entity_blob_columns_.empty())) {
GarbageCollectEntityBlobsIfNeeded();
} else {
ExtractLargeColumnValuesIfNeeded();
}
}
}
@@ -2040,32 +1421,21 @@ uint64_t CompactionIterator::ComputeBlobGarbageCollectionCutoffFileNumber(
}
std::unique_ptr<BlobFetcher> CompactionIterator::CreateBlobFetcherIfNeeded(
const CompactionProxy* compaction, const Version* input_version,
Env::IOActivity blob_read_io_activity) {
const Version* const version =
compaction != nullptr ? compaction->input_version() : input_version;
if (version == nullptr) {
const CompactionProxy* compaction) {
if (!compaction) {
return nullptr;
}
const Version* const version = compaction->input_version();
if (!version) {
return nullptr;
}
ReadOptions read_options;
read_options.io_activity = compaction != nullptr
? Env::IOActivity::kCompaction
: blob_read_io_activity;
read_options.io_activity = Env::IOActivity::kCompaction;
read_options.fill_cache = false;
BlobFileCache* blob_file_cache = nullptr;
bool allow_write_path_fallback = false;
if (compaction == nullptr) {
allow_write_path_fallback = true;
auto* cfd = version->cfd();
if (cfd != nullptr) {
blob_file_cache = cfd->blob_file_cache();
}
}
return std::unique_ptr<BlobFetcher>(new BlobFetcher(
version, read_options, blob_file_cache, allow_write_path_fallback));
return std::unique_ptr<BlobFetcher>(new BlobFetcher(version, read_options));
}
std::unique_ptr<PrefetchBufferCollection>
+23 -158
View File
@@ -7,12 +7,10 @@
#include <algorithm>
#include <cinttypes>
#include <deque>
#include <memory>
#include <optional>
#include <string>
#include <unordered_set>
#include <vector>
#include "db/blob/blob_index.h"
#include "db/compaction/compaction.h"
#include "db/compaction/compaction_iteration_stats.h"
#include "db/merge_helper.h"
@@ -21,69 +19,12 @@
#include "db/snapshot_checker.h"
#include "options/cf_options.h"
#include "rocksdb/compaction_filter.h"
#include "rocksdb/slice.h"
namespace ROCKSDB_NAMESPACE {
class BlobFileBuilder;
class BlobFileCache;
class BlobFetcher;
class Version;
class PrefetchBufferCollection;
class FilePrefetchBuffer;
class Version;
// Internal implementation of WideColumnBlobResolver for compaction.
// This class enables lazy loading of blob column values during compaction
// filter processing.
class CompactionBlobResolver : public WideColumnBlobResolver {
public:
CompactionBlobResolver()
: columns_(nullptr),
blob_columns_(nullptr),
blob_fetcher_(nullptr),
prefetch_buffers_(nullptr),
iter_stats_(nullptr) {}
// Set the fixed context (blob fetcher, prefetch buffers, stats) once.
void Init(BlobFetcher* blob_fetcher,
PrefetchBufferCollection* prefetch_buffers,
CompactionIterationStats* iter_stats);
Status ResolveColumn(size_t column_index, Slice* resolved_value) override;
bool IsBlobColumn(size_t column_index) const override;
size_t NumColumns() const override;
Status resolve_status() const {
if (!resolve_error_.has_value()) {
return Status::OK();
}
return *resolve_error_;
}
// Reset the resolver for a new entity. Clears resolved cache.
void Reset(const Slice& user_key, const std::vector<WideColumn>* columns,
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns,
bool track_resolve_error = false);
private:
Slice user_key_;
const std::vector<WideColumn>* columns_;
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns_;
BlobFetcher* blob_fetcher_;
PrefetchBufferCollection* prefetch_buffers_;
CompactionIterationStats* iter_stats_;
bool track_resolve_error_ = false;
// Cache for resolved blob values to avoid re-fetching. Uses a vector of
// (column_index, PinnableSlice) pairs -- typical entities have few blob
// columns (<5), making linear scan cheaper than hash map overhead.
std::vector<std::pair<size_t, std::unique_ptr<PinnableSlice>>>
resolved_cache_;
// Sticky resolver error for the current entity. This is enabled only for the
// lazy FilterV4 path so compaction can still fail even if the filter
// notices the error and returns kKeep.
std::optional<Status> resolve_error_;
};
// A wrapper of internal iterator whose purpose is to count how
// many entries there are in the iterator.
@@ -272,43 +213,34 @@ class CompactionIterator {
const std::atomic<bool>* shutting_down = nullptr,
const std::shared_ptr<Logger> info_log = nullptr,
const std::string* full_history_ts_low = nullptr,
std::optional<SequenceNumber> preserve_seqno_min = {},
const Version* input_version = nullptr,
Env::IOActivity blob_read_io_activity = Env::IOActivity::kCompaction);
std::optional<SequenceNumber> preserve_seqno_min = {});
// Constructor with custom CompactionProxy, used for tests.
CompactionIterator(
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
SequenceNumber last_sequence, std::vector<SequenceNumber>* snapshots,
SequenceNumber earliest_snapshot,
SequenceNumber earliest_write_conflict_snapshot,
SequenceNumber job_snapshot, const SnapshotChecker* snapshot_checker,
Env* env, bool report_detailed_time,
CompactionRangeDelAggregator* range_del_agg,
BlobFileBuilder* blob_file_builder, bool allow_data_in_errors,
bool enforce_single_del_contracts,
const std::atomic<bool>& manual_compaction_canceled,
std::unique_ptr<CompactionProxy> compaction,
bool must_count_input_entries,
const CompactionFilter* compaction_filter = nullptr,
const std::atomic<bool>* shutting_down = nullptr,
const std::shared_ptr<Logger> info_log = nullptr,
const std::string* full_history_ts_low = nullptr,
std::optional<SequenceNumber> preserve_seqno_min = {},
const Version* input_version = nullptr,
Env::IOActivity blob_read_io_activity = Env::IOActivity::kCompaction);
CompactionIterator(InternalIterator* input, const Comparator* cmp,
MergeHelper* merge_helper, SequenceNumber last_sequence,
std::vector<SequenceNumber>* snapshots,
SequenceNumber earliest_snapshot,
SequenceNumber earliest_write_conflict_snapshot,
SequenceNumber job_snapshot,
const SnapshotChecker* snapshot_checker, Env* env,
bool report_detailed_time,
CompactionRangeDelAggregator* range_del_agg,
BlobFileBuilder* blob_file_builder,
bool allow_data_in_errors,
bool enforce_single_del_contracts,
const std::atomic<bool>& manual_compaction_canceled,
std::unique_ptr<CompactionProxy> compaction,
bool must_count_input_entries,
const CompactionFilter* compaction_filter = nullptr,
const std::atomic<bool>* shutting_down = nullptr,
const std::shared_ptr<Logger> info_log = nullptr,
const std::string* full_history_ts_low = nullptr,
std::optional<SequenceNumber> preserve_seqno_min = {});
~CompactionIterator();
void ResetRecordCounts();
// Non-compaction table-file creation (e.g., flush/recovery) can still need
// blob resolution when direct-write wide entities already contain blob
// references. Plumb a fetcher explicitly in those cases.
void SetBlobFetcher(const Version* version, BlobFileCache* blob_file_cache,
Env::IOActivity io_activity,
bool allow_write_path_fallback);
// Seek to the beginning of the compaction iterator output.
//
// REQUIRED: Call only once.
@@ -370,12 +302,6 @@ class CompactionIterator {
// for regular values (kTypeValue).
void ExtractLargeValueIfNeeded();
// Extracts large column values from wide column entities to blob files.
// For each column whose value size >= min_blob_size, the value is written
// to a blob file and replaced with a blob index reference. Should only be
// called for wide column entities (kTypeWideColumnEntity).
void ExtractLargeColumnValuesIfNeeded();
// Relocates valid blobs residing in the oldest blob files if garbage
// collection is enabled. Relocated blobs are written to new blob files or
// inlined in the LSM tree depending on the current settings (i.e.
@@ -386,39 +312,6 @@ class CompactionIterator {
// algorithm is also called from here.
void GarbageCollectBlobIfNeeded();
// Garbage collects blob references within wide column entities. For entities
// that contain blob column references, this method checks if any of the
// referenced blob files need garbage collection. If so, the blob values are
// fetched and either relocated to new blob files or inlined based on
// current settings. Should only be called for wide column entities
// (kTypeWideColumnEntity) that have blob columns.
void GarbageCollectEntityBlobsIfNeeded();
// Helper for GarbageCollectEntityBlobsIfNeeded: Fetches blob values that need
// garbage collection based on the cutoff file number.
// Returns true if any blobs were fetched, false otherwise.
// On error, sets status_ and returns false.
bool FetchBlobsNeedingGC(
const std::vector<WideColumn>& columns,
const std::vector<std::pair<size_t, BlobIndex>>& blob_columns,
std::vector<std::pair<size_t, PinnableSlice>>* fetched_blob_values);
// Helper for GarbageCollectEntityBlobsIfNeeded: Relocates fetched blob
// values to new blob files if blob_file_builder_ is available.
// Returns the new blob column indices. On error, sets status_ and returns
// empty.
std::vector<std::pair<size_t, BlobIndex>> RelocateBlobValues(
const std::vector<std::pair<size_t, PinnableSlice>>& fetched_blob_values);
// Helper for GarbageCollectEntityBlobsIfNeeded: Serializes the entity after
// garbage collection, combining original columns with fetched values and
// new blob indices. On error, sets status_.
void SerializeEntityAfterGC(
const std::vector<WideColumn>& columns,
const std::vector<std::pair<size_t, BlobIndex>>& original_blob_columns,
const std::vector<std::pair<size_t, PinnableSlice>>& fetched_blob_values,
const std::vector<std::pair<size_t, BlobIndex>>& new_blob_columns);
// Invoke compaction filter if needed.
// Return true on success, false on failures (e.g.: kIOError).
bool InvokeFilterIfNeeded(bool* need_skip, Slice* skip_until);
@@ -459,8 +352,7 @@ class CompactionIterator {
static uint64_t ComputeBlobGarbageCollectionCutoffFileNumber(
const CompactionProxy* compaction);
static std::unique_ptr<BlobFetcher> CreateBlobFetcherIfNeeded(
const CompactionProxy* compaction, const Version* input_version,
Env::IOActivity blob_read_io_activity);
const CompactionProxy* compaction);
static std::unique_ptr<PrefetchBufferCollection>
CreatePrefetchBufferCollectionIfNeeded(const CompactionProxy* compaction);
@@ -484,7 +376,6 @@ class CompactionIterator {
BlobFileBuilder* blob_file_builder_;
std::unique_ptr<CompactionProxy> compaction_;
const CompactionFilter* compaction_filter_;
const bool filter_supports_v4_;
const std::atomic<bool>* shutting_down_;
const std::atomic<bool>& manual_compaction_canceled_;
const bool bottommost_level_;
@@ -588,32 +479,6 @@ class CompactionIterator {
PinnableSlice blob_value_;
std::string compaction_filter_value_;
InternalKey compaction_filter_skip_until_;
// Buffer for storing rewritten entity with blob references after
// extracting large column values to blob files.
std::string rewritten_entity_;
// Cached deserialized columns for the current wide-column entity, reused
// across iterations to avoid per-key heap allocations. This is the full
// sorted column set. For blob-backed columns, value() initially contains the
// serialized BlobIndex bytes from the entity.
std::vector<WideColumn> entity_columns_;
// Side list for the blob-backed subset of entity_columns_. Each pair is
// (column_index_in_entity_columns_, decoded_blob_index), so callers can tell
// which entries in entity_columns_ are blob references without reparsing.
std::vector<std::pair<size_t, BlobIndex>> entity_blob_columns_;
// Temporary Slice-based view over entity_columns_ used when reserializing an
// entity after blob extraction, to avoid copying names/values into strings.
WideColumns entity_wide_columns_;
// Reusable storage for existing_columns in InvokeFilterIfNeeded,
// avoiding per-key heap allocations.
WideColumns filter_existing_columns_;
// Reusable blob resolver for compaction filter, avoiding per-key heap
// allocation. Init() is called once; Reset() is called per entity.
CompactionBlobResolver blob_resolver_;
// True when entity_columns_/entity_blob_columns_ already describe the
// current input record (e.g., because InvokeFilterIfNeeded() deserialized
// it) and PrepareOutput() can skip redundant work. Reset for each candidate
// input record and whenever the filter rewrites the entity value.
bool entity_deserialized_{false};
// "level_ptrs" holds indices that remember which file of an associated
// level we were last checking during the last call to compaction->
// KeyNotExistsBeyondOutputLevel(). This allows future calls to the function
File diff suppressed because it is too large. Load diff
+44 -227
View File
@@ -33,7 +33,6 @@
#include "db/range_del_aggregator.h"
#include "db/version_edit.h"
#include "db/version_set.h"
#include "file/file_util.h"
#include "file/filename.h"
#include "file/read_write_util.h"
#include "file/sst_file_manager_impl.h"
@@ -58,7 +57,6 @@
#include "table/table_builder.h"
#include "table/unique_id_impl.h"
#include "test_util/sync_point.h"
#include "util/hash_containers.h"
#include "util/stop_watch.h"
namespace ROCKSDB_NAMESPACE {
@@ -105,8 +103,6 @@ const char* GetCompactionReasonString(CompactionReason compaction_reason) {
return "RoundRobinTtl";
case CompactionReason::kRefitLevel:
return "RefitLevel";
case CompactionReason::kReadTriggered:
return "ReadTriggered";
case CompactionReason::kNumOfReasons:
// fall through
default:
@@ -132,10 +128,6 @@ const char* GetCompactionProximalOutputRangeTypeString(
}
}
// Static constant for compaction abort flag - always false, used for
// compaction service jobs that don't support abort signaling
const std::atomic<int> CompactionJob::kCompactionAbortedFalse{0};
CompactionJob::CompactionJob(
int job_id, Compaction* compaction, const ImmutableDBOptions& db_options,
const MutableDBOptions& mutable_db_options, const FileOptions& file_options,
@@ -149,10 +141,10 @@ CompactionJob::CompactionJob(
CompactionJobStats* compaction_job_stats, Env::Priority thread_pri,
const std::shared_ptr<IOTracer>& io_tracer,
const std::atomic<bool>& manual_compaction_canceled,
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)
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)
: compact_(new CompactionState(compaction)),
internal_stats_(compaction->compaction_reason(), 1),
db_options_(db_options),
@@ -176,7 +168,6 @@ CompactionJob::CompactionJob(
versions_(versions),
shutting_down_(shutting_down),
manual_compaction_canceled_(manual_compaction_canceled),
compaction_aborted_(compaction_aborted),
db_directory_(db_directory),
blob_output_directory_(blob_output_directory),
db_mutex_(db_mutex),
@@ -717,7 +708,6 @@ void CompactionJob::InitializeCompactionRun() {
}
void CompactionJob::RunSubcompactions() {
TEST_SYNC_POINT("CompactionJob::RunSubcompactions:BeforeStart");
const size_t num_threads = compact_->sub_compact_states.size();
assert(num_threads > 0);
compact_->compaction->GetOrInitInputTableProperties();
@@ -763,71 +753,6 @@ void CompactionJob::RemoveEmptyOutputs() {
}
}
void CompactionJob::CleanupAbortedSubcompactions() {
ColumnFamilyData* cfd = compact_->compaction->column_family_data();
uint64_t total_sst_files_deleted = 0;
uint64_t total_blob_files_deleted = 0;
// Track the first file deletion error to report at the end
Status first_error;
int deletion_errors = 0;
// Mark all subcompactions as aborted and delete their output files
for (auto& sub_compact : compact_->sub_compact_states) {
// Mark this subcompaction as aborted
sub_compact.status =
Status::Incomplete(Status::SubCode::kCompactionAborted);
// Delete all files (SST and blob) tracked during compaction.
// GetOutputFilePaths() contains ALL file paths created, including
// in-progress files that may have been removed from outputs_ or
// blob_file_additions_.
for (const bool is_proximal_level : {false, true}) {
if (is_proximal_level &&
!compact_->compaction->SupportsPerKeyPlacement()) {
continue;
}
for (const std::string& file_path :
sub_compact.Outputs(is_proximal_level)->GetOutputFilePaths()) {
Status s = env_->DeleteFile(file_path);
if (s.ok()) {
// Count SST vs blob files by checking extension
if (file_path.find(".sst") != std::string::npos) {
total_sst_files_deleted++;
} else if (file_path.find(".blob") != std::string::npos) {
total_blob_files_deleted++;
}
} else if (!s.IsNotFound()) {
if (first_error.ok()) {
first_error = s;
}
deletion_errors++;
}
}
}
sub_compact.CleanupOutputs();
}
if (stats_) {
RecordTick(stats_, COMPACTION_ABORTED);
}
ROCKS_LOG_INFO(db_options_.info_log,
"[%s] [JOB %d] Compaction aborted: deleted %" PRIu64
" SST files and %" PRIu64 " blob files",
cfd->GetName().c_str(), job_id_, total_sst_files_deleted,
total_blob_files_deleted);
if (!first_error.ok()) {
ROCKS_LOG_ERROR(db_options_.info_log,
"[%s] [JOB %d] Cleanup completed with %d file deletion "
"errors. First error: %s",
cfd->GetName().c_str(), job_id_, deletion_errors,
first_error.ToString().c_str());
}
}
bool CompactionJob::HasNewBlobFiles() const {
for (const auto& state : compact_->sub_compact_states) {
if (state.Current().HasBlobFileAdditions()) {
@@ -899,10 +824,6 @@ Status CompactionJob::VerifyOutputFiles() {
}
auto verify_table = [&](SubcompactionState& subcompaction_state) {
// Collect file open metadata during verification when fast_sst_open
// is enabled, keyed by file number.
UnorderedMap<uint64_t, std::string> file_open_metadata_map;
for (const auto& output_file : subcompaction_state.GetOutputs()) {
// Verify that the table is usable
// We set for_compaction to false and don't
@@ -911,21 +832,17 @@ Status CompactionJob::VerifyOutputFiles() {
// use_direct_io_for_flush_and_compaction is true, we will regard this
// verification as user reads since the goal is to cache it here for
// further user reads
ReadOptions verification_read_options(Env::IOActivity::kCompaction);
verification_read_options.verify_checksums = true;
verification_read_options.readahead_size =
ReadOptions verify_table_read_options(Env::IOActivity::kCompaction);
verify_table_read_options.verify_checksums = true;
verify_table_read_options.readahead_size =
file_options_for_read_.compaction_readahead_size;
std::unique_ptr<TableReader> table_reader_guard;
TableReader* table_reader_ptr = table_reader_guard.get();
verification_read_options.rate_limiter_priority =
verify_table_read_options.rate_limiter_priority =
GetRateLimiterPriority();
std::string file_open_metadata;
std::string* file_open_metadata_ptr =
mutable_db_options_copy_.fast_sst_open ? &file_open_metadata
: nullptr;
InternalIterator* iter = cfd->table_cache()->NewIterator(
verification_read_options, file_options_, cfd->internal_comparator(),
verify_table_read_options, file_options_, cfd->internal_comparator(),
output_file.meta,
/*range_del_agg=*/nullptr, compact_->compaction->mutable_cf_options(),
/*table_reader_ptr=*/&table_reader_ptr,
@@ -936,10 +853,7 @@ Status CompactionJob::VerifyOutputFiles() {
MaxFileSizeForL0MetaPin(compact_->compaction->mutable_cf_options()),
/*smallest_compaction_key=*/nullptr,
/*largest_compaction_key=*/nullptr,
/*allow_unprepared_value=*/false,
/*range_del_read_seqno=*/nullptr,
/*range_del_iter=*/nullptr,
/*maybe_pin_table_handle=*/false, file_open_metadata_ptr);
/*allow_unprepared_value=*/false);
auto s = iter->status();
if (s.ok()) {
// Check for remote/local compaction and verify_output_flags flags
@@ -956,17 +870,12 @@ Status CompactionJob::VerifyOutputFiles() {
!!(verify_output_flags & VerifyOutputFlags::kVerifyBlockChecksum);
const bool should_verify_iteration =
!!(verify_output_flags & VerifyOutputFlags::kVerifyIteration);
const bool should_verify_file_checksum =
!!(verify_output_flags &
VerifyOutputFlags::kVerifyFileChecksum) &&
db_options_.file_checksum_gen_factory != nullptr &&
output_file.meta.file_checksum != kUnknownFileChecksum;
if (should_verify_block_checksum) {
assert(table_reader_ptr != nullptr);
// If verifying iteration as well, verify meta blocks here only to
// avoid redundant checks on data blocks
s = table_reader_ptr->VerifyChecksum(
verification_read_options, TableReaderCaller::kCompaction,
verify_table_read_options, TableReaderCaller::kCompaction,
/*meta_blocks_only=*/should_verify_iteration);
}
if (s.ok() && should_verify_iteration) {
@@ -987,24 +896,6 @@ Status CompactionJob::VerifyOutputFiles() {
"was computed when written");
}
}
if (s.ok() && should_verify_file_checksum) {
std::string file_checksum;
std::string file_checksum_func_name;
std::string fname =
GetTableFileName(output_file.meta.fd.GetNumber());
s = GenerateOneFileChecksum(
fs_.get(), fname, db_options_.file_checksum_gen_factory.get(),
output_file.meta.file_checksum_func_name, &file_checksum,
&file_checksum_func_name,
verification_read_options.readahead_size,
db_options_.allow_mmap_reads, io_tracer_,
db_options_.rate_limiter.get(), verification_read_options,
stats_, db_options_.clock, file_options_for_read_);
if (s.ok() && file_checksum != output_file.meta.file_checksum) {
s = Status::Corruption(
"File checksum mismatch for compaction output file " + fname);
}
}
}
}
@@ -1014,27 +905,6 @@ Status CompactionJob::VerifyOutputFiles() {
subcompaction_state.status = s;
break;
}
if (!file_open_metadata.empty()) {
file_open_metadata_map[output_file.meta.fd.GetNumber()] =
std::move(file_open_metadata);
}
}
// Apply collected file open metadata to mutable outputs
if (!file_open_metadata_map.empty()) {
auto apply_metadata =
[&file_open_metadata_map](
std::vector<CompactionOutputs::Output>& outputs) {
for (auto& output : outputs) {
auto it = file_open_metadata_map.find(output.meta.fd.GetNumber());
if (it != file_open_metadata_map.end()) {
output.meta.file_open_metadata = std::move(it->second);
}
}
};
apply_metadata(subcompaction_state.GetMutableCompactionOutputs());
apply_metadata(subcompaction_state.GetMutableProximalOutputs());
}
};
for (size_t i = 1; i < compact_->sub_compact_states.size(); i++) {
@@ -1134,15 +1004,6 @@ Status CompactionJob::Run() {
Status status = CollectSubcompactionErrors();
// If compaction was aborted or manually paused, clean up any output files
// from completed subcompactions to prevent orphaned files on disk.
// Skip cleanup for resumable compaction (when progress writer is set)
// because the output files are needed for resumption.
if ((status.IsCompactionAborted() || status.IsManualCompactionPaused()) &&
compaction_progress_writer_ == nullptr) {
CleanupAbortedSubcompactions();
}
if (status.ok()) {
status = SyncOutputDirectories();
}
@@ -1151,9 +1012,6 @@ Status CompactionJob::Run() {
status = VerifyOutputFiles();
}
TEST_SYNC_POINT_CALLBACK("CompactionJob::Run():AfterVerifyOutputFiles",
&status);
if (status.ok()) {
SetOutputTableProperties();
}
@@ -1340,16 +1198,6 @@ Status CompactionJob::Install(bool* compaction_released) {
<< pl_stats.bytes_written_blob;
}
// Propagate Install failure to compact_->status so that
// CleanupCompaction() -> SubcompactionState::Cleanup() sees the failure and
// calls ReleaseObsolete on output files' table cache entries. Without this,
// if Run() succeeds but InstallCompactionResults() fails, Cleanup would see
// overall_status = OK and skip ReleaseObsolete, leaking entries for output
// files that were never installed into any Version.
if (!status.ok() && compact_->status.ok()) {
compact_->status = status;
}
CleanupCompaction();
return status;
}
@@ -1567,10 +1415,10 @@ InternalIterator* CompactionJob::CreateInputIterator(
return input;
}
void CompactionJob::CreateBlobFileBuilder(
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
std::unique_ptr<BlobFileBuilder>& blob_file_builder,
const WriteOptions& write_options) {
void CompactionJob::CreateBlobFileBuilder(SubcompactionState* sub_compact,
ColumnFamilyData* cfd,
BlobFileResources& blob_resources,
const WriteOptions& write_options) {
const auto& mutable_cf_options =
sub_compact->compaction->mutable_cf_options();
@@ -1579,24 +1427,24 @@ void CompactionJob::CreateBlobFileBuilder(
if (mutable_cf_options.enable_blob_files &&
sub_compact->compaction->output_level() >=
mutable_cf_options.blob_file_starting_level) {
blob_file_builder = std::make_unique<BlobFileBuilder>(
blob_resources.blob_file_builder = std::make_unique<BlobFileBuilder>(
versions_, fs_.get(), &sub_compact->compaction->immutable_options(),
&mutable_cf_options, &file_options_, &write_options, db_id_,
db_session_id_, job_id_, cfd->GetID(), cfd->GetName(), write_hint_,
io_tracer_, blob_callback_, BlobFileCreationReason::kCompaction,
sub_compact->Current().GetOutputFilePathsPtr(),
&blob_resources.blob_file_paths,
sub_compact->Current().GetBlobFileAdditionsPtr());
} else {
blob_file_builder = nullptr;
blob_resources.blob_file_builder = nullptr;
}
}
std::unique_ptr<CompactionIterator> CompactionJob::CreateCompactionIterator(
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
InternalIterator* input, const CompactionFilter* compaction_filter,
MergeHelper& merge, std::unique_ptr<BlobFileBuilder>& blob_file_builder,
MergeHelper& merge, BlobFileResources& blob_resources,
const WriteOptions& write_options) {
CreateBlobFileBuilder(sub_compact, cfd, blob_file_builder, write_options);
CreateBlobFileBuilder(sub_compact, cfd, blob_resources, write_options);
const std::string* const full_history_ts_low =
full_history_ts_low_.empty() ? nullptr : &full_history_ts_low_;
@@ -1608,7 +1456,7 @@ std::unique_ptr<CompactionIterator> CompactionJob::CreateCompactionIterator(
job_context_->earliest_write_conflict_snapshot,
job_context_->GetJobSnapshotSequence(), job_context_->snapshot_checker,
env_, ShouldReportDetailedTime(env_, stats_), sub_compact->RangeDelAgg(),
blob_file_builder.get(), db_options_.allow_data_in_errors,
blob_resources.blob_file_builder.get(), db_options_.allow_data_in_errors,
db_options_.enforce_single_del_contracts, manual_compaction_canceled_,
sub_compact->compaction
->DoesInputReferenceBlobFiles() /* must_count_input_entries */,
@@ -1647,17 +1495,10 @@ Status CompactionJob::ProcessKeyValue(
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
CompactionIterator* c_iter, const CompactionFileOpenFunc& open_file_func,
const CompactionFileCloseFunc& close_file_func, uint64_t& prev_cpu_micros) {
// Cron interval for periodic operations: stats update, abort check,
// and sync points. Uses 1024 (power of 2) for efficient bitwise check.
const uint64_t kCronEveryMask = (1 << 10) - 1;
Status status;
const uint64_t kRecordStatsEvery = 1000;
[[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) {
return Status::Incomplete(Status::SubCode::kCompactionAborted);
}
Status status;
IterKey prev_iter_output_key;
ParsedInternalKey prev_iter_output_internal_key;
@@ -1670,16 +1511,8 @@ Status CompactionJob::ProcessKeyValue(
assert(!end.has_value() ||
cfd->user_comparator()->Compare(c_iter->user_key(), *end) < 0);
const uint64_t num_records = c_iter->iter_stats().num_input_records;
// 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) {
status = Status::Incomplete(Status::SubCode::kCompactionAborted);
break;
}
if (c_iter->iter_stats().num_input_records % kRecordStatsEvery ==
kRecordStatsEvery - 1) {
UpdateSubcompactionJobStatsIncrementally(
c_iter, &sub_compact->compaction_job_stats,
db_options_.clock->CPUMicros(), prev_cpu_micros);
@@ -1886,7 +1719,6 @@ Status CompactionJob::FinalizeBlobFiles(SubcompactionState* sub_compact,
}
void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
TEST_SYNC_POINT("CompactionJob::ProcessKeyValueCompaction:Start");
assert(sub_compact);
assert(sub_compact->compaction);
@@ -1940,11 +1772,11 @@ void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
false /* internal key corruption is expected */,
job_context_->GetLatestSnapshotSequence(), job_context_->snapshot_checker,
compact_->compaction->level(), db_options_.stats);
std::unique_ptr<BlobFileBuilder> blob_file_builder;
BlobFileResources blob_resources;
auto c_iter =
CreateCompactionIterator(sub_compact, cfd, input_iter, compaction_filter,
merge, blob_file_builder, write_options);
merge, blob_resources, write_options);
assert(c_iter);
c_iter->SeekToFirst();
@@ -1962,8 +1794,9 @@ void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
status = FinalizeProcessKeyValueStatus(cfd, input_iter, c_iter.get(), status);
FinalizeSubcompaction(sub_compact, status, open_file_func, close_file_func,
blob_file_builder.get(), c_iter.get(), input_iter,
start_cpu_micros, prev_cpu_micros, io_stats);
blob_resources.blob_file_builder.get(), c_iter.get(),
input_iter, start_cpu_micros, prev_cpu_micros,
io_stats);
NotifyOnSubcompactionCompleted(sub_compact);
}
@@ -2398,16 +2231,11 @@ Status CompactionJob::InstallCompactionResults(bool* compaction_released) {
*compaction_released = true;
};
Status s;
TEST_SYNC_POINT_CALLBACK(
"CompactionJob::InstallCompactionResults:BeforeLogAndApply", &s);
if (s.ok()) {
s = versions_->LogAndApply(compaction->column_family_data(), read_options,
write_options, edit, db_mutex_, db_directory_,
/*new_descriptor_log=*/false,
/*column_family_options=*/nullptr, manifest_wcb);
}
return s;
return versions_->LogAndApply(compaction->column_family_data(), read_options,
write_options, edit, db_mutex_, db_directory_,
/*new_descriptor_log=*/false,
/*column_family_options=*/nullptr,
manifest_wcb);
}
void CompactionJob::RecordCompactionIOStats() {
@@ -2467,10 +2295,6 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
Status s;
IOStatus io_s = NewWritableFile(fs_.get(), fname, &writable_file, fo_copy);
s = io_s;
if (io_s.ok()) {
// Track the SST file path for cleanup on abort.
outputs.AddOutputFilePath(fname);
}
if (sub_compact->io_status.ok()) {
sub_compact->io_status = io_s;
// Since this error is really a copy of the io_s that is checked below as s,
@@ -2547,16 +2371,8 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
return s;
}
// Enable hash computation if paranoid_file_checks is on or if
// verify_output_flags includes kVerifyIteration, so that
// VerifyOutputFiles() can compare the hash of the written data
// against a re-read of the output file.
bool enable_output_hash =
paranoid_file_checks_ ||
!!(sub_compact->compaction->mutable_cf_options().verify_output_flags &
VerifyOutputFlags::kVerifyIteration);
outputs.AddOutput(std::move(meta), cfd->internal_comparator(),
enable_output_hash);
paranoid_file_checks_);
}
writable_file->SetIOPriority(GetRateLimiterPriority());
@@ -2597,7 +2413,7 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
void CompactionJob::CleanupCompaction() {
for (SubcompactionState& sub_compact : compact_->sub_compact_states) {
sub_compact.Cleanup(table_cache_.get(), compact_->status);
sub_compact.Cleanup(table_cache_.get());
}
delete compact_;
compact_ = nullptr;
@@ -2989,17 +2805,12 @@ void CompactionJob::RestoreCompactionOutputs(
const auto& output_files = subcompaction_progress_per_level.GetOutputFiles();
const bool enable_output_hash =
paranoid_file_checks_ ||
!!(compact_->compaction->mutable_cf_options().verify_output_flags &
VerifyOutputFlags::kVerifyIteration);
for (size_t i = 0; i < output_files.size(); i++) {
FileMetaData file_copy = output_files[i];
outputs_to_restore->AddOutput(std::move(file_copy),
cfd->internal_comparator(),
enable_output_hash, true /* finished */);
paranoid_file_checks_, true /* finished */);
outputs_to_restore->UpdateTableProperties(
*output_files_table_properties[i]);
@@ -3022,6 +2833,12 @@ void CompactionJob::RestoreCompactionOutputs(
// - Status::NotFound(): No valid progress to resume from
// - Status::Corruption(): Resume key is invalid, beyond input range, or output
// restoration failed
// - Other non-OK status: Iterator errors or file system issues during
// restoration
//
// The caller must check for Status::IsIncomplete() to distinguish between
// "no resume needed" (proceed with `InternalIterator::SeekToFirst()`) vs
// "resume failed" scenarios.
Status CompactionJob::MaybeResumeSubcompactionProgressOnInputIterator(
SubcompactionState* sub_compact, InternalIterator* input_iter) {
const ReadOptions read_options(Env::IOActivity::kCompaction);
+10 -11
View File
@@ -142,9 +142,6 @@ class SubcompactionState;
class CompactionJob {
public:
// Constant false aborted flag, used for compaction service jobs
static const std::atomic<int> kCompactionAbortedFalse;
CompactionJob(int job_id, Compaction* compaction,
const ImmutableDBOptions& db_options,
const MutableDBOptions& mutable_db_options,
@@ -160,7 +157,6 @@ class CompactionJob {
Env::Priority thread_pri,
const std::shared_ptr<IOTracer>& io_tracer,
const std::atomic<bool>& manual_compaction_canceled,
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 = "",
@@ -307,7 +303,6 @@ class CompactionJob {
void RunSubcompactions();
void UpdateTimingStats(uint64_t start_micros);
void RemoveEmptyOutputs();
void CleanupAbortedSubcompactions();
bool HasNewBlobFiles() const;
Status CollectSubcompactionErrors();
Status SyncOutputDirectories();
@@ -372,6 +367,11 @@ class CompactionJob {
std::unique_ptr<InternalIterator> trim_history_iter;
};
struct BlobFileResources {
std::vector<std::string> blob_file_paths;
std::unique_ptr<BlobFileBuilder> blob_file_builder;
};
bool ShouldUseLocalCompaction(SubcompactionState* sub_compact);
CompactionIOStatsSnapshot InitializeIOStats();
Status SetupAndValidateCompactionFilter(
@@ -386,14 +386,14 @@ class CompactionJob {
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
SubcompactionInternalIterators& iterators,
SubcompactionKeyBoundaries& boundaries, ReadOptions& read_options);
void CreateBlobFileBuilder(
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
std::unique_ptr<BlobFileBuilder>& blob_file_builder,
const WriteOptions& write_options);
void CreateBlobFileBuilder(SubcompactionState* sub_compact,
ColumnFamilyData* cfd,
BlobFileResources& blob_resources,
const WriteOptions& write_options);
std::unique_ptr<CompactionIterator> CreateCompactionIterator(
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
InternalIterator* input_iter, const CompactionFilter* compaction_filter,
MergeHelper& merge, std::unique_ptr<BlobFileBuilder>& blob_file_builder,
MergeHelper& merge, BlobFileResources& blob_resources,
const WriteOptions& write_options);
std::pair<CompactionFileOpenFunc, CompactionFileCloseFunc> CreateFileHandlers(
SubcompactionState* sub_compact, SubcompactionKeyBoundaries& boundaries);
@@ -465,7 +465,6 @@ class CompactionJob {
VersionSet* versions_;
const std::atomic<bool>* shutting_down_;
const std::atomic<bool>& manual_compaction_canceled_;
const std::atomic<int>& compaction_aborted_;
FSDirectory* db_directory_;
FSDirectory* blob_output_directory_;
InstrumentedMutex* db_mutex_;
+7 -7
View File
@@ -82,7 +82,7 @@ class CompactionJobStatsTest : public testing::Test,
std::string dbname_;
std::string alternative_wal_dir_;
Env* env_;
std::unique_ptr<DB> db_;
DB* db_;
std::vector<ColumnFamilyHandle*> handles_;
uint32_t max_subcompactions_;
@@ -123,7 +123,7 @@ class CompactionJobStatsTest : public testing::Test,
static void SetUpTestCase() {}
static void TearDownTestCase() {}
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_.get()); }
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_); }
void CreateColumnFamilies(const std::vector<std::string>& cfs,
const Options& options) {
@@ -162,8 +162,7 @@ class CompactionJobStatsTest : public testing::Test,
column_families.emplace_back(cfs[i], options[i]);
}
DBOptions db_opts = DBOptions(options[0]);
auto s = DB::Open(db_opts, dbname_, column_families, &handles_, &db_);
return s;
return DB::Open(db_opts, dbname_, column_families, &handles_, &db_);
}
Status TryReopenWithColumnFamilies(const std::vector<std::string>& cfs,
@@ -180,7 +179,8 @@ class CompactionJobStatsTest : public testing::Test,
delete h;
}
handles_.clear();
db_.reset();
delete db_;
db_ = nullptr;
}
void DestroyAndReopen(const Options& options) {
@@ -743,7 +743,7 @@ TEST_P(CompactionJobStatsTest, CompactionJobStatsTest) {
}
ASSERT_OK(Flush(1));
ASSERT_OK(dbfull()->TEST_WaitForCompact());
ASSERT_OK(static_cast_with_check<DBImpl>(db_)->TEST_WaitForCompact());
stats_checker->set_verify_next_comp_io_stats(true);
std::atomic<bool> first_prepare_write(true);
@@ -944,7 +944,7 @@ TEST_P(CompactionJobStatsTest, UniversalCompactionTest) {
start_key += key_base) {
MakeTableWithKeyValues(&rnd, start_key, start_key + key_base - 1, kKeySize,
kValueSize, key_interval, compression_ratio, 1);
ASSERT_OK(dbfull()->TEST_WaitForCompact());
ASSERT_OK(static_cast_with_check<DBImpl>(db_)->TEST_WaitForCompact());
}
ASSERT_EQ(stats_checker->NumberOfUnverifiedStats(), 0U);
}
+455 -2
View File
@@ -676,8 +676,8 @@ class CompactionJobTestBase : public testing::Test {
&event_logger, false, false, dbname_, &compaction_job_stats_,
Env::Priority::USER, nullptr /* IOTracer */,
/*manual_compaction_canceled=*/kManualCompactionCanceledFalse,
CompactionJob::kCompactionAbortedFalse, env_->GenerateUniqueId(),
DBImpl::GenerateDbSessionId(nullptr), full_history_ts_low_);
env_->GenerateUniqueId(), DBImpl::GenerateDbSessionId(nullptr),
full_history_ts_low_);
VerifyInitializationOfCompactionJobStats(compaction_job_stats_);
compaction_job.Prepare(std::nullopt /*subcompact to be computed*/);
@@ -2409,6 +2409,459 @@ TEST_F(CompactionJobIOPriorityTest, GetRateLimiterPriority) {
kMaxSequenceNumber, 1, false, {kInvalidBlobFileNumber}, true,
Env::IO_LOW, Env::IO_LOW);
}
class ResumableCompactionJobTest : public CompactionJobTestBase {
public:
ResumableCompactionJobTest()
: CompactionJobTestBase(
test::PerThreadDBPath("allow_resumption_job_test"),
BytewiseComparator(), [](uint64_t /*ts*/) { return ""; },
/*test_io_priority=*/false, TableTypeForTest::kBlockBasedTable) {}
protected:
static constexpr const char* kCancelBeforeThisKey = "cancel_before_this_key";
std::string progress_dir_;
bool enable_cancel_ = false;
std::atomic<int> stop_count_{0};
std::atomic<bool> cancel_{false};
SequenceNumber cancel_before_seqno = kMaxSequenceNumber;
void SetUp() override {
CompactionJobTestBase::SetUp();
SyncPoint::GetInstance()->SetCallBack(
"CompactionOutputs::ShouldStopBefore::manual_decision",
[this](void* p) {
auto* pair = static_cast<std::pair<bool*, const Slice>*>(p);
*(pair->first) = true;
// Cancel after outputting a specific key
if (enable_cancel_) {
ParsedInternalKey parsed_key;
if (ParseInternalKey(pair->second, &parsed_key, true).ok()) {
if (parsed_key.user_key == kCancelBeforeThisKey &&
(cancel_before_seqno == kMaxSequenceNumber ||
parsed_key.sequence == cancel_before_seqno)) {
cancel_.store(true);
}
}
}
});
SyncPoint::GetInstance()->EnableProcessing();
}
void TearDown() override {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
if (env_->FileExists(progress_dir_).ok()) {
std::vector<std::string> files;
EXPECT_OK(env_->GetChildren(progress_dir_, &files));
for (const auto& file : files) {
if (file != "." && file != "..") {
EXPECT_OK(env_->DeleteFile(progress_dir_ + "/" + file));
}
}
EXPECT_OK(env_->DeleteDir(progress_dir_));
}
CompactionJobTestBase::TearDown();
}
void NewDB() {
if (env_->FileExists(progress_dir_).ok()) {
std::vector<std::string> files;
EXPECT_OK(env_->GetChildren(progress_dir_, &files));
for (const auto& file : files) {
if (file != "." && file != "..") {
EXPECT_OK(env_->DeleteFile(progress_dir_ + "/" + file));
}
}
EXPECT_OK(env_->DeleteDir(progress_dir_));
}
CompactionJobTestBase::NewDB();
progress_dir_ = test::PerThreadDBPath("compaction_progress");
ASSERT_OK(env_->CreateDirIfMissing(progress_dir_));
}
void EnableCompactionCancel() { enable_cancel_ = true; }
void DisableCompactionCancel() {
enable_cancel_ = false;
cancel_.store(false);
}
std::unique_ptr<log::Writer> CreateCompactionProgressWriter(
const std::string& compaction_progress_file) {
std::unique_ptr<FSWritableFile> file;
EXPECT_OK(fs_->NewWritableFile(compaction_progress_file, FileOptions(),
&file, nullptr));
auto file_writer = std::make_unique<WritableFileWriter>(
std::move(file), compaction_progress_file, FileOptions());
auto compaction_progress_writer =
std::make_unique<log::Writer>(std::move(file_writer), 0, false);
return compaction_progress_writer;
}
Status RunCompactionWithProgressTracking(
const CompactionProgress& compaction_progress,
log::Writer* compaction_progress_writer,
std::vector<SequenceNumber> snapshots = {},
std::shared_ptr<Statistics> stats = nullptr) {
mutex_.Lock();
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
auto files = cfd->current()->storage_info()->LevelFiles(0);
db_options_.statistics = stats;
db_options_.stats = db_options_.statistics.get();
std::vector<CompactionInputFiles> compaction_input_files;
CompactionInputFiles level;
level.level = 0;
level.files = files;
compaction_input_files.push_back(level);
Compaction compaction(
cfd->current()->storage_info(), cfd->ioptions(),
cfd->GetLatestMutableCFOptions(), mutable_db_options_,
compaction_input_files, 1, mutable_cf_options_.target_file_size_base,
mutable_cf_options_.max_compaction_bytes, 0, kNoCompression,
cfd->GetLatestMutableCFOptions().compression_opts,
Temperature::kUnknown, 0, {}, std::nullopt, nullptr,
CompactionReason::kManualCompaction);
compaction.FinalizeInputInfo(cfd->current());
LogBuffer log_buffer(InfoLogLevel::INFO_LEVEL, db_options_.info_log.get());
EventLogger event_logger(db_options_.info_log.get());
JobContext job_context(1, false);
job_context.InitSnapshotContext(nullptr, nullptr, kMaxSequenceNumber,
std::move(snapshots));
CompactionJobStats job_stats;
CompactionJob compaction_job(
0, &compaction, db_options_, mutable_db_options_, env_options_,
versions_.get(), &shutting_down_, &log_buffer, nullptr, nullptr,
nullptr, stats.get(), &mutex_, &error_handler_, &job_context,
table_cache_, &event_logger, false, false, dbname_, &job_stats,
Env::Priority::USER, nullptr, cancel_, env_->GenerateUniqueId(),
DBImpl::GenerateDbSessionId(nullptr), "");
compaction_job.Prepare(std::nullopt, compaction_progress,
compaction_progress_writer);
mutex_.Unlock();
compaction_job.Run().PermitUncheckedError();
EXPECT_OK(compaction_job.io_status());
mutex_.Lock();
bool compaction_released = false;
Status s = compaction_job.Install(&compaction_released);
mutex_.Unlock();
if (!compaction_released) {
compaction.ReleaseCompactionFiles(s);
}
return s;
}
SubcompactionProgress ReadAndParseProgress(
const std::string& compaction_progress_file) {
std::unique_ptr<FSSequentialFile> seq_file;
EXPECT_OK(fs_->NewSequentialFile(compaction_progress_file, FileOptions(),
&seq_file, nullptr));
auto file_reader = std::make_unique<SequentialFileReader>(
std::move(seq_file), compaction_progress_file, 0, nullptr);
log::Reader reader(nullptr, std::move(file_reader), nullptr, true, 0);
SubcompactionProgressBuilder builder;
std::string record;
Slice slice;
while (reader.ReadRecord(&slice, &record)) {
VersionEdit edit;
if (!edit.DecodeFrom(slice).ok()) {
continue;
}
builder.ProcessVersionEdit(edit);
}
EXPECT_TRUE(builder.HasAccumulatedSubcompactionProgress());
return builder.GetAccumulatedSubcompactionProgress();
}
// Test utility function to verify that compaction progress was correctly
// persisted to the progress file after compaction interruption.
//
// VERIFIES:
// - Progress file exists and has expected size (empty if no progress
// expected)
// - Next internal key to compact matches expected user key with proper format
// - Number of processed input records matches position in ordered input keys
// - Number of processed output records equals number of processed input
// records (by test design to simplify verification)
// - Each output file contains exactly one user key (by test design to
// simplify verification)
void VerifyCompactionProgressPersisted(
const std::string& compaction_progress_file,
const std::string& next_user_key_to_compact,
const std::vector<std::string>& ordered_intput_keys) {
ASSERT_OK(env_->FileExists(compaction_progress_file));
uint64_t file_size;
ASSERT_OK(env_->GetFileSize(compaction_progress_file, &file_size));
if (next_user_key_to_compact.empty()) {
ASSERT_EQ(file_size, 0);
return;
}
const auto& subcompaction_progress =
ReadAndParseProgress(compaction_progress_file);
ASSERT_FALSE(subcompaction_progress.next_internal_key_to_compact.empty());
ParsedInternalKey parsed_next_key;
ASSERT_OK(
ParseInternalKey(subcompaction_progress.next_internal_key_to_compact,
&parsed_next_key, true /* log_err_key */));
ASSERT_EQ(parsed_next_key.user_key, next_user_key_to_compact);
ASSERT_EQ(parsed_next_key.sequence, kMaxSequenceNumber);
ASSERT_EQ(parsed_next_key.type, kValueTypeForSeek);
auto it = std::find(ordered_intput_keys.begin(), ordered_intput_keys.end(),
next_user_key_to_compact);
ASSERT_TRUE(it != ordered_intput_keys.end());
auto next_key_index = std::distance(ordered_intput_keys.begin(), it);
ASSERT_EQ(subcompaction_progress.num_processed_input_records,
next_key_index);
ASSERT_EQ(subcompaction_progress.output_level_progress
.GetNumProcessedOutputRecords(),
next_key_index);
ASSERT_EQ(
subcompaction_progress.output_level_progress.GetOutputFiles().size(),
next_key_index);
for (size_t i = 0;
i <
subcompaction_progress.output_level_progress.GetOutputFiles().size();
++i) {
const auto& output_file =
subcompaction_progress.output_level_progress.GetOutputFiles()[i];
ASSERT_EQ(output_file.smallest.user_key().ToString(),
output_file.largest.user_key().ToString());
ASSERT_EQ(output_file.largest.user_key().ToString(),
ordered_intput_keys[i]);
}
}
void RunCancelAndResumeTest(
const std::initializer_list<mock::KVPair>& input_file_1,
const std::initializer_list<mock::KVPair>& input_file_2,
uint64_t last_sequence, const std::vector<uint64_t>& snapshots,
const std::string& expected_next_key_to_compact,
const std::vector<std::string>& expected_input_keys,
bool cancelled_past_mid_point = false) {
std::shared_ptr<Statistics> stats = ROCKSDB_NAMESPACE::CreateDBStatistics();
auto file1 = mock::MakeMockFile(input_file_1);
AddMockFile(file1);
auto file2 = mock::MakeMockFile(input_file_2);
AddMockFile(file2);
SetLastSequence(last_sequence);
// First compaction (will be cancelled)
std::string compaction_progress_file =
CompactionProgressFileName(progress_dir_, 123);
std::unique_ptr<log::Writer> compaction_progress_writer =
CreateCompactionProgressWriter(compaction_progress_file);
ASSERT_OK(stats->Reset());
EnableCompactionCancel();
Status status = RunCompactionWithProgressTracking(
CompactionProgress{}, compaction_progress_writer.get(), snapshots,
stats);
ASSERT_TRUE(status.IsManualCompactionPaused());
DisableCompactionCancel();
HistogramData cancelled_compaction_stats;
stats->histogramData(FILE_WRITE_COMPACTION_MICROS,
&cancelled_compaction_stats);
VerifyCompactionProgressPersisted(compaction_progress_file,
expected_next_key_to_compact,
expected_input_keys);
// Resume compaction
CompactionProgress compaction_progress;
if (expected_next_key_to_compact != "") {
compaction_progress.push_back(
ReadAndParseProgress(compaction_progress_file));
}
std::string compaction_progress_file_2 =
CompactionProgressFileName(progress_dir_, 234);
std::unique_ptr<log::Writer> compaction_progress_writer_2 =
CreateCompactionProgressWriter(compaction_progress_file_2);
ASSERT_OK(stats->Reset());
status = RunCompactionWithProgressTracking(
compaction_progress, compaction_progress_writer_2.get(),
{} /* snapshots */, stats);
ASSERT_OK(status);
if (cancelled_past_mid_point) {
HistogramData resumed_compaction_stats;
stats->histogramData(FILE_WRITE_COMPACTION_MICROS,
&resumed_compaction_stats);
ASSERT_GT(cancelled_compaction_stats.count,
resumed_compaction_stats.count);
}
}
};
TEST_F(ResumableCompactionJobTest, BasicProgressPersistence) {
NewDB();
auto file1 = mock::MakeMockFile({
{KeyStr("a", 1U, kTypeValue), "val1"},
{KeyStr("b", 2U, kTypeValue), "val2"},
});
AddMockFile(file1);
auto file2 = mock::MakeMockFile({
{KeyStr("c", 3U, kTypeValue), "val3"},
{KeyStr("d", 4U, kTypeValue), "val4"},
});
AddMockFile(file2);
SetLastSequence(4U);
std::string compaction_progress_file =
CompactionProgressFileName(progress_dir_, 123);
std::unique_ptr<log::Writer> compaction_progress_writer =
CreateCompactionProgressWriter(compaction_progress_file);
Status status = RunCompactionWithProgressTracking(
CompactionProgress(), compaction_progress_writer.get());
ASSERT_OK(status);
VerifyCompactionProgressPersisted(
compaction_progress_file, "d" /* next_user_key_to_compact */,
{"a", "b", "c", "d"} /* ordered_intput_keys */);
}
TEST_F(ResumableCompactionJobTest, BasicProgressResume) {
NewDB();
RunCancelAndResumeTest(
{{KeyStr("a", 1U, kTypeValue), "val1"},
{KeyStr("b", 2U, kTypeValue), "val2"}} /* input_file_1 */,
{{KeyStr("bb", 3U, kTypeValue), "val3"},
{KeyStr(kCancelBeforeThisKey, 4U, kTypeValue),
"val4"}} /* input_file_2 */,
4U /* last_sequence */, {} /* snapshots */,
kCancelBeforeThisKey /* expected_next_key_to_compact */,
{"a", "b", "bb", kCancelBeforeThisKey} /* expected_input_keys */,
true /* cancelled_past_mid_point */);
}
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnSameKey) {
NewDB();
// `cancel_before_seqno` is set to 0U to force cancellation after
// `kCancelBeforeThisKey@1` instead of `kCancelBeforeThisKey@2`.
// The seqno is 0 because `kCancelBeforeThisKey@1` will have its sequence
// number zeroed during compaction while `kCancelBeforeThisKey@2` won't be
cancel_before_seqno = 0U;
RunCancelAndResumeTest(
{{KeyStr(kCancelBeforeThisKey, 1U, kTypeValue),
"val1"}} /* input_file_1 */,
{{KeyStr(kCancelBeforeThisKey, 2U, kTypeValue), "val11"},
{KeyStr("d", 3U, kTypeValue), "val2"}} /* input_file_2 */,
3U /* last_sequence */, {1U} /* snapshots */,
"" /* expected_next_key_to_compact */,
{kCancelBeforeThisKey, kCancelBeforeThisKey,
"d"} /* expected_input_keys */);
}
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnDeleteRange) {
NewDB();
RunCancelAndResumeTest(
{{KeyStr("a", 1U, kTypeValue), "val1"},
{KeyStr("b", 2U, kTypeValue), "val2"},
{KeyStr(kCancelBeforeThisKey, 3U, kTypeValue),
"val3"}} /* input_file_1 */,
{{KeyStr(kCancelBeforeThisKey, 4U, kTypeRangeDeletion),
"range_deletion_end_key"},
{KeyStr("d", 5U, kTypeValue), "val4"}} /* input_file_2 */,
5U /* last_sequence */, {3U} /* snapshots */,
"b" /* expected_next_key_to_compact */,
{"a", "b", kCancelBeforeThisKey, kCancelBeforeThisKey,
"d"} /* expected_input_keys */);
}
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnMerge) {
merge_op_ = MergeOperators::CreateStringAppendOperator();
NewDB();
RunCancelAndResumeTest(
{{KeyStr("a", 1U, kTypeValue), "val1"},
{KeyStr("b", 2U, kTypeValue), "val2"}} /* input_file_1 */,
{{KeyStr("bb", 3U, kTypeValue), "val3"},
{KeyStr(kCancelBeforeThisKey, 4U, kTypeMerge),
"val4"}} /* input_file_2 */,
4U /* last_sequence */, {} /* snapshots */,
"bb" /* expected_next_key_to_compact */,
{"a", "b", "bb", kCancelBeforeThisKey} /* expected_input_keys */);
}
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnSingleDelete) {
NewDB();
RunCancelAndResumeTest(
{{KeyStr("a", 1U, kTypeValue), "val1"},
{KeyStr("b", 2U, kTypeValue), "val2"},
{KeyStr(kCancelBeforeThisKey, 3U, kTypeValue),
"val3"}} /* input_file_1 */,
{{KeyStr(kCancelBeforeThisKey, 4U, kTypeSingleDeletion), ""},
{KeyStr("d", 5U, kTypeValue), "val4"}} /* input_file_2 */,
5U /* last_sequence */, {3U} /* snapshots */,
"b" /* expected_next_key_to_compact */,
{"a", "b", kCancelBeforeThisKey, kCancelBeforeThisKey,
"d"} /* expected_input_keys */);
}
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnDeletionAtBottom) {
NewDB();
RunCancelAndResumeTest(
{{KeyStr("a", 1U, kTypeValue), "val1"},
{KeyStr("b", 2U, kTypeValue), "val2"},
{KeyStr(kCancelBeforeThisKey, 3U, kTypeValue),
"val3"}} /* input_file_1 */,
{{KeyStr(kCancelBeforeThisKey, 4U, kTypeDeletion), ""},
{KeyStr("d", 5U, kTypeValue), "val4"}} /* input_file_2 */,
5U /* last_sequence */, {3U} /* snapshots */,
"b" /* expected_next_key_to_compact */,
{"a", "b", kCancelBeforeThisKey, kCancelBeforeThisKey,
"d"} /* expected_input_keys */);
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
-20
View File
@@ -60,7 +60,6 @@ class CompactionOutputs {
}
const std::vector<Output>& GetOutputs() const { return outputs_; }
std::vector<Output>& GetMutableOutputs() { return outputs_; }
// Set new table builder for the current output
void NewBuilder(const TableBuilderOptions& tboptions);
@@ -85,19 +84,6 @@ class CompactionOutputs {
bool HasBlobFileAdditions() const { return !blob_file_additions_.empty(); }
// Get all file paths (SST and blob) created during compaction.
const std::vector<std::string>& GetOutputFilePaths() const {
return output_file_paths_;
}
std::vector<std::string>* GetOutputFilePathsPtr() {
return &output_file_paths_;
}
void AddOutputFilePath(const std::string& path) {
output_file_paths_.push_back(path);
}
BlobGarbageMeter* CreateBlobGarbageMeter() {
assert(!is_proximal_level_);
blob_garbage_meter_ = std::make_unique<BlobGarbageMeter>();
@@ -335,12 +321,6 @@ class CompactionOutputs {
std::vector<BlobFileAddition> blob_file_additions_;
std::unique_ptr<BlobGarbageMeter> blob_garbage_meter_;
// All file paths (SST and blob) created during compaction.
// Used for cleanup on abort - ensures orphan files are deleted even if
// they were removed from outputs_ or blob_file_additions_ (e.g., by
// RemoveLastEmptyOutput when file_size is 0 because builder was abandoned).
std::vector<std::string> output_file_paths_;
// Per level's output stat
InternalStats::CompactionStats stats_;
Loaded 100 of 603 files, more files were not shown because too many files have changed in this diff. Show more