mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-07 03:22:34 -05:00
Compare commits
186
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8600f85f6 | ||
|
|
1f910525a7 | ||
|
|
5239c4caeb | ||
|
|
2e21612c3e | ||
|
|
a12bd7d641 | ||
|
|
a69e9fdb7e | ||
|
|
fa8f4f1ef0 | ||
|
|
d534974711 | ||
|
|
cc8d9ea04d | ||
|
|
1fd2c202c7 | ||
|
|
c28b4f0e12 | ||
|
|
4e2fe35bbb | ||
|
|
b3b7668bf5 | ||
|
|
b12d3a6da6 | ||
|
|
5ba6eb5497 | ||
|
|
8fb8fdd349 | ||
|
|
959fa315b1 | ||
|
|
f4fa2d2386 | ||
|
|
ebaa925006 | ||
|
|
3ae314446b | ||
|
|
6842ba8455 | ||
|
|
a2abe3cfd6 | ||
|
|
2e7cf42cda | ||
|
|
3cdf942192 | ||
|
|
d8fc727592 | ||
|
|
ff16ff23ed | ||
|
|
03e5a0b9a8 | ||
|
|
763401b595 | ||
|
|
c4941760c9 | ||
|
|
809ed26be5 | ||
|
|
13ea89f66b | ||
|
|
4c1ffedb1e | ||
|
|
7f89e5fb09 | ||
|
|
b374ba5968 | ||
|
|
6ff96384dd | ||
|
|
a4139ef9e9 | ||
|
|
3ef8b1b743 | ||
|
|
4bc0bff0e4 | ||
|
|
49ec219955 | ||
|
|
790294f3ad | ||
|
|
9886d52fc4 | ||
|
|
36e50a61fc | ||
|
|
7b06a9ae7e | ||
|
|
df5695cf24 | ||
|
|
820c4a77c1 | ||
|
|
94b6566505 | ||
|
|
1cc216a45b | ||
|
|
978bf67426 | ||
|
|
6b8ca89f45 | ||
|
|
50852b5c8d | ||
|
|
9f474a1034 | ||
|
|
f3a20854e0 | ||
|
|
c47e35395b | ||
|
|
a9ac4df630 | ||
|
|
4254c705a2 | ||
|
|
bad2d5b0af | ||
|
|
1fe1b1bd46 | ||
|
|
f3f7feba92 | ||
|
|
b7395b3fa4 | ||
|
|
3b38fde293 | ||
|
|
3070f73e97 | ||
|
|
9d609dd6fe | ||
|
|
4d8a0acd76 | ||
|
|
3efe9460ba | ||
|
|
4dd05023c0 | ||
|
|
6a618d0686 | ||
|
|
ec832ff42c | ||
|
|
91fa765f3a | ||
|
|
92cedd58af | ||
|
|
3dd6d060e5 | ||
|
|
1b817cff45 | ||
|
|
fc85a700cf | ||
|
|
06699cd0fb | ||
|
|
0186937710 | ||
|
|
8ec2177bd1 | ||
|
|
156328107d | ||
|
|
3f51c0a185 | ||
|
|
65e77282ce | ||
|
|
a0ff5b143f | ||
|
|
64f8d5765b | ||
|
|
4cf28450a5 | ||
|
|
ae0c9faaef | ||
|
|
51561fc2cd | ||
|
|
ebd118711c | ||
|
|
ffe58fa976 | ||
|
|
d6b03d9c96 | ||
|
|
abc1f7ced5 | ||
|
|
01ec15f328 | ||
|
|
af4e32945b | ||
|
|
b611aa7309 | ||
|
|
830347c765 | ||
|
|
de6cdbb62d | ||
|
|
b8bbee0f3e | ||
|
|
24fac74206 | ||
|
|
f12465cb93 | ||
|
|
7832ee886f | ||
|
|
ae322e0a73 | ||
|
|
1e23ee053f | ||
|
|
effac3a918 | ||
|
|
c7f0e1fa93 | ||
|
|
141a1f0abf | ||
|
|
41e5eb1a4d | ||
|
|
875b8544df | ||
|
|
e96295a288 | ||
|
|
f897c1b2ef | ||
|
|
dae450b78e | ||
|
|
2af38206ad | ||
|
|
5db0603613 | ||
|
|
a965706e73 | ||
|
|
1a4b1e42cc | ||
|
|
47de3a3a2c | ||
|
|
926e2b589f | ||
|
|
453ebd6f37 | ||
|
|
24dc8306f8 | ||
|
|
0921ec1200 | ||
|
|
f620a6d039 | ||
|
|
c78144e452 | ||
|
|
056a0f9664 | ||
|
|
f8ff77db6a | ||
|
|
dcf3db95a6 | ||
|
|
ec31d0074f | ||
|
|
304ae7190a | ||
|
|
9de0ea32e1 | ||
|
|
77a0e347bb | ||
|
|
89e384ca6f | ||
|
|
3d99378791 | ||
|
|
f455ab7bd6 | ||
|
|
63c86160cd | ||
|
|
89322fdd9e | ||
|
|
b23fc77aca | ||
|
|
b6f498b2c9 | ||
|
|
bcaf2794dc | ||
|
|
1cb7594b87 | ||
|
|
166b4a4487 | ||
|
|
03bded03b4 | ||
|
|
ec22903914 | ||
|
|
0c6f741422 | ||
|
|
c66c14258f | ||
|
|
e43171d6ca | ||
|
|
1e1097d8d3 | ||
|
|
e44a99f560 | ||
|
|
5494bc1d76 | ||
|
|
91f227d9be | ||
|
|
cb8bc56d14 | ||
|
|
1ed5052486 | ||
|
|
db9449a154 | ||
|
|
26aff32db3 | ||
|
|
2afb387917 | ||
|
|
a066318a31 | ||
|
|
42eff8b632 | ||
|
|
cb43abb1f1 | ||
|
|
e27114b135 | ||
|
|
3ad23b2d94 | ||
|
|
8a86620653 | ||
|
|
071b5eff7a | ||
|
|
3aa706c2bf | ||
|
|
2dc6d6f765 | ||
|
|
ef5b145140 | ||
|
|
3b5cb114e3 | ||
|
|
16c81a27e9 | ||
|
|
3b66de3fac | ||
|
|
9a9cc1e2ef | ||
|
|
ea2f33c81a | ||
|
|
15f2f2bf6f | ||
|
|
9d3d3c4b61 | ||
|
|
cc19bd7555 | ||
|
|
c2580e2b7a | ||
|
|
f25fb41da6 | ||
|
|
b48d6f2c12 | ||
|
|
aa9160e8d7 | ||
|
|
da2c3c0ee6 | ||
|
|
8707abfc11 | ||
|
|
ce0fff9f41 | ||
|
|
372995470a | ||
|
|
1a8471b17e | ||
|
|
c311362ab6 | ||
|
|
5cb88bae30 | ||
|
|
300fb8cace | ||
|
|
5db2eb0f3c | ||
|
|
1ba60ee9ee | ||
|
|
e95f3759e9 | ||
|
|
de3616dc3b | ||
|
|
4213f9e14a | ||
|
|
49a4165e3a | ||
|
|
bd5b299b92 | ||
|
|
e7c3391640 |
No files matched your search
+1
-3
@@ -17,9 +17,7 @@ Checks: "-*,\
|
||||
cert-err58-cpp,\
|
||||
cert-msc30-c,\
|
||||
cert-msc50-cpp,\
|
||||
clang-analyzer-core.NullDereference,\
|
||||
clang-analyzer-core.StackAddressEscape,\
|
||||
clang-analyzer-deadcode.DeadStores,\
|
||||
clang-analyzer-*,\
|
||||
clang-diagnostic-*,\
|
||||
-clang-diagnostic-missing-designated-field-initializers,\
|
||||
concurrency-mt-unsafe,\
|
||||
|
||||
@@ -9,7 +9,16 @@ runs:
|
||||
steps:
|
||||
- name: Build folly and dependencies
|
||||
if: ${{ inputs.cache-hit != 'true' }}
|
||||
run: make build_folly
|
||||
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
|
||||
shell: bash
|
||||
- name: Skip folly build (using cached version)
|
||||
if: ${{ inputs.cache-hit == 'true' }}
|
||||
|
||||
@@ -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 }}
|
||||
key: folly-build-${{ runner.os }}-${{ runner.arch }}-${{ github.job_container.image }}-${{ steps.extract-folly-hash.outputs.hash }}-${{ env.PORTABLE == '1' && 'portable' || 'native' }}
|
||||
@@ -2,6 +2,9 @@ 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"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
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
|
||||
@@ -0,0 +1,15 @@
|
||||
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
|
||||
@@ -1,19 +1,31 @@
|
||||
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
|
||||
@@ -56,12 +68,22 @@ runs:
|
||||
msbuild build/rocksdb.sln /m:32 /p:LinkIncremental=false -property:Configuration=Debug -property:Platform=x64
|
||||
if(!$?) { Exit $LASTEXITCODE }
|
||||
echo ========================= Test RocksDB =========================
|
||||
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 }
|
||||
$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"
|
||||
}
|
||||
shell: pwsh
|
||||
- name: Show ccache stats
|
||||
shell: pwsh
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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');
|
||||
};
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/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"
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/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)"
|
||||
@@ -0,0 +1,118 @@
|
||||
// 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',
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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',
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,204 @@
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,347 @@
|
||||
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
@@ -0,0 +1,185 @@
|
||||
# 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.'
|
||||
);
|
||||
@@ -1,105 +1,51 @@
|
||||
name: clang-tidy
|
||||
name: Post clang-tidy PR comment
|
||||
on:
|
||||
push:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
workflow_run:
|
||||
workflows: ["clang-tidy"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
clang-tidy:
|
||||
if: github.repository_owner == 'facebook'
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
comment:
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
- 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_target" ]; then
|
||||
BASE="${{ github.event.pull_request.base.sha }}"
|
||||
else
|
||||
BASE="${{ github.event.before }}"
|
||||
fi
|
||||
if [ -z "$BASE" ] || echo "$BASE" | grep -q '^0\{40\}$'; then
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
echo "No valid diff base; skipping clang-tidy."
|
||||
else
|
||||
git fetch --depth=1 origin "$BASE"
|
||||
echo "ref=$BASE" >> "$GITHUB_OUTPUT"
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Install clang-tidy
|
||||
if: steps.diff-base.outputs.skip != 'true'
|
||||
run: apt-get update && apt-get install -y 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-18 \
|
||||
-DCMAKE_CXX_COMPILER=clang++-18 ..
|
||||
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
|
||||
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 clang-tidy results to PR
|
||||
if: github.event_name == 'pull_request_target' && always() && steps.diff-base.outputs.skip != 'true'
|
||||
|
||||
- name: Post or update PR comment
|
||||
if: steps.download.outcome == 'success'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const commentPath = 'clang-tidy-comment.md';
|
||||
if (!fs.existsSync(commentPath)) {
|
||||
core.info('No comment file generated; skipping PR comment.');
|
||||
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(commentPath, 'utf8');
|
||||
const marker = '<!-- clang-tidy-bot -->';
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
try {
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
core.info(`Updated existing comment ${existing.id}`);
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
core.info('Created new PR comment');
|
||||
}
|
||||
} catch (err) {
|
||||
core.warning(`Could not post PR comment: ${err.message}`);
|
||||
}
|
||||
- name: Fail if clang-tidy found issues
|
||||
if: steps.clang-tidy.outcome == 'failure'
|
||||
run: exit 1
|
||||
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 -->',
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
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
|
||||
@@ -0,0 +1,25 @@
|
||||
# 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
|
||||
@@ -0,0 +1,69 @@
|
||||
# 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)
|
||||
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
|
||||
@@ -0,0 +1,25 @@
|
||||
# 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
|
||||
@@ -0,0 +1,68 @@
|
||||
# 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
|
||||
@@ -41,22 +41,41 @@ jobs:
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: make V=1 -j32 check
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-clang-18-asan-ubsan-with-folly:
|
||||
build-linux-clang-21-asan-ubsan-with-folly:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
env:
|
||||
CC: clang-18
|
||||
CXX: clang++-18
|
||||
CC: clang-21
|
||||
CXX: clang++-21
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/build-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
|
||||
- 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:
|
||||
@@ -145,13 +164,13 @@ jobs:
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- name: Build rocksdb lib
|
||||
run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 make -j4 static_lib
|
||||
run: CC=clang-21 CXX=clang++-21 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"
|
||||
|
||||
+201
-72
@@ -72,6 +72,8 @@ 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
|
||||
@@ -92,7 +94,12 @@ 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')
|
||||
@@ -100,11 +107,14 @@ jobs:
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.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-mingw
|
||||
- run: update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix
|
||||
- name: Build cmake-mingw
|
||||
run: |-
|
||||
@@ -113,12 +123,14 @@ 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
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
labels: 32-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
@@ -127,12 +139,17 @@ 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 -j32 check
|
||||
- run: USE_FOLLY=1 LIB_MODE=static V=1 make -j64 check
|
||||
- uses: "./.github/actions/teardown-ccache"
|
||||
if: always()
|
||||
- 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')
|
||||
@@ -147,13 +164,18 @@ 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
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
labels: 32-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
@@ -162,26 +184,45 @@ 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 .. && make VERBOSE=1 -j20 && ctest -j20)"
|
||||
- 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()
|
||||
- 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
|
||||
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"
|
||||
- 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/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()
|
||||
- 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
|
||||
@@ -193,8 +234,13 @@ 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:
|
||||
@@ -207,6 +253,9 @@ 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
|
||||
@@ -223,6 +272,8 @@ 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')
|
||||
@@ -235,26 +286,36 @@ 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-18-no_test_run:
|
||||
if: needs.config.outputs.only_job == 'build-linux-clang-18-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
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
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 make -j32 all microbench
|
||||
- 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: make clean
|
||||
- run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 DEBUG_LEVEL=0 make -j32 release
|
||||
- run: CC=clang-21 CXX=clang++-21 USE_CLANG=1 DEBUG_LEVEL=0 make -j32 release
|
||||
- uses: "./.github/actions/teardown-ccache"
|
||||
if: always()
|
||||
- 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')
|
||||
@@ -262,35 +323,20 @@ jobs:
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.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: 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: needs.config.outputs.only_job == 'build-linux-clang18-clang-analyze' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
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')
|
||||
@@ -303,50 +349,98 @@ 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
|
||||
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"
|
||||
- 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/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()
|
||||
- uses: "./.github/actions/post-steps"
|
||||
with:
|
||||
artifact-prefix: "${{ github.job }}-${{ matrix.crash_test_target }}"
|
||||
# ======================= Linux with Sanitizers ===================== #
|
||||
build-linux-clang18-asan-ubsan:
|
||||
if: needs.config.outputs.only_job == 'build-linux-clang18-asan-ubsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
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
|
||||
runs-on:
|
||||
labels: 32-core-ubuntu
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [0, 1, 2]
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- 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/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()
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-clang18-mini-tsan:
|
||||
if: needs.config.outputs.only_job == 'build-linux-clang18-mini-tsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
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
|
||||
runs-on:
|
||||
labels: 32-core-ubuntu
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [0, 1, 2]
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- 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/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()
|
||||
- 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
|
||||
@@ -358,7 +452,12 @@ 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:
|
||||
@@ -372,11 +471,16 @@ 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:
|
||||
@@ -391,6 +495,9 @@ 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"
|
||||
@@ -410,6 +517,8 @@ 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
|
||||
@@ -417,12 +526,28 @@ jobs:
|
||||
if: needs.config.outputs.only_job == 'build-windows-vs2022' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
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')
|
||||
@@ -433,29 +558,20 @@ jobs:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
# 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: actions/checkout@v4.1.0
|
||||
- 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
|
||||
# post-steps skipped because of compatibility issues with docker image
|
||||
- uses: "./.github/actions/teardown-ccache"
|
||||
if: always()
|
||||
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
|
||||
@@ -465,29 +581,20 @@ jobs:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
# 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: actions/checkout@v4.1.0
|
||||
- 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
|
||||
# post-steps skipped because of compatibility issues with docker image
|
||||
- uses: "./.github/actions/teardown-ccache"
|
||||
if: always()
|
||||
build-macos-java:
|
||||
if: needs.config.outputs.only_job == 'build-macos-java' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
@@ -500,6 +607,9 @@ 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"
|
||||
@@ -511,6 +621,8 @@ 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')
|
||||
@@ -523,6 +635,9 @@ 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"
|
||||
@@ -534,6 +649,8 @@ 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')
|
||||
@@ -546,6 +663,9 @@ 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"
|
||||
@@ -557,6 +677,8 @@ 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')
|
||||
@@ -593,6 +715,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: sudo apt-get update && sudo apt-get install -y build-essential
|
||||
- run: sudo apt-get update && sudo apt-get install -y build-essential ccache
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: arm
|
||||
- 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"
|
||||
@@ -102,3 +102,11 @@ 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/**
|
||||
@@ -0,0 +1,9 @@
|
||||
# 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.
|
||||
@@ -32,13 +32,18 @@ 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/blog/blog_format.cc",
|
||||
"db/blog/blog_reader.cc",
|
||||
"db/blog/blog_writer.cc",
|
||||
"db/builder.cc",
|
||||
"db/c.cc",
|
||||
"db/coalescing_iterator.cc",
|
||||
@@ -106,8 +111,10 @@ 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",
|
||||
@@ -207,6 +214,7 @@ 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",
|
||||
@@ -329,6 +337,7 @@ 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",
|
||||
@@ -362,6 +371,9 @@ 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",
|
||||
@@ -369,10 +381,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/experimental/coro:blocking_wait",
|
||||
"//folly/experimental/coro:collect",
|
||||
"//folly/experimental/coro:coroutine",
|
||||
"//folly/experimental/coro:task",
|
||||
"//folly/coro:blocking_wait",
|
||||
"//folly/coro:collect",
|
||||
"//folly/coro:coroutine",
|
||||
"//folly/coro:task",
|
||||
"//folly/synchronization:distributed_mutex",
|
||||
], headers=glob(["**/*.h"]), link_whole=False, extra_test_libs=False)
|
||||
|
||||
@@ -4583,6 +4595,18 @@ cpp_unittest_wrapper(name="block_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="blog_format_test",
|
||||
srcs=["db/blog/blog_format_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="blog_writer_test",
|
||||
srcs=["db/blog/blog_writer_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="bloom_test",
|
||||
srcs=["util/bloom_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -4799,6 +4823,12 @@ 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"],
|
||||
@@ -4937,6 +4967,12 @@ 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"],
|
||||
@@ -5027,6 +5063,12 @@ 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"],
|
||||
@@ -5137,6 +5179,12 @@ 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"],
|
||||
@@ -5521,6 +5569,12 @@ 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"],
|
||||
@@ -5623,6 +5677,18 @@ 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"],
|
||||
@@ -5725,14 +5791,14 @@ cpp_unittest_wrapper(name="write_controller_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="write_prepared_transaction_test",
|
||||
srcs=["utilities/transactions/write_prepared_transaction_test.cc"],
|
||||
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_seqno",
|
||||
srcs=["utilities/transactions/write_prepared_transaction_test_seqno.cc"],
|
||||
cpp_unittest_wrapper(name="write_prepared_transaction_test",
|
||||
srcs=["utilities/transactions/write_prepared_transaction_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
@@ -227,6 +227,11 @@ The following patterns emerged as frequent sources of review feedback:
|
||||
* 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
|
||||
@@ -250,6 +255,9 @@ The following patterns emerged as frequent sources of review feedback:
|
||||
### 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
|
||||
@@ -258,6 +266,14 @@ The following patterns emerged as frequent sources of review feedback:
|
||||
### 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
|
||||
|
||||
@@ -271,6 +287,9 @@ The following patterns emerged as frequent sources of review feedback:
|
||||
* 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
|
||||
|
||||
+83
-5
@@ -82,10 +82,15 @@ 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_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
|
||||
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache)
|
||||
set(CMAKE_C_COMPILER_LAUNCHER ccache)
|
||||
set(CMAKE_CXX_COMPILER_LAUNCHER ccache)
|
||||
endif(CCACHE_FOUND)
|
||||
|
||||
option(WITH_JEMALLOC "build with JeMalloc" OFF)
|
||||
@@ -515,6 +520,15 @@ 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")
|
||||
@@ -660,10 +674,38 @@ 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)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--copy-dt-needed-entries")
|
||||
# --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()
|
||||
endif()
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
@@ -690,6 +732,7 @@ 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
|
||||
@@ -698,7 +741,11 @@ 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/blog/blog_format.cc
|
||||
db/blog/blog_reader.cc
|
||||
db/blog/blog_writer.cc
|
||||
db/builder.cc
|
||||
db/c.cc
|
||||
db/coalescing_iterator.cc
|
||||
@@ -766,8 +813,10 @@ 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
|
||||
@@ -842,6 +891,7 @@ 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
|
||||
@@ -948,6 +998,7 @@ 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
|
||||
@@ -999,6 +1050,9 @@ 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
|
||||
@@ -1114,8 +1168,23 @@ 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)
|
||||
list(APPEND THIRDPARTY_LIBS glog)
|
||||
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()
|
||||
endif()
|
||||
|
||||
set(ROCKSDB_STATIC_LIB rocksdb${ARTIFACT_SUFFIX})
|
||||
@@ -1361,9 +1430,12 @@ 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
|
||||
db/blog/blog_format_test.cc
|
||||
db/blog/blog_writer_test.cc
|
||||
db/column_family_test.cc
|
||||
db/compact_files_test.cc
|
||||
db/compaction/clipping_iterator_test.cc
|
||||
@@ -1398,6 +1470,7 @@ 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
|
||||
@@ -1450,6 +1523,7 @@ 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
|
||||
@@ -1530,7 +1604,9 @@ 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
|
||||
@@ -1548,10 +1624,12 @@ 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_test_seqno.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
|
||||
|
||||
+64
@@ -1,6 +1,70 @@
|
||||
# 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
|
||||
|
||||
### 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.0 (01/23/2026)
|
||||
### Public API Changes
|
||||
* New SetOptions API that allows setting options for multiple CFs, avoiding the need to reserialize OPTIONS file for each CF
|
||||
|
||||
@@ -450,7 +450,7 @@ ifndef USE_FOLLY
|
||||
endif
|
||||
|
||||
ifndef GTEST_THROW_ON_FAILURE
|
||||
export GTEST_THROW_ON_FAILURE=1
|
||||
export GTEST_THROW_ON_FAILURE=0
|
||||
endif
|
||||
ifndef GTEST_HAS_EXCEPTIONS
|
||||
export GTEST_HAS_EXCEPTIONS=1
|
||||
@@ -621,25 +621,24 @@ endif
|
||||
|
||||
ROCKSDBTESTS_SUBSET ?= $(TESTS)
|
||||
|
||||
# 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
|
||||
# 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).
|
||||
NON_PARALLEL_TEST = \
|
||||
c_test \
|
||||
env_test \
|
||||
deletefile_test \
|
||||
db_bloom_filter_test \
|
||||
$(PLUGIN_TESTS) \
|
||||
|
||||
PARALLEL_TEST = $(filter-out $(NON_PARALLEL_TEST), $(TESTS))
|
||||
PARALLEL_TEST = $(filter-out $(NON_PARALLEL_TEST), $(ROCKSDBTESTS_SUBSET))
|
||||
|
||||
# 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 \
|
||||
@@ -647,6 +646,7 @@ TESTS_PLATFORM_DEPENDENT := \
|
||||
dynamic_bloom_test \
|
||||
c_test \
|
||||
checkpoint_test \
|
||||
sorted_run_builder_test \
|
||||
crc32c_test \
|
||||
coding_test \
|
||||
inlineskiplist_test \
|
||||
@@ -807,7 +807,18 @@ endif # PLATFORM_SHARED_EXT
|
||||
rocksdbjavastatic rocksdbjava install install-static install-shared \
|
||||
uninstall analyze tools tools_lib check-headers checkout_folly clang-tidy
|
||||
|
||||
all: $(LIBRARY) $(BENCHMARKS) tools tools_lib test_libs $(TESTS)
|
||||
# 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_but_some_tests: $(LIBRARY) $(BENCHMARKS) tools tools_lib test_libs $(ROCKSDBTESTS_SUBSET)
|
||||
|
||||
@@ -871,21 +882,42 @@ 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_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//\//-}; \
|
||||
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; \
|
||||
printf '%s\n' \
|
||||
'#!/bin/sh' \
|
||||
"d=\$(TEST_TMPDIR)$$TEST_SCRIPT" \
|
||||
'mkdir -p $$d' \
|
||||
"TEST_TMPDIR=\$$d $(DRIVER) ./$$TEST_BINARY --gtest_filter=$$TEST_NAME" \
|
||||
"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_SCRIPT; \
|
||||
chmod a=rx $$TEST_SCRIPT; \
|
||||
SHARD_IDX=$$((SHARD_IDX + 1)); \
|
||||
done
|
||||
|
||||
gen_parallel_tests:
|
||||
@@ -909,8 +941,10 @@ 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 = \
|
||||
^.*MySQLStyleTransactionTest.*$$|^.*SnapshotConcurrentAccessTest.*$$|^.*SeqAdvanceConcurrentTest.*$$|^t/run-table_test-HarnessTest.Randomized$$|^t/run-db_test-.*(?:FileCreationRandomFailure|EncodeDecompressedBlockSizeTest)$$|^.*RecoverFromCorruptedWALWithoutFlush$$
|
||||
^.*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-.*$$
|
||||
prioritize_long_running_tests = \
|
||||
perl -pe 's,($(slow_test_regexp)),100 $$1,' \
|
||||
| sort -k1,1gr \
|
||||
@@ -945,7 +979,13 @@ check_0:
|
||||
'To monitor subtest <duration,pass/fail,name>,' \
|
||||
' run "make watch-log" in a separate window' ''; \
|
||||
{ \
|
||||
printf './%s\n' $(filter-out $(PARALLEL_TEST),$(TESTS)); \
|
||||
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; \
|
||||
find t -name 'run-*' -print; \
|
||||
} \
|
||||
| $(prioritize_long_running_tests) \
|
||||
@@ -1001,6 +1041,11 @@ check-progress:
|
||||
# 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) | \
|
||||
@@ -1016,6 +1061,7 @@ 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
|
||||
@@ -1023,6 +1069,7 @@ ifndef SKIP_FORMAT_BUCK_CHECKS
|
||||
$(MAKE) check-format
|
||||
$(MAKE) check-buck-targets
|
||||
$(MAKE) check-sources
|
||||
$(MAKE) check-workflow-yaml
|
||||
endif
|
||||
|
||||
# TODO add ldb_tests
|
||||
@@ -1033,6 +1080,10 @@ 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
|
||||
@@ -1216,12 +1267,43 @@ format-auto:
|
||||
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:
|
||||
@@ -1409,6 +1491,9 @@ 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)
|
||||
|
||||
@@ -1418,6 +1503,9 @@ db_readonly_with_timestamp_test: $(OBJ_DIR)/db/db_readonly_with_timestamp_test.o
|
||||
db_wide_basic_test: $(OBJ_DIR)/db/wide/db_wide_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_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)
|
||||
|
||||
@@ -1427,6 +1515,9 @@ 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)
|
||||
|
||||
@@ -1445,6 +1536,12 @@ db_logical_block_size_cache_test: $(OBJ_DIR)/db/db_logical_block_size_cache_test
|
||||
db_blob_index_test: $(OBJ_DIR)/db/blob/db_blob_index_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
blog_format_test: $(OBJ_DIR)/db/blog/blog_format_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
blog_writer_test: $(OBJ_DIR)/db/blog/blog_writer_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_block_cache_test: $(OBJ_DIR)/db/db_block_cache_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -1571,6 +1668,9 @@ backup_engine_test: $(OBJ_DIR)/utilities/backup/backup_engine_test.o $(TEST_LIBR
|
||||
checkpoint_test: $(OBJ_DIR)/utilities/checkpoint/checkpoint_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
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)
|
||||
|
||||
@@ -1589,6 +1689,12 @@ 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)
|
||||
|
||||
@@ -1640,6 +1746,9 @@ 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)
|
||||
|
||||
@@ -1838,7 +1947,7 @@ 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_test_seqno: $(OBJ_DIR)/utilities/transactions/write_prepared_transaction_test_seqno.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
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)
|
||||
@@ -2561,7 +2670,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 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 check-workflow-yaml clang-tidy jclean jtest package analyze tags rocksdbjavastatic% unity.% unity_test checkout_folly, $(MAKECMDGOALS))
|
||||
ifneq ("$(ROCKS_DEP_RULES)", "")
|
||||
-include $(DEPFILES)
|
||||
endif
|
||||
@@ -0,0 +1,318 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Blog File Format Specification - RocksDB</title>
|
||||
<style>
|
||||
body { font-family: 'Segoe UI', Arial, sans-serif; max-width: 960px; margin: 40px auto; padding: 0 20px; color: #222; line-height: 1.6; }
|
||||
h1 { border-bottom: 3px solid #333; padding-bottom: 8px; }
|
||||
h2 { border-bottom: 2px solid #999; padding-bottom: 4px; margin-top: 40px; }
|
||||
h3 { margin-top: 30px; }
|
||||
pre { background: #f4f4f4; border: 1px solid #ddd; border-radius: 4px; padding: 12px 16px; overflow-x: auto; font-size: 13px; line-height: 1.5; }
|
||||
code { background: #f0f0f0; padding: 1px 5px; border-radius: 3px; font-size: 13px; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 16px 0; }
|
||||
th, td { border: 1px solid #bbb; padding: 8px 12px; text-align: left; vertical-align: top; }
|
||||
th { background: #e8e8e8; font-weight: 600; }
|
||||
.diagram { background: #fafafa; border: 1px solid #ccc; border-radius: 4px; padding: 16px; margin: 16px 0; font-family: 'Courier New', monospace; font-size: 13px; white-space: pre; overflow-x: auto; line-height: 1.4; }
|
||||
.note { background: #fff8e1; border-left: 4px solid #ffc107; padding: 10px 14px; margin: 14px 0; }
|
||||
.decision { background: #e8f5e9; border-left: 4px solid #4caf50; padding: 10px 14px; margin: 14px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>"Blog" File Format Specification</h1>
|
||||
<p><em>A unified file format for WAL, Blob, and related data in RocksDB</em></p>
|
||||
<p>Status: Implementation in progress | Format name: "blog" (portmanteau of "blob" + "log")</p>
|
||||
|
||||
<!-- ======================================================================= -->
|
||||
<h2>Vocabulary</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>Term</th><th>Scope</th><th>Definition</th></tr>
|
||||
<tr><td><strong>Record</strong></td><td>Blog file</td><td>A top-level unit in the blog file body: blob record, WriteBatch record, preamble-start record, footer record.</td></tr>
|
||||
<tr><td><strong>Operation (op)</strong></td><td>WriteBatch</td><td>An entry within a WriteBatch: Put op, Delete op, Merge op, etc.</td></tr>
|
||||
<tr><td><strong>Compact format</strong></td><td>Record</td><td>A record with varint length ≤ 3 bytes. Omits the type byte (type from header).</td></tr>
|
||||
<tr><td><strong>Full format</strong></td><td>Record</td><td>A record with varint length > 3 bytes. Includes explicit type, pre-payload compression type, and prefix checksum.</td></tr>
|
||||
</table>
|
||||
|
||||
<!-- ======================================================================= -->
|
||||
<h2 id="s1">File-Level Structure</h2>
|
||||
|
||||
<p>A blog file consists of three regions: a <strong>fixed header</strong> with a <strong>property section</strong>, a <strong>body</strong> of records, and an optional <strong>footer</strong> composed of footer records.</p>
|
||||
|
||||
<div class="diagram">
|
||||
+---------------------------------------------------------------------+
|
||||
| FILE HEADER (40 bytes fixed) |
|
||||
| 96-bit magic, schema version, checksum type, compact record type, |
|
||||
| flags, escape sequence, reserved, property section size, checksum |
|
||||
+---------------------------------------------------------------------+
|
||||
| HEADER PROPERTY SECTION |
|
||||
| Named key-value pairs + 5-byte trailer (compression + checksum) |
|
||||
| Followed by arbitrarily long padding (0x00 or 0xFF) |
|
||||
+---------------------------------------------------------------------+
|
||||
| FILE BODY |
|
||||
| Sequence of records, each starting with the escape sequence. |
|
||||
| Record types: blob, WriteBatch, preamble-start, footer records. |
|
||||
+---------------------------------------------------------------------+
|
||||
| FILE FOOTER (optional) |
|
||||
| Footer index records (0xFD), footer properties record (0xFE), |
|
||||
| footer locator record (0xFF) -- must be last. |
|
||||
+---------------------------------------------------------------------+</div>
|
||||
|
||||
<h3>Fixed Header (40 bytes)</h3>
|
||||
|
||||
<table>
|
||||
<tr><th>Offset</th><th>Size</th><th>Field</th></tr>
|
||||
<tr><td>0</td><td>12</td><td>File magic: <code>kBlogFileMagic</code> -- opaque 12 bytes ("Blog" ASCII + XXH3_64bits("Blog"))</td></tr>
|
||||
<tr><td>12</td><td>1</td><td>Schema version (currently 0). Newer versions are rejected by older readers.</td></tr>
|
||||
<tr><td>13</td><td>1</td><td>Checksum type (<code>ChecksumType</code> enum: 0=none, 1=CRC32c, 4=XXH3). Configured via <code>DBOptions::blog_checksum</code>.</td></tr>
|
||||
<tr><td>14</td><td>1</td><td>Compact record type (<code>BlogRecordType</code> -- type for all compact-format records)</td></tr>
|
||||
<tr><td>15</td><td>1</td><td>Flags: bit 0 = <code>kBlogFileRecycled</code> (file may contain trailing stale data)</td></tr>
|
||||
<tr><td>16</td><td>10</td><td>Escape sequence: bytes [0-5] random (byte[0] not 0x00/0xFF), bytes [6-9] = lower32(XXH3_64bits_withSeed(bytes[0-5], kBlogEscapeSeqSeed)). First 4 bytes also serve as incarnation ID (little-endian via DecodeFixed32).</td></tr>
|
||||
<tr><td>26</td><td>6</td><td>Reserved (zero)</td></tr>
|
||||
<tr><td>32</td><td>4</td><td>Property section size (uint32 LE) -- includes 5-byte trailer</td></tr>
|
||||
<tr><td>36</td><td>4</td><td>Fixed header checksum -- covers bytes [0, 36)</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Header Property Section</h3>
|
||||
|
||||
<p>Immediately follows the fixed header. Contains named key-value pairs encoded as:</p>
|
||||
<pre>[name_len: varint32] [name: name_len bytes] [value_len: varint32] [value: value_len bytes] ...</pre>
|
||||
<p>Followed by a 5-byte trailer: <code>[compression_type: 1B][checksum: 4B]</code> (same as block-based table). The checksum covers the property data + compression_type, with a context checksum modifier using the incarnation ID at offset <code>kBlogFileFixedHeaderSize</code>.</p>
|
||||
|
||||
<p>After the property section, arbitrarily long padding (0x00 or 0xFF, differing from the last meaningful byte) aligns the first record's escape sequence. This supports stronger alignment requirements (flush/sync boundaries) or recycling files of all zeros.</p>
|
||||
|
||||
<h3>Property Name Convention</h3>
|
||||
<table>
|
||||
<tr><th>First letter</th><th>Meaning</th></tr>
|
||||
<tr><td>Uppercase (e.g. <code>"CompressionCompatibilityName"</code>)</td><td><strong>Required</strong> -- reader must recognize or reject the file</td></tr>
|
||||
<tr><td>Lowercase (e.g. <code>"role"</code>)</td><td><strong>Ignorable</strong> -- reader may skip unrecognized properties</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Known Header Properties</h3>
|
||||
<table>
|
||||
<tr><th>Name</th><th>Required?</th><th>Description</th></tr>
|
||||
<tr><td><code>"CompressionCompatibilityName"</code></td><td>Required (if any compression)</td><td>Compression manager compatibility name (e.g. "builtin_v2")</td></tr>
|
||||
<tr><td><code>"WriteBatchStreamingCompressionType"</code></td><td>Required (if streaming active)</td><td>2-digit hex of CompressionType for streaming WriteBatch compression (e.g. "07" for ZSTD). Presence means WriteBatch records with <code>kStreamingCompressionSentinel</code> (0x7F) trailer are part of a cross-record streaming context.</td></tr>
|
||||
<tr><td><code>"TimestampSize"</code></td><td>Required (if >0)</td><td>Size of user timestamps</td></tr>
|
||||
<tr><td><code>"PredecessorWalInfo"</code></td><td>Required (if tracking)</td><td>Predecessor WAL metadata for verification</td></tr>
|
||||
<tr><td><code>"role"</code></td><td>Ignorable</td><td>File purpose hint: "wal", "blob", "rlog"</td></tr>
|
||||
<tr><td><code>"compressionSettings"</code></td><td>Ignorable</td><td>Human-readable compression settings for debugging</td></tr>
|
||||
<tr><td><code>"creationTime"</code></td><td>Ignorable</td><td>Unix timestamp of file creation</td></tr>
|
||||
<tr><td><code>"creator"</code></td><td>Ignorable</td><td>Host/process identifier for debugging</td></tr>
|
||||
</table>
|
||||
|
||||
<!-- ======================================================================= -->
|
||||
<h2 id="s2">Escape Sequences</h2>
|
||||
|
||||
<div class="decision">
|
||||
<strong>Decision:</strong> 10-byte escape sequence: 6 random bytes + 4 derived bytes. First byte not 0x00 nor 0xFF (distinguishable from padding). Bytes [6-9] = lower32(XXH3_64bits_withSeed(bytes[0-5], kBlogEscapeSeqSeed)), enabling heuristic detection without knowing the header.
|
||||
</div>
|
||||
|
||||
<p>Escape sequences must be <strong>32-bit aligned</strong> in the file. The first 4 bytes (little-endian) serve as the <strong>incarnation ID</strong> for context checksums.</p>
|
||||
|
||||
<h3>Primary Roles</h3>
|
||||
<ul>
|
||||
<li><strong>WAL recycling detection:</strong> When recovery encounters undecodable data, scan for our escape sequence. If not found, the data is stale/recycled (different random sequence). The <code>kBlogFileRecycled</code> flag in the header indicates recycling is expected.</li>
|
||||
<li><strong>Non-linear recovery:</strong> After corruption, scan forward for the next escape sequence to resume recovery (<code>kSkipAnyCorruptedRecords</code>).</li>
|
||||
<li><strong>Record boundary discovery:</strong> Find record boundaries from any file position.</li>
|
||||
</ul>
|
||||
|
||||
<!-- ======================================================================= -->
|
||||
<h2 id="s3">Record Types and Formats</h2>
|
||||
|
||||
<h3>Record Type Values</h3>
|
||||
<table>
|
||||
<tr><th>Value</th><th>Name</th><th>Usage</th></tr>
|
||||
<tr><td colspan="3"><em>Body record types (0x01-0x7F)</em></td></tr>
|
||||
<tr><td>0x01</td><td><code>kBlogBlobRecord</code></td><td>Blob data (typically a separated value)</td></tr>
|
||||
<tr><td>0x02</td><td><code>kBlogPreambleStartRecord</code></td><td>Preamble marker (stub)</td></tr>
|
||||
<tr><td>0x03</td><td><code>kBlogWriteBatchRecord</code></td><td>Serialized WriteBatch</td></tr>
|
||||
<tr><td>0x04-0x7F</td><td></td><td><em>Reserved for future body record types</em></td></tr>
|
||||
<tr><td colspan="3"><em>Footer record types (0xF0-0xFF, in expected order of occurrence with gaps)</em></td></tr>
|
||||
<tr><td>0xF0</td><td><code>kBlogFooterIndexRecord</code></td><td>Large footer metadata (sparse index)</td></tr>
|
||||
<tr><td>0xF1-0xF3</td><td></td><td><em>Reserved for future footer data records</em></td></tr>
|
||||
<tr><td>0xF4-0xF7</td><td></td><td><em>Reserved for future footer data records</em></td></tr>
|
||||
<tr><td>0xF8</td><td><code>kBlogFooterPropertiesRecord</code></td><td>Small footer metadata (named properties)</td></tr>
|
||||
<tr><td>0xF9-0xFB</td><td></td><td><em>Reserved</em></td></tr>
|
||||
<tr><td>0xFC</td><td><code>kBlogFooterFileChecksumInfo</code></td><td>Full file checksum state dump. Allows inferring the file checksum from just reading the tail. Placeholder for future use.</td></tr>
|
||||
<tr><td>0xFD-0xFE</td><td></td><td><em>Reserved</em></td></tr>
|
||||
<tr><td>0xFF</td><td><code>kBlogFooterLocatorRecord</code></td><td>Footer locator with relative offsets. Must be the last record in the file.</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Length Semantics</h3>
|
||||
<ul>
|
||||
<li><code>length = 0</code>: No payload and no 5-byte trailer. Record is escape_seq + prefix fields + padding.</li>
|
||||
<li><code>length > 0</code>: Payload of <code>length</code> bytes followed by 5-byte trailer.</li>
|
||||
<li><code>length = 2^63 - 1</code>: Unspecified size. Reader scans forward for next escape sequence.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Compact Record Format (varint length ≤ 2 bytes, default record type)</h3>
|
||||
<div class="diagram">
|
||||
[escape_seq: 10B] [varint length: 1-2B]
|
||||
[payload: length bytes]
|
||||
[compression_type: 1B] [checksum: 4B]
|
||||
[padding: 0+ B]</div>
|
||||
<p>No type byte -- type comes from the header's <code>compact_record_type</code> field. Used for payloads < 16 KiB.</p>
|
||||
|
||||
<h3>Full Record Format (varint length > 2 bytes, or forced)</h3>
|
||||
<div class="diagram">
|
||||
[escape_seq: 10B] [varint length: 3+B] [type: 1B] [compression_type: 1B]
|
||||
[prefix_checksum: 4B]
|
||||
[payload: length bytes] (if length > 0)
|
||||
[compression_type: 1B] [checksum: 4B] (if length > 0)
|
||||
[padding: 0+ B]</div>
|
||||
<p>The <code>prefix_checksum</code> covers (varint + type + compression_type), with a context checksum modifier for defense-in-depth. Validates the length before attempting a potentially large read. The pre-payload <code>compression_type</code> enables streaming decompression without seeking to the trailer.</p>
|
||||
|
||||
<p>Records that don't match the compact_record_type (e.g. footer records, preamble-start) always use full format by encoding the varint with ≥ 3 bytes (irregular encoding).</p>
|
||||
|
||||
<p>All checksums in blog files (fixed header, property section trailer, record prefix, record trailer) use context checksum modifiers incorporating the incarnation ID, providing defense-in-depth for misplaced data detection even with <code>kNoChecksum</code>.</p>
|
||||
|
||||
<!-- ======================================================================= -->
|
||||
<h2 id="s4">Compression</h2>
|
||||
|
||||
<h3>Per-Record Compression</h3>
|
||||
<p>Each record's trailer <code>compression_type</code> byte indicates the compression algorithm used for that record's payload. The actual type comes from <code>Compressor::CompressBlock</code> output, not the configured type. This is the same approach used by SST block-based table builders.</p>
|
||||
|
||||
<h3>Streaming Compression (WriteBatch records only)</h3>
|
||||
<p>When the header property <code>WriteBatchStreamingCompressionType</code> is set, WriteBatch record payloads may use streaming compression across records. Such records use <code>kStreamingCompressionSentinel</code> (0x7F) as their trailer's compression_type byte, indicating "compressed using the streaming algorithm declared in the header."</p>
|
||||
|
||||
<p>A record with <code>kNoCompression</code> (0x00) in the trailer remains individually uncompressed even when streaming is active, enabling dynamic on/off switching based on compression effectiveness.</p>
|
||||
|
||||
<p>Non-WriteBatch records (blob records, footer records) are always outside the streaming context and use per-record compression, keeping blob data randomly readable.</p>
|
||||
|
||||
<div class="note">
|
||||
Currently only ZSTD is supported for streaming compression. The <code>WriteBatchStreamingCompressionType</code> property is required (uppercase first letter), so older readers that don't understand it will reject the file rather than misinterpret streaming-compressed payloads.
|
||||
</div>
|
||||
|
||||
<!-- ======================================================================= -->
|
||||
<h2 id="s5">Padding Scheme</h2>
|
||||
|
||||
<p>Padding fills the gap between a record's trailer (or the header property section) and the next escape sequence. The padding byte value is chosen to <strong>differ from the last meaningful byte</strong>, enabling unambiguous scanning.</p>
|
||||
|
||||
<table>
|
||||
<tr><th>Last meaningful byte</th><th>Pad with</th><th>Mandatory?</th></tr>
|
||||
<tr><td><code>0x00</code></td><td><code>0xFF</code></td><td>Yes -- at least 1 byte required</td></tr>
|
||||
<tr><td><code>0xFF</code></td><td><code>0x00</code></td><td>Yes -- at least 1 byte required</td></tr>
|
||||
<tr><td>Anything else</td><td><code>0x00</code> (writer's choice)</td><td>Only if needed for alignment</td></tr>
|
||||
</table>
|
||||
|
||||
<p>Padding may be arbitrarily long for stronger alignment requirements (e.g. flush/sync boundaries) or for recycling a file of all zeros. The reader skips padding by scanning forward over 0x00/0xFF bytes in 4-byte-aligned chunks until finding a byte that is neither, which is the first byte of the next escape sequence (guaranteed to be neither 0x00 nor 0xFF).</p>
|
||||
|
||||
<!-- ======================================================================= -->
|
||||
<h2 id="s6">Context Checksums</h2>
|
||||
|
||||
<div class="decision">
|
||||
<strong>Decision (adopted from SST format_version=6):</strong> Each record's checksum incorporates a 32-bit <strong>incarnation ID</strong> (first 4 bytes of the escape sequence). Uses <code>ChecksumModifierForContext(incarnation_id, record_offset)</code> -- the same mechanism as block-based SST files.
|
||||
</div>
|
||||
|
||||
<p>Context checksums provide defense-in-depth for recycling detection and general data integrity verification with zero per-record overhead.</p>
|
||||
|
||||
<p>The checksum type is configured from the CF's <code>BlockBasedTableOptions::checksum</code> setting (for blob files) or the default CF's setting (for WAL files). This is a reasonable but hopefully temporary way of configuring blog file internal checksums; a dedicated option may be added later.</p>
|
||||
|
||||
<!-- ======================================================================= -->
|
||||
<h2 id="s7">Footer</h2>
|
||||
|
||||
<p>The footer is composed of <strong>regular records</strong> using the full record format, maintaining simple forward parsability. A file is cleanly sealed when a valid footer locator record is the last record. Footer records appear in the order listed below.</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>Footer index records</strong> (0xF0): Large metadata like sparse indexes for range queries. Zero or more per file.</li>
|
||||
<li><strong>Footer properties record</strong> (0xF8): Named properties (same encoding as header properties, but the full record format's own trailer provides integrity). At most one per file.</li>
|
||||
<li><strong>Footer file checksum info</strong> (0xFC): Placeholder for future use. Would contain a dump of the full file checksum state, allowing the file checksum to be inferred from just reading the tail of the file.</li>
|
||||
<li><strong>Footer locator record</strong> (0xFF): Must be the <strong>last record</strong>. Contains relative offsets (in 4-byte units) to other footer records, sequential relative to each other in reverse order.</li>
|
||||
</ol>
|
||||
|
||||
<h3>Footer Locator Payload</h3>
|
||||
<pre>[record_type: 1B] [relative_offset_in_4B_units: uint32 LE] ...</pre>
|
||||
<p>First entry: distance from locator's escape_seq backward to the preceding footer record. Subsequent: from the previous entry backward to the next.</p>
|
||||
|
||||
<h3>Known Footer Properties</h3>
|
||||
<table>
|
||||
<tr><th>Name</th><th>Description</th></tr>
|
||||
<tr><td><code>"blobCount"</code></td><td>Number of blob records</td></tr>
|
||||
<tr><td><code>"totalBlobBytes"</code></td><td>Total uncompressed blob payload bytes</td></tr>
|
||||
<tr><td><code>"totalBlobBytesCompressed"</code></td><td>Total compressed blob payload bytes on disk</td></tr>
|
||||
<tr><td><code>"totalWriteBatchBytes"</code></td><td>Total uncompressed WriteBatch payload bytes</td></tr>
|
||||
<tr><td><code>"totalWriteBatchBytesCompressed"</code></td><td>Total compressed WriteBatch payload bytes on disk</td></tr>
|
||||
<tr><td><code>"minSequence"</code></td><td>Minimum sequence number</td></tr>
|
||||
<tr><td><code>"maxSequence"</code></td><td>Maximum sequence number</td></tr>
|
||||
</table>
|
||||
|
||||
<!-- ======================================================================= -->
|
||||
<h2 id="s8">WAL Recovery Modes</h2>
|
||||
|
||||
<p>The escape sequence is the <strong>primary mechanism</strong> for detecting WAL recycling and for non-linear data recovery. The <code>kBlogFileRecycled</code> flag in the header indicates the file may contain trailing stale data from a previous incarnation.</p>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th style="width:22%">Mode</th>
|
||||
<th style="width:48%">Behavior on Undecodable Data</th>
|
||||
<th style="width:30%">Escape Sequence Role</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>kAbsoluteConsistency</code></td>
|
||||
<td>Error on any failure, including incomplete trailing records. If recycled and no escape sequence found in trailing data, accept as clean end.</td>
|
||||
<td>Distinguishes stale recycled data from corruption.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>kTolerateCorruptedTailRecords</code></td>
|
||||
<td>Incomplete trailing record (Incomplete status): tolerate, stop at last complete record. Corruption: scan for escape sequence. Not found = clean end (recycled or preallocated). Found with valid record beyond = mid-file corruption (error).</td>
|
||||
<td>Distinguishes tail damage from mid-file corruption.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>kPointInTimeRecovery</code> (default)</td>
|
||||
<td>Stop at first inconsistency. Incomplete trailing records tolerated. Recycled trailing data accepted.</td>
|
||||
<td>Diagnostic improvement.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>kSkipAnyCorruptedRecords</code></td>
|
||||
<td>On corruption, scan forward for next escape sequence. If found, try to decode and verify; resume if valid. If not found, stop.</td>
|
||||
<td>Essential for record discovery after corruption.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- ======================================================================= -->
|
||||
<h2 id="s9">WriteBatch Blob Preamble</h2>
|
||||
|
||||
<p>A <strong>blob preamble</strong> is a sequence of blob records preceded by a preamble-start record (type 0x02, full format, length=0) and followed by the associated WriteBatch record. The blob records contain separated values referenced by <code>file_number = 0</code> blob references in the WriteBatch.</p>
|
||||
|
||||
<p>This functionality is stubbed in the current implementation: the preamble-start record type is defined and can be written/read, but the full preamble flow (in-memory serialization, blob reference encoding) is deferred.</p>
|
||||
|
||||
<!-- ======================================================================= -->
|
||||
<h2 id="s10">Replication Log Column Family (ShallowL0Store)</h2>
|
||||
|
||||
<p>Deferred. The blog format supports a "rlog" role hint for replication log storage, with sparse indexes in footer records and in-memory index maintenance. See design notes in the original proposal.</p>
|
||||
|
||||
<!-- ======================================================================= -->
|
||||
<h2 id="s11">Implementation Status</h2>
|
||||
|
||||
<h3>Resolved (from original Open Questions)</h3>
|
||||
<table>
|
||||
<tr><th>Question</th><th>Resolution</th></tr>
|
||||
<tr><td>Escape sequence size</td><td>10 bytes: 6 random + 4 derived. First byte not 0x00/0xFF.</td></tr>
|
||||
<tr><td>Header/footer encoding</td><td>40-byte fixed header + named property section with 5-byte trailer. Footer uses regular full-format records: index (0xF0), properties (0xF8), file checksum info (0xFC, placeholder), locator (0xFF). Numbered in expected occurrence order with gaps for future types.</td></tr>
|
||||
<tr><td>Preamble-start record format</td><td>Full format, length=0, type 0x02. Minimal -- just the type tag.</td></tr>
|
||||
<tr><td>WriteBatch record framing</td><td>Same compact/full format as blob records, type 0x03.</td></tr>
|
||||
<tr><td>Compact vs full format</td><td>Compact: varint ≤ 2B (payload < 16 KiB), matches compact_record_type, no type byte. Full: varint > 2B (possibly irregular), includes type + pre-payload compression_type + prefix_checksum with context modifier.</td></tr>
|
||||
<tr><td>Checksum context</td><td>All checksums (fixed header, property section, record prefix, record trailer) use context checksum modifiers with the incarnation ID, ensuring nonzero stored values even with <code>kNoChecksum</code>.</td></tr>
|
||||
<tr><td>Schema version</td><td>Starts at 0. Newer versions are rejected by older readers (returns <code>NotSupported</code>). Checksum is verified before schema version is checked.</td></tr>
|
||||
<tr><td>Streaming compression sentinel</td><td><code>kStreamingCompressionSentinel = 0x7F</code> defined in <code>CompressionType</code> enum (compression_type.h). Used in blog record trailers for streaming-compressed WriteBatch payloads.</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>DB Options</h3>
|
||||
<ul>
|
||||
<li><code>use_blog_format_for_wals</code> (bool, default false): New WAL files use blog format.</li>
|
||||
<li><code>use_blog_format_for_blobs</code> (bool, default false): New blob files use blog format.</li>
|
||||
<li><code>blog_checksum</code> (<code>ChecksumType</code>, default <code>kXXH3</code>): Checksum algorithm for blog format files. <code>ChecksumType</code> enum moved to <code>include/rocksdb/checksum_type.h</code> (from <code>table.h</code>) for use by both table and DB options.</li>
|
||||
</ul>
|
||||
<p>All are immutable <code>DBOptions</code>. Existing legacy WAL and blob files are always readable regardless of these settings.</p>
|
||||
|
||||
<h3>Compression Manager Integration</h3>
|
||||
<ul>
|
||||
<li><strong>Blog blob files</strong> use the CF's configured compression manager (via <code>MutableCFOptions::compression_manager</code>). Legacy blob files continue to use the builtin manager for backward compatibility.</li>
|
||||
<li><strong>Blog WAL files</strong> use per-record compression via <code>Compressor::CompressBlock</code>. The actual <code>CompressionType</code> from <code>CompressBlock</code> output is stored in the record trailer.</li>
|
||||
<li>Streaming compression infrastructure exists (<code>WriteBatchStreamingCompressionType</code> property, <code>kStreamingCompressionSentinel</code> 0x7F trailer value) but is not yet enabled by default.</li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -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/experimental/coro:blocking_wait",
|
||||
"//folly/experimental/coro:collect",
|
||||
"//folly/experimental/coro:coroutine",
|
||||
"//folly/experimental/coro:task",
|
||||
"//folly/coro:blocking_wait",
|
||||
"//folly/coro:collect",
|
||||
"//folly/coro:coroutine",
|
||||
"//folly/coro:task",
|
||||
"//folly/synchronization:distributed_mutex",
|
||||
],
|
||||
headers=LiteralValue("glob([\"**/*.h\"])")
|
||||
|
||||
@@ -148,6 +148,24 @@ 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" " "`
|
||||
|
||||
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/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
|
||||
@@ -1913,7 +1913,7 @@ sub drain_job_queue {
|
||||
}
|
||||
$last_progress_time = time();
|
||||
$ps_reported = 0;
|
||||
} elsif (not $ps_reported and (time() - $last_progress_time) >= 60) {
|
||||
} elsif (not $ps_reported and (time() - $last_progress_time) >= 300) {
|
||||
# No progress in at least 60 seconds: run ps
|
||||
print $Global::original_stderr "\n";
|
||||
my $script_dir = ::dirname($0);
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# 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
|
||||
#
|
||||
@@ -15,8 +17,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.0
|
||||
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:22.0
|
||||
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:22.2
|
||||
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:22.2
|
||||
#
|
||||
# Might need to change visibility to public through
|
||||
# https://github.com/orgs/facebook/packages/container/rocksdb_ubuntu/settings
|
||||
@@ -28,7 +30,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
|
||||
RUN apt-get install -y vim wget curl ccache
|
||||
# install tzdata noninteractive
|
||||
RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install tzdata
|
||||
# install git and default compilers
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# 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
|
||||
#
|
||||
@@ -15,8 +17,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.0
|
||||
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
#
|
||||
# Might need to change visibility to public through
|
||||
# https://github.com/orgs/facebook/packages/container/rocksdb_ubuntu/settings
|
||||
@@ -28,11 +30,15 @@ FROM ubuntu:24.04
|
||||
RUN apt-get update
|
||||
RUN apt-get upgrade -y
|
||||
# install basic tools
|
||||
RUN apt-get install -y vim wget curl
|
||||
RUN apt-get install -y vim wget curl ccache
|
||||
# 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
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,3 @@
|
||||
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.
|
||||
@@ -0,0 +1,54 @@
|
||||
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.
|
||||
@@ -0,0 +1,661 @@
|
||||
# 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 |
|
||||
@@ -0,0 +1,104 @@
|
||||
# Code Review Skill
|
||||
|
||||
This document defines the methodology for performing thorough, multi-perspective
|
||||
code reviews on RocksDB changes. It is used both by CI (GitHub Actions) and
|
||||
local review workflows.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting a review:
|
||||
- Read `CLAUDE.md` in the repository root for project-specific guidelines
|
||||
- Read any files in `claude_md/` for architecture context
|
||||
- Identify the commit/diff/PR to review and parse the changed files
|
||||
|
||||
## Review Methodology
|
||||
|
||||
Conduct the review from multiple perspectives, then synthesize findings into a
|
||||
single report. Each perspective catches different classes of bugs.
|
||||
|
||||
### Perspective 1: Codebase Context & Call-Chain Analysis
|
||||
|
||||
Before reviewing the diff itself, build deep context:
|
||||
- For each changed function/method, trace the call chain UPWARD 3-5 levels.
|
||||
Who consumes the changed behavior? What invariants do callers rely on?
|
||||
- Trace DOWNWARD into callees. What side effects do they have? What shared
|
||||
state do they mutate (counters, sequence numbers, flags, metadata)?
|
||||
- Identify the "sibling" or "reference" implementation that handles the same
|
||||
scenario the standard way. Compare behaviors.
|
||||
- For data written to shared structures (memtable, SST block, cache entry),
|
||||
find ALL readers and verify their visibility rules match the writer.
|
||||
|
||||
### Perspective 2: Correctness & Edge Cases
|
||||
|
||||
- Thread safety and concurrency (lock ordering, data races)
|
||||
- Error handling and Status propagation
|
||||
- Edge cases: empty inputs, overflow, boundary conditions
|
||||
- Data corruption scenarios
|
||||
- Behavioral contract changes: do return value semantics match callers?
|
||||
|
||||
### Perspective 3: Cross-Component & Adversarial Analysis
|
||||
|
||||
Check the change in ALL execution contexts:
|
||||
|
||||
| Context | Key difference |
|
||||
|---------|---------------|
|
||||
| WritePreparedTxnDB | read_callback_ controls visibility |
|
||||
| ReadOnly DB / SecondaryInstance | No mutable memtable |
|
||||
| CompactionService / Remote compaction | Different process |
|
||||
| User-defined timestamps | Extra key comparison dimension |
|
||||
| MemPurge | Memtable-to-memtable path |
|
||||
| BlobDB | Values in blob files, not inline |
|
||||
| Old snapshots | Seqno far behind current |
|
||||
| Concurrent writers | Lock-free vs locked paths |
|
||||
| FIFO / Universal compaction | Different invariants |
|
||||
| Prefix seek / total order seek | Different iterator behavior |
|
||||
|
||||
When the change claims a property ("logically redundant", "safe because X"),
|
||||
systematically try to break it: list preconditions, construct counterexamples.
|
||||
|
||||
### Perspective 4: Performance
|
||||
|
||||
RocksDB is a high-performance storage engine.
|
||||
- Memory allocation on hot paths
|
||||
- Unnecessary copies (prefer move semantics, Slice, string_view)
|
||||
- Cache efficiency and data locality
|
||||
- Loop optimization, branch prediction (LIKELY/UNLIKELY)
|
||||
|
||||
### Perspective 5: API, Compatibility & Testing
|
||||
|
||||
- Public API backwards compatibility
|
||||
- Serialization format correctness and versioning
|
||||
- Test coverage for edge cases, failure modes, and system-level interactions
|
||||
|
||||
## How to Explore the Codebase
|
||||
|
||||
Use available tools to explore deeply — do NOT just review the diff in isolation.
|
||||
The most critical bugs hide at component boundaries.
|
||||
|
||||
- Read the header files for changed implementations
|
||||
- Search for callers of changed functions (3-5 levels up)
|
||||
- Read existing tests to understand guaranteed behaviors
|
||||
- Check related implementations for conventions and patterns
|
||||
|
||||
## Output Format
|
||||
|
||||
### Summary
|
||||
Brief overall assessment (1-2 sentences).
|
||||
|
||||
### Issues Found
|
||||
Categorize by severity:
|
||||
- :red_circle: **Critical**: Must fix (correctness, data corruption, security)
|
||||
- :yellow_circle: **Suggestion**: Should consider (performance, edge cases)
|
||||
- :green_circle: **Nitpick**: Minor style issues
|
||||
|
||||
For each issue include:
|
||||
- File and line reference
|
||||
- Which review perspective found it
|
||||
- Description with root cause analysis
|
||||
- Suggested fix
|
||||
|
||||
### Cross-Component Analysis
|
||||
Execution context checks and assumption stress-test results.
|
||||
|
||||
### Positive Observations
|
||||
Good patterns, clever optimizations, or improvements.
|
||||
@@ -0,0 +1,7 @@
|
||||
# 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/`
|
||||
+18
-7
@@ -5,6 +5,8 @@
|
||||
|
||||
#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 {
|
||||
@@ -14,10 +16,13 @@ Status BlobFetcher::FetchBlob(const Slice& user_key,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
assert(version_);
|
||||
|
||||
return version_->GetBlob(read_options_, user_key, blob_index_slice,
|
||||
prefetch_buffer, blob_value, bytes_read);
|
||||
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;
|
||||
}
|
||||
|
||||
Status BlobFetcher::FetchBlob(const Slice& user_key,
|
||||
@@ -25,10 +30,16 @@ Status BlobFetcher::FetchBlob(const Slice& user_key,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
assert(version_);
|
||||
if (!allow_write_path_fallback_) {
|
||||
assert(version_);
|
||||
|
||||
return version_->GetBlob(read_options_, user_key, blob_index, prefetch_buffer,
|
||||
blob_value, bytes_read);
|
||||
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);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+13
-3
@@ -10,17 +10,25 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class BlobFileCache;
|
||||
class Version;
|
||||
class Slice;
|
||||
class FilePrefetchBuffer;
|
||||
class PinnableSlice;
|
||||
class BlobIndex;
|
||||
|
||||
// A thin wrapper around the blob retrieval functionality of Version.
|
||||
// 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.
|
||||
class BlobFetcher {
|
||||
public:
|
||||
BlobFetcher(const Version* version, const ReadOptions& read_options)
|
||||
: version_(version), read_options_(read_options) {}
|
||||
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) {}
|
||||
|
||||
Status FetchBlob(const Slice& user_key, const Slice& blob_index_slice,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
@@ -33,5 +41,7 @@ class BlobFetcher {
|
||||
private:
|
||||
const Version* version_;
|
||||
ReadOptions read_options_;
|
||||
BlobFileCache* blob_file_cache_;
|
||||
bool allow_write_path_fallback_;
|
||||
};
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+171
-48
@@ -14,6 +14,8 @@
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "db/blob/blob_log_writer.h"
|
||||
#include "db/blob/blob_source.h"
|
||||
#include "db/blog/blog_format.h"
|
||||
#include "db/blog/blog_writer.h"
|
||||
#include "db/event_helpers.h"
|
||||
#include "db/version_set.h"
|
||||
#include "file/filename.h"
|
||||
@@ -67,13 +69,14 @@ 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(
|
||||
CompressionOptions{}, blob_compression_type_)),
|
||||
// Blog format uses the CF's compression manager; legacy format must
|
||||
// use the builtin for compatibility.
|
||||
blob_compression_manager_(immutable_options->use_blog_format_for_blobs &&
|
||||
mutable_cf_options->compression_manager
|
||||
? mutable_cf_options->compression_manager
|
||||
: GetBuiltinV2CompressionManager()),
|
||||
blob_compressor_(blob_compression_manager_->GetCompressor(
|
||||
mutable_cf_options->blob_compression_opts, blob_compression_type_)),
|
||||
blob_compressor_wa_(blob_compressor_
|
||||
? blob_compressor_->ObtainWorkingArea()
|
||||
: Compressor::ManagedWorkingArea{}),
|
||||
@@ -111,7 +114,7 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
|
||||
assert(blob_index);
|
||||
assert(blob_index->empty());
|
||||
|
||||
if (value.size() < min_blob_size_) {
|
||||
if (value.empty() || value.size() < min_blob_size_) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -125,8 +128,10 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
|
||||
Slice blob = value;
|
||||
GrowableBuffer compressed_blob;
|
||||
|
||||
{
|
||||
const Status s = CompressBlobIfNeeded(&blob, &compressed_blob);
|
||||
if (!blog_writer_) {
|
||||
// Legacy format: compress before writing (compression type is implicit
|
||||
// from the configured blob_compression_type_).
|
||||
const Status s = LegacyCompressBlob(&blob, &compressed_blob);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -134,10 +139,13 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
|
||||
|
||||
uint64_t blob_file_number = 0;
|
||||
uint64_t blob_offset = 0;
|
||||
uint64_t blob_on_disk_size = 0;
|
||||
CompressionType actual_compression_type = blob_compression_type_;
|
||||
|
||||
{
|
||||
const Status s =
|
||||
WriteBlobToFile(key, blob, &blob_file_number, &blob_offset);
|
||||
WriteBlobToFile(key, blob, &blob_file_number, &blob_offset,
|
||||
&blob_on_disk_size, &actual_compression_type);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -160,8 +168,8 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
|
||||
}
|
||||
}
|
||||
|
||||
BlobIndex::EncodeBlob(blob_index, blob_file_number, blob_offset, blob.size(),
|
||||
blob_compression_type_);
|
||||
BlobIndex::EncodeBlob(blob_index, blob_file_number, blob_offset,
|
||||
blob_on_disk_size, actual_compression_type);
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
@@ -174,7 +182,9 @@ Status BlobFileBuilder::Finish() {
|
||||
return CloseBlobFile();
|
||||
}
|
||||
|
||||
bool BlobFileBuilder::IsBlobFileOpen() const { return !!writer_; }
|
||||
bool BlobFileBuilder::IsBlobFileOpen() const {
|
||||
return !!writer_ || !!blog_writer_;
|
||||
}
|
||||
|
||||
Status BlobFileBuilder::OpenBlobFileIfNeeded() {
|
||||
if (IsBlobFileOpen()) {
|
||||
@@ -233,37 +243,73 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
|
||||
immutable_options_->file_checksum_gen_factory.get(),
|
||||
tmp_set.Contains(FileType::kBlobFile), false));
|
||||
|
||||
constexpr bool do_flush = false;
|
||||
blob_file_number_ = blob_file_number;
|
||||
|
||||
std::unique_ptr<BlobLogWriter> blob_log_writer(new BlobLogWriter(
|
||||
std::move(file_writer), immutable_options_->clock, statistics,
|
||||
blob_file_number, immutable_options_->use_fsync, do_flush));
|
||||
if (immutable_options_->use_blog_format_for_blobs) {
|
||||
// Blog format path
|
||||
BlogFileHeader blog_header;
|
||||
blog_header.checksum_type = immutable_options_->blog_checksum;
|
||||
blog_header.compact_record_type = kBlogBlobRecord;
|
||||
blog_header.GenerateFromUniqueId(db_id_, db_session_id_, blob_file_number);
|
||||
blog_header.SetProperty(kBlogPropRole, "blob");
|
||||
if (!immutable_options_->db_host_id.empty()) {
|
||||
blog_header.SetProperty(kBlogPropDbHostId,
|
||||
immutable_options_->db_host_id);
|
||||
}
|
||||
blog_header.SetUint32Property(kBlogPropColumnFamilyId, column_family_id_);
|
||||
if (!column_family_name_.empty()) {
|
||||
blog_header.SetProperty(kBlogPropColumnFamilyName, column_family_name_);
|
||||
}
|
||||
if (blob_compressor_) {
|
||||
blog_header.SetProperty(kBlogPropCompressionCompatibilityName,
|
||||
blob_compression_manager_->CompatibilityName());
|
||||
}
|
||||
|
||||
constexpr bool has_ttl = false;
|
||||
constexpr ExpirationRange expiration_range;
|
||||
blog_writer_ =
|
||||
std::make_unique<BlogFileWriter>(std::move(file_writer), blog_header);
|
||||
|
||||
BlobLogHeader header(column_family_id_, blob_compression_type_, has_ttl,
|
||||
expiration_range);
|
||||
|
||||
{
|
||||
Status s = blob_log_writer->WriteHeader(*write_options_, header);
|
||||
Status s = blog_writer_->WriteHeader(*write_options_);
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"BlobFileBuilder::OpenBlobFileIfNeeded:WriteHeader", &s);
|
||||
"BlobFileBuilder::OpenBlobFileIfNeeded:WriteBlogHeader", &s);
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Legacy blob log format path
|
||||
constexpr bool do_flush = false;
|
||||
|
||||
writer_ = std::move(blob_log_writer);
|
||||
std::unique_ptr<BlobLogWriter> blob_log_writer(
|
||||
new BlobLogWriter(std::move(file_writer), immutable_options_->clock,
|
||||
statistics, immutable_options_->use_fsync, do_flush));
|
||||
|
||||
constexpr bool has_ttl = false;
|
||||
constexpr ExpirationRange expiration_range;
|
||||
|
||||
BlobLogHeader header(column_family_id_, blob_compression_type_, has_ttl,
|
||||
expiration_range);
|
||||
|
||||
{
|
||||
Status s = blob_log_writer->WriteHeader(*write_options_, header);
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"BlobFileBuilder::OpenBlobFileIfNeeded:WriteHeader", &s);
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
writer_ = std::move(blob_log_writer);
|
||||
}
|
||||
|
||||
assert(IsBlobFileOpen());
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlobFileBuilder::CompressBlobIfNeeded(
|
||||
Status BlobFileBuilder::LegacyCompressBlob(
|
||||
Slice* blob, GrowableBuffer* compressed_blob) const {
|
||||
assert(blob);
|
||||
assert(compressed_blob);
|
||||
@@ -292,17 +338,48 @@ Status BlobFileBuilder::CompressBlobIfNeeded(
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlobFileBuilder::WriteBlobToFile(const Slice& key, const Slice& blob,
|
||||
uint64_t* blob_file_number,
|
||||
uint64_t* blob_offset) {
|
||||
Status BlobFileBuilder::WriteBlobToFile(
|
||||
const Slice& key, const Slice& blob, uint64_t* blob_file_number,
|
||||
uint64_t* blob_offset, uint64_t* blob_on_disk_size,
|
||||
CompressionType* actual_compression_type) {
|
||||
assert(IsBlobFileOpen());
|
||||
assert(blob_file_number);
|
||||
assert(blob_offset);
|
||||
assert(blob_on_disk_size);
|
||||
assert(actual_compression_type);
|
||||
|
||||
uint64_t key_offset = 0;
|
||||
Status s;
|
||||
|
||||
Status s =
|
||||
writer_->AddRecord(*write_options_, key, blob, &key_offset, blob_offset);
|
||||
if (blog_writer_) {
|
||||
// Blog format: compress here using CompressBlock (like SST) so we
|
||||
// capture the actual compression type used in the record trailer.
|
||||
CompressionType actual_type = kNoCompression;
|
||||
GrowableBuffer compressed;
|
||||
if (blob_compressor_) {
|
||||
StopWatch stop_watch(immutable_options_->clock, immutable_options_->stats,
|
||||
BLOB_DB_COMPRESSION_MICROS);
|
||||
size_t max_compressed_size = blob.size();
|
||||
compressed.ResetForSize(max_compressed_size);
|
||||
s = blob_compressor_->CompressBlock(blob, compressed.data(),
|
||||
&compressed.MutableSize(),
|
||||
&actual_type, &blob_compressor_wa_);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
Slice write_data =
|
||||
(actual_type != kNoCompression) ? Slice(compressed) : blob;
|
||||
s = blog_writer_->AddBlobRecord(*write_options_, write_data, actual_type,
|
||||
blob_offset);
|
||||
*blob_on_disk_size = write_data.size();
|
||||
*actual_compression_type = actual_type;
|
||||
} else {
|
||||
uint64_t key_offset = 0;
|
||||
s = writer_->AddRecord(*write_options_, key, blob, &key_offset,
|
||||
blob_offset);
|
||||
*blob_on_disk_size = blob.size(); // legacy: already compressed by caller
|
||||
*actual_compression_type = blob_compression_type_;
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("BlobFileBuilder::WriteBlobToFile:AddRecord", &s);
|
||||
|
||||
@@ -310,10 +387,16 @@ Status BlobFileBuilder::WriteBlobToFile(const Slice& key, const Slice& blob,
|
||||
return s;
|
||||
}
|
||||
|
||||
*blob_file_number = writer_->get_log_number();
|
||||
*blob_file_number = blob_file_number_;
|
||||
|
||||
++blob_count_;
|
||||
blob_bytes_ += BlobLogRecord::kHeaderSize + key.size() + blob.size();
|
||||
if (blog_writer_) {
|
||||
// Blog format: blob_bytes_ tracks uncompressed payload size only
|
||||
// (no legacy record header, no key).
|
||||
blob_bytes_ += blob.size();
|
||||
} else {
|
||||
blob_bytes_ += BlobLogRecord::kHeaderSize + key.size() + blob.size();
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
@@ -321,22 +404,60 @@ Status BlobFileBuilder::WriteBlobToFile(const Slice& key, const Slice& blob,
|
||||
Status BlobFileBuilder::CloseBlobFile() {
|
||||
assert(IsBlobFileOpen());
|
||||
|
||||
BlobLogFooter footer;
|
||||
footer.blob_count = blob_count_;
|
||||
|
||||
std::string checksum_method;
|
||||
std::string checksum_value;
|
||||
Status s;
|
||||
|
||||
Status s = writer_->AppendFooter(*write_options_, footer, &checksum_method,
|
||||
&checksum_value);
|
||||
if (blog_writer_) {
|
||||
// Blog format: write footer properties and locator records.
|
||||
BlogFileFooterProperties footer_props;
|
||||
footer_props.SetBlobCount(blob_count_);
|
||||
footer_props.SetTotalBlobBytes(blob_bytes_);
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("BlobFileBuilder::WriteBlobToFile:AppendFooter", &s);
|
||||
uint64_t props_offset = blog_writer_->current_offset();
|
||||
s = blog_writer_->AddFooterPropertiesRecord(*write_options_, footer_props);
|
||||
|
||||
if (s.ok()) {
|
||||
BlogFileFooterLocator locator;
|
||||
uint64_t locator_offset = blog_writer_->current_offset();
|
||||
locator.entries.push_back(
|
||||
{kBlogFooterPropertiesRecord,
|
||||
static_cast<uint32_t>((locator_offset - props_offset) / 4)});
|
||||
s = blog_writer_->AddFooterLocatorRecord(*write_options_, locator);
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
s = blog_writer_->Sync(*write_options_);
|
||||
}
|
||||
if (s.ok()) {
|
||||
s = blog_writer_->Close(*write_options_);
|
||||
}
|
||||
if (s.ok()) {
|
||||
std::string method = blog_writer_->file()->GetFileChecksumFuncName();
|
||||
if (method != kUnknownFileChecksumFuncName) {
|
||||
checksum_method = std::move(method);
|
||||
}
|
||||
std::string value = blog_writer_->file()->GetFileChecksum();
|
||||
if (value != kUnknownFileChecksum) {
|
||||
checksum_value = std::move(value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
BlobLogFooter footer;
|
||||
footer.blob_count = blob_count_;
|
||||
|
||||
s = writer_->LegacyAppendFooterAndClose(*write_options_, footer,
|
||||
&checksum_method, &checksum_value);
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"BlobFileBuilder::WriteBlobToFile:LegacyAppendFooterAndClose", &s);
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
const uint64_t blob_file_number = writer_->get_log_number();
|
||||
const uint64_t blob_file_number = blob_file_number_;
|
||||
|
||||
if (blob_callback_) {
|
||||
s = blob_callback_->OnBlobFileCompleted(
|
||||
@@ -358,6 +479,7 @@ Status BlobFileBuilder::CloseBlobFile() {
|
||||
blob_count_, blob_bytes_);
|
||||
|
||||
writer_.reset();
|
||||
blog_writer_.reset();
|
||||
blob_count_ = 0;
|
||||
blob_bytes_ = 0;
|
||||
|
||||
@@ -367,7 +489,8 @@ Status BlobFileBuilder::CloseBlobFile() {
|
||||
Status BlobFileBuilder::CloseBlobFileIfNeeded() {
|
||||
assert(IsBlobFileOpen());
|
||||
|
||||
const WritableFileWriter* const file_writer = writer_->file();
|
||||
const WritableFileWriter* const file_writer =
|
||||
blog_writer_ ? blog_writer_->file() : writer_->file();
|
||||
assert(file_writer);
|
||||
|
||||
if (file_writer->GetFileSize() < blob_file_size_) {
|
||||
@@ -386,13 +509,13 @@ void BlobFileBuilder::Abandon(const Status& s) {
|
||||
// Blob files. So we can ignore the below error.
|
||||
blob_callback_
|
||||
->OnBlobFileCompleted(blob_file_paths_->back(), column_family_name_,
|
||||
job_id_, writer_->get_log_number(),
|
||||
creation_reason_, s, "", "", blob_count_,
|
||||
blob_bytes_)
|
||||
job_id_, blob_file_number_, creation_reason_, s,
|
||||
"", "", blob_count_, blob_bytes_)
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
|
||||
writer_.reset();
|
||||
blog_writer_.reset();
|
||||
blob_count_ = 0;
|
||||
blob_bytes_ = 0;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ class BlobLogWriter;
|
||||
class IOTracer;
|
||||
class BlobFileCompletionCallback;
|
||||
|
||||
class BlogFileWriter;
|
||||
|
||||
class BlobFileBuilder {
|
||||
public:
|
||||
BlobFileBuilder(VersionSet* versions, FileSystem* fs,
|
||||
@@ -78,10 +80,11 @@ class BlobFileBuilder {
|
||||
private:
|
||||
bool IsBlobFileOpen() const;
|
||||
Status OpenBlobFileIfNeeded();
|
||||
Status CompressBlobIfNeeded(Slice* blob,
|
||||
GrowableBuffer* compressed_blob) const;
|
||||
Status LegacyCompressBlob(Slice* blob, GrowableBuffer* compressed_blob) const;
|
||||
Status WriteBlobToFile(const Slice& key, const Slice& blob,
|
||||
uint64_t* blob_file_number, uint64_t* blob_offset);
|
||||
uint64_t* blob_file_number, uint64_t* blob_offset,
|
||||
uint64_t* blob_on_disk_size,
|
||||
CompressionType* actual_compression_type);
|
||||
Status CloseBlobFile();
|
||||
Status CloseBlobFileIfNeeded();
|
||||
|
||||
@@ -94,6 +97,7 @@ class BlobFileBuilder {
|
||||
uint64_t min_blob_size_;
|
||||
uint64_t blob_file_size_;
|
||||
CompressionType blob_compression_type_;
|
||||
std::shared_ptr<CompressionManager> blob_compression_manager_;
|
||||
std::unique_ptr<Compressor> blob_compressor_;
|
||||
mutable Compressor::ManagedWorkingArea blob_compressor_wa_;
|
||||
PrepopulateBlobCache prepopulate_blob_cache_;
|
||||
@@ -111,6 +115,8 @@ class BlobFileBuilder {
|
||||
std::vector<std::string>* blob_file_paths_;
|
||||
std::vector<BlobFileAddition>* blob_file_additions_;
|
||||
std::unique_ptr<BlobLogWriter> writer_;
|
||||
std::unique_ptr<BlogFileWriter> blog_writer_;
|
||||
uint64_t blob_file_number_ = 0;
|
||||
uint64_t blob_count_;
|
||||
uint64_t blob_bytes_;
|
||||
};
|
||||
|
||||
@@ -598,7 +598,7 @@ INSTANTIATE_TEST_CASE_P(
|
||||
"BlobFileBuilder::OpenBlobFileIfNeeded:NewWritableFile",
|
||||
"BlobFileBuilder::OpenBlobFileIfNeeded:WriteHeader",
|
||||
"BlobFileBuilder::WriteBlobToFile:AddRecord",
|
||||
"BlobFileBuilder::WriteBlobToFile:AppendFooter"}));
|
||||
"BlobFileBuilder::WriteBlobToFile:LegacyAppendFooterAndClose"}));
|
||||
|
||||
TEST_P(BlobFileBuilderIOErrorTest, IOError) {
|
||||
// Simulate an I/O error during the specified step of Add()
|
||||
|
||||
+125
-3
@@ -9,6 +9,7 @@
|
||||
#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"
|
||||
@@ -38,7 +39,8 @@ BlobFileCache::BlobFileCache(Cache* cache,
|
||||
|
||||
Status BlobFileCache::GetBlobFileReader(
|
||||
const ReadOptions& read_options, uint64_t blob_file_number,
|
||||
CacheHandleGuard<BlobFileReader>* blob_file_reader) {
|
||||
CacheHandleGuard<BlobFileReader>* blob_file_reader,
|
||||
bool allow_footer_skip_retry) {
|
||||
assert(blob_file_reader);
|
||||
assert(blob_file_reader->IsEmpty());
|
||||
|
||||
@@ -73,10 +75,22 @@ Status BlobFileCache::GetBlobFileReader(
|
||||
|
||||
{
|
||||
assert(file_options_);
|
||||
const Status s = BlobFileReader::Create(
|
||||
Status s = BlobFileReader::Create(
|
||||
*immutable_options_, read_options, *file_options_, column_family_id_,
|
||||
blob_file_read_hist_, blob_file_number, io_tracer_, &reader);
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -99,12 +113,120 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,35 @@ 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);
|
||||
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);
|
||||
|
||||
// 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
|
||||
|
||||
@@ -48,8 +48,7 @@ void WriteBlobFile(uint32_t column_family_id,
|
||||
constexpr bool do_flush = false;
|
||||
|
||||
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
|
||||
statistics, blob_file_number, use_fsync,
|
||||
do_flush);
|
||||
statistics, use_fsync, do_flush);
|
||||
|
||||
constexpr bool has_ttl = false;
|
||||
constexpr ExpirationRange expiration_range;
|
||||
@@ -77,8 +76,8 @@ void WriteBlobFile(uint32_t column_family_id,
|
||||
std::string checksum_method;
|
||||
std::string checksum_value;
|
||||
|
||||
ASSERT_OK(blob_log_writer.AppendFooter(WriteOptions(), footer,
|
||||
&checksum_method, &checksum_value));
|
||||
ASSERT_OK(blob_log_writer.LegacyAppendFooterAndClose(
|
||||
WriteOptions(), footer, &checksum_method, &checksum_value));
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
@@ -192,6 +191,101 @@ 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, 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();
|
||||
|
||||
@@ -0,0 +1,845 @@
|
||||
// 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 LegacyAppendFooterAndClose(), both of which call Sync().
|
||||
constexpr bool kDoFlushEachRecord = true;
|
||||
auto blob_log_writer = std::make_unique<BlobLogWriter>(
|
||||
std::move(file_writer), clock_, statistics_, 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->LegacyAppendFooterAndClose(
|
||||
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(¤t_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);
|
||||
}
|
||||
}
|
||||
|
||||
Status s = version != nullptr ? Status::Corruption("Invalid blob file number")
|
||||
: Status::NotFound();
|
||||
|
||||
if (blob_file_cache == nullptr) {
|
||||
return 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
|
||||
@@ -0,0 +1,294 @@
|
||||
// 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
|
||||
+209
-72
@@ -5,11 +5,14 @@
|
||||
|
||||
#include "db/blob/blob_file_reader.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "db/blob/blob_contents.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "db/blog/blog_format.h"
|
||||
#include "file/file_prefetch_buffer.h"
|
||||
#include "file/filename.h"
|
||||
#include "monitoring/statistics_impl.h"
|
||||
@@ -29,7 +32,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,
|
||||
const std::shared_ptr<IOTracer>& io_tracer, bool skip_footer_validation,
|
||||
std::unique_ptr<BlobFileReader>* blob_file_reader) {
|
||||
assert(blob_file_reader);
|
||||
assert(!*blob_file_reader);
|
||||
@@ -40,7 +43,8 @@ Status BlobFileReader::Create(
|
||||
{
|
||||
const Status s =
|
||||
OpenFile(immutable_options, file_options, blob_file_read_hist,
|
||||
blob_file_number, io_tracer, &file_size, &file_reader);
|
||||
blob_file_number, io_tracer, &file_size, &file_reader,
|
||||
/*skip_footer_size_check=*/skip_footer_validation);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -50,36 +54,30 @@ Status BlobFileReader::Create(
|
||||
|
||||
Statistics* const statistics = immutable_options.stats;
|
||||
|
||||
CompressionType compression_type = kNoCompression;
|
||||
// Construct with defaults; ReadHeader will detect blog vs legacy format
|
||||
// and populate the appropriate fields.
|
||||
blob_file_reader->reset(
|
||||
new BlobFileReader(std::move(file_reader), file_size, kNoCompression,
|
||||
/*decompressor=*/nullptr, immutable_options.clock,
|
||||
statistics, /*has_footer=*/false));
|
||||
|
||||
{
|
||||
const Status s =
|
||||
ReadHeader(file_reader.get(), read_options, column_family_id,
|
||||
statistics, &compression_type);
|
||||
(*blob_file_reader)->ReadHeader(read_options, column_family_id);
|
||||
if (!s.ok()) {
|
||||
blob_file_reader->reset();
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const Status s =
|
||||
ReadFooter(file_reader.get(), read_options, file_size, statistics);
|
||||
if (!skip_footer_validation) {
|
||||
const Status s = (*blob_file_reader)->ReadFooter(read_options);
|
||||
if (!s.ok()) {
|
||||
blob_file_reader->reset();
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Decompressor> decompressor;
|
||||
if (compression_type != kNoCompression) {
|
||||
// The blob format has always used compression format 2
|
||||
decompressor = GetBuiltinV2CompressionManager()->GetDecompressorOptimizeFor(
|
||||
compression_type);
|
||||
}
|
||||
|
||||
blob_file_reader->reset(new BlobFileReader(
|
||||
std::move(file_reader), file_size, compression_type,
|
||||
std::move(decompressor), immutable_options.clock, statistics));
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -87,7 +85,8 @@ 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) {
|
||||
std::unique_ptr<RandomAccessFileReader>* file_reader,
|
||||
bool skip_footer_size_check) {
|
||||
assert(file_size);
|
||||
assert(file_reader);
|
||||
|
||||
@@ -112,17 +111,26 @@ Status BlobFileReader::OpenFile(
|
||||
}
|
||||
}
|
||||
|
||||
if (*file_size < BlobLogHeader::kSize + BlobLogFooter::kSize) {
|
||||
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) {
|
||||
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, file_opts, &file, dbg);
|
||||
fs->NewRandomAccessFile(blob_file_path, reader_file_opts, &file, dbg);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -143,13 +151,17 @@ Status BlobFileReader::OpenFile(
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlobFileReader::ReadHeader(const RandomAccessFileReader* file_reader,
|
||||
const ReadOptions& read_options,
|
||||
uint32_t column_family_id,
|
||||
Statistics* statistics,
|
||||
CompressionType* compression_type) {
|
||||
assert(file_reader);
|
||||
assert(compression_type);
|
||||
Status BlobFileReader::ReadHeader(const ReadOptions& read_options,
|
||||
uint32_t column_family_id) {
|
||||
assert(file_reader_);
|
||||
|
||||
// Read ~4 KiB from the start to cover both legacy (30B) and blog (40B
|
||||
// fixed + property section) headers in a single I/O. This is enough for
|
||||
// typical blog headers; only if the property section exceeds ~4 KiB do
|
||||
// we need a second read.
|
||||
constexpr size_t kInitialReadSize = 4096;
|
||||
const size_t read_size =
|
||||
std::min(static_cast<uint64_t>(kInitialReadSize), file_size_);
|
||||
|
||||
Slice header_slice;
|
||||
Buffer buf;
|
||||
@@ -158,12 +170,9 @@ Status BlobFileReader::ReadHeader(const RandomAccessFileReader* file_reader,
|
||||
{
|
||||
TEST_SYNC_POINT("BlobFileReader::ReadHeader:ReadFromFile");
|
||||
|
||||
constexpr uint64_t read_offset = 0;
|
||||
constexpr size_t read_size = BlobLogHeader::kSize;
|
||||
|
||||
const Status s =
|
||||
ReadFromFile(file_reader, read_options, read_offset, read_size,
|
||||
statistics, &header_slice, &buf, &aligned_buf);
|
||||
ReadFromFile(file_reader_.get(), read_options, 0, read_size,
|
||||
statistics_, &header_slice, &buf, &aligned_buf);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -172,10 +181,58 @@ Status BlobFileReader::ReadHeader(const RandomAccessFileReader* file_reader,
|
||||
&header_slice);
|
||||
}
|
||||
|
||||
// Detect blog format by checking the 12-byte magic.
|
||||
if (BlogFileHeader::IsBlogFormat(header_slice.data(), header_slice.size())) {
|
||||
// Try to decode the blog header from the buffer we already read.
|
||||
// DecodeFromBuffer handles the case where the property section
|
||||
// extends beyond the buffer.
|
||||
BlogFileHeader blog_header;
|
||||
Slice buf_slice = header_slice;
|
||||
size_t additional_needed = 0;
|
||||
Status s = blog_header.DecodeFromBuffer(&buf_slice, &additional_needed);
|
||||
|
||||
if (s.IsIncomplete()) {
|
||||
// Need more data. Read the rest and retry.
|
||||
Slice extra_slice;
|
||||
Buffer extra_buf;
|
||||
AlignedBuf extra_aligned;
|
||||
s = ReadFromFile(file_reader_.get(), read_options, header_slice.size(),
|
||||
additional_needed, statistics_, &extra_slice, &extra_buf,
|
||||
&extra_aligned);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
std::string extended;
|
||||
extended.append(header_slice.data(), header_slice.size());
|
||||
extended.append(extra_slice.data(), extra_slice.size());
|
||||
Slice extended_slice(extended);
|
||||
s = blog_header.DecodeFromBuffer(&extended_slice, nullptr);
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
is_blog_format_ = true;
|
||||
blog_checksum_type_ = blog_header.checksum_type;
|
||||
blog_incarnation_id_ = blog_header.incarnation_id();
|
||||
memcpy(blog_escape_sequence_, blog_header.escape_sequence,
|
||||
kBlogEscapeSequenceSize);
|
||||
compression_type_ = kNoCompression;
|
||||
decompressor_ = GetBuiltinV2CompressionManager()->GetDecompressor();
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
// Legacy blob format. The initial read already covers the 30-byte header.
|
||||
if (header_slice.size() < BlobLogHeader::kSize) {
|
||||
return Status::Corruption("Blob file header too small");
|
||||
}
|
||||
|
||||
Slice legacy_slice(header_slice.data(), BlobLogHeader::kSize);
|
||||
BlobLogHeader header;
|
||||
|
||||
{
|
||||
const Status s = header.DecodeFrom(header_slice);
|
||||
const Status s = header.DecodeFrom(legacy_slice);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -191,16 +248,62 @@ Status BlobFileReader::ReadHeader(const RandomAccessFileReader* file_reader,
|
||||
return Status::Corruption("Column family ID mismatch");
|
||||
}
|
||||
|
||||
*compression_type = header.compression;
|
||||
compression_type_ = header.compression;
|
||||
if (compression_type_ != kNoCompression) {
|
||||
decompressor_ =
|
||||
GetBuiltinV2CompressionManager()->GetDecompressorOptimizeFor(
|
||||
compression_type_);
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlobFileReader::ReadFooter(const RandomAccessFileReader* file_reader,
|
||||
const ReadOptions& read_options,
|
||||
uint64_t file_size, Statistics* statistics) {
|
||||
assert(file_size >= BlobLogHeader::kSize + BlobLogFooter::kSize);
|
||||
assert(file_reader);
|
||||
Status BlobFileReader::ReadFooter(const ReadOptions& read_options) {
|
||||
assert(file_reader_);
|
||||
|
||||
if (is_blog_format_) {
|
||||
// Blog format: verify the file was cleanly sealed by scanning backward
|
||||
// from EOF for the last record's escape sequence. Read the trailing
|
||||
// ~4 KiB in a single I/O and scan within the buffer.
|
||||
if (file_size_ < kBlogFileFixedHeaderSize + kBlogEscapeSequenceSize) {
|
||||
return Status::Corruption("Blog blob file too small for footer");
|
||||
}
|
||||
|
||||
constexpr size_t kTrailingReadSize = 4096;
|
||||
const uint64_t min_offset = kBlogFileFixedHeaderSize;
|
||||
// Round trailing_offset up to 4-byte alignment so buffer positions
|
||||
// and file positions share the same alignment grid.
|
||||
const uint64_t trailing_offset =
|
||||
(file_size_ -
|
||||
std::min(static_cast<uint64_t>(kTrailingReadSize),
|
||||
file_size_ - min_offset) +
|
||||
3) &
|
||||
~uint64_t{3};
|
||||
const size_t trailing_size =
|
||||
static_cast<size_t>(file_size_ - trailing_offset);
|
||||
|
||||
Slice trailing_slice;
|
||||
Buffer trailing_buf;
|
||||
AlignedBuf trailing_aligned;
|
||||
Status s = ReadFromFile(file_reader_.get(), read_options, trailing_offset,
|
||||
trailing_size, statistics_, &trailing_slice,
|
||||
&trailing_buf, &trailing_aligned);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (VerifyBlogFooterLocator(trailing_slice.data(), trailing_size,
|
||||
trailing_offset, blog_escape_sequence_,
|
||||
blog_checksum_type_, blog_incarnation_id_)) {
|
||||
has_footer_ = true;
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
return Status::Corruption("Blog blob file: no footer locator record found");
|
||||
}
|
||||
|
||||
// Legacy blob format.
|
||||
assert(file_size_ >= BlobLogHeader::kSize + BlobLogFooter::kSize);
|
||||
|
||||
Slice footer_slice;
|
||||
Buffer buf;
|
||||
@@ -209,12 +312,12 @@ Status BlobFileReader::ReadFooter(const RandomAccessFileReader* file_reader,
|
||||
{
|
||||
TEST_SYNC_POINT("BlobFileReader::ReadFooter:ReadFromFile");
|
||||
|
||||
const uint64_t read_offset = file_size - BlobLogFooter::kSize;
|
||||
const uint64_t read_offset = file_size_ - BlobLogFooter::kSize;
|
||||
constexpr size_t read_size = BlobLogFooter::kSize;
|
||||
|
||||
const Status s =
|
||||
ReadFromFile(file_reader, read_options, read_offset, read_size,
|
||||
statistics, &footer_slice, &buf, &aligned_buf);
|
||||
ReadFromFile(file_reader_.get(), read_options, read_offset, read_size,
|
||||
statistics_, &footer_slice, &buf, &aligned_buf);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -238,6 +341,7 @@ Status BlobFileReader::ReadFooter(const RandomAccessFileReader* file_reader,
|
||||
return Status::Corruption("Unexpected TTL blob file");
|
||||
}
|
||||
|
||||
has_footer_ = true;
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -291,13 +395,14 @@ BlobFileReader::BlobFileReader(
|
||||
std::unique_ptr<RandomAccessFileReader>&& file_reader, uint64_t file_size,
|
||||
CompressionType compression_type,
|
||||
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
|
||||
Statistics* statistics)
|
||||
Statistics* statistics, bool has_footer)
|
||||
: file_reader_(std::move(file_reader)),
|
||||
file_size_(file_size),
|
||||
compression_type_(compression_type),
|
||||
decompressor_(std::move(decompressor)),
|
||||
clock_(clock),
|
||||
statistics_(statistics) {
|
||||
statistics_(statistics),
|
||||
has_footer_(has_footer) {
|
||||
assert(file_reader_);
|
||||
}
|
||||
|
||||
@@ -310,29 +415,40 @@ Status BlobFileReader::GetBlob(
|
||||
std::unique_ptr<BlobContents>* result, uint64_t* bytes_read) const {
|
||||
assert(result);
|
||||
|
||||
const uint64_t key_size = user_key.size();
|
||||
// Compute the read region. Both formats store the blob value at `offset`
|
||||
// with size `value_size`. The difference:
|
||||
// - Legacy: if verifying checksums, also read the record header (before
|
||||
// the value) which contains key + CRC.
|
||||
// - Blog: if verifying checksums, also read the 5-byte trailer (after
|
||||
// the value) which contains compression_type + checksum.
|
||||
uint64_t adjustment_before = 0; // bytes before the value to include
|
||||
uint64_t adjustment_after = 0; // bytes after the value to include
|
||||
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_)) {
|
||||
return Status::Corruption("Invalid blob offset");
|
||||
if (is_blog_format_) {
|
||||
if (read_options.verify_checksums) {
|
||||
adjustment_after = kBlogBlockTrailerSize;
|
||||
}
|
||||
} else {
|
||||
const uint64_t key_size = user_key.size();
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
|
||||
has_footer_)) {
|
||||
return Status::Corruption("Invalid blob offset");
|
||||
}
|
||||
if (compression_type != compression_type_) {
|
||||
return Status::Corruption("Compression type mismatch when reading blob");
|
||||
}
|
||||
if (read_options.verify_checksums) {
|
||||
adjustment_before =
|
||||
BlobLogRecord::CalculateAdjustmentForRecordHeader(key_size);
|
||||
}
|
||||
}
|
||||
|
||||
if (compression_type != compression_type_) {
|
||||
return Status::Corruption("Compression type mismatch when reading blob");
|
||||
}
|
||||
|
||||
// Note: if verify_checksum is set, we read the entire blob record to be able
|
||||
// to perform the verification; otherwise, we just read the blob itself. Since
|
||||
// the offset in BlobIndex actually points to the blob value, we need to make
|
||||
// an adjustment in the former case.
|
||||
const uint64_t adjustment =
|
||||
read_options.verify_checksums
|
||||
? BlobLogRecord::CalculateAdjustmentForRecordHeader(key_size)
|
||||
: 0;
|
||||
assert(offset >= adjustment);
|
||||
|
||||
const uint64_t record_offset = offset - adjustment;
|
||||
const uint64_t record_size = value_size + adjustment;
|
||||
assert(offset >= adjustment_before);
|
||||
const uint64_t record_offset = offset - adjustment_before;
|
||||
const uint64_t record_size =
|
||||
adjustment_before + value_size + adjustment_after;
|
||||
|
||||
// Read the data: try prefetch buffer first, then fall back to file read.
|
||||
Slice record_slice;
|
||||
Buffer buf;
|
||||
AlignedBuf aligned_buf;
|
||||
@@ -374,17 +490,30 @@ Status BlobFileReader::GetBlob(
|
||||
TEST_SYNC_POINT_CALLBACK("BlobFileReader::GetBlob:TamperWithResult",
|
||||
&record_slice);
|
||||
|
||||
// Verify checksum (format-specific).
|
||||
CompressionType actual_comp_type = compression_type;
|
||||
if (read_options.verify_checksums) {
|
||||
const Status s = VerifyBlob(record_slice, user_key, value_size);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
if (is_blog_format_) {
|
||||
const Status s = VerifyBlogRecordTrailer(
|
||||
blog_checksum_type_, record_slice.data(),
|
||||
static_cast<size_t>(value_size), blog_incarnation_id_, offset,
|
||||
&actual_comp_type);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
} else {
|
||||
const Status s = VerifyBlob(record_slice, user_key, value_size);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Slice value_slice(record_slice.data() + adjustment, value_size);
|
||||
// Extract the value slice (format-specific offset within the read region).
|
||||
const Slice value_slice(record_slice.data() + adjustment_before, value_size);
|
||||
|
||||
{
|
||||
const Status s = UncompressBlobIfNeeded(value_slice, compression_type,
|
||||
const Status s = UncompressBlobIfNeeded(value_slice, actual_comp_type,
|
||||
decompressor_.get(), allocator,
|
||||
clock_, statistics_, result);
|
||||
if (!s.ok()) {
|
||||
@@ -428,7 +557,8 @@ 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_)) {
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
|
||||
has_footer_)) {
|
||||
*req->status = Status::Corruption("Invalid blob offset");
|
||||
continue;
|
||||
}
|
||||
@@ -454,6 +584,13 @@ 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;
|
||||
|
||||
@@ -533,7 +670,7 @@ void BlobFileReader::MultiGetBlob(
|
||||
}
|
||||
|
||||
// Uncompress blob if needed
|
||||
Slice value_slice(record_slice.data() + adjustments[i], req->len);
|
||||
Slice value_slice(record_slice.data() + adjustments[j - 1], req->len);
|
||||
*req->status = UncompressBlobIfNeeded(
|
||||
value_slice, compression_type_, decompressor_.get(), allocator, clock_,
|
||||
statistics_, &blob_reqs[i].second);
|
||||
|
||||
+37
-10
@@ -36,7 +36,20 @@ class BlobFileReader {
|
||||
HistogramImpl* blob_file_read_hist,
|
||||
uint64_t blob_file_number,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
std::unique_ptr<BlobFileReader>* reader);
|
||||
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);
|
||||
|
||||
BlobFileReader(const BlobFileReader&) = delete;
|
||||
BlobFileReader& operator=(const BlobFileReader&) = delete;
|
||||
@@ -63,27 +76,32 @@ 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);
|
||||
Statistics* statistics, bool has_footer);
|
||||
|
||||
// `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);
|
||||
std::unique_ptr<RandomAccessFileReader>* file_reader,
|
||||
bool skip_footer_size_check);
|
||||
|
||||
static Status ReadHeader(const RandomAccessFileReader* file_reader,
|
||||
const ReadOptions& read_options,
|
||||
uint32_t column_family_id, Statistics* statistics,
|
||||
CompressionType* compression_type);
|
||||
// Read and validate the file header. Detects blog format vs legacy format,
|
||||
// setting is_blog_format_ and related fields accordingly. For legacy format,
|
||||
// validates column_family_id and sets compression_type_.
|
||||
Status ReadHeader(const ReadOptions& read_options, uint32_t column_family_id);
|
||||
|
||||
static Status ReadFooter(const RandomAccessFileReader* file_reader,
|
||||
const ReadOptions& read_options, uint64_t file_size,
|
||||
Statistics* statistics);
|
||||
// Validate the file footer. For legacy format, reads and validates the
|
||||
// BlobLogFooter. For blog format, scans backward for the footer locator
|
||||
// record to verify the file was cleanly sealed.
|
||||
Status ReadFooter(const ReadOptions& read_options);
|
||||
|
||||
using Buffer = std::unique_ptr<char[]>;
|
||||
|
||||
@@ -110,6 +128,15 @@ 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_;
|
||||
// True when the file uses blog format instead of legacy blob log format.
|
||||
bool is_blog_format_ = false;
|
||||
// For blog format: fields from the file header used for record checksum
|
||||
// verification and footer validation.
|
||||
uint32_t blog_incarnation_id_ = 0;
|
||||
ChecksumType blog_checksum_type_ = kNoChecksum;
|
||||
char blog_escape_sequence_[10] = {}; // kBlogEscapeSequenceSize
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -57,8 +57,7 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
||||
constexpr bool do_flush = false;
|
||||
|
||||
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
|
||||
statistics, blob_file_number, use_fsync,
|
||||
do_flush);
|
||||
statistics, use_fsync, do_flush);
|
||||
|
||||
BlobLogHeader header(column_family_id, compression, has_ttl,
|
||||
expiration_range_header);
|
||||
@@ -98,8 +97,8 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
||||
|
||||
std::string checksum_method;
|
||||
std::string checksum_value;
|
||||
ASSERT_OK(blob_log_writer.AppendFooter(WriteOptions(), footer,
|
||||
&checksum_method, &checksum_value));
|
||||
ASSERT_OK(blob_log_writer.LegacyAppendFooterAndClose(
|
||||
WriteOptions(), footer, &checksum_method, &checksum_value));
|
||||
}
|
||||
|
||||
// Creates a test blob file with a single blob in it. Note: this method
|
||||
@@ -465,7 +464,7 @@ TEST_F(BlobFileReaderTest, Malformed) {
|
||||
|
||||
BlobLogWriter blob_log_writer(std::move(file_writer),
|
||||
immutable_options.clock, statistics,
|
||||
blob_file_number, use_fsync, do_flush);
|
||||
use_fsync, do_flush);
|
||||
|
||||
BlobLogHeader header(column_family_id, kNoCompression, has_ttl,
|
||||
expiration_range);
|
||||
@@ -1011,6 +1010,92 @@ 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) {
|
||||
|
||||
@@ -8,70 +8,48 @@
|
||||
#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) {
|
||||
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();
|
||||
}
|
||||
|
||||
flows_[blob_file_number].AddInFlow(bytes);
|
||||
|
||||
return Status::OK();
|
||||
return ProcessFlow(key, value, /*is_inflow=*/true);
|
||||
}
|
||||
|
||||
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();
|
||||
return ProcessFlow(key, value, /*is_inflow=*/false);
|
||||
}
|
||||
|
||||
Status BlobGarbageMeter::Parse(const Slice& key, const Slice& value,
|
||||
uint64_t* blob_file_number, uint64_t* bytes) {
|
||||
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);
|
||||
|
||||
ParsedInternalKey ikey;
|
||||
|
||||
{
|
||||
constexpr bool log_err_key = false;
|
||||
const Status s = ParseInternalKey(key, &ikey, log_err_key);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (blob_index.IsInlined() || blob_index.HasTTL()) {
|
||||
return Status::Corruption("Unexpected TTL/inlined blob index");
|
||||
}
|
||||
|
||||
*blob_file_number = blob_index.file_number();
|
||||
*bytes =
|
||||
blob_index.size() +
|
||||
BlobLogRecord::CalculateAdjustmentForRecordHeader(ikey.user_key.size());
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlobGarbageMeter::ParseBlobIndexReference(const ParsedInternalKey& ikey,
|
||||
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);
|
||||
|
||||
if (ikey.type != kTypeBlobIndex) {
|
||||
return Status::OK();
|
||||
}
|
||||
@@ -85,16 +63,71 @@ Status BlobGarbageMeter::Parse(const Slice& key, const Slice& value,
|
||||
}
|
||||
}
|
||||
|
||||
if (blob_index.IsInlined() || blob_index.HasTTL()) {
|
||||
return Status::Corruption("Unexpected TTL/inlined blob index");
|
||||
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;
|
||||
}
|
||||
|
||||
*blob_file_number = blob_index.file_number();
|
||||
*bytes =
|
||||
blob_index.size() +
|
||||
BlobLogRecord::CalculateAdjustmentForRecordHeader(ikey.user_key.size());
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class BlobIndex;
|
||||
struct ParsedInternalKey;
|
||||
class Slice;
|
||||
|
||||
// A class that can be used to compute the amount of additional garbage
|
||||
@@ -93,8 +95,19 @@ class BlobGarbageMeter {
|
||||
}
|
||||
|
||||
private:
|
||||
static Status Parse(const Slice& key, const Slice& value,
|
||||
uint64_t* blob_file_number, uint64_t* bytes);
|
||||
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);
|
||||
|
||||
std::unordered_map<uint64_t, BlobInOutFlow> flows_;
|
||||
};
|
||||
|
||||
@@ -11,10 +11,23 @@
|
||||
#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;
|
||||
|
||||
@@ -188,6 +201,67 @@ 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) {
|
||||
|
||||
@@ -148,13 +148,26 @@ 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) {
|
||||
if (value_offset <
|
||||
BlobLogHeader::kSize + BlobLogRecord::kHeaderSize + 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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value_offset + value_size + BlobLogFooter::kSize > file_size) {
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,12 +20,10 @@ namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
BlobLogWriter::BlobLogWriter(std::unique_ptr<WritableFileWriter>&& dest,
|
||||
SystemClock* clock, Statistics* statistics,
|
||||
uint64_t log_number, bool use_fs, bool do_flush,
|
||||
uint64_t boffset)
|
||||
bool use_fs, bool do_flush, uint64_t boffset)
|
||||
: dest_(std::move(dest)),
|
||||
clock_(clock),
|
||||
statistics_(statistics),
|
||||
log_number_(log_number),
|
||||
block_offset_(boffset),
|
||||
use_fsync_(use_fs),
|
||||
do_flush_(do_flush),
|
||||
@@ -74,10 +72,9 @@ Status BlobLogWriter::WriteHeader(const WriteOptions& write_options,
|
||||
return s;
|
||||
}
|
||||
|
||||
Status BlobLogWriter::AppendFooter(const WriteOptions& write_options,
|
||||
BlobLogFooter& footer,
|
||||
std::string* checksum_method,
|
||||
std::string* checksum_value) {
|
||||
Status BlobLogWriter::LegacyAppendFooterAndClose(
|
||||
const WriteOptions& write_options, BlobLogFooter& footer,
|
||||
std::string* checksum_method, std::string* checksum_value) {
|
||||
assert(block_offset_ != 0);
|
||||
assert(last_elem_type_ == kEtFileHdr || last_elem_type_ == kEtRecord);
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ class BlobLogWriter {
|
||||
// "*dest" must be initially empty.
|
||||
// "*dest" must remain live while this BlobLogWriter is in use.
|
||||
BlobLogWriter(std::unique_ptr<WritableFileWriter>&& dest, SystemClock* clock,
|
||||
Statistics* statistics, uint64_t log_number, bool use_fsync,
|
||||
bool do_flush, uint64_t boffset = 0);
|
||||
Statistics* statistics, bool use_fsync, bool do_flush,
|
||||
uint64_t boffset = 0);
|
||||
// No copying allowed
|
||||
BlobLogWriter(const BlobLogWriter&) = delete;
|
||||
BlobLogWriter& operator=(const BlobLogWriter&) = delete;
|
||||
@@ -56,9 +56,10 @@ class BlobLogWriter {
|
||||
const Slice& val, uint64_t* key_offset,
|
||||
uint64_t* blob_offset);
|
||||
|
||||
Status AppendFooter(const WriteOptions& write_options, BlobLogFooter& footer,
|
||||
std::string* checksum_method,
|
||||
std::string* checksum_value);
|
||||
Status LegacyAppendFooterAndClose(const WriteOptions& write_options,
|
||||
BlobLogFooter& footer,
|
||||
std::string* checksum_method,
|
||||
std::string* checksum_value);
|
||||
|
||||
Status WriteHeader(const WriteOptions& write_options, BlobLogHeader& header);
|
||||
|
||||
@@ -66,15 +67,12 @@ class BlobLogWriter {
|
||||
|
||||
const WritableFileWriter* file() const { return dest_.get(); }
|
||||
|
||||
uint64_t get_log_number() const { return log_number_; }
|
||||
|
||||
Status Sync(const WriteOptions& write_options);
|
||||
|
||||
private:
|
||||
std::unique_ptr<WritableFileWriter> dest_;
|
||||
SystemClock* clock_;
|
||||
Statistics* statistics_;
|
||||
uint64_t log_number_;
|
||||
uint64_t block_offset_; // Current offset in block
|
||||
bool use_fsync_;
|
||||
bool do_flush_;
|
||||
|
||||
@@ -20,6 +20,21 @@
|
||||
|
||||
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,
|
||||
@@ -231,6 +246,38 @@ 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;
|
||||
}
|
||||
@@ -397,6 +444,89 @@ 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.
|
||||
|
||||
+153
-4
@@ -59,8 +59,7 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
||||
constexpr bool do_flush = false;
|
||||
|
||||
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
|
||||
statistics, blob_file_number, use_fsync,
|
||||
do_flush);
|
||||
statistics, use_fsync, do_flush);
|
||||
|
||||
BlobLogHeader header(column_family_id, compression, has_ttl,
|
||||
expiration_range_header);
|
||||
@@ -100,8 +99,8 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
||||
|
||||
std::string checksum_method;
|
||||
std::string checksum_value;
|
||||
ASSERT_OK(blob_log_writer.AppendFooter(WriteOptions(), footer,
|
||||
&checksum_method, &checksum_value));
|
||||
ASSERT_OK(blob_log_writer.LegacyAppendFooterAndClose(
|
||||
WriteOptions(), footer, &checksum_method, &checksum_value));
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
@@ -1044,6 +1043,156 @@ 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:
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
// 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
|
||||
@@ -0,0 +1,176 @@
|
||||
// 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
|
||||
@@ -3,17 +3,29 @@
|
||||
// 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 {
|
||||
@@ -51,6 +63,43 @@ 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();
|
||||
|
||||
@@ -2459,6 +2508,138 @@ 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
@@ -0,0 +1,621 @@
|
||||
// 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/blog/blog_format.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
|
||||
#include "env/unique_id_gen.h"
|
||||
#include "table/format.h"
|
||||
#include "table/unique_id_impl.h"
|
||||
#include "util/coding.h"
|
||||
#include "util/fastrange.h"
|
||||
#include "util/xxhash.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// --- Property encoding/decoding ---
|
||||
|
||||
void EncodeBlogProperties(const BlogPropertyMap& props, std::string* dst) {
|
||||
for (const auto& [name, value] : props) {
|
||||
PutLengthPrefixedSlice(dst, name);
|
||||
PutLengthPrefixedSlice(dst, value);
|
||||
}
|
||||
}
|
||||
|
||||
Status DecodeBlogProperties(Slice* input, BlogPropertyMap* props) {
|
||||
while (input->size() > 0) {
|
||||
Slice name_slice;
|
||||
Slice value_slice;
|
||||
if (!GetLengthPrefixedSlice(input, &name_slice)) {
|
||||
return Status::Corruption("Blog properties: truncated property name");
|
||||
}
|
||||
if (!GetLengthPrefixedSlice(input, &value_slice)) {
|
||||
return Status::Corruption("Blog properties: truncated property value");
|
||||
}
|
||||
props->emplace_back(name_slice.ToString(), value_slice.ToString());
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
// --- BlogFileHeader ---
|
||||
|
||||
uint32_t BlogFileHeader::incarnation_id() const {
|
||||
return DecodeFixed32(escape_sequence);
|
||||
}
|
||||
|
||||
void BlogFileHeader::EncodeTo(std::string* dst) const {
|
||||
// --- Fixed header (40 bytes) ---
|
||||
// Bytes [0-11]: File magic
|
||||
dst->append(kBlogFileMagic, kBlogFileMagicSize);
|
||||
// Byte [12]: Schema version
|
||||
dst->push_back(static_cast<char>(schema_version));
|
||||
// Byte [13]: Checksum type
|
||||
dst->push_back(static_cast<char>(checksum_type));
|
||||
// Byte [14]: Compact record type
|
||||
dst->push_back(static_cast<char>(compact_record_type));
|
||||
// Byte [15]: Flags
|
||||
dst->push_back(static_cast<char>(flags));
|
||||
// Bytes [16-25]: Escape sequence
|
||||
dst->append(escape_sequence, kBlogEscapeSequenceSize);
|
||||
// Bytes [26-31]: Reserved
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
dst->push_back(0);
|
||||
}
|
||||
|
||||
// Encode properties into a temporary buffer to measure size.
|
||||
std::string prop_buf;
|
||||
EncodeBlogProperties(properties, &prop_buf);
|
||||
char prop_comp_type = static_cast<char>(kNoCompression);
|
||||
|
||||
// Build property section: [properties][compression_type][checksum]
|
||||
uint32_t prop_checksum = ComputeBuiltinChecksumWithLastByte(
|
||||
checksum_type, prop_buf.data(), prop_buf.size(), prop_comp_type);
|
||||
prop_checksum +=
|
||||
ChecksumModifierForContext(incarnation_id(), kBlogFileFixedHeaderSize);
|
||||
std::string prop_section = std::move(prop_buf);
|
||||
prop_section.push_back(prop_comp_type);
|
||||
PutFixed32(&prop_section, prop_checksum);
|
||||
|
||||
// Bytes [32-35]: Property section size (trailer included, no padding)
|
||||
PutFixed32(dst, static_cast<uint32_t>(prop_section.size()));
|
||||
|
||||
// Bytes [36-39]: Fixed header checksum (covers bytes [0, 36))
|
||||
// Uses context checksum with incarnation_id at offset 0 for
|
||||
// defense-in-depth (misplaced data detection).
|
||||
assert(dst->size() == 36);
|
||||
uint32_t header_checksum =
|
||||
ComputeBuiltinChecksum(checksum_type, dst->data(), dst->size()) +
|
||||
ChecksumModifierForContext(incarnation_id(), 0);
|
||||
PutFixed32(dst, header_checksum);
|
||||
|
||||
// Property section follows the fixed header
|
||||
assert(dst->size() == kBlogFileFixedHeaderSize);
|
||||
dst->append(prop_section);
|
||||
}
|
||||
|
||||
Status BlogFileHeader::DecodeFrom(Slice* input) {
|
||||
if (input->size() < kBlogFileFixedHeaderSize) {
|
||||
return Status::Corruption("Blog header too short: have " +
|
||||
std::to_string(input->size()) + " bytes, need " +
|
||||
std::to_string(kBlogFileFixedHeaderSize));
|
||||
}
|
||||
|
||||
const char* p = input->data();
|
||||
|
||||
// Verify magic
|
||||
if (memcmp(p, kBlogFileMagic, kBlogFileMagicSize) != 0) {
|
||||
return Status::Corruption("Blog header: bad magic (first 4 bytes: 0x" +
|
||||
Slice(p, 4).ToString(true) + ")");
|
||||
}
|
||||
|
||||
// Parse fields needed for checksum computation before verifying.
|
||||
checksum_type = static_cast<ChecksumType>(p[13]);
|
||||
memcpy(escape_sequence, p + 16, kBlogEscapeSequenceSize);
|
||||
|
||||
// Verify fixed header checksum FIRST, before trusting any other fields.
|
||||
uint32_t prop_section_size = DecodeFixed32(p + 32);
|
||||
uint32_t stored_header_checksum = DecodeFixed32(p + 36);
|
||||
uint32_t computed_header_checksum =
|
||||
ComputeBuiltinChecksum(checksum_type, p, 36) +
|
||||
ChecksumModifierForContext(incarnation_id(), 0);
|
||||
if (stored_header_checksum != computed_header_checksum) {
|
||||
return Status::Corruption(
|
||||
"Blog header: fixed header checksum mismatch (stored: 0x" +
|
||||
Slice(p + 36, 4).ToString(true) + ", computed: 0x" +
|
||||
Slice(reinterpret_cast<const char*>(&computed_header_checksum), 4)
|
||||
.ToString(true) +
|
||||
")");
|
||||
}
|
||||
|
||||
// Now that checksum is verified, validate the remaining fields.
|
||||
schema_version = static_cast<uint8_t>(p[12]);
|
||||
if (schema_version > kBlogCurrentSchemaVersion) {
|
||||
return Status::NotSupported("Blog header: schema version " +
|
||||
std::to_string(schema_version) +
|
||||
" is newer than supported version " +
|
||||
std::to_string(kBlogCurrentSchemaVersion));
|
||||
}
|
||||
compact_record_type = static_cast<BlogRecordType>(p[14]);
|
||||
flags = static_cast<uint8_t>(p[15]);
|
||||
|
||||
if (!VerifyEscapeSequence(escape_sequence)) {
|
||||
return Status::Corruption(
|
||||
"Blog header: escape sequence derived bytes mismatch (first byte: 0x" +
|
||||
Slice(escape_sequence, 1).ToString(true) + ")");
|
||||
}
|
||||
|
||||
// Advance past fixed header
|
||||
input->remove_prefix(kBlogFileFixedHeaderSize);
|
||||
|
||||
// Read property section
|
||||
if (input->size() < prop_section_size) {
|
||||
return Status::Corruption("Blog header: truncated property section (have " +
|
||||
std::to_string(input->size()) + " bytes, need " +
|
||||
std::to_string(prop_section_size) + ")");
|
||||
}
|
||||
|
||||
if (prop_section_size < kBlogBlockTrailerSize) {
|
||||
return Status::Corruption("Blog header: property section too small (" +
|
||||
std::to_string(prop_section_size) +
|
||||
" bytes, min " +
|
||||
std::to_string(kBlogBlockTrailerSize) + ")");
|
||||
}
|
||||
|
||||
// Property section layout: [properties][comp_type:1B][checksum:4B]
|
||||
const char* prop_data = input->data();
|
||||
size_t prop_data_size = prop_section_size - kBlogBlockTrailerSize;
|
||||
char prop_comp_type = prop_data[prop_data_size];
|
||||
uint32_t stored_prop_checksum = DecodeFixed32(prop_data + prop_data_size + 1);
|
||||
uint32_t computed_prop_checksum = ComputeBuiltinChecksumWithLastByte(
|
||||
checksum_type, prop_data, prop_data_size, prop_comp_type);
|
||||
computed_prop_checksum +=
|
||||
ChecksumModifierForContext(incarnation_id(), kBlogFileFixedHeaderSize);
|
||||
if (stored_prop_checksum != computed_prop_checksum) {
|
||||
return Status::Corruption(
|
||||
"Blog header: property section checksum mismatch (prop_size=" +
|
||||
std::to_string(prop_data_size) + ", comp_type=0x" +
|
||||
Slice(&prop_comp_type, 1).ToString(true) + ")");
|
||||
}
|
||||
|
||||
// Decode properties
|
||||
Slice prop_slice(prop_data, prop_data_size);
|
||||
properties.clear();
|
||||
Status s = DecodeBlogProperties(&prop_slice, &properties);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// Check for unrecognized required properties
|
||||
for (const auto& [name, value] : properties) {
|
||||
if (IsBlogRequiredProperty(name)) {
|
||||
if (name != kBlogPropCompressionCompatibilityName &&
|
||||
name != kBlogPropTimestampSize &&
|
||||
name != kBlogPropPredecessorWalInfo &&
|
||||
name != kBlogPropWriteBatchStreamingCompressionType) {
|
||||
return Status::NotSupported(
|
||||
"Blog header: unrecognized required property: \"" + name + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input->remove_prefix(prop_section_size);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void BlogFileHeader::SetEscapeSequenceFromU64(uint64_t v) {
|
||||
// Byte 0: derived from top 4 bytes via FastRange32 into [1, 254],
|
||||
// avoiding 0x00 and 0xFF (which are padding bytes).
|
||||
uint32_t top = static_cast<uint32_t>(v >> 32);
|
||||
escape_sequence[0] = static_cast<char>(FastRange32(top, 254) + 1);
|
||||
assert(escape_sequence[0] != '\0');
|
||||
assert(escape_sequence[0] != '\xFF');
|
||||
|
||||
// Bytes 1-5: lowest 5 bytes as-is (little-endian).
|
||||
EncodeFixed32(escape_sequence + 1, static_cast<uint32_t>(v));
|
||||
escape_sequence[5] = static_cast<char>(v >> 32);
|
||||
// (byte at index 5 overlaps with top byte used for FastRange32, but
|
||||
// that's fine -- it's just input entropy, not the derived byte 0)
|
||||
|
||||
// Bytes 6-9: XXH3-derived suffix for heuristic detection. This would enable
|
||||
// us to scan through a file missing its header and, with some hashing effort,
|
||||
// detect escape sequences with high accuracy (expect 1 false positive per
|
||||
// ~16GB; further validation/pruning can be done trying to parse records
|
||||
// and/or picking the most common candidate escape sequence).
|
||||
uint64_t derived = XXH3_64bits_withSeed(
|
||||
escape_sequence, kBlogEscapeSeqRandomPartSize, kBlogEscapeSeqSeed);
|
||||
EncodeFixed32(escape_sequence + kBlogEscapeSeqRandomPartSize,
|
||||
static_cast<uint32_t>(derived));
|
||||
}
|
||||
|
||||
void BlogFileHeader::GenerateFromUniqueId(const std::string& db_id,
|
||||
const std::string& db_session_id,
|
||||
uint64_t file_number) {
|
||||
if (db_id.empty() || db_session_id.empty() || file_number == 0) {
|
||||
GenerateRandomFields();
|
||||
return;
|
||||
}
|
||||
|
||||
// Derive escape sequence from the same unique ID scheme as SST files.
|
||||
UniqueId64x2 unique_id;
|
||||
Status s =
|
||||
GetSstInternalUniqueId(db_id, db_session_id, file_number, &unique_id);
|
||||
if (!s.ok()) {
|
||||
GenerateRandomFields();
|
||||
return;
|
||||
}
|
||||
|
||||
// The internal unique ID is intentionally not fully random/mixed. Mix it
|
||||
// so that it is.
|
||||
InternalUniqueIdToExternal(&unique_id);
|
||||
|
||||
SetEscapeSequenceFromU64(unique_id[0]);
|
||||
|
||||
// Store the inputs as ignorable header properties for debugging/tracking.
|
||||
SetProperty(kBlogPropDbId, db_id);
|
||||
SetProperty(kBlogPropDbSessionId, db_session_id);
|
||||
SetUint64Property(kBlogPropFileNumber, file_number);
|
||||
}
|
||||
|
||||
void BlogFileHeader::GenerateRandomFields() {
|
||||
uint64_t rand_a;
|
||||
uint64_t rand_b;
|
||||
GenerateRawUniqueId(&rand_a, &rand_b);
|
||||
// Use rand_a for escape sequence, rand_b is unused extra entropy.
|
||||
(void)rand_b;
|
||||
SetEscapeSequenceFromU64(rand_a);
|
||||
}
|
||||
|
||||
bool BlogFileHeader::VerifyEscapeSequence(const char* seq) {
|
||||
uint8_t first = static_cast<uint8_t>(seq[0]);
|
||||
if (first == 0x00 || first == 0xFF) {
|
||||
return false;
|
||||
}
|
||||
// Verify derived part
|
||||
uint64_t derived = XXH3_64bits_withSeed(seq, kBlogEscapeSeqRandomPartSize,
|
||||
kBlogEscapeSeqSeed);
|
||||
uint32_t expected = static_cast<uint32_t>(derived);
|
||||
uint32_t actual = DecodeFixed32(seq + kBlogEscapeSeqRandomPartSize);
|
||||
return expected == actual;
|
||||
}
|
||||
|
||||
void BlogFileHeader::SetProperty(const std::string& name,
|
||||
const std::string& value) {
|
||||
for (auto& [k, v] : properties) {
|
||||
if (k == name) {
|
||||
v = value;
|
||||
return;
|
||||
}
|
||||
}
|
||||
properties.emplace_back(name, value);
|
||||
}
|
||||
|
||||
std::string BlogFileHeader::GetProperty(const std::string& name) const {
|
||||
for (const auto& [k, v] : properties) {
|
||||
if (k == name) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
bool BlogFileHeader::HasProperty(const std::string& name) const {
|
||||
for (const auto& [k, v] : properties) {
|
||||
if (k == name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BlogFileHeader::SetUint64Property(const std::string& name,
|
||||
uint64_t value) {
|
||||
std::string encoded;
|
||||
PutVarint64(&encoded, value);
|
||||
SetProperty(name, encoded);
|
||||
}
|
||||
|
||||
void BlogFileHeader::SetUint32Property(const std::string& name,
|
||||
uint32_t value) {
|
||||
std::string encoded;
|
||||
PutVarint32(&encoded, value);
|
||||
SetProperty(name, encoded);
|
||||
}
|
||||
|
||||
bool BlogFileHeader::GetUint64Property(const std::string& name,
|
||||
uint64_t* value) const {
|
||||
std::string raw = GetProperty(name);
|
||||
if (raw.empty()) {
|
||||
return false;
|
||||
}
|
||||
Slice s(raw);
|
||||
return GetVarint64(&s, value);
|
||||
}
|
||||
|
||||
bool BlogFileHeader::GetUint32Property(const std::string& name,
|
||||
uint32_t* value) const {
|
||||
std::string raw = GetProperty(name);
|
||||
if (raw.empty()) {
|
||||
return false;
|
||||
}
|
||||
Slice s(raw);
|
||||
return GetVarint32(&s, value);
|
||||
}
|
||||
|
||||
Status BlogFileHeader::DecodeFromBuffer(Slice* buffer,
|
||||
size_t* additional_bytes_needed) {
|
||||
if (buffer->size() < kBlogFileFixedHeaderSize) {
|
||||
return Status::Corruption("Blog header: buffer too small for fixed header");
|
||||
}
|
||||
|
||||
// Check if the full header (fixed + property section) fits in the buffer.
|
||||
uint32_t prop_section_size = DecodeFixed32(buffer->data() + 32);
|
||||
size_t total_header_size = kBlogFileFixedHeaderSize + prop_section_size;
|
||||
|
||||
if (buffer->size() < total_header_size) {
|
||||
if (additional_bytes_needed) {
|
||||
*additional_bytes_needed = total_header_size - buffer->size();
|
||||
}
|
||||
return Status::Incomplete(
|
||||
"Blog header: need more data for property section",
|
||||
std::to_string(total_header_size));
|
||||
}
|
||||
|
||||
// Full header is available. Decode it.
|
||||
Slice header_slice(buffer->data(), total_header_size);
|
||||
Status s = DecodeFrom(&header_slice);
|
||||
if (s.ok()) {
|
||||
buffer->remove_prefix(total_header_size);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// --- Footer locator verification ---
|
||||
|
||||
bool VerifyBlogFooterLocator(const char* buffer, size_t buffer_size,
|
||||
uint64_t buffer_file_offset,
|
||||
const char* expected_escape_seq,
|
||||
ChecksumType checksum_type,
|
||||
uint32_t incarnation_id) {
|
||||
// Scan backward in 4-byte steps for the escape sequence.
|
||||
// Buffer must start at a 4-byte-aligned file offset.
|
||||
for (size_t pos = buffer_size; pos >= 4; pos -= 4) {
|
||||
size_t esc_start = pos - 4;
|
||||
uint8_t first_byte = static_cast<uint8_t>(buffer[esc_start]);
|
||||
if (first_byte == 0x00 || first_byte == 0xFF) {
|
||||
continue; // padding byte
|
||||
}
|
||||
// Check if this is our escape sequence
|
||||
if (esc_start + kBlogEscapeSequenceSize > buffer_size ||
|
||||
memcmp(buffer + esc_start, expected_escape_seq,
|
||||
kBlogEscapeSequenceSize) != 0) {
|
||||
continue; // not our escape sequence
|
||||
}
|
||||
|
||||
// Found escape sequence. Parse the full-format record that follows.
|
||||
// Full format: [escape_seq:10B] [varint length:4+B] [type:1B]
|
||||
// [compression_type:1B] [prefix_checksum:4B]
|
||||
// [payload:length B] [comp:1B] [checksum:4B]
|
||||
const char* rec = buffer + esc_start + kBlogEscapeSequenceSize;
|
||||
size_t remaining = buffer_size - esc_start - kBlogEscapeSequenceSize;
|
||||
|
||||
// Parse varint length (must be >= 4 bytes for full format)
|
||||
Slice varint_slice(rec, remaining);
|
||||
uint64_t length = 0;
|
||||
const char* varint_end = GetVarint64Ptr(rec, rec + remaining, &length);
|
||||
if (!varint_end) {
|
||||
continue; // truncated varint
|
||||
}
|
||||
size_t varint_len = static_cast<size_t>(varint_end - rec);
|
||||
if (varint_len <= kBlogCompactVarintMaxBytes) {
|
||||
continue; // not full format
|
||||
}
|
||||
|
||||
// Need type(1) + comp(1) + prefix_checksum(4) after varint
|
||||
size_t after_varint = remaining - varint_len;
|
||||
if (after_varint < 6) {
|
||||
continue; // truncated prefix
|
||||
}
|
||||
|
||||
BlogRecordType type =
|
||||
static_cast<BlogRecordType>(static_cast<uint8_t>(varint_end[0]));
|
||||
if (type != kBlogFooterLocatorRecord) {
|
||||
continue; // not a footer locator
|
||||
}
|
||||
|
||||
// Verify prefix checksum over (varint + type + compression_type)
|
||||
size_t prefix_data_size = varint_len + 2; // varint + type + comp
|
||||
uint64_t esc_file_offset = buffer_file_offset + esc_start;
|
||||
uint32_t stored_prefix_checksum = DecodeFixed32(varint_end + 2);
|
||||
uint32_t computed_prefix_checksum =
|
||||
ComputeBuiltinChecksum(checksum_type, rec, prefix_data_size) +
|
||||
ChecksumModifierForContext(incarnation_id, esc_file_offset);
|
||||
if (stored_prefix_checksum != computed_prefix_checksum) {
|
||||
continue; // prefix checksum mismatch
|
||||
}
|
||||
|
||||
// If length > 0, verify the payload trailer checksum
|
||||
if (length > 0) {
|
||||
size_t after_prefix = after_varint - 6; // after type+comp+prefix_cksum
|
||||
if (after_prefix < length + kBlogBlockTrailerSize) {
|
||||
continue; // truncated payload or trailer
|
||||
}
|
||||
const char* payload = varint_end + 6;
|
||||
uint64_t payload_file_offset = buffer_file_offset + esc_start +
|
||||
kBlogEscapeSequenceSize + varint_len + 6;
|
||||
CompressionType actual_comp;
|
||||
Status s = VerifyBlogRecordTrailer(
|
||||
checksum_type, payload, static_cast<size_t>(length), incarnation_id,
|
||||
payload_file_offset, &actual_comp);
|
||||
if (!s.ok()) {
|
||||
continue; // trailer checksum mismatch
|
||||
}
|
||||
}
|
||||
|
||||
// Valid footer locator record found.
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- BlogFileFooterLocator ---
|
||||
|
||||
void BlogFileFooterLocator::EncodeTo(std::string* dst) const {
|
||||
for (const auto& entry : entries) {
|
||||
dst->push_back(static_cast<char>(entry.record_type));
|
||||
PutFixed32(dst, entry.relative_offset_4B);
|
||||
}
|
||||
}
|
||||
|
||||
Status BlogFileFooterLocator::DecodeFrom(const Slice& input) {
|
||||
Slice s = input;
|
||||
entries.clear();
|
||||
while (s.size() >= 5) { // 1 byte type + 4 bytes offset
|
||||
entries.push_back(
|
||||
{.record_type = static_cast<BlogRecordType>(static_cast<uint8_t>(s[0])),
|
||||
.relative_offset_4B = DecodeFixed32(s.data() + 1)});
|
||||
s.remove_prefix(5);
|
||||
}
|
||||
if (s.size() != 0) {
|
||||
return Status::Corruption(
|
||||
"Blog footer locator: trailing bytes after entries");
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
// --- BlogFileFooterProperties ---
|
||||
|
||||
void BlogFileFooterProperties::SetBlobCount(uint64_t count) {
|
||||
std::string val;
|
||||
PutVarint64(&val, count);
|
||||
properties.emplace_back("blobCount", std::move(val));
|
||||
}
|
||||
|
||||
void BlogFileFooterProperties::SetTotalBlobBytes(uint64_t bytes) {
|
||||
std::string val;
|
||||
PutVarint64(&val, bytes);
|
||||
properties.emplace_back("totalBlobBytes", std::move(val));
|
||||
}
|
||||
|
||||
void BlogFileFooterProperties::SetSequenceRange(uint64_t min_seq,
|
||||
uint64_t max_seq) {
|
||||
std::string min_val;
|
||||
PutVarint64(&min_val, min_seq);
|
||||
properties.emplace_back("minSequence", std::move(min_val));
|
||||
std::string max_val;
|
||||
PutVarint64(&max_val, max_seq);
|
||||
properties.emplace_back("maxSequence", std::move(max_val));
|
||||
}
|
||||
|
||||
void BlogFileFooterProperties::EncodeTo(std::string* dst) const {
|
||||
EncodeBlogProperties(properties, dst);
|
||||
}
|
||||
|
||||
Status BlogFileFooterProperties::DecodeFrom(const Slice& input) {
|
||||
Slice s = input;
|
||||
properties.clear();
|
||||
return DecodeBlogProperties(&s, &properties);
|
||||
}
|
||||
|
||||
// --- Padding ---
|
||||
|
||||
void ComputeBlogPaddingParams(uint8_t last_meaningful_byte,
|
||||
size_t current_offset, uint8_t* pad_byte,
|
||||
size_t* pad_count) {
|
||||
// Determine how many bytes needed to reach next 4-byte alignment
|
||||
size_t remainder = current_offset % 4;
|
||||
size_t needed = (remainder == 0) ? 0 : (4 - remainder);
|
||||
|
||||
// Choose pad byte to differ from last_meaningful_byte
|
||||
if (last_meaningful_byte == 0x00) {
|
||||
*pad_byte = 0xFF;
|
||||
// Must have at least 1 byte of padding to distinguish from checksum
|
||||
if (needed == 0) {
|
||||
needed = 4;
|
||||
}
|
||||
} else if (last_meaningful_byte == 0xFF) {
|
||||
*pad_byte = 0x00;
|
||||
if (needed == 0) {
|
||||
needed = 4;
|
||||
}
|
||||
} else {
|
||||
*pad_byte = 0x00; // Writer's choice; 0x00 is conventional
|
||||
}
|
||||
*pad_count = needed;
|
||||
}
|
||||
|
||||
void ComputeBlogPadding(uint8_t last_meaningful_byte, size_t current_offset,
|
||||
std::string* dst) {
|
||||
uint8_t pad_byte;
|
||||
size_t pad_count;
|
||||
ComputeBlogPaddingParams(last_meaningful_byte, current_offset, &pad_byte,
|
||||
&pad_count);
|
||||
dst->append(pad_count, static_cast<char>(pad_byte));
|
||||
}
|
||||
|
||||
// --- Checksum ---
|
||||
|
||||
uint32_t ComputeBlogRecordChecksum(ChecksumType type, const char* data,
|
||||
size_t data_size, char compression_type_byte,
|
||||
uint32_t base_context_checksum,
|
||||
uint64_t record_offset) {
|
||||
uint32_t checksum = ComputeBuiltinChecksumWithLastByte(type, data, data_size,
|
||||
compression_type_byte);
|
||||
checksum += ChecksumModifierForContext(base_context_checksum, record_offset);
|
||||
return checksum;
|
||||
}
|
||||
|
||||
// --- Record trailer verification ---
|
||||
|
||||
Status VerifyBlogRecordTrailer(ChecksumType checksum_type, const char* payload,
|
||||
size_t payload_size, uint32_t incarnation_id,
|
||||
uint64_t payload_file_offset,
|
||||
CompressionType* actual_compression_type) {
|
||||
// Trailer is the 5 bytes immediately after the payload:
|
||||
// [compression_type: 1B][checksum: 4B]
|
||||
const char* trailer = payload + payload_size;
|
||||
char comp_byte = trailer[0];
|
||||
uint32_t stored_checksum = DecodeFixed32(trailer + 1);
|
||||
uint32_t computed_checksum =
|
||||
ComputeBlogRecordChecksum(checksum_type, payload, payload_size, comp_byte,
|
||||
incarnation_id, payload_file_offset);
|
||||
if (stored_checksum != computed_checksum) {
|
||||
return Status::Corruption("Blog record checksum mismatch");
|
||||
}
|
||||
if (actual_compression_type) {
|
||||
*actual_compression_type =
|
||||
static_cast<CompressionType>(static_cast<uint8_t>(comp_byte));
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
// --- Irregular varint ---
|
||||
|
||||
void PutBlogIrregularVarint64(std::string* dst, uint64_t value,
|
||||
size_t min_encoded_bytes) {
|
||||
// Encode as standard varint, then pad with trailing zero-data bytes
|
||||
// to reach min_encoded_bytes. The result decodes to the same value.
|
||||
char buf[10];
|
||||
char* end = EncodeVarint64(buf, value);
|
||||
size_t natural_len = static_cast<size_t>(end - buf);
|
||||
|
||||
if (natural_len >= min_encoded_bytes) {
|
||||
dst->append(buf, natural_len);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the continuation bit on what was the terminal byte, then append
|
||||
// (extra - 1) continuation bytes (0x80) and one terminal byte (0x00).
|
||||
size_t extra = min_encoded_bytes - natural_len;
|
||||
buf[natural_len - 1] |= static_cast<char>(0x80);
|
||||
dst->append(buf, natural_len);
|
||||
for (size_t i = 0; i + 1 < extra; ++i) {
|
||||
dst->push_back(static_cast<char>(0x80));
|
||||
}
|
||||
dst->push_back(static_cast<char>(0x00));
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,299 @@
|
||||
// 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 <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/status.h"
|
||||
#include "rocksdb/table.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// Record types used in blog file body and footer.
|
||||
// Types 0x01-0x7F are for body records.
|
||||
// Types 0xF0-0xFF are for footer records (in expected order of occurrence,
|
||||
// with gaps for future additions).
|
||||
enum BlogRecordType : uint8_t {
|
||||
kBlogBlobRecord = 0x01,
|
||||
kBlogPreambleStartRecord = 0x02,
|
||||
kBlogWriteBatchRecord = 0x03,
|
||||
// 0x04-0x7F: reserved for future body record types
|
||||
|
||||
kBlogFooterIndexRecord = 0xF0, // sparse index or similar large metadata
|
||||
// 0xF1-0xF3: reserved for future footer data records
|
||||
kBlogFooterPropertiesRecord = 0xF8, // small metadata (named properties)
|
||||
// 0xF9-0xFB: reserved
|
||||
kBlogFooterFileChecksumInfo = 0xFC, // full file checksum state dump, so
|
||||
// the file checksum can be inferred
|
||||
// from just reading the tail
|
||||
// 0xFD-0xFE: reserved
|
||||
kBlogFooterLocatorRecord = 0xFF, // relative offsets to other footer
|
||||
// records; must be last
|
||||
};
|
||||
|
||||
// --- Constants ---
|
||||
|
||||
// 12-byte file magic: "Blog" (ASCII) followed by XXH3_64bits("Blog", 4).
|
||||
// Used as an opaque identifier for blog format files.
|
||||
static constexpr char kBlogFileMagic[12] = {
|
||||
'\x42', '\x6C', '\x6F', '\x67', // "Blog"
|
||||
'\x9F', '\xFC', '\x41', '\x52', '\xF7', '\xF2', '\x2C', '\x49' // XXH3_64
|
||||
};
|
||||
static constexpr size_t kBlogFileMagicSize = 12;
|
||||
|
||||
// Fixed header size in bytes (before property section).
|
||||
static constexpr size_t kBlogFileFixedHeaderSize = 40;
|
||||
|
||||
// Escape sequence: 6 random bytes + 4 derived bytes.
|
||||
static constexpr size_t kBlogEscapeSequenceSize = 10;
|
||||
static constexpr size_t kBlogEscapeSeqRandomPartSize = 6;
|
||||
|
||||
// Seed for deriving bytes [6-9] of escape sequence via
|
||||
// XXH3_64bits_withSeed(random_6_bytes, 6, kBlogEscapeSeqSeed).
|
||||
// Chosen arbitrarily; must never change once the format is released.
|
||||
static constexpr uint64_t kBlogEscapeSeqSeed =
|
||||
0x52636B73426C6F67ULL; // "RcksBlg\0"
|
||||
|
||||
// Sentinel for unspecified-size records. Encodes to a 9-byte varint,
|
||||
// which always triggers the full record format.
|
||||
static constexpr uint64_t kBlogUnspecifiedSize = (uint64_t{1} << 63) - 1;
|
||||
|
||||
// Maximum payload size that encodes to a compact-format varint (<= 2 bytes).
|
||||
// varint of 16383 (0x3FFF) uses 2 bytes; 16384 uses 3 bytes.
|
||||
// Records larger than this get full format with prefix checksum protection.
|
||||
static constexpr uint64_t kBlogMaxCompactPayloadSize = (1u << 14) - 1;
|
||||
|
||||
// Compact format varint threshold: varints of this many bytes or fewer
|
||||
// use compact format (no type byte, no prefix checksum).
|
||||
static constexpr size_t kBlogCompactVarintMaxBytes = 2;
|
||||
|
||||
// Block trailer size: 1 byte compression_type + 4 bytes checksum.
|
||||
static constexpr size_t kBlogBlockTrailerSize = 5;
|
||||
|
||||
// kStreamingCompressionSentinel (0x7F) is defined in compression_type.h
|
||||
// and used in blog record trailers to indicate streaming compression.
|
||||
// A record with kNoCompression in the trailer remains individually
|
||||
// uncompressed even when streaming is active (dynamic on/off).
|
||||
|
||||
// --- Named Properties ---
|
||||
|
||||
// Named property: string key -> string value.
|
||||
// Case convention: Uppercase first letter = required (reader must recognize
|
||||
// or reject the file). Lowercase first letter = ignorable.
|
||||
using BlogPropertyMap = std::vector<std::pair<std::string, std::string>>;
|
||||
|
||||
// Encode properties as a sequence of length-prefixed name/value pairs.
|
||||
// No count prefix; section is terminated by reaching the end of the slice.
|
||||
void EncodeBlogProperties(const BlogPropertyMap& props, std::string* dst);
|
||||
|
||||
// Decode properties from a slice, consuming all bytes.
|
||||
Status DecodeBlogProperties(Slice* input, BlogPropertyMap* props);
|
||||
|
||||
// Returns true if a property name starts with an uppercase letter,
|
||||
// meaning the reader must recognize it or reject the file.
|
||||
inline bool IsBlogRequiredProperty(const Slice& name) {
|
||||
return name.size() > 0 && name[0] >= 'A' && name[0] <= 'Z';
|
||||
}
|
||||
|
||||
// Known header property names
|
||||
static constexpr const char* kBlogPropCompressionCompatibilityName =
|
||||
"CompressionCompatibilityName";
|
||||
static constexpr const char* kBlogPropTimestampSize = "TimestampSize";
|
||||
static constexpr const char* kBlogPropPredecessorWalInfo = "PredecessorWalInfo";
|
||||
// Required (uppercase) when streaming compression is active for WriteBatch
|
||||
// records. Value is a 2-digit hex encoding of the CompressionType used
|
||||
// (e.g. "07" for kZSTD). The presence of this property means WriteBatch
|
||||
// record payloads with kStreamingCompressionSentinel in their trailer
|
||||
// are part of a streaming compression context that spans across records.
|
||||
static constexpr const char* kBlogPropWriteBatchStreamingCompressionType =
|
||||
"WriteBatchStreamingCompressionType";
|
||||
static constexpr const char* kBlogPropRole = "role";
|
||||
static constexpr const char* kBlogPropCompressionSettings =
|
||||
"compressionSettings";
|
||||
static constexpr const char* kBlogPropCreationTime = "creationTime";
|
||||
static constexpr const char* kBlogPropCreator = "creator";
|
||||
// Ignorable properties for file identity and diagnostics.
|
||||
static constexpr const char* kBlogPropDbId = "dbId";
|
||||
static constexpr const char* kBlogPropDbSessionId = "dbSessionId";
|
||||
static constexpr const char* kBlogPropFileNumber = "fileNumber";
|
||||
static constexpr const char* kBlogPropDbHostId = "dbHostId";
|
||||
static constexpr const char* kBlogPropColumnFamilyId = "columnFamilyId";
|
||||
static constexpr const char* kBlogPropColumnFamilyName = "columnFamilyName";
|
||||
|
||||
// --- BlogFileHeader ---
|
||||
|
||||
// Bit flags for BlogFileHeader::flags (byte [15] in the fixed header).
|
||||
enum BlogFileHeaderFlags : uint8_t {
|
||||
kBlogFileRecycled = 0x01, // File may contain trailing stale data from a
|
||||
// previous incarnation. Recovery should tolerate
|
||||
// trailing data that doesn't match our escape
|
||||
// sequence (recycled/stale, not corruption).
|
||||
};
|
||||
|
||||
static constexpr uint8_t kBlogCurrentSchemaVersion = 0;
|
||||
|
||||
struct BlogFileHeader {
|
||||
uint8_t schema_version = kBlogCurrentSchemaVersion;
|
||||
ChecksumType checksum_type = kXXH3;
|
||||
BlogRecordType compact_record_type = kBlogBlobRecord;
|
||||
uint8_t flags = 0; // BlogFileHeaderFlags, byte [15] in fixed header
|
||||
// 10-byte escape sequence: bytes [0-5] random (byte[0] not 0x00/0xFF),
|
||||
// bytes [6-9] = lower32(XXH3_64bits_withSeed(bytes[0-5],
|
||||
// kBlogEscapeSeqSeed)).
|
||||
char escape_sequence[kBlogEscapeSequenceSize] = {};
|
||||
|
||||
bool is_recycled() const { return flags & kBlogFileRecycled; }
|
||||
|
||||
BlogPropertyMap properties;
|
||||
|
||||
// Incarnation ID = first 4 bytes of escape_sequence (little-endian via
|
||||
// DecodeFixed32). Guaranteed nonzero since byte[0] is not 0x00.
|
||||
uint32_t incarnation_id() const;
|
||||
|
||||
// Encode the full header (fixed prefix + property section + checksums)
|
||||
// into dst.
|
||||
void EncodeTo(std::string* dst) const;
|
||||
|
||||
// Decode from input. Consumes the bytes read.
|
||||
Status DecodeFrom(Slice* input);
|
||||
|
||||
// Generate escape_sequence from the file's unique ID (db_id, db_session_id,
|
||||
// file_number), using the same scheme as SST internal unique IDs. Falls back
|
||||
// to GenerateRandomFields() if any input is empty/zero. Also sets the
|
||||
// ignorable dbId/dbSessionId/fileNumber header properties.
|
||||
void GenerateFromUniqueId(const std::string& db_id,
|
||||
const std::string& db_session_id,
|
||||
uint64_t file_number);
|
||||
|
||||
// Generate escape_sequence from random bytes. Appropriate when unique ID
|
||||
// inputs are not available.
|
||||
void GenerateRandomFields();
|
||||
|
||||
private:
|
||||
// Populate escape_sequence from a 64 bits of entropy.
|
||||
void SetEscapeSequenceFromU64(uint64_t v);
|
||||
|
||||
public:
|
||||
// Verify that the escape sequence's derived bytes match.
|
||||
static bool VerifyEscapeSequence(const char* seq);
|
||||
|
||||
// Check if a buffer starts with the blog file magic number.
|
||||
static bool IsBlogFormat(const char* data, size_t size) {
|
||||
return size >= kBlogFileMagicSize &&
|
||||
memcmp(data, kBlogFileMagic, kBlogFileMagicSize) == 0;
|
||||
}
|
||||
|
||||
// Decode a blog file header from a buffer that may or may not contain
|
||||
// the full header. Returns OK and consumes the header bytes on success.
|
||||
// If the buffer contains the fixed header but not the full property
|
||||
// section, returns the number of additional bytes needed in
|
||||
// *additional_bytes_needed and returns Status::Incomplete().
|
||||
// This enables single-read header parsing from a ~4 KiB buffer.
|
||||
Status DecodeFromBuffer(Slice* buffer, size_t* additional_bytes_needed);
|
||||
|
||||
// Raw property helpers (string values)
|
||||
void SetProperty(const std::string& name, const std::string& value);
|
||||
std::string GetProperty(const std::string& name) const;
|
||||
|
||||
// Typed property setters/getters for properties that aren't natively strings.
|
||||
void SetUint64Property(const std::string& name, uint64_t value);
|
||||
void SetUint32Property(const std::string& name, uint32_t value);
|
||||
bool GetUint64Property(const std::string& name, uint64_t* value) const;
|
||||
bool GetUint32Property(const std::string& name, uint32_t* value) const;
|
||||
bool HasProperty(const std::string& name) const;
|
||||
};
|
||||
|
||||
// --- Footer types ---
|
||||
|
||||
// Entry in a footer locator record. Offsets are in units of 4 bytes,
|
||||
// sequential relative to each other in reverse order of footer records.
|
||||
// First entry: offset from locator's escape_seq to the preceding footer
|
||||
// record's escape_seq. Subsequent entries: offset from the previous
|
||||
// entry's escape_seq to the next earlier footer record's escape_seq.
|
||||
// Verify that a blog file's trailing data contains a valid footer locator
|
||||
// record. Scans backward from the end of the buffer for the escape sequence,
|
||||
// then parses and verifies the full record (varint length, type, prefix
|
||||
// checksum, payload, trailer checksum). The buffer must start at a 4-byte-
|
||||
// aligned file offset. Returns true if a valid footer locator record is found.
|
||||
bool VerifyBlogFooterLocator(const char* buffer, size_t buffer_size,
|
||||
uint64_t buffer_file_offset,
|
||||
const char* expected_escape_seq,
|
||||
ChecksumType checksum_type,
|
||||
uint32_t incarnation_id);
|
||||
|
||||
struct BlogFooterLocatorEntry {
|
||||
BlogRecordType record_type;
|
||||
uint32_t relative_offset_4B;
|
||||
};
|
||||
|
||||
struct BlogFileFooterLocator {
|
||||
std::vector<BlogFooterLocatorEntry> entries;
|
||||
|
||||
// Encode as the payload of a footer locator record.
|
||||
void EncodeTo(std::string* dst) const;
|
||||
Status DecodeFrom(const Slice& input);
|
||||
};
|
||||
|
||||
struct BlogFileFooterProperties {
|
||||
BlogPropertyMap properties;
|
||||
|
||||
// Convenience setters that encode values into the property map.
|
||||
void SetBlobCount(uint64_t count);
|
||||
void SetTotalBlobBytes(uint64_t bytes);
|
||||
void SetSequenceRange(uint64_t min_seq, uint64_t max_seq);
|
||||
|
||||
// Encode as named properties (no 5-byte trailer; that comes from
|
||||
// the enclosing full record format).
|
||||
void EncodeTo(std::string* dst) const;
|
||||
Status DecodeFrom(const Slice& input);
|
||||
};
|
||||
|
||||
// --- Padding helpers ---
|
||||
|
||||
// Compute padding needed to reach the next 4-byte-aligned offset,
|
||||
// choosing the pad byte value to differ from last_meaningful_byte.
|
||||
// Appends padding bytes to dst.
|
||||
void ComputeBlogPadding(uint8_t last_meaningful_byte, size_t current_offset,
|
||||
std::string* dst);
|
||||
|
||||
// Same as above, but returns the pad byte value and count without
|
||||
// allocating. For use on hot paths.
|
||||
void ComputeBlogPaddingParams(uint8_t last_meaningful_byte,
|
||||
size_t current_offset, uint8_t* pad_byte,
|
||||
size_t* pad_count);
|
||||
|
||||
// --- Checksum helpers ---
|
||||
|
||||
// Compute a context-aware checksum over data + compression_type byte,
|
||||
// suitable for a blog record trailer. Uses the same algorithm and context
|
||||
// checksum approach as block-based SST files.
|
||||
uint32_t ComputeBlogRecordChecksum(ChecksumType type, const char* data,
|
||||
size_t data_size, char compression_type_byte,
|
||||
uint32_t base_context_checksum,
|
||||
uint64_t record_offset);
|
||||
|
||||
// Verify a blog record's 5-byte trailer (compression_type + checksum).
|
||||
// payload points to the record payload of payload_size bytes; the trailer
|
||||
// immediately follows. Returns OK if checksum matches, Corruption otherwise.
|
||||
// On success, *actual_compression_type is set from the trailer.
|
||||
Status VerifyBlogRecordTrailer(ChecksumType checksum_type, const char* payload,
|
||||
size_t payload_size, uint32_t incarnation_id,
|
||||
uint64_t payload_file_offset,
|
||||
CompressionType* actual_compression_type);
|
||||
|
||||
// Encode an irregular varint: a value encoded with more bytes than the
|
||||
// minimum required, ensuring varint length > 3 bytes to trigger full
|
||||
// record format. The value is encoded correctly and will decode to the
|
||||
// same value via standard GetVarint64.
|
||||
void PutBlogIrregularVarint64(std::string* dst, uint64_t value,
|
||||
size_t min_encoded_bytes);
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,539 @@
|
||||
// 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/blog/blog_format.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "table/format.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "util/coding.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class BlogFormatTest : public testing::Test {};
|
||||
|
||||
// --- Property encoding/decoding ---
|
||||
|
||||
TEST_F(BlogFormatTest, EncodeDecodePropertiesEmpty) {
|
||||
BlogPropertyMap props;
|
||||
std::string encoded;
|
||||
EncodeBlogProperties(props, &encoded);
|
||||
ASSERT_EQ(encoded.size(), 0u);
|
||||
|
||||
BlogPropertyMap decoded;
|
||||
Slice input(encoded);
|
||||
ASSERT_OK(DecodeBlogProperties(&input, &decoded));
|
||||
ASSERT_TRUE(decoded.empty());
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, EncodeDecodePropertiesMultiple) {
|
||||
BlogPropertyMap props = {
|
||||
{"CompressionCompatibilityName", "zstd"},
|
||||
{"role", "wal"},
|
||||
{"compressionSettings", "level=3"},
|
||||
};
|
||||
std::string encoded;
|
||||
EncodeBlogProperties(props, &encoded);
|
||||
ASSERT_GT(encoded.size(), 0u);
|
||||
|
||||
BlogPropertyMap decoded;
|
||||
Slice input(encoded);
|
||||
ASSERT_OK(DecodeBlogProperties(&input, &decoded));
|
||||
ASSERT_EQ(decoded.size(), 3u);
|
||||
ASSERT_EQ(decoded[0].first, "CompressionCompatibilityName");
|
||||
ASSERT_EQ(decoded[0].second, "zstd");
|
||||
ASSERT_EQ(decoded[1].first, "role");
|
||||
ASSERT_EQ(decoded[1].second, "wal");
|
||||
ASSERT_EQ(decoded[2].first, "compressionSettings");
|
||||
ASSERT_EQ(decoded[2].second, "level=3");
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, DecodePropertiesTruncated) {
|
||||
// Write a name but no value
|
||||
std::string encoded;
|
||||
PutLengthPrefixedSlice(&encoded, "key");
|
||||
// Truncate before value
|
||||
BlogPropertyMap decoded;
|
||||
Slice input(encoded);
|
||||
ASSERT_TRUE(DecodeBlogProperties(&input, &decoded).IsCorruption());
|
||||
}
|
||||
|
||||
// --- IsBlogRequiredProperty ---
|
||||
|
||||
TEST_F(BlogFormatTest, IsBlogRequiredProperty) {
|
||||
ASSERT_TRUE(IsBlogRequiredProperty("CompressionCompatibilityName"));
|
||||
ASSERT_TRUE(IsBlogRequiredProperty("TimestampSize"));
|
||||
ASSERT_TRUE(IsBlogRequiredProperty("A"));
|
||||
ASSERT_FALSE(IsBlogRequiredProperty("role"));
|
||||
ASSERT_FALSE(IsBlogRequiredProperty("compressionSettings"));
|
||||
ASSERT_FALSE(IsBlogRequiredProperty("a"));
|
||||
ASSERT_FALSE(IsBlogRequiredProperty(""));
|
||||
}
|
||||
|
||||
// --- Escape sequence generation ---
|
||||
|
||||
TEST_F(BlogFormatTest, GenerateRandomFieldsConstraints) {
|
||||
// Generate many escape sequences and verify constraints
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
BlogFileHeader header;
|
||||
header.GenerateRandomFields();
|
||||
|
||||
// First byte must not be 0x00 or 0xFF
|
||||
uint8_t first = static_cast<uint8_t>(header.escape_sequence[0]);
|
||||
ASSERT_NE(first, 0x00u) << "iteration " << i;
|
||||
ASSERT_NE(first, 0xFFu) << "iteration " << i;
|
||||
|
||||
// Derived bytes must match
|
||||
ASSERT_TRUE(BlogFileHeader::VerifyEscapeSequence(header.escape_sequence))
|
||||
<< "iteration " << i;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, IncarnationIdNonzero) {
|
||||
BlogFileHeader header;
|
||||
header.GenerateRandomFields();
|
||||
// incarnation_id is first 4 bytes little-endian; since byte[0] is not 0,
|
||||
// the incarnation_id must be nonzero.
|
||||
ASSERT_NE(header.incarnation_id(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, VerifyEscapeSequenceRejectsInvalid) {
|
||||
char seq[kBlogEscapeSequenceSize] = {};
|
||||
// All zeros: first byte is 0x00 -> invalid
|
||||
ASSERT_FALSE(BlogFileHeader::VerifyEscapeSequence(seq));
|
||||
|
||||
// Valid first byte but wrong derived part
|
||||
seq[0] = 0x42;
|
||||
seq[1] = 0x13;
|
||||
// Derived part is random garbage, likely won't match
|
||||
ASSERT_FALSE(BlogFileHeader::VerifyEscapeSequence(seq));
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, HeuristicEscapeSequenceDetection) {
|
||||
// Generate a valid escape sequence and verify we can detect it
|
||||
// heuristically (by checking derived bytes against random part).
|
||||
BlogFileHeader header;
|
||||
header.GenerateRandomFields();
|
||||
|
||||
// VerifyEscapeSequence does exactly this heuristic check
|
||||
ASSERT_TRUE(BlogFileHeader::VerifyEscapeSequence(header.escape_sequence));
|
||||
|
||||
// Corrupt one random byte -> should fail verification
|
||||
char corrupted[kBlogEscapeSequenceSize];
|
||||
memcpy(corrupted, header.escape_sequence, kBlogEscapeSequenceSize);
|
||||
corrupted[2] ^= 0x42;
|
||||
// Might still pass if the corruption happens to produce a valid derived
|
||||
// part, but astronomically unlikely
|
||||
// (We don't ASSERT_FALSE here because of the tiny probability)
|
||||
}
|
||||
|
||||
// --- BlogFileHeader encode/decode ---
|
||||
|
||||
TEST_F(BlogFormatTest, HeaderEncodeDecodeMinimal) {
|
||||
BlogFileHeader header;
|
||||
header.checksum_type = kXXH3;
|
||||
header.compact_record_type = kBlogWriteBatchRecord;
|
||||
header.GenerateRandomFields();
|
||||
|
||||
std::string encoded;
|
||||
header.EncodeTo(&encoded);
|
||||
ASSERT_GE(encoded.size(), kBlogFileFixedHeaderSize);
|
||||
|
||||
BlogFileHeader decoded;
|
||||
Slice input(encoded);
|
||||
ASSERT_OK(decoded.DecodeFrom(&input));
|
||||
|
||||
ASSERT_EQ(decoded.schema_version, header.schema_version);
|
||||
ASSERT_EQ(decoded.checksum_type, header.checksum_type);
|
||||
ASSERT_EQ(decoded.compact_record_type, header.compact_record_type);
|
||||
ASSERT_EQ(memcmp(decoded.escape_sequence, header.escape_sequence,
|
||||
kBlogEscapeSequenceSize),
|
||||
0);
|
||||
ASSERT_TRUE(decoded.properties.empty());
|
||||
ASSERT_EQ(input.size(), 0u); // all bytes consumed
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, HeaderEncodeDecodeWithProperties) {
|
||||
BlogFileHeader header;
|
||||
header.checksum_type = kCRC32c;
|
||||
header.compact_record_type = kBlogBlobRecord;
|
||||
header.GenerateRandomFields();
|
||||
header.SetProperty("CompressionCompatibilityName", "lz4");
|
||||
header.SetProperty("role", "blob");
|
||||
header.SetProperty("compressionSettings", "level=1");
|
||||
|
||||
std::string encoded;
|
||||
header.EncodeTo(&encoded);
|
||||
|
||||
BlogFileHeader decoded;
|
||||
Slice input(encoded);
|
||||
ASSERT_OK(decoded.DecodeFrom(&input));
|
||||
|
||||
ASSERT_EQ(decoded.GetProperty("CompressionCompatibilityName"), "lz4");
|
||||
ASSERT_EQ(decoded.GetProperty("role"), "blob");
|
||||
ASSERT_EQ(decoded.GetProperty("compressionSettings"), "level=1");
|
||||
ASSERT_EQ(input.size(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, HeaderRejectsUnknownRequiredProperty) {
|
||||
BlogFileHeader header;
|
||||
header.GenerateRandomFields();
|
||||
header.SetProperty("FutureRequired", "something");
|
||||
|
||||
std::string encoded;
|
||||
header.EncodeTo(&encoded);
|
||||
|
||||
BlogFileHeader decoded;
|
||||
Slice input(encoded);
|
||||
Status s = decoded.DecodeFrom(&input);
|
||||
ASSERT_TRUE(s.IsNotSupported()) << s.ToString();
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, HeaderRejectsNewerSchemaVersion) {
|
||||
BlogFileHeader header;
|
||||
header.GenerateRandomFields();
|
||||
|
||||
std::string encoded;
|
||||
header.EncodeTo(&encoded);
|
||||
|
||||
// Tamper with schema version byte (offset 12) to a future version.
|
||||
// Must also recompute the fixed header checksum (bytes [36-39]) with
|
||||
// context modifier using the incarnation_id.
|
||||
encoded[12] = static_cast<char>(kBlogCurrentSchemaVersion + 1);
|
||||
uint32_t new_checksum =
|
||||
ComputeBuiltinChecksum(header.checksum_type, encoded.data(), 36) +
|
||||
ChecksumModifierForContext(header.incarnation_id(), 0);
|
||||
EncodeFixed32(&encoded[36], new_checksum);
|
||||
|
||||
BlogFileHeader decoded;
|
||||
Slice input(encoded);
|
||||
Status s = decoded.DecodeFrom(&input);
|
||||
ASSERT_TRUE(s.IsNotSupported()) << s.ToString();
|
||||
ASSERT_NE(s.ToString().find("newer than supported"), std::string::npos)
|
||||
<< s.ToString();
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, HeaderCurrentSchemaVersionAccepted) {
|
||||
BlogFileHeader header;
|
||||
header.GenerateRandomFields();
|
||||
ASSERT_EQ(header.schema_version, kBlogCurrentSchemaVersion);
|
||||
|
||||
std::string encoded;
|
||||
header.EncodeTo(&encoded);
|
||||
|
||||
BlogFileHeader decoded;
|
||||
Slice input(encoded);
|
||||
ASSERT_OK(decoded.DecodeFrom(&input));
|
||||
ASSERT_EQ(decoded.schema_version, kBlogCurrentSchemaVersion);
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, TypedPropertyRoundTrip) {
|
||||
BlogFileHeader header;
|
||||
header.GenerateRandomFields();
|
||||
header.SetUint64Property("fileNumber", 12345678ULL);
|
||||
header.SetUint32Property("columnFamilyId", 42);
|
||||
|
||||
std::string encoded;
|
||||
header.EncodeTo(&encoded);
|
||||
|
||||
BlogFileHeader decoded;
|
||||
Slice input(encoded);
|
||||
ASSERT_OK(decoded.DecodeFrom(&input));
|
||||
|
||||
uint64_t fn = 0;
|
||||
ASSERT_TRUE(decoded.GetUint64Property("fileNumber", &fn));
|
||||
ASSERT_EQ(fn, 12345678ULL);
|
||||
|
||||
uint32_t cf_id = 0;
|
||||
ASSERT_TRUE(decoded.GetUint32Property("columnFamilyId", &cf_id));
|
||||
ASSERT_EQ(cf_id, 42u);
|
||||
|
||||
// Missing property returns false
|
||||
uint64_t missing = 0;
|
||||
ASSERT_FALSE(decoded.GetUint64Property("nonexistent", &missing));
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, HeaderAcceptsUnknownIgnorableProperty) {
|
||||
BlogFileHeader header;
|
||||
header.GenerateRandomFields();
|
||||
header.SetProperty("futureIgnorable", "something");
|
||||
|
||||
std::string encoded;
|
||||
header.EncodeTo(&encoded);
|
||||
|
||||
BlogFileHeader decoded;
|
||||
Slice input(encoded);
|
||||
ASSERT_OK(decoded.DecodeFrom(&input));
|
||||
ASSERT_EQ(decoded.GetProperty("futureIgnorable"), "something");
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, HeaderChecksumCorruption) {
|
||||
BlogFileHeader header;
|
||||
header.GenerateRandomFields();
|
||||
|
||||
std::string encoded;
|
||||
header.EncodeTo(&encoded);
|
||||
|
||||
// Corrupt a byte in the fixed header
|
||||
encoded[14] ^= 0xFF;
|
||||
|
||||
BlogFileHeader decoded;
|
||||
Slice input(encoded);
|
||||
ASSERT_TRUE(decoded.DecodeFrom(&input).IsCorruption());
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, HeaderPropertyChecksumCorruption) {
|
||||
BlogFileHeader header;
|
||||
header.GenerateRandomFields();
|
||||
header.SetProperty("role", "wal");
|
||||
|
||||
std::string encoded;
|
||||
header.EncodeTo(&encoded);
|
||||
|
||||
// Corrupt a byte in the property section (after fixed header)
|
||||
if (encoded.size() > kBlogFileFixedHeaderSize + 2) {
|
||||
encoded[kBlogFileFixedHeaderSize + 2] ^= 0xFF;
|
||||
}
|
||||
|
||||
BlogFileHeader decoded;
|
||||
Slice input(encoded);
|
||||
Status s = decoded.DecodeFrom(&input);
|
||||
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, HeaderSetPropertyOverwrites) {
|
||||
BlogFileHeader header;
|
||||
header.SetProperty("role", "wal");
|
||||
ASSERT_EQ(header.GetProperty("role"), "wal");
|
||||
|
||||
header.SetProperty("role", "blob");
|
||||
ASSERT_EQ(header.GetProperty("role"), "blob");
|
||||
// Should not have duplicate entries
|
||||
int count = 0;
|
||||
for (const auto& [k, v] : header.properties) {
|
||||
if (k == "role") {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(count, 1);
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, HeaderMagicValues) {
|
||||
BlogFileHeader header;
|
||||
header.GenerateRandomFields();
|
||||
|
||||
std::string encoded;
|
||||
header.EncodeTo(&encoded);
|
||||
|
||||
// Verify magic bytes
|
||||
ASSERT_GE(encoded.size(), kBlogFileMagicSize);
|
||||
ASSERT_EQ(memcmp(encoded.data(), kBlogFileMagic, kBlogFileMagicSize), 0);
|
||||
}
|
||||
|
||||
// --- Footer locator ---
|
||||
|
||||
TEST_F(BlogFormatTest, FooterLocatorEncodeDecodeEmpty) {
|
||||
BlogFileFooterLocator locator;
|
||||
|
||||
std::string encoded;
|
||||
locator.EncodeTo(&encoded);
|
||||
ASSERT_EQ(encoded.size(), 0u);
|
||||
|
||||
BlogFileFooterLocator decoded;
|
||||
ASSERT_OK(decoded.DecodeFrom(Slice(encoded)));
|
||||
ASSERT_TRUE(decoded.entries.empty());
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, FooterLocatorEncodeDecodeMultiple) {
|
||||
BlogFileFooterLocator locator;
|
||||
locator.entries.push_back({kBlogFooterPropertiesRecord, 50});
|
||||
locator.entries.push_back({kBlogFooterIndexRecord, 85});
|
||||
|
||||
std::string encoded;
|
||||
locator.EncodeTo(&encoded);
|
||||
ASSERT_EQ(encoded.size(), 10u); // 2 * (1 + 4)
|
||||
|
||||
BlogFileFooterLocator decoded;
|
||||
ASSERT_OK(decoded.DecodeFrom(Slice(encoded)));
|
||||
ASSERT_EQ(decoded.entries.size(), 2u);
|
||||
ASSERT_EQ(decoded.entries[0].record_type, kBlogFooterPropertiesRecord);
|
||||
ASSERT_EQ(decoded.entries[0].relative_offset_4B, 50u);
|
||||
ASSERT_EQ(decoded.entries[1].record_type, kBlogFooterIndexRecord);
|
||||
ASSERT_EQ(decoded.entries[1].relative_offset_4B, 85u);
|
||||
}
|
||||
|
||||
// --- Footer properties ---
|
||||
|
||||
TEST_F(BlogFormatTest, FooterPropertiesEncodeDecodeRoundTrip) {
|
||||
BlogFileFooterProperties props;
|
||||
props.SetBlobCount(42);
|
||||
props.SetTotalBlobBytes(123456);
|
||||
props.SetSequenceRange(100, 200);
|
||||
|
||||
std::string encoded;
|
||||
props.EncodeTo(&encoded);
|
||||
|
||||
BlogFileFooterProperties decoded;
|
||||
ASSERT_OK(decoded.DecodeFrom(Slice(encoded)));
|
||||
ASSERT_EQ(decoded.properties.size(), 4u);
|
||||
}
|
||||
|
||||
// --- Padding ---
|
||||
|
||||
TEST_F(BlogFormatTest, PaddingLastByteZero) {
|
||||
// When last byte is 0x00, must pad with 0xFF, at least 1 byte
|
||||
std::string pad;
|
||||
// At offset 0 (already aligned), still need at least 1 byte
|
||||
ComputeBlogPadding(0x00, 0, &pad);
|
||||
ASSERT_GE(pad.size(), 1u);
|
||||
for (char c : pad) {
|
||||
ASSERT_EQ(static_cast<uint8_t>(c), 0xFFu);
|
||||
}
|
||||
// Result must be 4-byte aligned
|
||||
ASSERT_EQ(pad.size() % 4, 0u);
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, PaddingLastByteFF) {
|
||||
// When last byte is 0xFF, must pad with 0x00, at least 1 byte
|
||||
std::string pad;
|
||||
ComputeBlogPadding(0xFF, 0, &pad);
|
||||
ASSERT_GE(pad.size(), 1u);
|
||||
for (char c : pad) {
|
||||
ASSERT_EQ(static_cast<uint8_t>(c), 0x00u);
|
||||
}
|
||||
ASSERT_EQ(pad.size() % 4, 0u);
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, PaddingLastByteOther) {
|
||||
// When last byte is something else, pad only if needed for alignment
|
||||
std::string pad;
|
||||
ComputeBlogPadding(0x42, 0, &pad);
|
||||
// Already aligned, no padding needed
|
||||
ASSERT_EQ(pad.size(), 0u);
|
||||
|
||||
pad.clear();
|
||||
ComputeBlogPadding(0x42, 1, &pad);
|
||||
// Need 3 bytes to reach alignment
|
||||
ASSERT_EQ(pad.size(), 3u);
|
||||
for (char c : pad) {
|
||||
ASSERT_EQ(static_cast<uint8_t>(c), 0x00u);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, PaddingAlignment) {
|
||||
for (size_t offset = 0; offset < 8; ++offset) {
|
||||
std::string pad;
|
||||
ComputeBlogPadding(0x42, offset, &pad);
|
||||
size_t after = offset + pad.size();
|
||||
ASSERT_EQ(after % 4, 0u) << "offset=" << offset;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Irregular varint ---
|
||||
|
||||
TEST_F(BlogFormatTest, IrregularVarintZero) {
|
||||
std::string encoded;
|
||||
PutBlogIrregularVarint64(&encoded, 0, 4);
|
||||
ASSERT_GE(encoded.size(), 4u);
|
||||
|
||||
// Must decode back to 0
|
||||
Slice input(encoded);
|
||||
uint64_t value;
|
||||
ASSERT_TRUE(GetVarint64(&input, &value));
|
||||
ASSERT_EQ(value, 0u);
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, IrregularVarintSmallValue) {
|
||||
std::string encoded;
|
||||
PutBlogIrregularVarint64(&encoded, 42, 4);
|
||||
ASSERT_GE(encoded.size(), 4u);
|
||||
|
||||
Slice input(encoded);
|
||||
uint64_t value;
|
||||
ASSERT_TRUE(GetVarint64(&input, &value));
|
||||
ASSERT_EQ(value, 42u);
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, IrregularVarintLargeValue) {
|
||||
// A value that naturally needs 3 bytes, padded to 5
|
||||
uint64_t original = 100000;
|
||||
std::string encoded;
|
||||
PutBlogIrregularVarint64(&encoded, original, 5);
|
||||
ASSERT_GE(encoded.size(), 5u);
|
||||
|
||||
Slice input(encoded);
|
||||
uint64_t value;
|
||||
ASSERT_TRUE(GetVarint64(&input, &value));
|
||||
ASSERT_EQ(value, original);
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, IrregularVarintNaturallyLargeEnough) {
|
||||
// A value that naturally needs 5+ bytes should not be padded further
|
||||
uint64_t original = 1ULL << 35;
|
||||
std::string encoded;
|
||||
PutBlogIrregularVarint64(&encoded, original, 4);
|
||||
// Natural encoding is 6 bytes, already >= 4
|
||||
ASSERT_EQ(encoded.size(), 6u);
|
||||
|
||||
Slice input(encoded);
|
||||
uint64_t value;
|
||||
ASSERT_TRUE(GetVarint64(&input, &value));
|
||||
ASSERT_EQ(value, original);
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, UnspecifiedSizeVarint) {
|
||||
std::string encoded;
|
||||
PutVarint64(&encoded, kBlogUnspecifiedSize);
|
||||
// 2^63 - 1 should encode to 9 bytes (63 bits / 7 = 9)
|
||||
ASSERT_EQ(encoded.size(), 9u);
|
||||
|
||||
Slice input(encoded);
|
||||
uint64_t value;
|
||||
ASSERT_TRUE(GetVarint64(&input, &value));
|
||||
ASSERT_EQ(value, kBlogUnspecifiedSize);
|
||||
}
|
||||
|
||||
// --- Checksum ---
|
||||
|
||||
TEST_F(BlogFormatTest, ComputeBlogRecordChecksumBasic) {
|
||||
const char* data = "hello world";
|
||||
size_t size = strlen(data);
|
||||
|
||||
// With no context (incarnation_id = 0), should match standard checksum
|
||||
uint32_t cksum = ComputeBlogRecordChecksum(
|
||||
kXXH3, data, size, static_cast<char>(kNoCompression), 0, 0);
|
||||
uint32_t expected = ComputeBuiltinChecksumWithLastByte(
|
||||
kXXH3, data, size, static_cast<char>(kNoCompression));
|
||||
ASSERT_EQ(cksum, expected);
|
||||
}
|
||||
|
||||
TEST_F(BlogFormatTest, ComputeBlogRecordChecksumWithContext) {
|
||||
const char* data = "hello world";
|
||||
size_t size = strlen(data);
|
||||
|
||||
uint32_t cksum1 = ComputeBlogRecordChecksum(
|
||||
kXXH3, data, size, static_cast<char>(kNoCompression), 0x12345678, 100);
|
||||
uint32_t cksum2 = ComputeBlogRecordChecksum(
|
||||
kXXH3, data, size, static_cast<char>(kNoCompression), 0x12345678, 200);
|
||||
|
||||
// Different offsets should produce different checksums
|
||||
ASSERT_NE(cksum1, cksum2);
|
||||
|
||||
// Different incarnation IDs should produce different checksums
|
||||
uint32_t cksum3 = ComputeBlogRecordChecksum(
|
||||
kXXH3, data, size, static_cast<char>(kNoCompression), 0xABCDEF01, 100);
|
||||
ASSERT_NE(cksum1, cksum3);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
// 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/blog/blog_reader.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
|
||||
#include "table/format.h"
|
||||
#include "util/coding.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
BlogFileReader::BlogFileReader(std::unique_ptr<SequentialFileReader>&& file,
|
||||
Reporter* reporter, bool verify_checksums)
|
||||
: seq_file_(std::move(file)),
|
||||
reporter_(reporter),
|
||||
verify_checksums_(verify_checksums) {}
|
||||
|
||||
Status BlogFileReader::ReadHeader(BlogFileHeader* header) {
|
||||
assert(!header_read_);
|
||||
|
||||
// Read the fixed header to get property_section_size
|
||||
std::string fixed_buf;
|
||||
Slice fixed_slice;
|
||||
Status s = ReadBytes(kBlogFileFixedHeaderSize, &fixed_slice, &fixed_buf);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// Peek at property_section_size from bytes [32-35]
|
||||
uint32_t prop_section_size = DecodeFixed32(fixed_slice.data() + 32);
|
||||
|
||||
// Read the property section
|
||||
std::string prop_buf;
|
||||
Slice prop_slice;
|
||||
s = ReadBytes(prop_section_size, &prop_slice, &prop_buf);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// Combine into a single buffer for decoding
|
||||
std::string full_header;
|
||||
full_header.append(fixed_slice.data(), fixed_slice.size());
|
||||
full_header.append(prop_slice.data(), prop_slice.size());
|
||||
|
||||
Slice input(full_header);
|
||||
s = header->DecodeFrom(&input);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
header_ = *header;
|
||||
header_read_ = true;
|
||||
|
||||
// Skip padding after header to reach the first record's escape sequence.
|
||||
// Padding may be arbitrarily long (for stronger alignment or recycled
|
||||
// files of all zeros). EOF here is fine (header-only file, no records).
|
||||
s = SkipPadding();
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
return s;
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlogFileReader::ReadRecord(BlogRecordType* type, Slice* payload,
|
||||
std::string* scratch, uint64_t* record_offset,
|
||||
CompressionType* record_compression_type) {
|
||||
if (!header_read_) {
|
||||
return Status::Corruption("Blog reader: header not read");
|
||||
}
|
||||
if (eof_) {
|
||||
return Status::NotFound();
|
||||
}
|
||||
|
||||
Status s;
|
||||
scratch->clear();
|
||||
|
||||
// Read escape sequence. If we have a pending prefix from header padding
|
||||
// skip or inter-record padding skip, use it as the first 4 bytes.
|
||||
std::string esc_buf;
|
||||
if (!pending_escape_prefix_.empty()) {
|
||||
esc_buf = std::move(pending_escape_prefix_);
|
||||
pending_escape_prefix_.clear();
|
||||
Slice rest_slice;
|
||||
std::string rest_buf;
|
||||
s = ReadBytes(kBlogEscapeSequenceSize - 4, &rest_slice, &rest_buf);
|
||||
if (!s.ok()) {
|
||||
eof_ = true;
|
||||
// Short read after we had a pending prefix = incomplete trailing record
|
||||
return Status::Incomplete("Blog reader: incomplete escape sequence");
|
||||
}
|
||||
esc_buf.append(rest_slice.data(), rest_slice.size());
|
||||
} else {
|
||||
Slice esc_slice;
|
||||
s = ReadBytes(kBlogEscapeSequenceSize, &esc_slice, &esc_buf);
|
||||
if (!s.ok()) {
|
||||
eof_ = true;
|
||||
return Status::NotFound(); // clean EOF
|
||||
}
|
||||
if (esc_slice.data() != esc_buf.data()) {
|
||||
esc_buf.assign(esc_slice.data(), esc_slice.size());
|
||||
}
|
||||
}
|
||||
|
||||
// Verify escape sequence matches
|
||||
if (memcmp(esc_buf.data(), header_.escape_sequence,
|
||||
kBlogEscapeSequenceSize) != 0) {
|
||||
return ReportCorruption(kBlogEscapeSequenceSize,
|
||||
"escape sequence mismatch");
|
||||
}
|
||||
|
||||
uint64_t rec_offset = offset_ - kBlogEscapeSequenceSize;
|
||||
if (record_offset) {
|
||||
*record_offset = rec_offset;
|
||||
}
|
||||
last_record_offset_ = rec_offset;
|
||||
|
||||
// Read varint length. scratch receives the raw varint bytes for use
|
||||
// in prefix checksum verification (avoids fragile re-encoding).
|
||||
uint64_t length = 0;
|
||||
s = ReadVarint64(&length, scratch);
|
||||
if (!s.ok()) {
|
||||
eof_ = true;
|
||||
return Status::Incomplete("Blog reader: incomplete varint");
|
||||
}
|
||||
size_t varint_len = scratch->size();
|
||||
|
||||
bool is_full_format = (varint_len > kBlogCompactVarintMaxBytes);
|
||||
|
||||
BlogRecordType rec_type;
|
||||
std::string prefix_rest_buf;
|
||||
|
||||
if (is_full_format) {
|
||||
// Full format: read type + compression_type + prefix_checksum
|
||||
Slice prefix_rest_slice;
|
||||
s = ReadBytes(6, &prefix_rest_slice, &prefix_rest_buf); // 1+1+4
|
||||
if (!s.ok()) {
|
||||
eof_ = true;
|
||||
return Status::Incomplete("Blog reader: incomplete full record prefix");
|
||||
}
|
||||
|
||||
rec_type =
|
||||
static_cast<BlogRecordType>(static_cast<uint8_t>(prefix_rest_slice[0]));
|
||||
|
||||
if (verify_checksums_) {
|
||||
// Build the prefix data from the original varint bytes (in scratch)
|
||||
// plus type and compression_type bytes.
|
||||
scratch->push_back(prefix_rest_slice[0]); // type
|
||||
scratch->push_back(prefix_rest_slice[1]); // comp_type
|
||||
|
||||
uint32_t stored_prefix_checksum =
|
||||
DecodeFixed32(prefix_rest_slice.data() + 2);
|
||||
uint32_t computed_prefix_checksum =
|
||||
ComputeBuiltinChecksum(header_.checksum_type, scratch->data(),
|
||||
scratch->size()) +
|
||||
ChecksumModifierForContext(header_.incarnation_id(), rec_offset);
|
||||
if (stored_prefix_checksum != computed_prefix_checksum) {
|
||||
return ReportCorruption(varint_len + 6, "prefix checksum mismatch");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rec_type = header_.compact_record_type;
|
||||
}
|
||||
|
||||
*type = rec_type;
|
||||
|
||||
if (length == 0) {
|
||||
payload->clear();
|
||||
if (record_compression_type) {
|
||||
*record_compression_type = kNoCompression;
|
||||
}
|
||||
s = SkipPadding();
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
return s;
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
if (length == kBlogUnspecifiedSize) {
|
||||
return ReportCorruption(0, "unspecified-size records not yet implemented");
|
||||
}
|
||||
|
||||
// Read payload
|
||||
scratch->clear();
|
||||
Slice payload_slice;
|
||||
s = ReadBytes(static_cast<size_t>(length), &payload_slice, scratch);
|
||||
if (!s.ok()) {
|
||||
eof_ = true;
|
||||
return Status::Incomplete("Blog reader: incomplete payload");
|
||||
}
|
||||
if (payload_slice.data() != scratch->data()) {
|
||||
scratch->assign(payload_slice.data(), payload_slice.size());
|
||||
}
|
||||
|
||||
// Read 5-byte trailer
|
||||
Slice trailer_slice;
|
||||
std::string trailer_buf;
|
||||
s = ReadBytes(kBlogBlockTrailerSize, &trailer_slice, &trailer_buf);
|
||||
if (!s.ok()) {
|
||||
eof_ = true;
|
||||
return Status::Incomplete("Blog reader: incomplete trailer");
|
||||
}
|
||||
|
||||
char trailer_comp_type = trailer_slice[0];
|
||||
uint32_t stored_checksum = DecodeFixed32(trailer_slice.data() + 1);
|
||||
|
||||
if (verify_checksums_) {
|
||||
uint32_t computed_checksum = ComputeBlogRecordChecksum(
|
||||
header_.checksum_type, scratch->data(), scratch->size(),
|
||||
trailer_comp_type, header_.incarnation_id(),
|
||||
offset_ - kBlogBlockTrailerSize - length);
|
||||
if (stored_checksum != computed_checksum) {
|
||||
return ReportCorruption(
|
||||
static_cast<size_t>(length) + kBlogBlockTrailerSize,
|
||||
"record checksum mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
*payload = Slice(*scratch);
|
||||
if (record_compression_type) {
|
||||
*record_compression_type =
|
||||
static_cast<CompressionType>(static_cast<uint8_t>(trailer_comp_type));
|
||||
}
|
||||
|
||||
// Skip padding to next record.
|
||||
s = SkipPadding();
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlogFileReader::ScanForEscapeSequence(uint64_t* found_offset) {
|
||||
// Scan forward 4 bytes at a time (escape sequences are 32-bit aligned).
|
||||
// Check first 4 bytes of escape sequence, then verify remaining 6 bytes.
|
||||
uint32_t first4;
|
||||
memcpy(&first4, header_.escape_sequence, 4);
|
||||
|
||||
std::string buf;
|
||||
// Align to 4-byte boundary first
|
||||
size_t remainder = offset_ % 4;
|
||||
if (remainder != 0) {
|
||||
size_t skip = 4 - remainder;
|
||||
Slice skip_slice;
|
||||
Status s = ReadBytes(skip, &skip_slice, &buf);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
while (!eof_) {
|
||||
Slice chunk_slice;
|
||||
Status s = ReadBytes(4, &chunk_slice, &buf);
|
||||
if (!s.ok()) {
|
||||
if (eof_) {
|
||||
return Status::NotFound("escape sequence not found before EOF");
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
uint32_t candidate;
|
||||
memcpy(&candidate, chunk_slice.data(), 4);
|
||||
if (candidate == first4) {
|
||||
// Potential match - verify remaining 6 bytes
|
||||
uint64_t candidate_offset = offset_ - 4;
|
||||
Slice rest_slice;
|
||||
std::string rest_buf;
|
||||
s = ReadBytes(kBlogEscapeSequenceSize - 4, &rest_slice, &rest_buf);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (memcmp(rest_slice.data(), header_.escape_sequence + 4,
|
||||
kBlogEscapeSequenceSize - 4) == 0) {
|
||||
*found_offset = candidate_offset;
|
||||
return Status::OK();
|
||||
}
|
||||
// False match, continue scanning. But we consumed 6 extra bytes that
|
||||
// might not be aligned. Re-align.
|
||||
remainder = offset_ % 4;
|
||||
if (remainder != 0) {
|
||||
size_t skip = 4 - remainder;
|
||||
Slice skip_s;
|
||||
s = ReadBytes(skip, &skip_s, &buf);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Status::NotFound("escape sequence not found before EOF");
|
||||
}
|
||||
|
||||
Status BlogFileReader::SkipPadding() {
|
||||
// Skip arbitrarily long padding (0x00 or 0xFF bytes). Padding is
|
||||
// 32-bit aligned. Since the escape sequence's first byte is guaranteed
|
||||
// to be neither 0x00 nor 0xFF, we align to 4 bytes then scan 4-byte
|
||||
// chunks until we find one that starts with a non-padding byte.
|
||||
size_t remainder = offset_ % 4;
|
||||
if (remainder != 0) {
|
||||
Slice skip_slice;
|
||||
std::string skip_buf;
|
||||
Status s = ReadBytes(4 - remainder, &skip_slice, &skip_buf);
|
||||
if (!s.ok()) {
|
||||
if (eof_) {
|
||||
return Status::NotFound();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
while (true) {
|
||||
Slice chunk_slice;
|
||||
std::string chunk_buf;
|
||||
Status s = ReadBytes(4, &chunk_slice, &chunk_buf);
|
||||
if (!s.ok()) {
|
||||
if (eof_) {
|
||||
return Status::NotFound();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
uint8_t first = static_cast<uint8_t>(chunk_slice[0]);
|
||||
if (first != 0x00 && first != 0xFF) {
|
||||
// Found the start of the next escape sequence.
|
||||
pending_escape_prefix_.assign(chunk_slice.data(), 4);
|
||||
return Status::OK();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Status BlogFileReader::ReadBytes(size_t n, Slice* result,
|
||||
std::string* scratch) {
|
||||
scratch->resize(n);
|
||||
Slice read_result;
|
||||
IOStatus ios =
|
||||
seq_file_->Read(n, &read_result, scratch->data(), Env::IO_TOTAL);
|
||||
if (!ios.ok()) {
|
||||
return static_cast<Status>(ios);
|
||||
}
|
||||
if (read_result.size() < n) {
|
||||
eof_ = true;
|
||||
// Return Incomplete -- the caller (ReadRecord) determines whether
|
||||
// this is an acceptable incomplete tail vs. genuine corruption.
|
||||
return Status::Incomplete("Blog reader", "unexpected EOF");
|
||||
}
|
||||
offset_ += n;
|
||||
*result = Slice(scratch->data(), n);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
// FIXME? Consider a better encoding that could be processed more efficiently
|
||||
Status BlogFileReader::ReadVarint64(uint64_t* value, std::string* scratch) {
|
||||
// Varints can be up to 10 bytes. Read one byte at a time.
|
||||
// The raw varint bytes are appended to scratch so the caller can use
|
||||
// them for checksum verification without re-encoding.
|
||||
scratch->clear();
|
||||
std::string byte_buf;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
Slice byte_slice;
|
||||
Status s = ReadBytes(1, &byte_slice, &byte_buf);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
scratch->push_back(byte_slice[0]);
|
||||
// Check if this is the terminal byte (high bit not set)
|
||||
if ((static_cast<uint8_t>(byte_slice[0]) & 0x80) == 0) {
|
||||
Slice varint_slice(*scratch);
|
||||
if (!GetVarint64(&varint_slice, value)) {
|
||||
return ReportCorruption(scratch->size(), "invalid varint");
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
}
|
||||
return ReportCorruption(10, "varint too long");
|
||||
}
|
||||
|
||||
Status BlogFileReader::ReportCorruption(size_t bytes, const std::string& msg) {
|
||||
Status s = Status::Corruption("Blog reader", msg);
|
||||
if (reporter_) {
|
||||
reporter_->Corruption(bytes, s);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,101 @@
|
||||
// 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 <memory>
|
||||
#include <string>
|
||||
|
||||
#include "db/blog/blog_format.h"
|
||||
#include "file/random_access_file_reader.h"
|
||||
#include "file/sequence_file_reader.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// Reads blog-format files sequentially (WAL replay) or at random offsets
|
||||
// (blob lookups). Supports footer reading by backward scanning.
|
||||
class BlogFileReader {
|
||||
public:
|
||||
// Error reporter, same pattern as log::Reader::Reporter.
|
||||
class Reporter {
|
||||
public:
|
||||
virtual ~Reporter() = default;
|
||||
virtual void Corruption(size_t bytes, const Status& status) = 0;
|
||||
};
|
||||
|
||||
// Construct for sequential reading (WAL replay, forward scan).
|
||||
BlogFileReader(std::unique_ptr<SequentialFileReader>&& file,
|
||||
Reporter* reporter, bool verify_checksums);
|
||||
|
||||
~BlogFileReader() = default;
|
||||
|
||||
// Take ownership of a reporter (e.g., an adapter wrapping another type).
|
||||
void SetOwnedReporter(std::unique_ptr<Reporter> reporter) {
|
||||
owned_reporter_ = std::move(reporter);
|
||||
reporter_ = owned_reporter_.get();
|
||||
}
|
||||
|
||||
// Read and validate the file header. Must be called first.
|
||||
Status ReadHeader(BlogFileHeader* header);
|
||||
|
||||
// Read the next record sequentially. Returns:
|
||||
// OK - record read successfully
|
||||
// NotFound - clean EOF (no more records, only padding/zeros remain)
|
||||
// Incomplete - partial record at tail (crash mid-write)
|
||||
// Corruption - data integrity error
|
||||
// On OK, *type is the record type and *payload contains the payload.
|
||||
// *scratch is used as backing storage for payload.
|
||||
Status ReadRecord(BlogRecordType* type, Slice* payload, std::string* scratch,
|
||||
uint64_t* record_offset = nullptr,
|
||||
CompressionType* record_compression_type = nullptr);
|
||||
|
||||
// Scan forward from the current position for the next occurrence of
|
||||
// the file's escape sequence. Used for WAL recovery and unspecified-
|
||||
// size record parsing.
|
||||
// On success, *found_offset is the file offset of the escape sequence.
|
||||
Status ScanForEscapeSequence(uint64_t* found_offset);
|
||||
|
||||
bool IsEOF() const { return eof_; }
|
||||
uint64_t LastRecordOffset() const { return last_record_offset_; }
|
||||
uint64_t current_offset() const { return offset_; }
|
||||
const BlogFileHeader& header() const { return header_; }
|
||||
|
||||
private:
|
||||
// Skip arbitrarily long padding (0x00 or 0xFF bytes) after a record or
|
||||
// header. Aligns to 4-byte boundary first, then scans 4-byte chunks
|
||||
// until finding one whose first byte is not 0x00/0xFF. Stashes that
|
||||
// chunk in pending_escape_prefix_ for the next ReadRecord call.
|
||||
// Returns OK on success, NotFound on clean EOF (no more records),
|
||||
// or an error Status on I/O failure.
|
||||
Status SkipPadding();
|
||||
|
||||
// Read exactly n bytes sequentially. Returns Corruption on short read.
|
||||
Status ReadBytes(size_t n, Slice* result, std::string* scratch);
|
||||
|
||||
// Read a varint64 from the sequential stream.
|
||||
Status ReadVarint64(uint64_t* value, std::string* scratch);
|
||||
|
||||
// Report corruption and return a Corruption status.
|
||||
Status ReportCorruption(size_t bytes, const std::string& msg);
|
||||
|
||||
std::unique_ptr<SequentialFileReader> seq_file_;
|
||||
Reporter* reporter_;
|
||||
std::unique_ptr<Reporter> owned_reporter_; // optional, for adapter lifetime
|
||||
bool verify_checksums_;
|
||||
BlogFileHeader header_;
|
||||
bool header_read_ = false;
|
||||
bool eof_ = false;
|
||||
uint64_t offset_ = 0;
|
||||
uint64_t last_record_offset_ = 0;
|
||||
|
||||
// First 4 bytes of escape sequence consumed during header padding skip.
|
||||
// ReadRecord uses these as the beginning of the first escape sequence.
|
||||
std::string pending_escape_prefix_;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,320 @@
|
||||
// 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/blog/blog_writer.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "table/format.h"
|
||||
#include "util/coding.h"
|
||||
#include "util/string_util.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
BlogFileWriter::BlogFileWriter(std::unique_ptr<WritableFileWriter>&& dest,
|
||||
const BlogFileHeader& header, bool manual_flush)
|
||||
: dest_(std::move(dest)), header_(header), manual_flush_(manual_flush) {}
|
||||
|
||||
BlogFileWriter::~BlogFileWriter() = default;
|
||||
|
||||
IOStatus BlogFileWriter::WriteHeader(const WriteOptions& wo) {
|
||||
assert(!header_written_);
|
||||
std::string buf;
|
||||
header_.EncodeTo(&buf);
|
||||
// Pad after header properties using the same padding scheme as records.
|
||||
uint8_t last_byte = static_cast<uint8_t>(buf.back());
|
||||
ComputeBlogPadding(last_byte, buf.size(), &buf);
|
||||
IOStatus s = EmitBytes(wo, buf);
|
||||
if (s.ok()) {
|
||||
header_written_ = true;
|
||||
}
|
||||
if (s.ok() && !manual_flush_) {
|
||||
IOOptions opts;
|
||||
s = WritableFileWriter::PrepareIOOptions(wo, opts);
|
||||
if (s.ok()) {
|
||||
s = dest_->Flush(opts);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
IOStatus BlogFileWriter::AddBlobRecord(const WriteOptions& wo,
|
||||
const Slice& payload,
|
||||
CompressionType comp_type,
|
||||
uint64_t* blob_offset) {
|
||||
return AddRecord(wo, kBlogBlobRecord, payload, comp_type, blob_offset);
|
||||
}
|
||||
|
||||
IOStatus BlogFileWriter::AddWriteBatchRecord(const WriteOptions& wo,
|
||||
const Slice& wb_data,
|
||||
CompressionType comp_type) {
|
||||
// Stub: WriteBatch record writing is not yet integrated (WAL integration
|
||||
// deferred). The record format is the same as blob records with type 0x03.
|
||||
return AddRecord(wo, kBlogWriteBatchRecord, wb_data, comp_type, nullptr);
|
||||
}
|
||||
|
||||
IOStatus BlogFileWriter::AddPreambleStartRecord(const WriteOptions& wo) {
|
||||
return AddRecord(wo, kBlogPreambleStartRecord, Slice(), kNoCompression,
|
||||
nullptr, /*force_full=*/true);
|
||||
}
|
||||
|
||||
IOStatus BlogFileWriter::AddFooterIndexRecord(const WriteOptions& wo,
|
||||
const Slice& index_data) {
|
||||
return AddRecord(wo, kBlogFooterIndexRecord, index_data, kNoCompression,
|
||||
nullptr, /*force_full=*/true);
|
||||
}
|
||||
|
||||
IOStatus BlogFileWriter::AddFooterPropertiesRecord(
|
||||
const WriteOptions& wo, const BlogFileFooterProperties& props) {
|
||||
std::string payload;
|
||||
props.EncodeTo(&payload);
|
||||
return AddRecord(wo, kBlogFooterPropertiesRecord, payload, kNoCompression,
|
||||
nullptr, /*force_full=*/true);
|
||||
}
|
||||
|
||||
IOStatus BlogFileWriter::AddFooterLocatorRecord(
|
||||
const WriteOptions& wo, const BlogFileFooterLocator& locator) {
|
||||
std::string payload;
|
||||
locator.EncodeTo(&payload);
|
||||
return AddRecord(wo, kBlogFooterLocatorRecord, payload, kNoCompression,
|
||||
nullptr, /*force_full=*/true);
|
||||
}
|
||||
|
||||
IOStatus BlogFileWriter::WriteBuffer(const WriteOptions& wo) {
|
||||
IOOptions opts;
|
||||
IOStatus s = WritableFileWriter::PrepareIOOptions(wo, opts);
|
||||
if (s.ok()) {
|
||||
s = dest_->Flush(opts);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
IOStatus BlogFileWriter::Sync(const WriteOptions& wo, bool use_fsync) {
|
||||
IOOptions opts;
|
||||
IOStatus s = WritableFileWriter::PrepareIOOptions(wo, opts);
|
||||
if (s.ok()) {
|
||||
s = dest_->Sync(opts, use_fsync);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
IOStatus BlogFileWriter::Close(const WriteOptions& wo) {
|
||||
IOOptions opts;
|
||||
IOStatus s = WritableFileWriter::PrepareIOOptions(wo, opts);
|
||||
if (s.ok()) {
|
||||
s = dest_->Close(opts);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
IOStatus BlogFileWriter::AddRecord(const WriteOptions& wo, BlogRecordType type,
|
||||
const Slice& payload,
|
||||
CompressionType comp_type,
|
||||
uint64_t* payload_offset, bool force_full) {
|
||||
assert(header_written_);
|
||||
|
||||
// Determine varint length of payload size
|
||||
uint16_t varint_len = VarintLength(payload.size());
|
||||
|
||||
// Use compact format when:
|
||||
// 1. varint fits in <= kBlogCompactVarintMaxBytes (payload < ~16 KiB)
|
||||
// 2. record type matches the header's compact_record_type
|
||||
// 3. not forced to full format (e.g. footer/preamble records)
|
||||
bool use_compact =
|
||||
!force_full && (varint_len <= kBlogCompactVarintMaxBytes) &&
|
||||
(type == header_.compact_record_type) && (payload.size() > 0);
|
||||
|
||||
IOStatus s;
|
||||
if (use_compact) {
|
||||
s = EmitCompactRecord(wo, payload, comp_type, payload_offset);
|
||||
} else {
|
||||
s = EmitFullRecord(wo, type, payload, comp_type, payload_offset);
|
||||
}
|
||||
|
||||
if (s.ok() && !manual_flush_) {
|
||||
IOOptions opts;
|
||||
s = WritableFileWriter::PrepareIOOptions(wo, opts);
|
||||
if (s.ok()) {
|
||||
s = dest_->Flush(opts);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
IOStatus BlogFileWriter::EmitCompactRecord(const WriteOptions& wo,
|
||||
const Slice& payload,
|
||||
CompressionType comp_type,
|
||||
uint64_t* payload_offset) {
|
||||
// Compact format:
|
||||
// [escape_seq: 10B] [varint length: 1-3B]
|
||||
// [payload: length bytes]
|
||||
// [compression_type: 1B] [checksum: 4B]
|
||||
// [padding: 0+B]
|
||||
assert(payload.size() > 0); // length=0 compact records not expected
|
||||
|
||||
std::string prefix;
|
||||
// Escape sequence
|
||||
prefix.append(header_.escape_sequence, kBlogEscapeSequenceSize);
|
||||
// Varint length
|
||||
PutVarint64(&prefix, payload.size());
|
||||
|
||||
IOStatus s = EmitBytes(wo, prefix);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// Record payload offset for caller
|
||||
if (payload_offset) {
|
||||
*payload_offset = offset_;
|
||||
}
|
||||
|
||||
// Payload
|
||||
s = EmitBytes(wo, payload);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// 5-byte trailer: compression_type + checksum
|
||||
char trailer[kBlogBlockTrailerSize];
|
||||
trailer[0] = static_cast<char>(comp_type);
|
||||
uint32_t checksum = ComputeBlogRecordChecksum(
|
||||
header_.checksum_type, payload.data(), payload.size(), trailer[0],
|
||||
header_.incarnation_id(),
|
||||
offset_ - payload.size()); // record offset = start of payload
|
||||
EncodeFixed32(trailer + 1, checksum);
|
||||
s = EmitBytes(wo, trailer, kBlogBlockTrailerSize);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// Padding (stack-allocated, no heap allocation)
|
||||
uint8_t last_byte = static_cast<uint8_t>(trailer[kBlogBlockTrailerSize - 1]);
|
||||
uint8_t pad_byte;
|
||||
size_t pad_count;
|
||||
ComputeBlogPaddingParams(last_byte, offset_, &pad_byte, &pad_count);
|
||||
if (pad_count > 0) {
|
||||
char pad_buf[4];
|
||||
memset(pad_buf, pad_byte, pad_count);
|
||||
s = EmitBytes(wo, pad_buf, pad_count);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
IOStatus BlogFileWriter::EmitFullRecord(const WriteOptions& wo,
|
||||
BlogRecordType type,
|
||||
const Slice& payload,
|
||||
CompressionType comp_type,
|
||||
uint64_t* payload_offset) {
|
||||
// Full format:
|
||||
// [escape_seq: 10B] [varint length: 4+B] [type: 1B] [compression_type: 1B]
|
||||
// [prefix_checksum: 4B]
|
||||
// (if length > 0):
|
||||
// [payload: length bytes]
|
||||
// [compression_type: 1B] [checksum: 4B]
|
||||
// [padding: 0+B]
|
||||
|
||||
std::string prefix;
|
||||
// Escape sequence
|
||||
prefix.append(header_.escape_sequence, kBlogEscapeSequenceSize);
|
||||
|
||||
// Varint length (must be > kBlogCompactVarintMaxBytes to trigger full format)
|
||||
constexpr size_t kFullVarintMinBytes = kBlogCompactVarintMaxBytes + 1;
|
||||
size_t varint_start = prefix.size();
|
||||
if (payload.size() == 0) {
|
||||
PutBlogIrregularVarint64(&prefix, 0, kFullVarintMinBytes);
|
||||
} else {
|
||||
uint16_t natural_len = VarintLength(payload.size());
|
||||
if (natural_len >= kFullVarintMinBytes) {
|
||||
PutVarint64(&prefix, payload.size());
|
||||
} else {
|
||||
PutBlogIrregularVarint64(&prefix, payload.size(), kFullVarintMinBytes);
|
||||
}
|
||||
}
|
||||
|
||||
// Type byte
|
||||
prefix.push_back(static_cast<char>(type));
|
||||
// Pre-payload compression type
|
||||
prefix.push_back(static_cast<char>(comp_type));
|
||||
|
||||
// Prefix checksum covers (varint length + type + compression_type),
|
||||
// with context modifier for defense-in-depth.
|
||||
const char* prefix_data = prefix.data() + varint_start;
|
||||
size_t prefix_data_size = prefix.size() - varint_start;
|
||||
uint32_t prefix_checksum =
|
||||
ComputeBuiltinChecksum(header_.checksum_type, prefix_data,
|
||||
prefix_data_size) +
|
||||
ChecksumModifierForContext(header_.incarnation_id(), offset_);
|
||||
PutFixed32(&prefix, prefix_checksum);
|
||||
|
||||
IOStatus s = EmitBytes(wo, prefix);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// Track the last meaningful byte for padding computation.
|
||||
// EncodeFixed32 is little-endian: last byte written = (value >> 24) & 0xFF.
|
||||
uint8_t last_byte;
|
||||
|
||||
if (payload.size() > 0) {
|
||||
// Record payload offset for caller
|
||||
if (payload_offset) {
|
||||
*payload_offset = offset_;
|
||||
}
|
||||
|
||||
// Payload
|
||||
s = EmitBytes(wo, payload);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// 5-byte trailer: compression_type + checksum
|
||||
char trailer[kBlogBlockTrailerSize];
|
||||
trailer[0] = static_cast<char>(comp_type);
|
||||
uint32_t checksum = ComputeBlogRecordChecksum(
|
||||
header_.checksum_type, payload.data(), payload.size(), trailer[0],
|
||||
header_.incarnation_id(), offset_ - payload.size());
|
||||
EncodeFixed32(trailer + 1, checksum);
|
||||
s = EmitBytes(wo, trailer, kBlogBlockTrailerSize);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
last_byte = static_cast<uint8_t>(trailer[kBlogBlockTrailerSize - 1]);
|
||||
} else {
|
||||
// No payload, no trailer. Last meaningful byte is the last byte of
|
||||
// prefix_checksum (little-endian).
|
||||
last_byte = static_cast<uint8_t>(prefix_checksum >> 24);
|
||||
}
|
||||
|
||||
uint8_t pad_byte;
|
||||
size_t pad_count;
|
||||
ComputeBlogPaddingParams(last_byte, offset_, &pad_byte, &pad_count);
|
||||
if (pad_count > 0) {
|
||||
char pad_buf[4];
|
||||
memset(pad_buf, pad_byte, pad_count);
|
||||
s = EmitBytes(wo, pad_buf, pad_count);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
IOStatus BlogFileWriter::EmitBytes(const WriteOptions& wo, const Slice& data) {
|
||||
return EmitBytes(wo, data.data(), data.size());
|
||||
}
|
||||
|
||||
IOStatus BlogFileWriter::EmitBytes(const WriteOptions& wo, const char* data,
|
||||
size_t len) {
|
||||
IOOptions opts;
|
||||
IOStatus s = WritableFileWriter::PrepareIOOptions(wo, opts);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
s = dest_->Append(opts, Slice(data, len));
|
||||
if (s.ok()) {
|
||||
offset_ += len;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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 <memory>
|
||||
#include <string>
|
||||
|
||||
#include "db/blog/blog_format.h"
|
||||
#include "file/writable_file_writer.h"
|
||||
#include "rocksdb/io_status.h"
|
||||
#include "rocksdb/options.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// Writes blog-format files (WAL or blob). The caller constructs a
|
||||
// BlogFileHeader, calls WriteHeader(), then adds records via
|
||||
// AddBlobRecord/AddWriteBatchRecord, and optionally appends footer records
|
||||
// before closing.
|
||||
//
|
||||
// Format selection (compact vs full) is automatic based on payload size and
|
||||
// record type. See blog_format.h for the wire format details.
|
||||
class BlogFileWriter {
|
||||
public:
|
||||
BlogFileWriter(std::unique_ptr<WritableFileWriter>&& dest,
|
||||
const BlogFileHeader& header, bool manual_flush = false);
|
||||
~BlogFileWriter();
|
||||
|
||||
// Write the file header. Must be called exactly once, before any records.
|
||||
IOStatus WriteHeader(const WriteOptions& wo);
|
||||
|
||||
// Write a blob record. Selects compact or full format automatically.
|
||||
// On success, *blob_offset is the file offset of the payload start.
|
||||
IOStatus AddBlobRecord(const WriteOptions& wo, const Slice& payload,
|
||||
CompressionType comp_type, uint64_t* blob_offset);
|
||||
|
||||
// Write a WriteBatch record. Selects compact or full format automatically.
|
||||
IOStatus AddWriteBatchRecord(const WriteOptions& wo, const Slice& wb_data,
|
||||
CompressionType comp_type = kNoCompression);
|
||||
|
||||
// Write a preamble-start record (stub). Uses full format with length=0.
|
||||
IOStatus AddPreambleStartRecord(const WriteOptions& wo);
|
||||
|
||||
// Write a footer index record (full format).
|
||||
IOStatus AddFooterIndexRecord(const WriteOptions& wo,
|
||||
const Slice& index_data);
|
||||
|
||||
// Write a footer properties record (full format).
|
||||
IOStatus AddFooterPropertiesRecord(const WriteOptions& wo,
|
||||
const BlogFileFooterProperties& props);
|
||||
|
||||
// Write a footer locator record (full format). Must be the last record.
|
||||
IOStatus AddFooterLocatorRecord(const WriteOptions& wo,
|
||||
const BlogFileFooterLocator& locator);
|
||||
|
||||
IOStatus WriteBuffer(const WriteOptions& wo);
|
||||
IOStatus Sync(const WriteOptions& wo, bool use_fsync = false);
|
||||
IOStatus Close(const WriteOptions& wo);
|
||||
|
||||
uint64_t current_offset() const { return offset_; }
|
||||
WritableFileWriter* file() { return dest_.get(); }
|
||||
const BlogFileHeader& header() const { return header_; }
|
||||
|
||||
private:
|
||||
// Write a record. Chooses compact format when varint fits in <= 3 bytes
|
||||
// and type matches compact_record_type, unless force_full is true.
|
||||
IOStatus AddRecord(const WriteOptions& wo, BlogRecordType type,
|
||||
const Slice& payload, CompressionType comp_type,
|
||||
uint64_t* payload_offset, bool force_full = false);
|
||||
|
||||
// Emit a compact-format record.
|
||||
IOStatus EmitCompactRecord(const WriteOptions& wo, const Slice& payload,
|
||||
CompressionType comp_type,
|
||||
uint64_t* payload_offset);
|
||||
|
||||
// Emit a full-format record.
|
||||
IOStatus EmitFullRecord(const WriteOptions& wo, BlogRecordType type,
|
||||
const Slice& payload, CompressionType comp_type,
|
||||
uint64_t* payload_offset);
|
||||
|
||||
// Append raw bytes to the file and advance offset_.
|
||||
IOStatus EmitBytes(const WriteOptions& wo, const Slice& data);
|
||||
IOStatus EmitBytes(const WriteOptions& wo, const char* data, size_t len);
|
||||
|
||||
std::unique_ptr<WritableFileWriter> dest_;
|
||||
BlogFileHeader header_;
|
||||
uint64_t offset_ = 0;
|
||||
bool manual_flush_;
|
||||
bool header_written_ = false;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,452 @@
|
||||
// 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/blog/blog_writer.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "db/blog/blog_reader.h"
|
||||
#include "file/sequence_file_reader.h"
|
||||
#include "file/writable_file_writer.h"
|
||||
#include "rocksdb/write_buffer_manager.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "test_util/testutil.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class BlogWriterTest : public testing::Test {
|
||||
protected:
|
||||
// Shared contents buffer: StringSink writes here, StringSource reads from it.
|
||||
Slice reader_contents_;
|
||||
test::StringSink* sink_;
|
||||
|
||||
BlogWriterTest() : sink_(new test::StringSink(&reader_contents_)) {}
|
||||
|
||||
// Create a writer with the given header.
|
||||
std::unique_ptr<BlogFileWriter> MakeWriter(const BlogFileHeader& header) {
|
||||
std::unique_ptr<FSWritableFile> sink_holder(sink_);
|
||||
std::unique_ptr<WritableFileWriter> file_writer(new WritableFileWriter(
|
||||
std::move(sink_holder), "" /* file name */, FileOptions()));
|
||||
return std::make_unique<BlogFileWriter>(std::move(file_writer), header);
|
||||
}
|
||||
|
||||
std::unique_ptr<BlogFileReader> MakeReaderWithReporter(
|
||||
BlogFileReader::Reporter* r) {
|
||||
return MakeReaderImpl(r);
|
||||
}
|
||||
|
||||
// Create a reader over the data written so far.
|
||||
std::unique_ptr<BlogFileReader> MakeReader() {
|
||||
return MakeReaderImpl(nullptr);
|
||||
}
|
||||
|
||||
std::unique_ptr<BlogFileReader> MakeReaderImpl(
|
||||
BlogFileReader::Reporter* reporter) {
|
||||
// Create an FSSequentialFile that reads from reader_contents_
|
||||
class MemSequentialFile : public FSSequentialFile {
|
||||
public:
|
||||
Slice& contents_;
|
||||
size_t pos_ = 0;
|
||||
explicit MemSequentialFile(Slice& contents) : contents_(contents) {}
|
||||
IOStatus Read(size_t n, const IOOptions& /*opts*/, Slice* result,
|
||||
char* scratch, IODebugContext* /*dbg*/) override {
|
||||
size_t avail = contents_.size() - pos_;
|
||||
if (n > avail) {
|
||||
n = avail;
|
||||
}
|
||||
if (n > 0) {
|
||||
memcpy(scratch, contents_.data() + pos_, n);
|
||||
pos_ += n;
|
||||
}
|
||||
*result = Slice(scratch, n);
|
||||
return IOStatus::OK();
|
||||
}
|
||||
IOStatus Skip(uint64_t n) override {
|
||||
pos_ += static_cast<size_t>(n);
|
||||
return IOStatus::OK();
|
||||
}
|
||||
};
|
||||
|
||||
auto* source = new MemSequentialFile(reader_contents_);
|
||||
std::unique_ptr<FSSequentialFile> source_holder(source);
|
||||
std::unique_ptr<SequentialFileReader> file_reader(
|
||||
new SequentialFileReader(std::move(source_holder), "" /* file name */));
|
||||
return std::make_unique<BlogFileReader>(std::move(file_reader), reporter,
|
||||
true /* verify_checksums */);
|
||||
}
|
||||
|
||||
// Helper to create a default header for blob files.
|
||||
BlogFileHeader MakeBlobHeader() {
|
||||
BlogFileHeader header;
|
||||
header.checksum_type = kXXH3;
|
||||
header.compact_record_type = kBlogBlobRecord;
|
||||
header.GenerateRandomFields();
|
||||
return header;
|
||||
}
|
||||
|
||||
// Helper to create a default header for WAL files.
|
||||
BlogFileHeader MakeWalHeader() {
|
||||
BlogFileHeader header;
|
||||
header.checksum_type = kXXH3;
|
||||
header.compact_record_type = kBlogWriteBatchRecord;
|
||||
header.GenerateRandomFields();
|
||||
header.SetProperty("role", "wal");
|
||||
return header;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(BlogWriterTest, WriteAndReadHeader) {
|
||||
auto header = MakeBlobHeader();
|
||||
auto writer = MakeWriter(header);
|
||||
|
||||
WriteOptions wo;
|
||||
ASSERT_OK(writer->WriteHeader(wo));
|
||||
|
||||
auto reader = MakeReader();
|
||||
BlogFileHeader read_header;
|
||||
ASSERT_OK(reader->ReadHeader(&read_header));
|
||||
|
||||
ASSERT_EQ(read_header.schema_version, header.schema_version);
|
||||
ASSERT_EQ(read_header.checksum_type, header.checksum_type);
|
||||
ASSERT_EQ(read_header.compact_record_type, header.compact_record_type);
|
||||
ASSERT_EQ(memcmp(read_header.escape_sequence, header.escape_sequence,
|
||||
kBlogEscapeSequenceSize),
|
||||
0);
|
||||
}
|
||||
|
||||
TEST_F(BlogWriterTest, WriteSingleBlobRecord) {
|
||||
auto header = MakeBlobHeader();
|
||||
auto writer = MakeWriter(header);
|
||||
|
||||
WriteOptions wo;
|
||||
ASSERT_OK(writer->WriteHeader(wo));
|
||||
|
||||
std::string blob_data(100, 'A');
|
||||
uint64_t blob_offset = 0;
|
||||
ASSERT_OK(writer->AddBlobRecord(wo, blob_data, kNoCompression, &blob_offset));
|
||||
ASSERT_GT(blob_offset, 0u);
|
||||
|
||||
// Verify alignment: offset after record should be 4-byte aligned
|
||||
ASSERT_EQ(writer->current_offset() % 4, 0u);
|
||||
|
||||
// Read it back
|
||||
auto reader = MakeReader();
|
||||
BlogFileHeader read_header;
|
||||
ASSERT_OK(reader->ReadHeader(&read_header));
|
||||
|
||||
BlogRecordType type;
|
||||
Slice payload;
|
||||
std::string scratch;
|
||||
uint64_t rec_offset;
|
||||
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch, &rec_offset));
|
||||
ASSERT_EQ(type, kBlogBlobRecord);
|
||||
ASSERT_EQ(payload.size(), 100u);
|
||||
ASSERT_EQ(payload.ToString(), blob_data);
|
||||
}
|
||||
|
||||
TEST_F(BlogWriterTest, WriteMultipleBlobRecords) {
|
||||
auto header = MakeBlobHeader();
|
||||
auto writer = MakeWriter(header);
|
||||
|
||||
WriteOptions wo;
|
||||
ASSERT_OK(writer->WriteHeader(wo));
|
||||
|
||||
std::vector<std::string> blobs = {"hello", std::string(1000, 'X'), "short",
|
||||
std::string(500, 'Y')};
|
||||
for (const auto& blob : blobs) {
|
||||
uint64_t offset;
|
||||
ASSERT_OK(writer->AddBlobRecord(wo, blob, kNoCompression, &offset));
|
||||
}
|
||||
|
||||
auto reader = MakeReader();
|
||||
BlogFileHeader rh;
|
||||
ASSERT_OK(reader->ReadHeader(&rh));
|
||||
|
||||
for (const auto& expected : blobs) {
|
||||
BlogRecordType type;
|
||||
Slice payload;
|
||||
std::string scratch;
|
||||
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
|
||||
ASSERT_EQ(type, kBlogBlobRecord);
|
||||
ASSERT_EQ(payload.ToString(), expected);
|
||||
}
|
||||
|
||||
// No more records
|
||||
BlogRecordType type;
|
||||
Slice payload;
|
||||
std::string scratch;
|
||||
ASSERT_TRUE(reader->ReadRecord(&type, &payload, &scratch).IsNotFound());
|
||||
}
|
||||
|
||||
TEST_F(BlogWriterTest, WriteWriteBatchRecord) {
|
||||
auto header = MakeWalHeader();
|
||||
auto writer = MakeWriter(header);
|
||||
|
||||
WriteOptions wo;
|
||||
ASSERT_OK(writer->WriteHeader(wo));
|
||||
|
||||
std::string wb_data(200, 'W');
|
||||
ASSERT_OK(writer->AddWriteBatchRecord(wo, wb_data));
|
||||
|
||||
auto reader = MakeReader();
|
||||
BlogFileHeader rh;
|
||||
ASSERT_OK(reader->ReadHeader(&rh));
|
||||
|
||||
BlogRecordType type;
|
||||
Slice payload;
|
||||
std::string scratch;
|
||||
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
|
||||
ASSERT_EQ(type, kBlogWriteBatchRecord);
|
||||
ASSERT_EQ(payload.ToString(), wb_data);
|
||||
}
|
||||
|
||||
TEST_F(BlogWriterTest, CompactVsFullFormat) {
|
||||
auto header = MakeBlobHeader();
|
||||
auto writer = MakeWriter(header);
|
||||
|
||||
WriteOptions wo;
|
||||
ASSERT_OK(writer->WriteHeader(wo));
|
||||
|
||||
// Small blob (< 2 MiB) should use compact format (kBlogBlobRecord matches
|
||||
// compact_record_type)
|
||||
std::string small_blob(100, 'S');
|
||||
uint64_t offset1;
|
||||
ASSERT_OK(writer->AddBlobRecord(wo, small_blob, kNoCompression, &offset1));
|
||||
|
||||
// Large blob (> 2 MiB) should use full format
|
||||
std::string large_blob(3 * 1024 * 1024, 'L');
|
||||
uint64_t offset2;
|
||||
ASSERT_OK(writer->AddBlobRecord(wo, large_blob, kNoCompression, &offset2));
|
||||
|
||||
// Read both back
|
||||
auto reader = MakeReader();
|
||||
BlogFileHeader rh;
|
||||
ASSERT_OK(reader->ReadHeader(&rh));
|
||||
|
||||
BlogRecordType type;
|
||||
Slice payload;
|
||||
std::string scratch;
|
||||
|
||||
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
|
||||
ASSERT_EQ(type, kBlogBlobRecord);
|
||||
ASSERT_EQ(payload.size(), small_blob.size());
|
||||
|
||||
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
|
||||
ASSERT_EQ(type, kBlogBlobRecord);
|
||||
ASSERT_EQ(payload.size(), large_blob.size());
|
||||
ASSERT_EQ(payload, Slice(large_blob));
|
||||
}
|
||||
|
||||
TEST_F(BlogWriterTest, PreambleStartRecord) {
|
||||
auto header = MakeWalHeader();
|
||||
auto writer = MakeWriter(header);
|
||||
|
||||
WriteOptions wo;
|
||||
ASSERT_OK(writer->WriteHeader(wo));
|
||||
uint64_t pre_offset = writer->current_offset();
|
||||
ASSERT_EQ(pre_offset % 4, 0u) << "Pre-record offset not aligned";
|
||||
ASSERT_OK(writer->AddPreambleStartRecord(wo));
|
||||
uint64_t post_offset = writer->current_offset();
|
||||
ASSERT_EQ(post_offset % 4, 0u) << "Post-record offset not aligned";
|
||||
|
||||
// Use a reporter to capture errors
|
||||
class TestReporter : public BlogFileReader::Reporter {
|
||||
public:
|
||||
std::string last_msg;
|
||||
void Corruption(size_t /*bytes*/, const Status& s) override {
|
||||
last_msg = s.ToString();
|
||||
}
|
||||
};
|
||||
TestReporter reporter;
|
||||
|
||||
auto reader = MakeReaderWithReporter(&reporter);
|
||||
BlogFileHeader rh;
|
||||
ASSERT_OK(reader->ReadHeader(&rh));
|
||||
|
||||
BlogRecordType type;
|
||||
Slice payload;
|
||||
std::string scratch;
|
||||
Status rs = reader->ReadRecord(&type, &payload, &scratch);
|
||||
if (!rs.ok()) {
|
||||
fprintf(stderr, "ReadRecord failed: %s, Reporter: %s, EOF=%d\n",
|
||||
rs.ToString().c_str(), reporter.last_msg.c_str(), reader->IsEOF());
|
||||
}
|
||||
ASSERT_OK(rs);
|
||||
ASSERT_EQ(type, kBlogPreambleStartRecord);
|
||||
ASSERT_TRUE(payload.empty());
|
||||
}
|
||||
|
||||
TEST_F(BlogWriterTest, FooterRecords) {
|
||||
auto header = MakeBlobHeader();
|
||||
auto writer = MakeWriter(header);
|
||||
|
||||
WriteOptions wo;
|
||||
ASSERT_OK(writer->WriteHeader(wo));
|
||||
|
||||
// Write a blob record first
|
||||
std::string blob_data(50, 'B');
|
||||
uint64_t blob_offset;
|
||||
ASSERT_OK(writer->AddBlobRecord(wo, blob_data, kNoCompression, &blob_offset));
|
||||
|
||||
// Write footer properties
|
||||
uint64_t props_offset = writer->current_offset();
|
||||
BlogFileFooterProperties props;
|
||||
props.SetBlobCount(1);
|
||||
props.SetTotalBlobBytes(50);
|
||||
ASSERT_OK(writer->AddFooterPropertiesRecord(wo, props));
|
||||
|
||||
// Write footer locator
|
||||
BlogFileFooterLocator locator;
|
||||
uint64_t locator_offset = writer->current_offset();
|
||||
locator.entries.push_back(
|
||||
{kBlogFooterPropertiesRecord,
|
||||
static_cast<uint32_t>((locator_offset - props_offset) / 4)});
|
||||
ASSERT_OK(writer->AddFooterLocatorRecord(wo, locator));
|
||||
|
||||
// Read all records
|
||||
auto reader = MakeReader();
|
||||
BlogFileHeader rh;
|
||||
ASSERT_OK(reader->ReadHeader(&rh));
|
||||
|
||||
BlogRecordType type;
|
||||
Slice payload;
|
||||
std::string scratch;
|
||||
|
||||
// Blob record
|
||||
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
|
||||
ASSERT_EQ(type, kBlogBlobRecord);
|
||||
|
||||
// Footer properties record
|
||||
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
|
||||
ASSERT_EQ(type, kBlogFooterPropertiesRecord);
|
||||
BlogFileFooterProperties read_props;
|
||||
ASSERT_OK(read_props.DecodeFrom(payload));
|
||||
ASSERT_EQ(read_props.properties.size(), 2u);
|
||||
|
||||
// Footer locator record
|
||||
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
|
||||
ASSERT_EQ(type, kBlogFooterLocatorRecord);
|
||||
BlogFileFooterLocator read_locator;
|
||||
ASSERT_OK(read_locator.DecodeFrom(payload));
|
||||
ASSERT_EQ(read_locator.entries.size(), 1u);
|
||||
ASSERT_EQ(read_locator.entries[0].record_type, kBlogFooterPropertiesRecord);
|
||||
}
|
||||
|
||||
TEST_F(BlogWriterTest, ChecksumCorruptionDetected) {
|
||||
auto header = MakeBlobHeader();
|
||||
auto writer = MakeWriter(header);
|
||||
|
||||
WriteOptions wo;
|
||||
ASSERT_OK(writer->WriteHeader(wo));
|
||||
|
||||
std::string blob_data(100, 'C');
|
||||
uint64_t blob_offset;
|
||||
ASSERT_OK(writer->AddBlobRecord(wo, blob_data, kNoCompression, &blob_offset));
|
||||
|
||||
// Corrupt one byte in the blob payload
|
||||
std::string& contents = sink_->contents_;
|
||||
ASSERT_GT(contents.size(), blob_offset + 10);
|
||||
contents[blob_offset + 10] ^= 0xFF;
|
||||
|
||||
// Reading should detect the corruption
|
||||
auto reader = MakeReader();
|
||||
BlogFileHeader rh;
|
||||
ASSERT_OK(reader->ReadHeader(&rh));
|
||||
|
||||
BlogRecordType type;
|
||||
Slice payload;
|
||||
std::string scratch;
|
||||
ASSERT_TRUE(reader->ReadRecord(&type, &payload, &scratch).IsCorruption());
|
||||
}
|
||||
|
||||
TEST_F(BlogWriterTest, MixedRecordTypes) {
|
||||
// Use blob compact type but also write WriteBatch records (full format)
|
||||
auto header = MakeBlobHeader();
|
||||
auto writer = MakeWriter(header);
|
||||
|
||||
WriteOptions wo;
|
||||
ASSERT_OK(writer->WriteHeader(wo));
|
||||
|
||||
// Blob (compact format)
|
||||
std::string blob(50, 'B');
|
||||
uint64_t blob_offset;
|
||||
ASSERT_OK(writer->AddBlobRecord(wo, blob, kNoCompression, &blob_offset));
|
||||
|
||||
// WriteBatch (full format since it doesn't match compact_record_type)
|
||||
std::string wb(60, 'W');
|
||||
ASSERT_OK(writer->AddWriteBatchRecord(wo, wb));
|
||||
|
||||
// Another blob (compact)
|
||||
std::string blob2(70, 'D');
|
||||
uint64_t blob2_offset;
|
||||
ASSERT_OK(writer->AddBlobRecord(wo, blob2, kNoCompression, &blob2_offset));
|
||||
|
||||
// Read all back
|
||||
auto reader = MakeReader();
|
||||
BlogFileHeader rh;
|
||||
ASSERT_OK(reader->ReadHeader(&rh));
|
||||
|
||||
BlogRecordType type;
|
||||
Slice payload;
|
||||
std::string scratch;
|
||||
|
||||
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
|
||||
ASSERT_EQ(type, kBlogBlobRecord);
|
||||
ASSERT_EQ(payload.size(), 50u);
|
||||
|
||||
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
|
||||
ASSERT_EQ(type, kBlogWriteBatchRecord);
|
||||
ASSERT_EQ(payload.size(), 60u);
|
||||
|
||||
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
|
||||
ASSERT_EQ(type, kBlogBlobRecord);
|
||||
ASSERT_EQ(payload.size(), 70u);
|
||||
|
||||
ASSERT_TRUE(reader->ReadRecord(&type, &payload, &scratch).IsNotFound());
|
||||
}
|
||||
|
||||
TEST_F(BlogWriterTest, AlignmentInvariant) {
|
||||
auto header = MakeBlobHeader();
|
||||
auto writer = MakeWriter(header);
|
||||
|
||||
WriteOptions wo;
|
||||
ASSERT_OK(writer->WriteHeader(wo));
|
||||
|
||||
// Write records of various sizes and verify alignment is maintained
|
||||
for (int size = 1; size <= 200; ++size) {
|
||||
std::string data(size, static_cast<char>(size & 0xFF));
|
||||
uint64_t offset;
|
||||
ASSERT_OK(writer->AddBlobRecord(wo, data, kNoCompression, &offset));
|
||||
ASSERT_EQ(writer->current_offset() % 4, 0u)
|
||||
<< "Alignment violated after record of size " << size;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BlogWriterTest, HeaderWithProperties) {
|
||||
auto header = MakeBlobHeader();
|
||||
header.SetProperty("CompressionCompatibilityName", "zstd");
|
||||
header.SetProperty("role", "blob");
|
||||
header.SetProperty("compressionSettings", "level=3");
|
||||
auto writer = MakeWriter(header);
|
||||
|
||||
WriteOptions wo;
|
||||
ASSERT_OK(writer->WriteHeader(wo));
|
||||
|
||||
auto reader = MakeReader();
|
||||
BlogFileHeader rh;
|
||||
ASSERT_OK(reader->ReadHeader(&rh));
|
||||
ASSERT_EQ(rh.GetProperty("CompressionCompatibilityName"), "zstd");
|
||||
ASSERT_EQ(rh.GetProperty("role"), "blob");
|
||||
ASSERT_EQ(rh.GetProperty("compressionSettings"), "level=3");
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
+74
-8
@@ -13,7 +13,10 @@
|
||||
#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"
|
||||
@@ -87,7 +90,8 @@ 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) {
|
||||
InternalStats::CompactionStats* flush_stats,
|
||||
std::vector<BlobFileGarbage>* blob_file_garbages, bool fast_sst_open) {
|
||||
assert((tboptions.column_family_id ==
|
||||
TablePropertiesCollectorFactory::Context::kUnknownColumnFamily) ==
|
||||
tboptions.column_family_name.empty());
|
||||
@@ -99,7 +103,24 @@ Status BuildTable(
|
||||
/*enable_hash=*/paranoid_file_checks);
|
||||
Status s;
|
||||
meta->fd.file_size = 0;
|
||||
iter->SeekToFirst();
|
||||
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();
|
||||
std::unique_ptr<CompactionRangeDelAggregator> range_del_agg(
|
||||
new CompactionRangeDelAggregator(&tboptions.internal_comparator,
|
||||
snapshots, full_history_ts_low));
|
||||
@@ -128,7 +149,7 @@ Status BuildTable(
|
||||
|
||||
TableProperties tp;
|
||||
bool table_file_created = false;
|
||||
if (iter->Valid() || !range_del_agg->IsEmpty()) {
|
||||
if (input->Valid() || !range_del_agg->IsEmpty()) {
|
||||
std::unique_ptr<CompactionFilter> compaction_filter;
|
||||
if (ioptions.compaction_filter_factory != nullptr &&
|
||||
ioptions.compaction_filter_factory->ShouldFilterTableFileCreation(
|
||||
@@ -211,7 +232,7 @@ Status BuildTable(
|
||||
|
||||
const std::atomic<bool> kManualCompactionCanceledFalse{false};
|
||||
CompactionIterator c_iter(
|
||||
iter, ucmp, &merge, kMaxSequenceNumber, &snapshots, earliest_snapshot,
|
||||
input, 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,
|
||||
@@ -219,7 +240,17 @@ 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);
|
||||
/*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);
|
||||
}
|
||||
|
||||
SequenceNumber smallest_preferred_seqno = kMaxSequenceNumber;
|
||||
std::string key_after_flush_buf;
|
||||
@@ -268,6 +299,14 @@ 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()) {
|
||||
@@ -322,6 +361,8 @@ 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());
|
||||
@@ -330,6 +371,14 @@ 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) {
|
||||
@@ -447,6 +496,8 @@ 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,
|
||||
@@ -457,7 +508,10 @@ Status BuildTable(
|
||||
MaxFileSizeForL0MetaPin(mutable_cf_options),
|
||||
/*smallest_compaction_key=*/nullptr,
|
||||
/*largest_compaction_key*/ nullptr,
|
||||
/*allow_unprepared_value*/ false));
|
||||
/*allow_unprepared_value*/ false,
|
||||
/*range_del_read_seqno=*/nullptr,
|
||||
/*range_del_iter=*/nullptr,
|
||||
/*maybe_pin_table_handle=*/false, file_open_metadata_ptr));
|
||||
s = it->status();
|
||||
if (s.ok() && paranoid_file_checks) {
|
||||
OutputValidator file_validator(tboptions.internal_comparator,
|
||||
@@ -475,8 +529,20 @@ Status BuildTable(
|
||||
}
|
||||
|
||||
// Check for input iterator errors
|
||||
if (!iter->status().ok()) {
|
||||
s = iter->status();
|
||||
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 (!s.ok() || meta->fd.GetFileSize() == 0) {
|
||||
|
||||
+4
-1
@@ -31,6 +31,7 @@ struct FileMetaData;
|
||||
|
||||
class VersionSet;
|
||||
class BlobFileAddition;
|
||||
class BlobFileGarbage;
|
||||
class SnapshotChecker;
|
||||
class TableCache;
|
||||
class TableBuilder;
|
||||
@@ -79,6 +80,8 @@ 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);
|
||||
InternalStats::CompactionStats* flush_stats = nullptr,
|
||||
std::vector<BlobFileGarbage>* blob_file_garbages = nullptr,
|
||||
bool fast_sst_open = false);
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -1759,9 +1759,13 @@ 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;
|
||||
SaveError(errptr, db->rep->CreateColumnFamily(
|
||||
ColumnFamilyOptions(column_family_options->rep),
|
||||
std::string(column_family_name), &(handle->rep)));
|
||||
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;
|
||||
}
|
||||
handle->immortal = false;
|
||||
return handle;
|
||||
}
|
||||
@@ -3052,6 +3056,7 @@ class H : public WriteBatch::Handler {
|
||||
(*log_data_)(state_, blob.data(), blob.size());
|
||||
}
|
||||
}
|
||||
Status MarkNoop(bool /* empty_batch */) override { return Status::OK(); }
|
||||
};
|
||||
|
||||
class HCF : public WriteBatch::Handler {
|
||||
@@ -3085,6 +3090,7 @@ class HCF : public WriteBatch::Handler {
|
||||
(*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,
|
||||
@@ -4351,6 +4357,15 @@ 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) {
|
||||
@@ -4635,6 +4650,16 @@ 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();
|
||||
}
|
||||
@@ -4734,6 +4759,26 @@ 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;
|
||||
@@ -5789,6 +5834,16 @@ 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;
|
||||
}
|
||||
@@ -7525,9 +7580,13 @@ 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;
|
||||
SaveError(errptr, txn_db->rep->CreateColumnFamily(
|
||||
ColumnFamilyOptions(column_family_options->rep),
|
||||
std::string(column_family_name), &(handle->rep)));
|
||||
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;
|
||||
}
|
||||
handle->immortal = false;
|
||||
return handle;
|
||||
}
|
||||
@@ -8257,6 +8316,11 @@ 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,
|
||||
|
||||
+79
@@ -205,6 +205,27 @@ 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) {
|
||||
@@ -2643,6 +2664,10 @@ 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));
|
||||
|
||||
@@ -2867,6 +2892,14 @@ 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));
|
||||
@@ -3086,6 +3119,12 @@ 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));
|
||||
@@ -3850,10 +3889,35 @@ 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");
|
||||
@@ -4880,6 +4944,21 @@ 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);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ 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);
|
||||
@@ -22,13 +23,22 @@ 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) {
|
||||
wide_columns_.push_back(*current.column);
|
||||
add_column(*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
|
||||
@@ -37,7 +47,7 @@ void CoalescingIterator::Coalesce(
|
||||
current = heap.top();
|
||||
heap.pop();
|
||||
}
|
||||
wide_columns_.push_back(*current.column);
|
||||
add_column(*current.column);
|
||||
|
||||
if (WideColumnsHelper::HasDefaultColumn(wide_columns_)) {
|
||||
value_ = WideColumnsHelper::GetDefaultColumn(wide_columns_);
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "db/multi_cf_iterator_impl.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -45,6 +49,7 @@ class CoalescingIterator : public Iterator {
|
||||
void Reset() {
|
||||
value_.clear();
|
||||
wide_columns_.clear();
|
||||
owned_columns_.clear();
|
||||
}
|
||||
|
||||
bool PrepareValue() override { return impl_.PrepareValue(); }
|
||||
@@ -79,6 +84,7 @@ 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;
|
||||
|
||||
+80
-37
@@ -11,12 +11,14 @@
|
||||
|
||||
#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"
|
||||
@@ -495,6 +497,15 @@ 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;
|
||||
}
|
||||
|
||||
@@ -601,7 +612,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)
|
||||
const std::string& db_session_id, bool read_only, bool fast_sst_open)
|
||||
: id_(id),
|
||||
name_(name),
|
||||
dummy_versions_(_dummy_versions),
|
||||
@@ -661,7 +672,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));
|
||||
db_session_id, fast_sst_open));
|
||||
blob_file_cache_.reset(
|
||||
new BlobFileCache(_table_cache, &ioptions(), soptions(), id_,
|
||||
internal_stats_->GetBlobFileReadHist(), io_tracer));
|
||||
@@ -782,6 +793,11 @@ 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);
|
||||
@@ -1523,6 +1539,32 @@ 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) {
|
||||
@@ -1563,38 +1605,40 @@ 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 (cf_options.compaction_options_fifo.use_kv_ratio_compaction) {
|
||||
if (cf_options.compaction_style != kCompactionStyleFIFO) {
|
||||
return Status::InvalidArgument(
|
||||
"use_kv_ratio_compaction is only supported with FIFO compaction "
|
||||
"style.");
|
||||
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.");
|
||||
}
|
||||
if (!cf_options.compaction_options_fifo.allow_compaction) {
|
||||
// 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(
|
||||
"use_kv_ratio_compaction requires allow_compaction = true. "
|
||||
"allow_compaction enables intra-L0 compaction, and "
|
||||
"use_kv_ratio_compaction selects the picking strategy.");
|
||||
"open_files_async requires skip_stats_update_on_db_open = true.");
|
||||
}
|
||||
if (cf_options.compaction_options_fifo.max_data_files_size == 0) {
|
||||
return Status::InvalidArgument(
|
||||
"use_kv_ratio_compaction requires max_data_files_size > 0 to "
|
||||
"compute the target compacted file size from data capacity.");
|
||||
}
|
||||
}
|
||||
|
||||
if (cf_options.compaction_options_fifo.max_data_files_size > 0 &&
|
||||
cf_options.compaction_options_fifo.max_data_files_size <
|
||||
cf_options.compaction_options_fifo.max_table_files_size) {
|
||||
return Status::InvalidArgument(
|
||||
"max_data_files_size (total data = SST + blob) must be >= "
|
||||
"max_table_files_size (SST only) when non-zero, since total data "
|
||||
"always includes SST data.");
|
||||
}
|
||||
|
||||
std::vector<uint32_t> supported{0, 1, 2, 4, 8};
|
||||
@@ -1765,16 +1809,14 @@ 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)
|
||||
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)
|
||||
: max_column_family_(0),
|
||||
file_options_(file_options),
|
||||
dummy_cfd_(new ColumnFamilyData(
|
||||
@@ -1791,7 +1833,8 @@ ColumnFamilySet::ColumnFamilySet(const std::string& dbname,
|
||||
block_cache_tracer_(block_cache_tracer),
|
||||
io_tracer_(io_tracer),
|
||||
db_id_(db_id),
|
||||
db_session_id_(db_session_id) {
|
||||
db_session_id_(db_session_id),
|
||||
fast_sst_open_(fast_sst_open) {
|
||||
// initialize linked list
|
||||
dummy_cfd_->prev_ = dummy_cfd_;
|
||||
dummy_cfd_->next_ = dummy_cfd_;
|
||||
@@ -1858,7 +1901,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);
|
||||
db_id_, db_session_id_, read_only, fast_sst_open_);
|
||||
column_families_.insert({name, id});
|
||||
column_family_data_.insert({id, new_cfd});
|
||||
auto ucmp = new_cfd->user_comparator();
|
||||
|
||||
+41
-9
@@ -49,6 +49,7 @@ class InstrumentedMutex;
|
||||
class InstrumentedMutexLock;
|
||||
struct SuperVersionContext;
|
||||
class BlobFileCache;
|
||||
class BlobFilePartitionManager;
|
||||
class BlobSource;
|
||||
|
||||
extern const double kIncSlowdownRatio;
|
||||
@@ -415,6 +416,19 @@ 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
|
||||
@@ -605,16 +619,23 @@ 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);
|
||||
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);
|
||||
|
||||
std::vector<std::string> GetDbPaths() const;
|
||||
|
||||
@@ -648,6 +669,8 @@ 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_;
|
||||
|
||||
@@ -713,6 +736,10 @@ 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
|
||||
@@ -758,7 +785,8 @@ 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);
|
||||
const std::string& db_id, const std::string& db_session_id,
|
||||
bool fast_sst_open = false);
|
||||
~ColumnFamilySet();
|
||||
|
||||
ColumnFamilyData* GetDefault() const;
|
||||
@@ -794,6 +822,9 @@ 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_; }
|
||||
@@ -842,6 +873,7 @@ 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
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
|
||||
#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"
|
||||
@@ -20,8 +22,106 @@
|
||||
#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,
|
||||
@@ -38,7 +138,8 @@ 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)
|
||||
std::optional<SequenceNumber> preserve_seqno_min,
|
||||
const Version* input_version, Env::IOActivity blob_read_io_activity)
|
||||
: CompactionIterator(
|
||||
input, cmp, merge_helper, last_sequence, snapshots, earliest_snapshot,
|
||||
earliest_write_conflict_snapshot, job_snapshot, snapshot_checker, env,
|
||||
@@ -47,7 +148,8 @@ 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) {}
|
||||
full_history_ts_low, preserve_seqno_min, input_version,
|
||||
blob_read_io_activity) {}
|
||||
|
||||
CompactionIterator::CompactionIterator(
|
||||
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
|
||||
@@ -65,7 +167,8 @@ 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)
|
||||
std::optional<SequenceNumber> preserve_seqno_min,
|
||||
const Version* input_version, Env::IOActivity blob_read_io_activity)
|
||||
: input_(input, cmp, must_count_input_entries),
|
||||
cmp_(cmp),
|
||||
merge_helper_(merge_helper),
|
||||
@@ -80,6 +183,8 @@ 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() &&
|
||||
@@ -98,7 +203,8 @@ CompactionIterator::CompactionIterator(
|
||||
merge_out_iter_(merge_helper_),
|
||||
blob_garbage_collection_cutoff_file_number_(
|
||||
ComputeBlobGarbageCollectionCutoffFileNumber(compaction_.get())),
|
||||
blob_fetcher_(CreateBlobFetcherIfNeeded(compaction_.get())),
|
||||
blob_fetcher_(CreateBlobFetcherIfNeeded(compaction_.get(), input_version,
|
||||
blob_read_io_activity)),
|
||||
prefetch_buffers_(
|
||||
CreatePrefetchBufferCollectionIfNeeded(compaction_.get())),
|
||||
current_key_committed_(false),
|
||||
@@ -120,6 +226,8 @@ 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();
|
||||
@@ -131,6 +239,24 @@ 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;
|
||||
@@ -258,9 +384,10 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
compaction_filter_skip_until_.rep());
|
||||
if (decision == CompactionFilter::Decision::kUndetermined &&
|
||||
!compaction_filter_->IsStackedBlobDbInternalCompactionFilter()) {
|
||||
if (!compaction_) {
|
||||
status_ =
|
||||
Status::Corruption("Unexpected blob index outside of compaction");
|
||||
if (!blob_fetcher_) {
|
||||
status_ = Status::NotSupported(
|
||||
"Blob-backed value filtering requires blob read support in the "
|
||||
"current table-file creation context");
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
@@ -287,8 +414,6 @@ 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);
|
||||
@@ -309,7 +434,19 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
const Slice* existing_val = nullptr;
|
||||
const WideColumns* existing_col = nullptr;
|
||||
|
||||
WideColumns existing_columns;
|
||||
// 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;
|
||||
|
||||
if (ikey_.type != kTypeWideColumnEntity) {
|
||||
if (!blob_value_.empty()) {
|
||||
@@ -318,23 +455,125 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
existing_val = &value_;
|
||||
}
|
||||
} else {
|
||||
Slice value_copy = value_;
|
||||
const Status s =
|
||||
WideColumnSerialization::Deserialize(value_copy, existing_columns);
|
||||
// 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;
|
||||
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
// 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);
|
||||
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 +=
|
||||
@@ -342,10 +581,10 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
}
|
||||
|
||||
if (decision == CompactionFilter::Decision::kUndetermined) {
|
||||
// Should not reach here, since FilterV2/FilterV3 should never return
|
||||
// Should not reach here, since FilterV2/V3/V4 should never return
|
||||
// kUndetermined.
|
||||
status_ = Status::NotSupported(
|
||||
"FilterV2/FilterV3 should never return kUndetermined");
|
||||
"FilterV2/V3/V4 should never return kUndetermined");
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
@@ -354,7 +593,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/FilterV3 documentation.
|
||||
// Keep the key as per FilterV2/V3/V4 documentation.
|
||||
decision = CompactionFilter::Decision::kKeep;
|
||||
}
|
||||
|
||||
@@ -442,6 +681,16 @@ 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;
|
||||
@@ -450,9 +699,14 @@ 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();
|
||||
@@ -1266,6 +1520,340 @@ 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_)) {
|
||||
@@ -1273,6 +1861,37 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1421,21 +2040,32 @@ uint64_t CompactionIterator::ComputeBlobGarbageCollectionCutoffFileNumber(
|
||||
}
|
||||
|
||||
std::unique_ptr<BlobFetcher> CompactionIterator::CreateBlobFetcherIfNeeded(
|
||||
const CompactionProxy* compaction) {
|
||||
if (!compaction) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const Version* const version = compaction->input_version();
|
||||
if (!version) {
|
||||
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) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.io_activity = Env::IOActivity::kCompaction;
|
||||
read_options.io_activity = compaction != nullptr
|
||||
? Env::IOActivity::kCompaction
|
||||
: blob_read_io_activity;
|
||||
read_options.fill_cache = false;
|
||||
|
||||
return std::unique_ptr<BlobFetcher>(new BlobFetcher(version, read_options));
|
||||
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));
|
||||
}
|
||||
|
||||
std::unique_ptr<PrefetchBufferCollection>
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
#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"
|
||||
@@ -19,12 +21,69 @@
|
||||
#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.
|
||||
@@ -213,34 +272,43 @@ 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 = {});
|
||||
std::optional<SequenceNumber> preserve_seqno_min = {},
|
||||
const Version* input_version = nullptr,
|
||||
Env::IOActivity blob_read_io_activity = Env::IOActivity::kCompaction);
|
||||
|
||||
// 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 = {});
|
||||
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();
|
||||
|
||||
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.
|
||||
@@ -302,6 +370,12 @@ 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.
|
||||
@@ -312,6 +386,39 @@ 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);
|
||||
@@ -352,7 +459,8 @@ class CompactionIterator {
|
||||
static uint64_t ComputeBlobGarbageCollectionCutoffFileNumber(
|
||||
const CompactionProxy* compaction);
|
||||
static std::unique_ptr<BlobFetcher> CreateBlobFetcherIfNeeded(
|
||||
const CompactionProxy* compaction);
|
||||
const CompactionProxy* compaction, const Version* input_version,
|
||||
Env::IOActivity blob_read_io_activity);
|
||||
static std::unique_ptr<PrefetchBufferCollection>
|
||||
CreatePrefetchBufferCollectionIfNeeded(const CompactionProxy* compaction);
|
||||
|
||||
@@ -376,6 +484,7 @@ 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_;
|
||||
@@ -479,6 +588,32 @@ 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
+105
-15
@@ -33,6 +33,7 @@
|
||||
#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"
|
||||
@@ -57,6 +58,7 @@
|
||||
#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 {
|
||||
@@ -103,6 +105,8 @@ const char* GetCompactionReasonString(CompactionReason compaction_reason) {
|
||||
return "RoundRobinTtl";
|
||||
case CompactionReason::kRefitLevel:
|
||||
return "RefitLevel";
|
||||
case CompactionReason::kReadTriggered:
|
||||
return "ReadTriggered";
|
||||
case CompactionReason::kNumOfReasons:
|
||||
// fall through
|
||||
default:
|
||||
@@ -895,6 +899,10 @@ 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
|
||||
@@ -903,17 +911,21 @@ 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 verify_table_read_options(Env::IOActivity::kCompaction);
|
||||
verify_table_read_options.verify_checksums = true;
|
||||
verify_table_read_options.readahead_size =
|
||||
ReadOptions verification_read_options(Env::IOActivity::kCompaction);
|
||||
verification_read_options.verify_checksums = true;
|
||||
verification_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();
|
||||
verify_table_read_options.rate_limiter_priority =
|
||||
verification_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(
|
||||
verify_table_read_options, file_options_, cfd->internal_comparator(),
|
||||
verification_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,
|
||||
@@ -924,7 +936,10 @@ Status CompactionJob::VerifyOutputFiles() {
|
||||
MaxFileSizeForL0MetaPin(compact_->compaction->mutable_cf_options()),
|
||||
/*smallest_compaction_key=*/nullptr,
|
||||
/*largest_compaction_key=*/nullptr,
|
||||
/*allow_unprepared_value=*/false);
|
||||
/*allow_unprepared_value=*/false,
|
||||
/*range_del_read_seqno=*/nullptr,
|
||||
/*range_del_iter=*/nullptr,
|
||||
/*maybe_pin_table_handle=*/false, file_open_metadata_ptr);
|
||||
auto s = iter->status();
|
||||
if (s.ok()) {
|
||||
// Check for remote/local compaction and verify_output_flags flags
|
||||
@@ -941,12 +956,17 @@ 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(
|
||||
verify_table_read_options, TableReaderCaller::kCompaction,
|
||||
verification_read_options, TableReaderCaller::kCompaction,
|
||||
/*meta_blocks_only=*/should_verify_iteration);
|
||||
}
|
||||
if (s.ok() && should_verify_iteration) {
|
||||
@@ -967,6 +987,24 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -976,6 +1014,27 @@ 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++) {
|
||||
@@ -1092,6 +1151,9 @@ Status CompactionJob::Run() {
|
||||
status = VerifyOutputFiles();
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("CompactionJob::Run():AfterVerifyOutputFiles",
|
||||
&status);
|
||||
|
||||
if (status.ok()) {
|
||||
SetOutputTableProperties();
|
||||
}
|
||||
@@ -1278,6 +1340,16 @@ 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;
|
||||
}
|
||||
@@ -2326,11 +2398,16 @@ Status CompactionJob::InstallCompactionResults(bool* compaction_released) {
|
||||
*compaction_released = true;
|
||||
};
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
void CompactionJob::RecordCompactionIOStats() {
|
||||
@@ -2470,8 +2547,16 @@ 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(),
|
||||
paranoid_file_checks_);
|
||||
enable_output_hash);
|
||||
}
|
||||
|
||||
writable_file->SetIOPriority(GetRateLimiterPriority());
|
||||
@@ -2512,7 +2597,7 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
|
||||
void CompactionJob::CleanupCompaction() {
|
||||
for (SubcompactionState& sub_compact : compact_->sub_compact_states) {
|
||||
sub_compact.Cleanup(table_cache_.get());
|
||||
sub_compact.Cleanup(table_cache_.get(), compact_->status);
|
||||
}
|
||||
delete compact_;
|
||||
compact_ = nullptr;
|
||||
@@ -2904,12 +2989,17 @@ 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(),
|
||||
paranoid_file_checks_, true /* finished */);
|
||||
enable_output_hash, true /* finished */);
|
||||
|
||||
outputs_to_restore->UpdateTableProperties(
|
||||
*output_files_table_properties[i]);
|
||||
|
||||
@@ -60,6 +60,7 @@ 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);
|
||||
|
||||
@@ -102,10 +102,10 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
|
||||
for (auto ritr = level_files.rbegin(); ritr != level_files.rend(); ++ritr) {
|
||||
FileMetaData* f = *ritr;
|
||||
assert(f);
|
||||
if (f->fd.table_reader && f->fd.table_reader->GetTableProperties()) {
|
||||
TableReader* reader = f->fd.pinned_reader.Get();
|
||||
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
|
||||
uint64_t newest_key_time = f->TryGetNewestKeyTime();
|
||||
uint64_t creation_time =
|
||||
f->fd.table_reader->GetTableProperties()->creation_time;
|
||||
uint64_t creation_time = reader->GetTableProperties()->creation_time;
|
||||
uint64_t est_newest_key_time = newest_key_time == kUnknownNewestKeyTime
|
||||
? creation_time
|
||||
: newest_key_time;
|
||||
@@ -166,8 +166,9 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
|
||||
assert(f);
|
||||
uint64_t newest_key_time = f->TryGetNewestKeyTime();
|
||||
uint64_t creation_time = 0;
|
||||
if (f->fd.table_reader && f->fd.table_reader->GetTableProperties()) {
|
||||
creation_time = f->fd.table_reader->GetTableProperties()->creation_time;
|
||||
TableReader* reader = f->fd.pinned_reader.Get();
|
||||
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
|
||||
creation_time = reader->GetTableProperties()->creation_time;
|
||||
}
|
||||
uint64_t est_newest_key_time = newest_key_time == kUnknownNewestKeyTime
|
||||
? creation_time
|
||||
@@ -473,8 +474,28 @@ Compaction* FIFOCompactionPicker::PickIntraL0Compaction(
|
||||
}
|
||||
|
||||
if (fifo_opts.use_kv_ratio_compaction) {
|
||||
return PickRatioBasedIntraL0Compaction(
|
||||
cf_name, mutable_cf_options, mutable_db_options, vstorage, log_buffer);
|
||||
if (fifo_opts.max_data_files_size == 0) {
|
||||
ROCKS_LOG_BUFFER(
|
||||
log_buffer,
|
||||
"[%s] FIFO kv-ratio compaction: skipping — "
|
||||
"max_data_files_size is 0, cannot compute target file size. ",
|
||||
cf_name.c_str());
|
||||
} else if (fifo_opts.max_data_files_size < fifo_opts.max_table_files_size) {
|
||||
ROCKS_LOG_BUFFER(log_buffer,
|
||||
"[%s] FIFO kv-ratio compaction: skipping — "
|
||||
"max_data_files_size (%" PRIu64
|
||||
") < max_table_files_size "
|
||||
"(%" PRIu64 ").",
|
||||
cf_name.c_str(), fifo_opts.max_data_files_size,
|
||||
fifo_opts.max_table_files_size);
|
||||
} else {
|
||||
return PickRatioBasedIntraL0Compaction(cf_name, mutable_cf_options,
|
||||
mutable_db_options, vstorage,
|
||||
log_buffer);
|
||||
}
|
||||
ROCKS_LOG_BUFFER(
|
||||
log_buffer, "[%s] FIFO: falling back to cost-based intra-L0 compaction",
|
||||
cf_name.c_str());
|
||||
}
|
||||
|
||||
// Old intra-L0 path: merge small files using PickCostBasedIntraL0Compaction.
|
||||
|
||||
@@ -36,6 +36,9 @@ bool LevelCompactionPicker::NeedsCompaction(
|
||||
if (!vstorage->FilesMarkedForForcedBlobGC().empty()) {
|
||||
return true;
|
||||
}
|
||||
if (!vstorage->ReadTriggeredCompactionFiles().empty()) {
|
||||
return true;
|
||||
}
|
||||
for (int i = 0; i <= vstorage->MaxInputLevel(); i++) {
|
||||
if (vstorage->CompactionScore(i) >= 1) {
|
||||
return true;
|
||||
@@ -325,6 +328,14 @@ void LevelCompactionBuilder::SetupInitialFiles() {
|
||||
compaction_reason_ = CompactionReason::kForcedBlobGC;
|
||||
return;
|
||||
}
|
||||
|
||||
// Read-triggered compaction
|
||||
PickFileToCompact(vstorage_->ReadTriggeredCompactionFiles(),
|
||||
CompactToNextLevel::kYes);
|
||||
if (!start_level_inputs_.empty()) {
|
||||
compaction_reason_ = CompactionReason::kReadTriggered;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool LevelCompactionBuilder::SetupOtherL0FilesIfNeeded() {
|
||||
@@ -404,7 +415,15 @@ void LevelCompactionBuilder::SetupOtherFilesWithRoundRobinExpansion() {
|
||||
tmp_start_level_inputs = start_level_inputs_;
|
||||
// TODO (zichen): Future parallel round-robin may also need to update this
|
||||
// Constraint 1b (only expand till the end)
|
||||
for (size_t i = start_index + 1; i < level_files.size(); i++) {
|
||||
size_t next_index = start_index + 1;
|
||||
const FileMetaData* last_file = start_level_inputs_.files.back();
|
||||
for (size_t j = next_index; j < level_files.size(); j++) {
|
||||
if (level_files[j] == last_file) {
|
||||
next_index = j + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (size_t i = next_index; i < level_files.size(); i++) {
|
||||
auto* f = level_files[i];
|
||||
if (f->being_compacted) {
|
||||
// Constraint 1a
|
||||
|
||||
@@ -178,7 +178,8 @@ class CompactionPickerTestBase : public testing::Test {
|
||||
}
|
||||
TableProperties tp;
|
||||
tp.newest_key_time = newest_key_time;
|
||||
f->fd.table_reader = new mock::MockTableReader(mock::KVVector{}, tp);
|
||||
f->fd.pinned_reader.TEST_SetReader(
|
||||
new mock::MockTableReader(mock::KVVector{}, tp));
|
||||
|
||||
vstorage->AddFile(level, f);
|
||||
files_.emplace_back(f);
|
||||
@@ -272,8 +273,9 @@ class CompactionPickerTestBase : public testing::Test {
|
||||
|
||||
void ClearFiles() {
|
||||
for (auto& file : files_) {
|
||||
if (file->fd.table_reader != nullptr) {
|
||||
delete file->fd.table_reader;
|
||||
TableReader* t = file->fd.pinned_reader.Get();
|
||||
if (t != nullptr) {
|
||||
delete t;
|
||||
}
|
||||
}
|
||||
files_.clear();
|
||||
@@ -982,6 +984,186 @@ TEST_F(CompactionPickerTest, UniversalIncrementalSpace1) {
|
||||
ASSERT_EQ(14U, compaction->input(1, 3)->fd.GetNumber());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, ReadTriggeredCompactionDisabled) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
// threshold=0 means disabled, no files should be marked
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 0.0;
|
||||
Add(0, 1U, "100", "200", 1000U);
|
||||
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(999999);
|
||||
UpdateVersionStorageInfo();
|
||||
ASSERT_TRUE(vstorage_->ReadTriggeredCompactionFiles().empty());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, ReadTriggeredCompactionBelowThreshold) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 1.0;
|
||||
// file_size=1000, reads=500 => reads_per_byte=0.5 < 1.0
|
||||
Add(0, 1U, "100", "200", 1000U);
|
||||
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(500);
|
||||
UpdateVersionStorageInfo();
|
||||
ASSERT_TRUE(vstorage_->ReadTriggeredCompactionFiles().empty());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, ReadTriggeredCompactionAboveThreshold) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 0.5;
|
||||
// file_size=1000, reads=600 => reads_per_byte=0.6 > 0.5
|
||||
Add(0, 1U, "100", "200", 1000U);
|
||||
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(600);
|
||||
// file_size=1000, reads=300 => reads_per_byte=0.3 < 0.5 (not marked)
|
||||
Add(1, 2U, "300", "400", 1000U);
|
||||
file_map_[2U].first->stats.num_collapsible_entry_reads_sampled.store(300);
|
||||
// file_size=1000, reads=800 => reads_per_byte=0.8 > 0.5 (hottest)
|
||||
Add(2, 3U, "500", "600", 1000U);
|
||||
file_map_[3U].first->stats.num_collapsible_entry_reads_sampled.store(800);
|
||||
// Add a file at the bottom so L2 is not the last non-empty level
|
||||
Add(4, 4U, "700", "800", 1000U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
const auto& marked = vstorage_->ReadTriggeredCompactionFiles();
|
||||
ASSERT_EQ(marked.size(), 2);
|
||||
// Sorted by reads_per_byte descending: file 3 (0.8) then file 1 (0.6)
|
||||
ASSERT_EQ(marked[0].second->fd.GetNumber(), 3U);
|
||||
ASSERT_EQ(marked[1].second->fd.GetNumber(), 1U);
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, NeedsCompactionReadTriggered) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 0.1;
|
||||
Add(1, 1U, "100", "200", 1000U);
|
||||
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(500);
|
||||
Add(3, 2U, "300", "400", 1000U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
ASSERT_FALSE(vstorage_->ReadTriggeredCompactionFiles().empty());
|
||||
ASSERT_TRUE(level_compaction_picker.NeedsCompaction(vstorage_.get()));
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, ReadTriggeredPicksFile) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 0.1;
|
||||
Add(1, 1U, "100", "200", 1000U);
|
||||
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(500);
|
||||
Add(3, 2U, "300", "400", 1000U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
std::unique_ptr<Compaction> compaction(level_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_,
|
||||
/*existing_snapshots=*/{}, /*snapshot_checker=*/nullptr, vstorage_.get(),
|
||||
&log_buffer_, /*full_history_ts_low=*/""));
|
||||
ASSERT_TRUE(compaction.get() != nullptr);
|
||||
ASSERT_EQ(compaction->compaction_reason(), CompactionReason::kReadTriggered);
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, UniversalReadTriggeredCompaction) {
|
||||
const uint64_t kFileSize = 100000;
|
||||
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 0.001;
|
||||
// Set trigger high so size amp / sorted run pickers don't fire
|
||||
mutable_cf_options_.level0_file_num_compaction_trigger = 10;
|
||||
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
|
||||
|
||||
NewVersionStorage(5, kCompactionStyleUniversal);
|
||||
|
||||
// Hot file at L2 with data at L4 below it
|
||||
Add(0, 1U, "150", "200", kFileSize, 0, 500, 550);
|
||||
Add(2, 2U, "301", "350", kFileSize, 0, 201, 250);
|
||||
Add(4, 3U, "301", "400", kFileSize, 0, 101, 150);
|
||||
|
||||
// Mark file 2 (L2) as having high reads
|
||||
file_map_[2U].first->stats.num_collapsible_entry_reads_sampled.store(
|
||||
kFileSize);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
ASSERT_TRUE(universal_compaction_picker.NeedsCompaction(vstorage_.get()));
|
||||
|
||||
std::unique_ptr<Compaction> compaction(
|
||||
universal_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_,
|
||||
/*existing_snapshots=*/{}, /*snapshot_checker=*/nullptr,
|
||||
vstorage_.get(), &log_buffer_, /*full_history_ts_low=*/""));
|
||||
|
||||
ASSERT_TRUE(compaction);
|
||||
ASSERT_EQ(compaction->compaction_reason(), CompactionReason::kReadTriggered);
|
||||
ASSERT_EQ(compaction->start_level(), 2);
|
||||
ASSERT_EQ(compaction->output_level(), 4);
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, ReadTriggeredSkipsLastLevel) {
|
||||
const uint64_t kFileSize = 100000;
|
||||
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 0.001;
|
||||
mutable_cf_options_.level0_file_num_compaction_trigger = 10;
|
||||
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
|
||||
|
||||
NewVersionStorage(5, kCompactionStyleUniversal);
|
||||
|
||||
Add(0, 1U, "150", "200", kFileSize, 0, 500, 550);
|
||||
Add(4, 3U, "301", "350", kFileSize, 0, 101, 150);
|
||||
|
||||
// File 3 is at the last non-empty level — should NOT be marked for
|
||||
// read-triggered compaction. Bottommost file cleanup is handled
|
||||
// separately by ComputeBottommostFilesMarkedForCompaction().
|
||||
file_map_[3U].first->stats.num_collapsible_entry_reads_sampled.store(
|
||||
kFileSize);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
ASSERT_TRUE(vstorage_->ReadTriggeredCompactionFiles().empty());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, UniversalReadTriggeredNoPickWhenNotMarked) {
|
||||
const uint64_t kFileSize = 100000;
|
||||
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 0.001;
|
||||
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
|
||||
|
||||
NewVersionStorage(5, kCompactionStyleUniversal);
|
||||
|
||||
Add(0, 1U, "150", "200", kFileSize, 0, 500, 550);
|
||||
Add(4, 3U, "301", "350", kFileSize, 0, 101, 150);
|
||||
|
||||
// No reads on any file
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
ASSERT_TRUE(vstorage_->ReadTriggeredCompactionFiles().empty());
|
||||
// Not enough sorted runs to trigger compaction either
|
||||
ASSERT_FALSE(universal_compaction_picker.NeedsCompaction(vstorage_.get()));
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, UniversalReadTriggeredIntraL0) {
|
||||
const uint64_t kFileSize = 100000;
|
||||
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 0.001;
|
||||
mutable_cf_options_.level0_file_num_compaction_trigger = 10;
|
||||
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
|
||||
|
||||
// Single-level universal with overlapping L0 files
|
||||
NewVersionStorage(1, kCompactionStyleUniversal);
|
||||
|
||||
// Two L0 files with overlapping key ranges
|
||||
Add(0, 1U, "100", "300", kFileSize, 0, 500, 550);
|
||||
Add(0, 2U, "200", "400", kFileSize, 0, 400, 450);
|
||||
|
||||
// Mark file 1 as hot
|
||||
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(
|
||||
kFileSize);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
ASSERT_FALSE(vstorage_->ReadTriggeredCompactionFiles().empty());
|
||||
ASSERT_TRUE(universal_compaction_picker.NeedsCompaction(vstorage_.get()));
|
||||
|
||||
std::unique_ptr<Compaction> compaction(
|
||||
universal_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_,
|
||||
/*existing_snapshots=*/{}, /*snapshot_checker=*/nullptr,
|
||||
vstorage_.get(), &log_buffer_, /*full_history_ts_low=*/""));
|
||||
|
||||
ASSERT_TRUE(compaction);
|
||||
ASSERT_EQ(compaction->compaction_reason(), CompactionReason::kReadTriggered);
|
||||
ASSERT_EQ(compaction->start_level(), 0);
|
||||
ASSERT_EQ(compaction->output_level(), 0);
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, UniversalIncrementalSpace2) {
|
||||
const uint64_t kFileSize = 100000;
|
||||
|
||||
@@ -5205,8 +5387,8 @@ TEST_F(CompactionPickerTest, FIFORatioBasedCompactionFallbackToOldPath) {
|
||||
|
||||
// ============================================================================
|
||||
// FIFO Option Validation Tests
|
||||
// Tests that ColumnFamilyData::ValidateOptions rejects invalid configurations
|
||||
// for use_kv_ratio_compaction.
|
||||
// Tests that ColumnFamilyData::ValidateOptions accepts kv_ratio configs
|
||||
// (prerequisites are checked at runtime in the picker, not at validation time).
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CompactionPickerTest, FIFOOptionValidation) {
|
||||
@@ -5222,20 +5404,30 @@ TEST_F(CompactionPickerTest, FIFOOptionValidation) {
|
||||
return ColumnFamilyData::ValidateOptions(DBOptions(), cf_opts);
|
||||
};
|
||||
|
||||
// use_kv_ratio_compaction requires FIFO compaction style
|
||||
ASSERT_TRUE(validate([](auto& o) {
|
||||
o.compaction_style = kCompactionStyleLevel;
|
||||
}).IsInvalidArgument());
|
||||
// These used to be InvalidArgument, now accepted (handled at runtime
|
||||
// in the picker with fallback to cost-based intra-L0 compaction):
|
||||
|
||||
// use_kv_ratio_compaction requires allow_compaction
|
||||
ASSERT_TRUE(validate([](auto& o) {
|
||||
o.compaction_options_fifo.allow_compaction = false;
|
||||
}).IsInvalidArgument());
|
||||
// use_kv_ratio_compaction with non-FIFO compaction style
|
||||
ASSERT_OK(
|
||||
validate([](auto& o) { o.compaction_style = kCompactionStyleLevel; }));
|
||||
|
||||
// use_kv_ratio_compaction requires max_data_files_size > 0
|
||||
ASSERT_TRUE(validate([](auto& o) {
|
||||
o.compaction_options_fifo.max_data_files_size = 0;
|
||||
}).IsInvalidArgument());
|
||||
// use_kv_ratio_compaction without allow_compaction
|
||||
ASSERT_OK(validate(
|
||||
[](auto& o) { o.compaction_options_fifo.allow_compaction = false; }));
|
||||
|
||||
// use_kv_ratio_compaction without max_data_files_size
|
||||
ASSERT_OK(validate(
|
||||
[](auto& o) { o.compaction_options_fifo.max_data_files_size = 0; }));
|
||||
|
||||
// max_data_files_size < max_table_files_size
|
||||
ASSERT_OK(validate([](auto& o) {
|
||||
o.compaction_options_fifo.use_kv_ratio_compaction = false;
|
||||
o.compaction_options_fifo.max_data_files_size = 0;
|
||||
o.compaction_options_fifo.max_table_files_size = 1ULL * 1024 * 1024 * 1024;
|
||||
o.compaction_options_fifo.max_data_files_size = 500ULL * 1024 * 1024;
|
||||
}));
|
||||
|
||||
// These should still pass:
|
||||
|
||||
// Accepts multi-level (for migration from level/universal to FIFO)
|
||||
ASSERT_OK(validate([](auto& o) { o.num_levels = 4; }));
|
||||
@@ -5243,16 +5435,6 @@ TEST_F(CompactionPickerTest, FIFOOptionValidation) {
|
||||
// Accepts valid single-level config
|
||||
ASSERT_OK(validate([](auto& /*o*/) {}));
|
||||
|
||||
// max_data_files_size < max_table_files_size is invalid when non-zero
|
||||
ASSERT_TRUE(validate([](auto& o) {
|
||||
o.compaction_options_fifo.use_kv_ratio_compaction = false;
|
||||
o.compaction_options_fifo.max_data_files_size = 0;
|
||||
o.compaction_options_fifo.max_table_files_size =
|
||||
1ULL * 1024 * 1024 * 1024;
|
||||
o.compaction_options_fifo.max_data_files_size =
|
||||
500ULL * 1024 * 1024;
|
||||
}).IsInvalidArgument());
|
||||
|
||||
// max_data_files_size == max_table_files_size is valid
|
||||
ASSERT_OK(validate([](auto& o) {
|
||||
o.compaction_options_fifo.use_kv_ratio_compaction = false;
|
||||
@@ -5262,6 +5444,50 @@ TEST_F(CompactionPickerTest, FIFOOptionValidation) {
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, FIFORatioBasedFallbackOnInvalidConfig) {
|
||||
// When use_kv_ratio_compaction is true but prerequisites are not met,
|
||||
// the picker should fall back to cost-based intra-L0 compaction.
|
||||
|
||||
// Sub-test 1: max_data_files_size == 0 -> falls back to cost-based
|
||||
{
|
||||
SetupFIFORatioBased(/*max_table_files_size=*/10 * 1024 * 1024,
|
||||
/*max_data_files_size=*/0,
|
||||
/*trigger=*/4,
|
||||
/*allow_compaction=*/true,
|
||||
/*use_kv_ratio=*/true);
|
||||
mutable_cf_options_.max_compaction_bytes = 10ULL * 1024 * 1024;
|
||||
FIFOCompactionPicker picker(ioptions_, &icmp_);
|
||||
Add(0, 1U, "100", "200", 64 * 1024);
|
||||
Add(0, 2U, "200", "300", 64 * 1024);
|
||||
Add(0, 3U, "300", "400", 64 * 1024);
|
||||
Add(0, 4U, "400", "500", 64 * 1024);
|
||||
auto compaction = PickFIFOCompaction(picker);
|
||||
// kv_ratio skipped, cost-based should pick these files
|
||||
ASSERT_NE(nullptr, compaction.get());
|
||||
ASSERT_EQ(CompactionReason::kFIFOReduceNumFiles,
|
||||
compaction->compaction_reason());
|
||||
}
|
||||
|
||||
// Sub-test 2: max_data_files_size < max_table_files_size -> falls back
|
||||
{
|
||||
SetupFIFORatioBased(/*max_table_files_size=*/1ULL * 1024 * 1024 * 1024,
|
||||
/*max_data_files_size=*/500ULL * 1024 * 1024,
|
||||
/*trigger=*/4,
|
||||
/*allow_compaction=*/true,
|
||||
/*use_kv_ratio=*/true);
|
||||
mutable_cf_options_.max_compaction_bytes = 10ULL * 1024 * 1024;
|
||||
FIFOCompactionPicker picker(ioptions_, &icmp_);
|
||||
Add(0, 1U, "100", "200", 64 * 1024);
|
||||
Add(0, 2U, "200", "300", 64 * 1024);
|
||||
Add(0, 3U, "300", "400", 64 * 1024);
|
||||
Add(0, 4U, "400", "500", 64 * 1024);
|
||||
auto compaction = PickFIFOCompaction(picker);
|
||||
ASSERT_NE(nullptr, compaction.get());
|
||||
ASSERT_EQ(CompactionReason::kFIFOReduceNumFiles,
|
||||
compaction->compaction_reason());
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FIFO Ratio-Based Compaction: Multi-Level Migration Graceful Skip
|
||||
// Tests that PickRatioBasedIntraL0Compaction gracefully skips when non-L0
|
||||
|
||||
@@ -275,6 +275,21 @@ class UniversalCompactionBuilder {
|
||||
return c;
|
||||
}
|
||||
|
||||
Compaction* MaybePickReadTriggeredCompaction(
|
||||
Compaction* const prev_picked_c) {
|
||||
if (prev_picked_c != nullptr ||
|
||||
vstorage_->ReadTriggeredCompactionFiles().empty()) {
|
||||
return prev_picked_c;
|
||||
}
|
||||
Compaction* c = PickReadTriggeredCompaction();
|
||||
if (c != nullptr) {
|
||||
ROCKS_LOG_BUFFER(log_buffer_,
|
||||
"[%s] Universal: picked for read triggered compaction\n",
|
||||
cf_name_.c_str());
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
// Pick Universal compaction to limit read amplification
|
||||
Compaction* PickCompactionToReduceSortedRuns(
|
||||
unsigned int ratio, unsigned int max_number_of_files_to_compact);
|
||||
@@ -292,6 +307,14 @@ class UniversalCompactionBuilder {
|
||||
|
||||
Compaction* PickDeleteTriggeredCompaction();
|
||||
|
||||
Compaction* PickReadTriggeredCompaction();
|
||||
|
||||
// Given already-selected start_level_inputs, find the first non-empty output
|
||||
// level, compute overlapping inputs, and create a Compaction. Can produce
|
||||
// intra-L0 compaction if there is only level 0.
|
||||
Compaction* BuildCompactionToNextLevel(
|
||||
CompactionInputFiles& start_level_inputs, CompactionReason reason);
|
||||
|
||||
// Returns true if this given file (that is marked be compaction) should be
|
||||
// skipped from being picked for now. We do this to best use standalone range
|
||||
// tombstone files.
|
||||
@@ -594,6 +617,9 @@ bool UniversalCompactionPicker::NeedsCompaction(
|
||||
if (!vstorage->FilesMarkedForCompaction().empty()) {
|
||||
return true;
|
||||
}
|
||||
if (!vstorage->ReadTriggeredCompactionFiles().empty()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -759,6 +785,7 @@ Compaction* UniversalCompactionBuilder::PickCompaction() {
|
||||
if (sorted_runs_.size() == 0 ||
|
||||
(vstorage_->FilesMarkedForPeriodicCompaction().empty() &&
|
||||
vstorage_->FilesMarkedForCompaction().empty() &&
|
||||
vstorage_->ReadTriggeredCompactionFiles().empty() &&
|
||||
sorted_runs_.size() < (unsigned int)file_num_compaction_trigger)) {
|
||||
ROCKS_LOG_BUFFER(log_buffer_, "[%s] Universal: nothing to do\n",
|
||||
cf_name_.c_str());
|
||||
@@ -782,6 +809,7 @@ Compaction* UniversalCompactionBuilder::PickCompaction() {
|
||||
c = MaybePickCompactionToReduceSortedRuns(c, file_num_compaction_trigger,
|
||||
ratio);
|
||||
c = MaybePickDeleteTriggeredCompaction(c);
|
||||
c = MaybePickReadTriggeredCompaction(c);
|
||||
|
||||
if (c == nullptr) {
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
@@ -1461,9 +1489,6 @@ Compaction* UniversalCompactionBuilder::PickIncrementalForReduceSizeAmp(
|
||||
// CompactOnDeleteCollector due to the presence of tombstones.
|
||||
Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
|
||||
CompactionInputFiles start_level_inputs;
|
||||
int output_level;
|
||||
std::vector<CompactionInputFiles> inputs;
|
||||
std::vector<FileMetaData*> grandparents;
|
||||
|
||||
if (vstorage_->num_levels() == 1) {
|
||||
// This is single level universal. Since we're basically trying to reclaim
|
||||
@@ -1474,7 +1499,6 @@ Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
|
||||
|
||||
start_level_inputs.level = 0;
|
||||
start_level_inputs.files.clear();
|
||||
output_level = 0;
|
||||
// Find the first file marked for compaction. Ignore the last file
|
||||
for (size_t loop = 0; loop + 1 < sorted_runs_.size(); loop++) {
|
||||
SortedRun* sr = &sorted_runs_[loop];
|
||||
@@ -1503,13 +1527,9 @@ Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
|
||||
FileMetaData* f = vstorage_->LevelFiles(0)[loop];
|
||||
start_level_inputs.files.push_back(f);
|
||||
}
|
||||
if (start_level_inputs.size() <= 1) {
|
||||
// If only the last file in L0 is marked for compaction, ignore it
|
||||
return nullptr;
|
||||
}
|
||||
inputs.push_back(start_level_inputs);
|
||||
} else {
|
||||
int start_level;
|
||||
int output_level;
|
||||
|
||||
// For multi-level universal, the strategy is to make this look more like
|
||||
// leveled. We pick one of the files marked for compaction and compact with
|
||||
@@ -1522,100 +1542,10 @@ Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
|
||||
if (start_level_inputs.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int max_output_level = vstorage_->MaxOutputLevel(allow_ingest_behind_);
|
||||
// Pick the first non-empty level after the start_level
|
||||
for (output_level = start_level + 1; output_level <= max_output_level;
|
||||
output_level++) {
|
||||
if (vstorage_->NumLevelFiles(output_level) != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If all higher levels are empty, pick the highest level as output level
|
||||
if (output_level > max_output_level) {
|
||||
if (start_level == 0) {
|
||||
output_level = max_output_level;
|
||||
} else {
|
||||
// If start level is non-zero and all higher levels are empty, this
|
||||
// compaction will translate into a trivial move. Since the idea is
|
||||
// to reclaim space and trivial move doesn't help with that, we
|
||||
// skip compaction in this case and return nullptr
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
assert(output_level <= max_output_level);
|
||||
|
||||
if (!MeetsOutputLevelRequirements(output_level)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (output_level != 0) {
|
||||
// For standalone range deletion, we don't want to compact it with newer
|
||||
// L0 files that it doesn't cover.
|
||||
const FileMetaData* starting_l0_file =
|
||||
(start_level == 0 && start_level_inputs.size() == 1 &&
|
||||
start_level_inputs.files[0]->FileIsStandAloneRangeTombstone())
|
||||
? start_level_inputs.files[0]
|
||||
: nullptr;
|
||||
|
||||
if (start_level == 0) {
|
||||
if (!picker_->GetOverlappingL0Files(vstorage_, &start_level_inputs,
|
||||
output_level, nullptr,
|
||||
starting_l0_file)) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
CompactionInputFiles output_level_inputs;
|
||||
int parent_index = -1;
|
||||
|
||||
output_level_inputs.level = output_level;
|
||||
if (!picker_->SetupOtherInputs(cf_name_, mutable_cf_options_, vstorage_,
|
||||
&start_level_inputs, &output_level_inputs,
|
||||
&parent_index, -1, false,
|
||||
starting_l0_file)) {
|
||||
return nullptr;
|
||||
}
|
||||
inputs.push_back(start_level_inputs);
|
||||
if (!output_level_inputs.empty()) {
|
||||
inputs.push_back(output_level_inputs);
|
||||
}
|
||||
if (picker_->FilesRangeOverlapWithCompaction(
|
||||
inputs, output_level,
|
||||
Compaction::EvaluateProximalLevel(vstorage_, mutable_cf_options_,
|
||||
ioptions_, start_level,
|
||||
output_level))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
picker_->GetGrandparents(vstorage_, start_level_inputs,
|
||||
output_level_inputs, &grandparents);
|
||||
} else {
|
||||
inputs.push_back(start_level_inputs);
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t estimated_total_size = 0;
|
||||
// Use size of the output level as estimated file size
|
||||
for (FileMetaData* f : vstorage_->LevelFiles(output_level)) {
|
||||
estimated_total_size += f->fd.GetFileSize();
|
||||
}
|
||||
uint32_t path_id =
|
||||
GetPathId(ioptions_, mutable_cf_options_, estimated_total_size);
|
||||
return new Compaction(
|
||||
vstorage_, ioptions_, mutable_cf_options_, mutable_db_options_,
|
||||
std::move(inputs), output_level,
|
||||
MaxFileSizeForLevel(mutable_cf_options_, output_level,
|
||||
kCompactionStyleUniversal),
|
||||
/* max_grandparent_overlap_bytes */ GetMaxOverlappingBytes(), path_id,
|
||||
GetCompressionType(vstorage_, mutable_cf_options_, output_level, 1),
|
||||
GetCompressionOptions(mutable_cf_options_, vstorage_, output_level),
|
||||
Temperature::kUnknown,
|
||||
/* max_subcompactions */ 0, grandparents, earliest_snapshot_,
|
||||
snapshot_checker_, CompactionReason::kFilesMarkedForCompaction,
|
||||
/* trim_ts */ "", score_,
|
||||
/* l0_files_might_overlap */ true);
|
||||
return BuildCompactionToNextLevel(
|
||||
start_level_inputs, CompactionReason::kFilesMarkedForCompaction);
|
||||
}
|
||||
|
||||
Compaction* UniversalCompactionBuilder::PickCompactionToOldest(
|
||||
@@ -1782,6 +1712,153 @@ Compaction* UniversalCompactionBuilder::PickPeriodicCompaction() {
|
||||
return c;
|
||||
}
|
||||
|
||||
Compaction* UniversalCompactionBuilder::PickReadTriggeredCompaction() {
|
||||
ROCKS_LOG_BUFFER(log_buffer_, "[%s] Universal: Read Triggered Compaction",
|
||||
cf_name_.c_str());
|
||||
|
||||
if (vstorage_->ReadTriggeredCompactionFiles().empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CompactionInputFiles start_level_inputs;
|
||||
int start_level = -1;
|
||||
|
||||
for (const auto& level_file : vstorage_->ReadTriggeredCompactionFiles()) {
|
||||
assert(!level_file.second->being_compacted);
|
||||
start_level = level_file.first;
|
||||
|
||||
if (start_level == 0 &&
|
||||
!picker_->level0_compactions_in_progress()->empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
start_level_inputs.files = {level_file.second};
|
||||
start_level_inputs.level = start_level;
|
||||
if (picker_->ExpandInputsToCleanCut(cf_name_, vstorage_,
|
||||
&start_level_inputs)) {
|
||||
break;
|
||||
}
|
||||
start_level_inputs.files.clear();
|
||||
}
|
||||
|
||||
if (start_level_inputs.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// For intra L0 compactions, pick up all overlapping files
|
||||
if (vstorage_->num_levels() == 1 && start_level_inputs.level == 0) {
|
||||
if (!picker_->GetOverlappingL0Files(vstorage_, &start_level_inputs,
|
||||
/*output_level=*/0, nullptr, nullptr)) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
return BuildCompactionToNextLevel(start_level_inputs,
|
||||
CompactionReason::kReadTriggered);
|
||||
}
|
||||
|
||||
Compaction* UniversalCompactionBuilder::BuildCompactionToNextLevel(
|
||||
CompactionInputFiles& start_level_inputs, CompactionReason reason) {
|
||||
int start_level = start_level_inputs.level;
|
||||
int max_output_level = vstorage_->MaxOutputLevel(allow_ingest_behind_);
|
||||
|
||||
// Pick the first non-empty level after start_level as output.
|
||||
int output_level;
|
||||
for (output_level = start_level + 1; output_level <= max_output_level;
|
||||
output_level++) {
|
||||
if (vstorage_->NumLevelFiles(output_level) != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If all higher levels are empty, pick the highest level as output level
|
||||
// for L0 start. For non-L0 start, a trivial move doesn't help reclaim
|
||||
// space, so skip.
|
||||
if (output_level > max_output_level) {
|
||||
if (start_level == 0) {
|
||||
output_level = max_output_level;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (start_level_inputs.size() <= 1 && output_level == 0) {
|
||||
// If only the last file in L0 is marked for compaction, ignore it
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<CompactionInputFiles> inputs;
|
||||
std::vector<FileMetaData*> grandparents;
|
||||
|
||||
if (output_level != 0) {
|
||||
if (!MeetsOutputLevelRequirements(output_level)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// For standalone range deletion, we don't want to compact it with newer
|
||||
// L0 files that it doesn't cover.
|
||||
const FileMetaData* starting_l0_file =
|
||||
(start_level == 0 && start_level_inputs.size() == 1 &&
|
||||
start_level_inputs.files[0]->FileIsStandAloneRangeTombstone())
|
||||
? start_level_inputs.files[0]
|
||||
: nullptr;
|
||||
|
||||
if (start_level == 0) {
|
||||
if (!picker_->GetOverlappingL0Files(vstorage_, &start_level_inputs,
|
||||
output_level, nullptr,
|
||||
starting_l0_file)) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
CompactionInputFiles output_level_inputs;
|
||||
int parent_index = -1;
|
||||
|
||||
output_level_inputs.level = output_level;
|
||||
if (!picker_->SetupOtherInputs(
|
||||
cf_name_, mutable_cf_options_, vstorage_, &start_level_inputs,
|
||||
&output_level_inputs, &parent_index, -1, false, starting_l0_file)) {
|
||||
return nullptr;
|
||||
}
|
||||
inputs.push_back(start_level_inputs);
|
||||
if (!output_level_inputs.empty()) {
|
||||
inputs.push_back(output_level_inputs);
|
||||
}
|
||||
if (picker_->FilesRangeOverlapWithCompaction(
|
||||
inputs, output_level,
|
||||
Compaction::EvaluateProximalLevel(vstorage_, mutable_cf_options_,
|
||||
ioptions_, start_level,
|
||||
output_level))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
picker_->GetGrandparents(vstorage_, start_level_inputs, output_level_inputs,
|
||||
&grandparents);
|
||||
} else {
|
||||
inputs.push_back(start_level_inputs);
|
||||
}
|
||||
|
||||
uint64_t estimated_total_size = 0;
|
||||
for (FileMetaData* f : vstorage_->LevelFiles(output_level)) {
|
||||
estimated_total_size += f->fd.GetFileSize();
|
||||
}
|
||||
uint32_t path_id =
|
||||
GetPathId(ioptions_, mutable_cf_options_, estimated_total_size);
|
||||
return new Compaction(
|
||||
vstorage_, ioptions_, mutable_cf_options_, mutable_db_options_,
|
||||
std::move(inputs), output_level,
|
||||
MaxFileSizeForLevel(mutable_cf_options_, output_level,
|
||||
kCompactionStyleUniversal),
|
||||
/* max_grandparent_overlap_bytes */ GetMaxOverlappingBytes(), path_id,
|
||||
GetCompressionType(vstorage_, mutable_cf_options_, output_level, 1),
|
||||
GetCompressionOptions(mutable_cf_options_, vstorage_, output_level),
|
||||
Temperature::kUnknown,
|
||||
/* max_subcompactions */ 0, grandparents, earliest_snapshot_,
|
||||
snapshot_checker_, reason,
|
||||
/* trim_ts */ "", score_,
|
||||
/* l0_files_might_overlap */ true);
|
||||
}
|
||||
|
||||
uint64_t UniversalCompactionBuilder::GetMaxOverlappingBytes() const {
|
||||
if (!mutable_cf_options_.compaction_options_universal.incremental) {
|
||||
return std::numeric_limits<uint64_t>::max();
|
||||
|
||||
@@ -409,6 +409,34 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, SkipWALRecoveryInOpenAndCompact) {
|
||||
// Verify that OpenAndCompact skips WAL recovery when opening the secondary
|
||||
// instance. WAL replay is unnecessary for remote compaction since it only
|
||||
// needs the LSM state from MANIFEST.
|
||||
Options options = CurrentOptions();
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
// Track whether FindAndRecoverLogFiles is called during compaction
|
||||
std::atomic_bool wal_recovery_called{false};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::FindAndRecoverLogFiles:Begin",
|
||||
[&](void* /* arg */) { wal_recovery_called.store(true); });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Generate data and trigger compaction (which uses OpenAndCompact)
|
||||
GenerateTestData();
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
// WAL recovery should NOT have been called during OpenAndCompact
|
||||
ASSERT_FALSE(wal_recovery_called.load());
|
||||
|
||||
// Data should still be correct (compaction worked without WAL recovery)
|
||||
VerifyTestData();
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, ManualCompaction) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
@@ -1113,6 +1141,8 @@ TEST_F(CompactionServiceTest, CorruptedOutputVerifyOutputFlags) {
|
||||
VerifyOutputFlags::kVerifyBlockChecksum,
|
||||
VerifyOutputFlags::kEnableForRemoteCompaction |
|
||||
VerifyOutputFlags::kVerifyIteration,
|
||||
VerifyOutputFlags::kEnableForRemoteCompaction |
|
||||
VerifyOutputFlags::kVerifyFileChecksum,
|
||||
VerifyOutputFlags::kVerifyAll}) {
|
||||
SCOPED_TRACE(
|
||||
"verify_output_flags=" +
|
||||
@@ -1124,6 +1154,7 @@ TEST_F(CompactionServiceTest, CorruptedOutputVerifyOutputFlags) {
|
||||
options.disable_auto_compactions = true;
|
||||
options.paranoid_file_checks = false;
|
||||
options.verify_output_flags = verify_output_flags;
|
||||
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
|
||||
ReopenWithCompactionService(&options);
|
||||
GenerateTestData();
|
||||
|
||||
@@ -1159,10 +1190,13 @@ TEST_F(CompactionServiceTest, CorruptedOutputVerifyOutputFlags) {
|
||||
!!(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);
|
||||
|
||||
Status s = db_->CompactRange(CompactRangeOptions(), &start, &end);
|
||||
if (is_enabled_for_remote_compaction &&
|
||||
(should_verify_block_checksum || should_verify_iteration)) {
|
||||
(should_verify_block_checksum || should_verify_iteration ||
|
||||
should_verify_file_checksum)) {
|
||||
ASSERT_NOK(s);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
} else {
|
||||
|
||||
@@ -38,11 +38,11 @@ OutputIterator SubcompactionState::GetOutputs() const {
|
||||
compaction_outputs_.outputs_);
|
||||
}
|
||||
|
||||
void SubcompactionState::Cleanup(Cache* cache) {
|
||||
void SubcompactionState::Cleanup(Cache* cache, const Status& overall_status) {
|
||||
proximal_level_outputs_.Cleanup();
|
||||
compaction_outputs_.Cleanup();
|
||||
|
||||
if (!status.ok()) {
|
||||
if (!status.ok() || !overall_status.ok()) {
|
||||
for (const auto& out : GetOutputs()) {
|
||||
// If this file was inserted into the table cache then remove it here
|
||||
// because this compaction was not committed. This is not strictly
|
||||
|
||||
@@ -81,6 +81,15 @@ class SubcompactionState {
|
||||
// it returns both the last level outputs and proximal level outputs.
|
||||
OutputIterator GetOutputs() const;
|
||||
|
||||
// Get mutable access to outputs for post-processing (e.g., retrieving
|
||||
// file open metadata).
|
||||
std::vector<CompactionOutputs::Output>& GetMutableCompactionOutputs() {
|
||||
return compaction_outputs_.GetMutableOutputs();
|
||||
}
|
||||
std::vector<CompactionOutputs::Output>& GetMutableProximalOutputs() {
|
||||
return proximal_level_outputs_.GetMutableOutputs();
|
||||
}
|
||||
|
||||
// Assign range dels aggregator. The various tombstones will potentially
|
||||
// be filtered to different outputs.
|
||||
void AssignRangeDelAggregator(
|
||||
@@ -170,7 +179,7 @@ class SubcompactionState {
|
||||
}
|
||||
}
|
||||
|
||||
void Cleanup(Cache* cache);
|
||||
void Cleanup(Cache* cache, const Status& overall_status);
|
||||
|
||||
void AggregateCompactionOutputStats(
|
||||
InternalStats::CompactionStatsFull& internal_stats) const;
|
||||
|
||||
@@ -2535,6 +2535,319 @@ TEST_P(DBMultiGetTestWithParam, MultiGetBatchedValueSizeMultiLevelMerge) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetMemtableBatchLookup) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
// Skip for unbatched MultiGet
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Insert sorted keys into memtable
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), "val" + std::to_string(i)));
|
||||
}
|
||||
// Delete some keys
|
||||
ASSERT_OK(Delete(1, Key(25)));
|
||||
ASSERT_OK(Delete(1, Key(75)));
|
||||
|
||||
// MultiGet a batch of keys - mix of existing, deleted, and missing
|
||||
// Store key strings to keep Slice data alive
|
||||
std::vector<std::string> key_strs = {Key(0), Key(10), Key(25), Key(50),
|
||||
Key(75), Key(99), Key(200)};
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "val0");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "val10");
|
||||
ASSERT_TRUE(statuses[2].IsNotFound()); // deleted
|
||||
ASSERT_OK(statuses[3]);
|
||||
ASSERT_EQ(values[3].ToString(), "val50");
|
||||
ASSERT_TRUE(statuses[4].IsNotFound()); // deleted
|
||||
ASSERT_OK(statuses[5]);
|
||||
ASSERT_EQ(values[5].ToString(), "val99");
|
||||
ASSERT_TRUE(statuses[6].IsNotFound()); // never inserted
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupOverwrite) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Insert, then overwrite some keys
|
||||
for (int i = 0; i < 50; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), "old" + std::to_string(i)));
|
||||
}
|
||||
for (int i = 0; i < 50; i += 5) {
|
||||
ASSERT_OK(Put(1, Key(i), "new" + std::to_string(i)));
|
||||
}
|
||||
|
||||
std::vector<std::string> key_strs;
|
||||
for (int i = 0; i < 50; i += 5) {
|
||||
key_strs.push_back(Key(i));
|
||||
}
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
for (size_t i = 0; i < keys.size(); i++) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
ASSERT_EQ(values[i].ToString(), "new" + std::to_string(i * 5));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupWithFlush) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Put data into SST
|
||||
for (int i = 0; i < 50; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), "sst" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
// Put different data into memtable (overlapping some keys)
|
||||
for (int i = 25; i < 75; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), "mem" + std::to_string(i)));
|
||||
}
|
||||
|
||||
// MultiGet keys spanning both SST and memtable
|
||||
std::vector<std::string> key_strs = {Key(10), Key(30), Key(60), Key(80)};
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "sst10");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "mem30");
|
||||
ASSERT_OK(statuses[2]);
|
||||
ASSERT_EQ(values[2].ToString(), "mem60");
|
||||
ASSERT_TRUE(statuses[3].IsNotFound());
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupWithMerge) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Put base values
|
||||
ASSERT_OK(Put(1, Key(1), "a"));
|
||||
ASSERT_OK(Put(1, Key(2), "x"));
|
||||
// Merge on top
|
||||
ASSERT_OK(Merge(1, Key(1), "b"));
|
||||
ASSERT_OK(Merge(1, Key(1), "c"));
|
||||
ASSERT_OK(Merge(1, Key(2), "y"));
|
||||
|
||||
std::vector<std::string> key_strs = {Key(1), Key(2), Key(3)};
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "a,b,c");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "x,y");
|
||||
ASSERT_TRUE(statuses[2].IsNotFound());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetBatchLookupDisabledByDefault) {
|
||||
// Verify that finger search is off by default and MultiGet still works
|
||||
Options options = CurrentOptions();
|
||||
ASSERT_FALSE(options.memtable_batch_lookup_optimization);
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "k1", "v1"));
|
||||
ASSERT_OK(Put(1, "k2", "v2"));
|
||||
|
||||
std::vector<Slice> keys = {"k1", "k2", "k3"};
|
||||
std::vector<PinnableSlice> values(3);
|
||||
std::vector<Status> statuses(3);
|
||||
|
||||
db_->MultiGet(ReadOptions(), handles_[1], 3, keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "v1");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "v2");
|
||||
ASSERT_TRUE(statuses[2].IsNotFound());
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupWithParanoid) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
options.paranoid_memory_checks = true;
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Insert sorted keys into memtable
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), "val" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Delete(1, Key(25)));
|
||||
|
||||
// MultiGet with both batch optimization and paranoid checks enabled
|
||||
std::vector<std::string> key_strs = {Key(0), Key(10), Key(25),
|
||||
Key(50), Key(99), Key(200)};
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "val0");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "val10");
|
||||
ASSERT_TRUE(statuses[2].IsNotFound()); // deleted
|
||||
ASSERT_OK(statuses[3]);
|
||||
ASSERT_EQ(values[3].ToString(), "val50");
|
||||
ASSERT_OK(statuses[4]);
|
||||
ASSERT_EQ(values[4].ToString(), "val99");
|
||||
ASSERT_TRUE(statuses[5].IsNotFound()); // never inserted
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupSnapshot) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, Key(1), "v1_old"));
|
||||
ASSERT_OK(Put(1, Key(2), "v2_old"));
|
||||
|
||||
const Snapshot* snap = db_->GetSnapshot();
|
||||
|
||||
// Write new values after snapshot
|
||||
ASSERT_OK(Put(1, Key(1), "v1_new"));
|
||||
ASSERT_OK(Put(1, Key(2), "v2_new"));
|
||||
ASSERT_OK(Put(1, Key(3), "v3_new"));
|
||||
|
||||
// MultiGet with snapshot should see old values
|
||||
std::vector<std::string> key_strs = {Key(1), Key(2), Key(3)};
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.snapshot = snap;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "v1_old");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "v2_old");
|
||||
ASSERT_TRUE(statuses[2].IsNotFound()); // didn't exist at snapshot
|
||||
|
||||
db_->ReleaseSnapshot(snap);
|
||||
|
||||
// MultiGet without snapshot should see new values
|
||||
for (auto& v : values) {
|
||||
v.Reset();
|
||||
}
|
||||
db_->MultiGet(ReadOptions(), handles_[1], keys.size(), keys.data(),
|
||||
values.data(), statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "v1_new");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "v2_new");
|
||||
ASSERT_OK(statuses[2]);
|
||||
ASSERT_EQ(values[2].ToString(), "v3_new");
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(DBMultiGetTestWithParam, DBMultiGetTestWithParam,
|
||||
testing::Combine(testing::Bool(), testing::Bool()));
|
||||
|
||||
|
||||
@@ -466,6 +466,90 @@ TEST_F(DBBlockCacheTest, WarmCacheWithDataBlocksDuringFlush) {
|
||||
options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD));
|
||||
}
|
||||
|
||||
// Cache wrapper that tracks the priority of each Insert call.
|
||||
namespace {
|
||||
class PriorityTrackingCache : public CacheWrapper {
|
||||
public:
|
||||
explicit PriorityTrackingCache(std::shared_ptr<Cache> target)
|
||||
: CacheWrapper(std::move(target)) {}
|
||||
|
||||
const char* Name() const override { return "PriorityTrackingCache"; }
|
||||
|
||||
Status Insert(const Slice& key, ObjectPtr value,
|
||||
const CacheItemHelper* helper, size_t charge,
|
||||
Handle** handle = nullptr, Priority priority = Priority::LOW,
|
||||
const Slice& compressed_value = Slice(),
|
||||
CompressionType type = kNoCompression) override {
|
||||
insert_priorities_.push_back(priority);
|
||||
return CacheWrapper::Insert(key, value, helper, charge, handle, priority,
|
||||
compressed_value, type);
|
||||
}
|
||||
|
||||
void ResetPriorities() { insert_priorities_.clear(); }
|
||||
|
||||
bool HasPriority(Priority p) const {
|
||||
return std::find(insert_priorities_.begin(), insert_priorities_.end(), p) !=
|
||||
insert_priorities_.end();
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<Priority> insert_priorities_;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_F(DBBlockCacheTest, WarmCacheWithDataBlocksDuringCompaction) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
auto tracking_cache =
|
||||
std::make_shared<PriorityTrackingCache>(NewLRUCache(1 << 25, 0, false));
|
||||
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.block_cache = tracking_cache;
|
||||
table_options.cache_index_and_filter_blocks = false;
|
||||
table_options.prepopulate_block_cache =
|
||||
BlockBasedTableOptions::PrepopulateBlockCache::kFlushAndCompaction;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
DestroyAndReopen(options);
|
||||
|
||||
std::string value(kValueSize, 'a');
|
||||
|
||||
// Flush warming: inserts should use LOW priority.
|
||||
tracking_cache->ResetPriorities();
|
||||
ASSERT_OK(Put("key", value));
|
||||
ASSERT_OK(Flush());
|
||||
EXPECT_TRUE(tracking_cache->HasPriority(Cache::Priority::LOW));
|
||||
EXPECT_FALSE(tracking_cache->HasPriority(Cache::Priority::BOTTOM));
|
||||
|
||||
// Write overlapping key to force a real merge compaction (not trivial move).
|
||||
ASSERT_OK(Put("key", value + "2"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
auto data_add_before =
|
||||
options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD);
|
||||
|
||||
// Compaction warming: data block inserts should use BOTTOM priority.
|
||||
// Internal cache bookkeeping (e.g., cache entry stats) may insert at HIGH,
|
||||
// so we check for BOTTOM presence and LOW absence.
|
||||
tracking_cache->ResetPriorities();
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForceOptimized;
|
||||
ASSERT_OK(db_->CompactRange(cro, /*begin=*/nullptr, /*end=*/nullptr));
|
||||
EXPECT_GT(options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD),
|
||||
data_add_before);
|
||||
EXPECT_TRUE(tracking_cache->HasPriority(Cache::Priority::BOTTOM));
|
||||
EXPECT_FALSE(tracking_cache->HasPriority(Cache::Priority::LOW));
|
||||
|
||||
// Compaction output is in cache — reads should have zero misses.
|
||||
auto data_miss_before =
|
||||
options.statistics->getTickerCount(BLOCK_CACHE_DATA_MISS);
|
||||
ASSERT_EQ(value + "2", Get("key"));
|
||||
EXPECT_EQ(data_miss_before,
|
||||
options.statistics->getTickerCount(BLOCK_CACHE_DATA_MISS));
|
||||
}
|
||||
|
||||
// This test cache data, index and filter blocks during flush.
|
||||
class DBBlockCacheTest1 : public DBTestBase,
|
||||
public ::testing::WithParamInterface<uint32_t> {
|
||||
@@ -620,6 +704,36 @@ TEST_F(DBBlockCacheTest, DynamicOptions) {
|
||||
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
|
||||
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
|
||||
|
||||
// Switch to kFlushAndCompaction
|
||||
++i;
|
||||
ASSERT_OK(dbfull()->SetOptions(
|
||||
{{"block_based_table_factory",
|
||||
"{prepopulate_block_cache=kFlushAndCompaction;}"}}));
|
||||
|
||||
ASSERT_OK(Put(std::to_string(i), value));
|
||||
ASSERT_OK(Flush());
|
||||
// Flush warming still works
|
||||
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
|
||||
|
||||
ASSERT_EQ(value, Get(std::to_string(i)));
|
||||
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
|
||||
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
|
||||
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
|
||||
|
||||
// Switch back to kDisable
|
||||
++i;
|
||||
ASSERT_OK(dbfull()->SetOptions(
|
||||
{{"block_based_table_factory", "{prepopulate_block_cache=kDisable;}"}}));
|
||||
|
||||
ASSERT_OK(Put(std::to_string(i), value));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
|
||||
|
||||
ASSERT_EQ(value, Get(std::to_string(i)));
|
||||
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
|
||||
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
|
||||
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
|
||||
|
||||
++i;
|
||||
// NOT YET SUPPORTED
|
||||
// FIXME: find a way to make this fail again (until well supported)
|
||||
|
||||
Loaded 100 of 421 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user