Compare commits

..
12 Commits
Author SHA1 Message Date
Peter Dillinger a908554251 Finalize patch release 11.0.4 2026-03-11 13:40:25 -07:00
Peter Dillinger c5af7e0de2 Re-remove deprecated DB::Open raw pointer variants
(This essentially reverts commit 2e5f177eeb2532714e5b99f81163a657921a6d5c.)
2026-03-06 11:39:45 -08:00
Xingbo Wang 44f3e1d68d 11.0.3 patch 2026-03-04 11:12:19 -08:00
Xingbo Wang c31c3ef191 Relax option sanitization for kv ratio compaction (#14397)
Summary:
The kv ratio compaction option sanitization is too strict. Sometimes, it blocks engine start up. Relax the validation and convert it to runtime check.

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

Test Plan: Unit test

Reviewed By: pdillinger

Differential Revision: D94709471

Pulled By: xingbowang

fbshipit-source-id: cc076c397b3acfa426112063224771a196684798
2026-03-04 11:12:19 -08:00
Hui Xiao 87832c60a2 11.0.2 patch 2026-02-26 22:35:00 -08:00
Hui Xiao 2e5f177eeb Revert removing deprecated DB::Open raw pointer variants 2026-02-26 22:34:50 -08:00
Hui Xiao 5c9a51b2c2 11.0.1 Patch 2026-02-26 20:01:23 -08:00
Hui Xiao 6aa939b288 Pass statistics and fix null clock in SstFileReader::MultiGet (#14393)
Summary:
**Summary:**
This is to sync an internal change of passing `Statistics*` to the GetContext constructor and collecting more stats as well as fix a bug this change created.

SstFileReader::MultiGet was passing nullptr for both `SystemClock*` and `Statistics*` to the GetContext constructor.  After  `Statistics*` was passed to the GetContext constructor (the internal change), this caused a segfault when a merge operation was triggered with statistics enabled, because the merge helper's StopWatchNano attempted to dereference the null clock pointer. Fix by passing `r->ioptions.clock` from the reader's options.

Additionally, add `assert(clock_)` guards to `StopWatchNano::Start()` and `ElapsedNanos()` to catch null clock bugs in debug builds. Can't do so in release build because it's on hot path.

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

Test Plan:
- `./sst_file_reader_test --gtest_filter='SstFileReaderTableMultiGetTest.Basic'` exercises merge with statistics enabled, previously segfaulted without the fix with the clock, now passes.
- `./sst_file_reader_test` - all tests pass.

Reviewed By: xingbowang

Differential Revision: D94599343

Pulled By: hx235

fbshipit-source-id: 0a748bb00ee27bb202d01d410b52657101c05de0
2026-02-26 19:46:00 -08:00
Josh Kang 2e985ae69d Fix missing corruption check for key overflows (#14384)
Summary:
The refactoring in https://github.com/facebook/rocksdb/pull/14287/changes#diff-9f58e06172d3c8b15496fb4e8216a9ceee07ea2648e4c93d6a105d578697aa07L101-L103 unintentionally removed a corruption check for a key existing past its limit, but does account for a value existing past its limit.

This fix adds an explicit key check as well

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

Test Plan: CI

Reviewed By: pdillinger

Differential Revision: D94275062

Pulled By: joshkang97

fbshipit-source-id: ba793bbb966f0bae33df4c98e498e3d6a2b04087
2026-02-25 13:00:41 -08:00
Peter Dillinger d180f08a66 Restore blob_dump_tool compression support (partially revert PR #14266) (#14382)
Summary:
PR https://github.com/facebook/rocksdb/issues/14266 ("Remove compression support") removed compression-related functionality from blob_dump_tool and ldb commands. While this was valid for the legacy stacked BlobDB (which no longer supports compression), the integrated blob storage (`enable_blob_files`) uses the same blob file format and fully supports compression via `blob_compression_type`.

This partial revert restores the ability to view uncompressed blob data from compressed blob files, which is essential for debugging and analysis of integrated blob storage.

Restored functionality:
- `blob_dump --show_uncompressed_blob` option
- `ldb dump --dump_uncompressed_blobs` option
- `ldb dump_live_files --dump_uncompressed_blobs` option
- Related ldb_test.py test coverage

The decompression implementation in utilities/blob_db/blob_dump_tool.cc has been updated to use the modern
`GetBuiltinV2CompressionManager()->GetDecompressorOptimizeFor()` API.

Suggested follow-up:
* Tests for blob_dump (or integrate it into sst_dump/ldb?)

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

Test Plan:
Restored ldb_test.py
Some manual testing

Reviewed By: mszeszko-meta

Differential Revision: D94257588

Pulled By: pdillinger

fbshipit-source-id: c6d8a556a51ec9422df208ae161806ccc1f20b36
2026-02-24 16:02:43 -08:00
xingbowang b7f2c73fc8 Fix clang tidy workflow (#14380)
Summary:
Fix clang tidy workflow

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

Test Plan: Unit test

Reviewed By: joshkang97

Differential Revision: D94237335

Pulled By: xingbowang

fbshipit-source-id: ed794cc13e684b3d6d11e20ce33edb4d6de79c9b
2026-02-24 15:37:59 -08:00
Hui Xiao f96414d976 11.0.fb 2026-02-23 20:43:17 -08:00
368 changed files with 3112 additions and 59396 deletions

No files matched your search

+3 -1
View File
@@ -17,7 +17,9 @@ Checks: "-*,\
cert-err58-cpp,\
cert-msc30-c,\
cert-msc50-cpp,\
clang-analyzer-*,\
clang-analyzer-core.NullDereference,\
clang-analyzer-core.StackAddressEscape,\
clang-analyzer-deadcode.DeadStores,\
clang-diagnostic-*,\
-clang-diagnostic-missing-designated-field-initializers,\
concurrency-mt-unsafe,\
+1 -10
View File
@@ -9,16 +9,7 @@ runs:
steps:
- name: Build folly and dependencies
if: ${{ inputs.cache-hit != 'true' }}
run: |
clean_path=()
IFS=: read -ra path_entries <<< "$PATH"
for entry in "${path_entries[@]}"; do
if [[ "$entry" != "/usr/lib/ccache" ]]; then
clean_path+=("$entry")
fi
done
export PATH="$(IFS=:; echo "${clean_path[*]}")"
make build_folly
run: make build_folly
shell: bash
- name: Skip folly build (using cached version)
if: ${{ inputs.cache-hit == 'true' }}
+1 -1
View File
@@ -30,4 +30,4 @@ runs:
# - The docker image, which may not always be specified/known
# - Hash of folly.mk, which includes the folly repository commit hash
# NOTE: this is still only intended for DEBUG folly builds
key: folly-build-${{ runner.os }}-${{ runner.arch }}-${{ github.job_container.image }}-${{ steps.extract-folly-hash.outputs.hash }}-${{ env.PORTABLE == '1' && 'portable' || 'native' }}
key: folly-build-${{ runner.os }}-${{ runner.arch }}-${{ github.job_container.image }}-${{ steps.extract-folly-hash.outputs.hash }}
-3
View File
@@ -2,9 +2,6 @@ name: pre-steps
runs:
using: composite
steps:
- name: Install lld linker for faster builds
run: apt-get update -y && apt-get install -y lld 2>/dev/null || true
shell: bash
- name: Setup Environment Variables
run: |-
echo "GTEST_THROW_ON_FAILURE=0" >> "$GITHUB_ENV"
-54
View File
@@ -1,54 +0,0 @@
name: setup-ccache
description: Setup ccache for faster C++ compilation caching
inputs:
cache-key-prefix:
description: Unique prefix for the cache key (e.g., 'build-linux')
required: true
portable:
description: Set PORTABLE=1 to disable -march=native (set to "false" for jobs linking pre-built Folly)
required: false
default: "true"
runs:
using: composite
steps:
- name: Set ccache environment variables
run: |
echo "CCACHE_DIR=${{ github.workspace }}/.ccache" >> $GITHUB_ENV
echo "CCACHE_BASEDIR=${{ github.workspace }}" >> $GITHUB_ENV
echo "CCACHE_NOHASHDIR=true" >> $GITHUB_ENV
echo "CCACHE_COMPILERCHECK=content" >> $GITHUB_ENV
echo "CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros" >> $GITHUB_ENV
echo "CCACHE_MAXSIZE=4G" >> $GITHUB_ENV
if [ "${{ inputs.portable }}" = "true" ]; then
echo "PORTABLE=1" >> $GITHUB_ENV
fi
shell: bash
- name: Restore ccache
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/.ccache
key: ccache-${{ inputs.cache-key-prefix }}-${{ github.head_ref || github.ref }}-${{ github.sha }}
restore-keys: |-
ccache-${{ inputs.cache-key-prefix }}-${{ github.head_ref || github.ref }}-
ccache-${{ inputs.cache-key-prefix }}-refs/heads/main-
- name: Install ccache
run: |
if [ "$RUNNER_OS" = "macOS" ]; then
which ccache || brew install ccache
else
which ccache || (apt-get update && apt-get install -y ccache)
fi
shell: bash
- name: Add ccache to PATH
run: |
if [ "$RUNNER_OS" = "macOS" ]; then
echo "$(brew --prefix ccache)/libexec" >> $GITHUB_PATH
else
echo "/usr/lib/ccache" >> $GITHUB_PATH
fi
shell: bash
- name: Zero ccache stats and set build marker
run: |
ccache -z
touch "$CCACHE_DIR/.build_marker"
shell: bash
@@ -1,15 +0,0 @@
name: teardown-ccache
description: Trim stale ccache entries and print stats
runs:
using: composite
steps:
- name: Trim and print ccache stats
run: |
if [ -z "$CCACHE_DIR" ]; then
echo "teardown-ccache: CCACHE_DIR not set, skipping (setup-ccache may not have run)"
exit 0
fi
.github/scripts/ccache-trim.sh || true
ccache -s || echo "teardown-ccache: ccache not found, skipping stats"
if: always()
shell: bash
+12 -34
View File
@@ -1,31 +1,19 @@
name: windows-build-steps
inputs:
suite-run:
description: Comma-separated list of test suites to run (empty to skip C++ tests)
required: false
default: arena_test,db_basic_test,db_test,db_test2,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test
run-java:
description: Whether to run Java tests
required: false
default: "true"
runs:
using: composite
steps:
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v1.3.1
- name: Cache ccache directory
id: ccache-cache
uses: actions/cache@v4
with:
path: C:\a\rocksdb\rocksdb\.ccache
key: rocksdb-build-${{ runner.os }}-${{ runner.arch }}-ccache-${{ hashFiles('CMakeLists.txt', 'cmake/**/*.cmake') }}-v1
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
max-size: "10GB"
key: ccache-windows-${{ github.workflow }}
restore-keys: |
ccache-windows-
- name: Configure ccache
shell: pwsh
run: |
ccache --set-config=base_dir=C:\a\rocksdb\rocksdb
ccache --set-config=hash_dir=false
ccache --set-config=compiler_check=content
- name: Custom steps
env:
THIRDPARTY_HOME: ${{ github.workspace }}/thirdparty
@@ -68,22 +56,12 @@ runs:
msbuild build/rocksdb.sln /m:32 /p:LinkIncremental=false -property:Configuration=Debug -property:Platform=x64
if(!$?) { Exit $LASTEXITCODE }
echo ========================= Test RocksDB =========================
$suiteRun = "${{ inputs.suite-run }}"
if ($suiteRun -ne "") {
$suiteArray = $suiteRun -split ','
build_tools\run_ci_db_test.ps1 -SuiteRun $suiteArray -Concurrency 16
if(!$?) { Exit $LASTEXITCODE }
} else {
echo "Skipping C++ tests (suite-run is empty)"
}
if ("${{ inputs.run-java }}" -eq "true") {
echo ======================== Test RocksJava ========================
cd build\java
& ctest -C Debug -j 16
if(!$?) { Exit $LASTEXITCODE }
} else {
echo "Skipping Java tests"
}
build_tools\run_ci_db_test.ps1 -SuiteRun arena_test,db_basic_test,db_test,db_test2,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test -Concurrency 16
if(!$?) { Exit $LASTEXITCODE }
echo ======================== Test RocksJava ========================
cd build\java
& ctest -C Debug -j 16
if(!$?) { Exit $LASTEXITCODE }
shell: pwsh
- name: Show ccache stats
shell: pwsh
-39
View File
@@ -1,39 +0,0 @@
#!/bin/bash
# Trim ccache to keep only entries accessed during the current build.
#
# Usage:
# 1. Before build: touch "$CCACHE_DIR/.build_marker"
# 2. Run build (ccache updates mtime on hits, creates new files for misses)
# 3. After build: .github/scripts/ccache-trim.sh
#
# This removes cache files not accessed during the build (stale entries from
# previous commits). Only intended for CI where each run builds one commit.
# Do NOT use on local builds where multiple worktrees may share the cache.
set -e
CCACHE_DIR="${CCACHE_DIR:?CCACHE_DIR must be set}"
MARKER="$CCACHE_DIR/.build_marker"
if [ ! -f "$MARKER" ]; then
echo "ccache-trim: No .build_marker found, skipping (was the marker created before build?)"
exit 0
fi
# Count files before cleanup
before=$(find "$CCACHE_DIR" -type f \( -name '*R' -o -name '*M' \) | wc -l)
# Delete cache files (results and manifests) older than the marker
find "$CCACHE_DIR" -type f \( -name '*R' -o -name '*M' \) ! -newer "$MARKER" -delete
# Clean up empty directories
find "$CCACHE_DIR" -mindepth 2 -type d -empty -delete 2>/dev/null || true
# Recalculate size counters
ccache -c 2>/dev/null || true
after=$(find "$CCACHE_DIR" -type f \( -name '*R' -o -name '*M' \) | wc -l)
echo "ccache-trim: $before -> $after cache files (removed $((before - after)) stale entries)"
# Clean up marker
rm -f "$MARKER"
-32
View File
@@ -1,32 +0,0 @@
#!/bin/bash
# Compute test shard for parallel CI execution.
# Distributes tests round-robin across N shards for balanced load.
# Outputs ROCKSDBTESTS_SUBSET — the list of test binaries for this shard.
# The Makefile uses this to build and run only the assigned tests.
#
# Usage: compute-test-shard.sh <shard_index> <num_shards>
set -euo pipefail
shard=${1:?Usage: compute-test-shard.sh <shard_index> <num_shards>}
nshards=${2:?Usage: compute-test-shard.sh <shard_index> <num_shards>}
# Get sorted test list (db_test first since it's the heaviest, then alpha)
make -s list_all_tests 2>/dev/null | tr ' ' '\n' | grep '_test$' | sort -u > /tmp/sorted.txt
(echo db_test; grep -v '^db_test$' /tmp/sorted.txt) > /tmp/all_tests.txt
total=$(wc -l < /tmp/all_tests.txt)
# Round-robin: assign test i to shard (i % nshards).
# This spreads heavy tests (which are scattered alphabetically) evenly.
awk -v s="$shard" -v n="$nshards" 'NR > 0 && (NR - 1) % n == s' /tmp/all_tests.txt > /tmp/include.txt
included=$(wc -l < /tmp/include.txt)
first=$(head -1 /tmp/include.txt)
last=$(tail -1 /tmp/include.txt)
# Output space-separated list for ROCKSDBTESTS_SUBSET
subset=$(tr '\n' ' ' < /tmp/include.txt)
echo "subset=${subset}" >> "$GITHUB_OUTPUT"
echo "Shard $shard/$nshards: $included tests (round-robin), first=$first last=$last (total $total)"
-115
View File
@@ -1,115 +0,0 @@
// Parse Claude Code execution log and produce a markdown review comment.
//
// Usage from actions/github-script:
// const parse = require('./.github/scripts/parse-claude-review.js');
// const markdown = parse({ executionFile, conclusion, meta });
//
// Parameters:
// executionFile - path to the JSON execution log from claude-code-base-action
// conclusion - 'success' or 'failure' from the action output
// meta - { trigger, headSha, reviewer, isQuery, isPartial }
const fs = require('fs');
module.exports = function parseClaude({executionFile, conclusion, meta}) {
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 = meta.trigger === 'auto' ?
`*Auto-triggered after CI passed — reviewing commit ${
meta.headSha.substring(0, 7)}*` :
`*Requested by @${meta.reviewer}*`;
return [
`## ${icon} ${headerTitle}`,
'',
triggerLine,
'',
'---',
'',
responseBody,
'',
'---',
'',
'<details>',
'<summary>ℹ️ About this response</summary>',
'',
'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',
'</details>',
].join('\n');
};
-57
View File
@@ -1,57 +0,0 @@
// Shared PR comment posting utility.
// Used by both clang-tidy-comment.yml and claude-review-comment.yml.
//
// Usage from actions/github-script:
// const post = require('./.github/scripts/post-pr-comment.js');
// await post({ github, context, core, prNumber, body, marker });
//
// Parameters:
// github - octokit instance from actions/github-script
// context - GitHub Actions context
// core - @actions/core for logging
// prNumber - PR number to comment on
// body - comment body (markdown string)
// marker - HTML comment marker for dedup (e.g. '<!-- claude-review -->')
// If an existing comment with this marker is found, it is updated.
// If not found, a new comment is created.
module.exports = async function postPrComment(
{github, context, core, prNumber, body, marker}) {
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}`;
// Search for existing comment with this marker
const {data: comments} = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body: markedBody,
});
core.info(`Updated existing comment ${existing.id}`);
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: markedBody,
});
core.info('Created new PR comment');
}
};
+24 -17
View File
@@ -12,21 +12,14 @@ jobs:
if: github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout scripts
uses: actions/checkout@v4
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: true
- name: Download clang-tidy results
id: download
uses: actions/download-artifact@v4.1.3
uses: actions/download-artifact@v4.0.0
with:
name: clang-tidy-result
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Post or update PR comment
if: steps.download.outcome == 'success'
uses: actions/github-script@v7
@@ -39,13 +32,27 @@ jobs:
}
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 -->',
const marker = '<!-- clang-tidy-bot -->';
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');
}
+4 -4
View File
@@ -11,7 +11,7 @@ jobs:
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
steps:
- uses: actions/checkout@v4.1.0
with:
@@ -36,14 +36,14 @@ jobs:
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
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-21 \
-DCMAKE_CXX_COMPILER=clang++-21 ..
-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
-146
View File
@@ -1,146 +0,0 @@
# Claude Code Review — Comment Posting Workflow
#
# Companion to claude-review.yml. Downloads the review artifact and posts
# it as a PR comment. This workflow has write permissions but never runs
# AI or processes untrusted code.
#
# Security: Separating analysis (has ANTHROPIC_API_KEY, no write perms)
# from posting (has GITHUB_TOKEN write perms, no AI) prevents a crafted
# PR from tricking Claude into exfiltrating tokens.
name: Post Claude Review Comment
on:
workflow_run:
workflows: ["Claude Code Review"]
types: [completed]
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: claude-review-result
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Post or update PR comment
if: steps.download.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (!fs.existsSync('claude-review-comment.md') || !fs.existsSync('pr_number.txt')) {
core.info('No review results found; skipping.');
return;
}
const body = fs.readFileSync('claude-review-comment.md', 'utf8');
const prNumber = parseInt(fs.readFileSync('pr_number.txt', 'utf8').trim());
const trigger = fs.existsSync('trigger_type.txt')
? fs.readFileSync('trigger_type.txt', 'utf8').trim()
: 'auto';
// Use different markers for auto vs manual so they don't
// overwrite each other. Auto-reviews update in place (one
// per PR). Manual reviews always create new comments.
const marker = trigger === 'auto'
? '<!-- claude-review-auto -->'
: `<!-- claude-review-manual-${Date.now()} -->`;
const post = require('./.github/scripts/post-pr-comment.js');
await post({
github,
context,
core,
prNumber,
body,
marker,
});
- 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());
if (!commentId) return;
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId,
content: 'rocket'
});
# Notify on failure (for manual triggers)
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: claude-review-result
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());
if (!commentId) return;
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId,
content: 'confused'
});
# Unauthorized user notice
unauthorized-notice:
runs-on: ubuntu-latest
if: >-
github.event.workflow_run.event == 'issue_comment' &&
github.event.workflow_run.conclusion == 'skipped'
steps:
- name: Check if unauthorized
id: check
uses: actions/github-script@v7
with:
script: |
// This job only runs when the analysis workflow was skipped,
// which happens when the user is not in the authorized list.
// We cannot easily get the original comment from workflow_run,
// so this is a best-effort notice via workflow logs.
core.info('Analysis workflow was skipped — likely unauthorized user.');
-592
View File
@@ -1,592 +0,0 @@
# Claude Code Review — Analysis Workflow
#
# This workflow runs Claude to review PRs. It produces a markdown review
# as an artifact but does NOT post comments (no write permissions).
# The companion workflow (claude-review-comment.yml) handles posting.
#
# Triggers:
# 1. workflow_run: Auto-review after pr-jobs passes
# 2. issue_comment: Manual /claude-review or /claude-query by maintainers
# 3. workflow_dispatch: Manual testing
#
# Security:
# - This job has contents:read ONLY (no PR write, no issue write)
# - ANTHROPIC_API_KEY is used here but never coexists with write tokens
# - Claude uses read-only tools (View, GlobTool, GrepTool) plus Write
# (only for incremental findings file, not repo modifications)
# - Review methodology: claude_md/code_review.md
name: Claude Code Review
on:
workflow_run:
workflows: ["facebook/rocksdb/pr-jobs"]
types: [completed]
issue_comment:
types: [created]
workflow_dispatch:
inputs:
pr_number:
description: PR number to review
required: true
type: number
model:
description: Claude model to use
required: false
type: choice
options:
- claude-opus-4-6
- claude-sonnet-4-20250514
default: claude-opus-4-6
permissions:
contents: read
jobs:
# ======================== Auto Review ======================== #
auto-review:
name: Auto Claude Review
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
if: >-
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request'
steps:
- name: Get PR info
id: pr_info
uses: actions/github-script@v7
with:
script: |
const headSha = context.payload.workflow_run.head_sha;
let prNumber = null;
const prs = context.payload.workflow_run.pull_requests;
if (prs && prs.length > 0) {
prNumber = prs[0].number;
} else {
// PR may not be registered yet if push and PR creation raced.
// Retry up to 5 times with 10s delay.
for (let attempt = 0; attempt < 5; attempt++) {
if (attempt > 0) {
core.info(`PR not found for SHA ${headSha}, retrying in 10s (attempt ${attempt + 1}/5)...`);
await new Promise(r => setTimeout(r, 10000));
}
const { data: pulls } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
sort: 'updated',
direction: 'desc',
per_page: 100
});
const match = pulls.find(p => p.head.sha === headSha);
if (match) {
prNumber = match.number;
break;
}
}
}
if (!prNumber) {
core.warning(
`Could not find PR for SHA ${headSha} after retries. ` +
`This can happen when the PR head advanced before this ` +
`workflow_run-triggered job started. Skipping stale review run.`
);
core.setOutput('skip', 'true');
core.setOutput('skip_reason', `stale_sha:${headSha}`);
return;
}
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
core.setOutput('skip', 'false');
core.setOutput('pr_number', prNumber.toString());
core.setOutput('head_repo', pr.data.head.repo.clone_url);
core.setOutput('head_ref', pr.data.head.ref);
core.setOutput('head_sha', pr.data.head.sha);
core.setOutput('base_sha', pr.data.base.sha);
core.setOutput('base_ref', pr.data.base.ref);
core.setOutput('pr_title', pr.data.title);
core.setOutput('pr_body', pr.data.body || '');
core.setOutput('pr_author', pr.data.user.login);
- name: Skip stale run
if: steps.pr_info.outputs.skip == 'true'
run: |-
echo "Skipping stale auto-review run: ${{ steps.pr_info.outputs.skip_reason }}"
- name: Check for existing review
if: steps.pr_info.outputs.skip != 'true'
id: check_existing
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ steps.pr_info.outputs.pr_number }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
with:
script: |
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(process.env.PR_NUMBER),
per_page: 100
});
const shortSha = process.env.HEAD_SHA.substring(0, 7);
const existing = comments.data.find(c =>
c.body.includes('<!-- claude-review -->') &&
c.body.includes(shortSha)
);
core.setOutput('skip', existing ? 'true' : 'false');
- name: Checkout base repository
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
uses: actions/checkout@v4
with:
ref: ${{ steps.pr_info.outputs.base_ref }}
fetch-depth: 0
persist-credentials: false
- name: Generate diff and prompt
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
env:
HEAD_REPO: ${{ steps.pr_info.outputs.head_repo }}
HEAD_REF: ${{ steps.pr_info.outputs.head_ref }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
PR_BODY: ${{ steps.pr_info.outputs.pr_body }}
PR_AUTHOR: ${{ steps.pr_info.outputs.pr_author }}
run: |
git remote add fork "${HEAD_REPO}" || true
git fetch fork "${HEAD_REF}" --depth=100
git diff "${BASE_SHA}...${HEAD_SHA}" > /tmp/pr.diff
DIFF_STATS=$(git diff --stat "${BASE_SHA}...${HEAD_SHA}" | tail -1)
DIFF_SIZE=$(wc -c < /tmp/pr.diff)
IS_TRUNCATED=false
if [ "$DIFF_SIZE" -gt 100000 ]; then
head -c 100000 /tmp/pr.diff > /tmp/pr_truncated.diff
mv /tmp/pr_truncated.diff /tmp/pr.diff
IS_TRUNCATED=true
fi
cat claude_md/ci_review_prompt.md > /tmp/prompt.txt
cat >> /tmp/prompt.txt << PROMPT_EOF
## Pull Request Information
- **Title:** ${PR_TITLE}
- **Author:** ${PR_AUTHOR}
- **Changes:** ${DIFF_STATS}
PROMPT_EOF
if [ "${IS_TRUNCATED}" = "true" ]; then
echo "- **Note:** Diff truncated due to size." >> /tmp/prompt.txt
fi
printf '\n## PR Description\n' >> /tmp/prompt.txt
printf '%s\n' "${PR_BODY}" >> /tmp/prompt.txt
printf '\n---\n\n## Diff to Review\n\n' >> /tmp/prompt.txt
cat /tmp/pr.diff >> /tmp/prompt.txt
- name: Run Claude
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
id: claude
uses: anthropics/claude-code-base-action@beta
with:
prompt_file: /tmp/prompt.txt
model: claude-opus-4-6
max_turns: "300"
timeout_minutes: "60"
allowed_tools: "View,GlobTool,GrepTool,Write,Task"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Recover partial review on turn limit
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
id: recover
uses: actions/github-script@v7
env:
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
with:
script: |
const fs = require('fs');
const log = JSON.parse(fs.readFileSync(process.env.EXECUTION_FILE, 'utf8'));
const result = log.find(m => m.type === 'result');
const hitLimit = result && result.subtype === 'error_max_turns';
const hasFindings = fs.existsSync('review-findings.md');
core.setOutput('hit_limit', hitLimit ? 'true' : 'false');
core.setOutput('has_findings', hasFindings ? 'true' : 'false');
core.setOutput('needs_recovery', (hitLimit && hasFindings) ? 'true' : 'false');
- name: Format partial findings
if: >-
steps.pr_info.outputs.skip != 'true' &&
steps.check_existing.outputs.skip != 'true' &&
steps.recover.outputs.needs_recovery == 'true'
id: format_recovery
uses: anthropics/claude-code-base-action@beta
with:
prompt_file: claude_md/ci_recovery_prompt.md
model: claude-sonnet-4-20250514
max_turns: "10"
allowed_tools: "View,GlobTool,GrepTool"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Build review comment
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
uses: actions/github-script@v7
env:
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
RECOVERY_FILE: ${{ steps.format_recovery.outputs.execution_file }}
CONCLUSION: ${{ steps.claude.outputs.conclusion }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
NEEDS_RECOVERY: ${{ steps.recover.outputs.needs_recovery }}
with:
script: |
const parse = require('./.github/scripts/parse-claude-review.js');
const fs = require('fs');
let execFile = process.env.EXECUTION_FILE;
if (process.env.NEEDS_RECOVERY === 'true') {
const rf = process.env.RECOVERY_FILE;
if (rf && fs.existsSync(rf)) {
execFile = rf;
}
}
const body = parse({
executionFile: execFile,
conclusion: process.env.CONCLUSION,
meta: {
trigger: 'auto',
headSha: process.env.HEAD_SHA,
reviewer: '',
isQuery: false,
isPartial: process.env.NEEDS_RECOVERY === 'true',
}
});
fs.writeFileSync('claude-review-comment.md', body);
- name: Save PR number
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
run: echo "${{ steps.pr_info.outputs.pr_number }}" > pr_number.txt
- name: Save trigger metadata
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
run: echo "auto" > trigger_type.txt
- name: Upload review artifact
if: always() && steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
uses: actions/upload-artifact@v4
with:
name: claude-review-result
path: |
claude-review-comment.md
pr_number.txt
trigger_type.txt
if-no-files-found: ignore
retention-days: 1
- name: Upload execution log
if: always() && steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
uses: actions/upload-artifact@v4
with:
name: claude-review-execution-log
path: ${{ steps.claude.outputs.execution_file }}
retention-days: 7
if-no-files-found: warn
- name: Upload recovery execution log
if: always() && steps.pr_info.outputs.skip != 'true' && steps.recover.outputs.needs_recovery == 'true'
uses: actions/upload-artifact@v4
with:
name: claude-review-recovery-log
path: ${{ steps.format_recovery.outputs.execution_file }}
retention-days: 7
if-no-files-found: ignore
# ======================== Manual Review ======================== #
manual-review:
name: Manual Claude Review
runs-on: ubuntu-latest
permissions:
contents: read
if: >-
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
(contains(github.event.comment.body, '/claude-review') || contains(github.event.comment.body, '/claude-query')) &&
contains(fromJSON('["pdillinger", "jaykorean", "hx235", "dannyhchen", "joshkang97", "zaidoon1", "omkarhgawde", "xingbowang", "anand1976", "virajthakur", "nmk70", "archang19", "mszeszko-meta", "yoshinorim", "cbi42"]'), github.event.comment.user.login)
)
steps:
- name: Detect command type
id: command
uses: actions/github-script@v7
with:
script: |
if (context.eventName === 'workflow_dispatch') {
core.setOutput('type', 'review');
core.setOutput('query', '');
return;
}
const body = context.payload.comment.body;
if (body.includes('/claude-review')) {
core.setOutput('type', 'review');
const match = body.match(/\/claude-review\s+([\s\S]*)/);
core.setOutput('query', match ? match[1].trim() : '');
} else if (body.includes('/claude-query')) {
const match = body.match(/\/claude-query\s+([\s\S]*)/);
const query = match ? match[1].trim() : '';
if (!query) {
core.setFailed('No query provided. Usage: /claude-query <your question>');
return;
}
core.setOutput('type', 'query');
core.setOutput('query', query);
} else {
core.setFailed('Unknown command');
}
- name: Get PR details
id: pr_info
uses: actions/github-script@v7
env:
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
with:
script: |
const prNumber = context.eventName === 'workflow_dispatch'
? parseInt(process.env.INPUT_PR_NUMBER, 10)
: context.issue.number;
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
core.setOutput('pr_number', prNumber.toString());
core.setOutput('head_repo', pr.data.head.repo.clone_url);
core.setOutput('head_ref', pr.data.head.ref);
core.setOutput('head_sha', pr.data.head.sha);
core.setOutput('base_sha', pr.data.base.sha);
core.setOutput('base_ref', pr.data.base.ref);
core.setOutput('pr_title', pr.data.title);
core.setOutput('pr_body', pr.data.body || '');
core.setOutput('pr_author', pr.data.user.login);
- name: Checkout base repository
uses: actions/checkout@v4
with:
ref: ${{ steps.pr_info.outputs.base_ref }}
fetch-depth: 0
persist-credentials: false
- name: Generate diff and prompt
env:
HEAD_REPO: ${{ steps.pr_info.outputs.head_repo }}
HEAD_REF: ${{ steps.pr_info.outputs.head_ref }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
PR_BODY: ${{ steps.pr_info.outputs.pr_body }}
PR_AUTHOR: ${{ steps.pr_info.outputs.pr_author }}
DIFF_STATS: ""
COMMAND_TYPE: ${{ steps.command.outputs.type }}
ADDITIONAL_CONTEXT: ${{ steps.command.outputs.query }}
USER_QUERY: ${{ steps.command.outputs.query }}
run: |
git remote add fork "${HEAD_REPO}" || true
git fetch fork "${HEAD_REF}" --depth=100
git diff "${BASE_SHA}...${HEAD_SHA}" > /tmp/pr.diff
DIFF_STATS=$(git diff --stat "${BASE_SHA}...${HEAD_SHA}" | tail -1)
DIFF_SIZE=$(wc -c < /tmp/pr.diff)
IS_TRUNCATED=false
if [ "$DIFF_SIZE" -gt 100000 ]; then
head -c 100000 /tmp/pr.diff > /tmp/pr_truncated.diff
mv /tmp/pr_truncated.diff /tmp/pr.diff
IS_TRUNCATED=true
fi
if [ "${COMMAND_TYPE}" = "query" ]; then
# Query prompt
cat claude_md/ci_query_prompt.md > /tmp/prompt.txt
printf '\n---\n\n' >> /tmp/prompt.txt
cat >> /tmp/prompt.txt << PROMPT_EOF
## Pull Request Context
- **Title:** ${PR_TITLE}
- **Author:** ${PR_AUTHOR}
- **Changes:** ${DIFF_STATS}
## PR Description
PROMPT_EOF
printf '%s\n' "${PR_BODY}" >> /tmp/prompt.txt
printf '\n## Question\n' >> /tmp/prompt.txt
printf '%s\n' "${USER_QUERY}" >> /tmp/prompt.txt
printf '\n---\n## PR Diff (for reference)\n' >> /tmp/prompt.txt
cat /tmp/pr.diff >> /tmp/prompt.txt
else
# Review prompt
cat claude_md/ci_review_prompt.md > /tmp/prompt.txt
cat >> /tmp/prompt.txt << PROMPT_EOF
## Pull Request Information
- **Title:** ${PR_TITLE}
- **Author:** ${PR_AUTHOR}
- **Changes:** ${DIFF_STATS}
PROMPT_EOF
if [ "${IS_TRUNCATED}" = "true" ]; then
echo "- **Note:** Diff truncated due to size." >> /tmp/prompt.txt
fi
printf '\n## PR Description\n' >> /tmp/prompt.txt
printf '%s\n' "${PR_BODY}" >> /tmp/prompt.txt
if [ -n "${ADDITIONAL_CONTEXT}" ]; then
printf '\n## Additional Instructions from Reviewer\n' >> /tmp/prompt.txt
printf '%s\n' "${ADDITIONAL_CONTEXT}" >> /tmp/prompt.txt
fi
printf '\n---\n\n## Diff to Review\n\n' >> /tmp/prompt.txt
cat /tmp/pr.diff >> /tmp/prompt.txt
fi
- name: Run Claude
id: claude
uses: anthropics/claude-code-base-action@beta
with:
prompt_file: /tmp/prompt.txt
model: ${{ github.event_name == 'workflow_dispatch' && inputs.model || 'claude-opus-4-6' }}
max_turns: "300"
timeout_minutes: "60"
allowed_tools: "View,GlobTool,GrepTool,Write,Task"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Recover partial review on turn limit
id: recover
uses: actions/github-script@v7
env:
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
COMMAND_TYPE: ${{ steps.command.outputs.type }}
with:
script: |
const fs = require('fs');
// Skip recovery for queries — only reviews use incremental findings
if (process.env.COMMAND_TYPE === 'query') {
core.setOutput('needs_recovery', 'false');
return;
}
const log = JSON.parse(fs.readFileSync(process.env.EXECUTION_FILE, 'utf8'));
const result = log.find(m => m.type === 'result');
const hitLimit = result && result.subtype === 'error_max_turns';
const hasFindings = fs.existsSync('review-findings.md');
core.setOutput('hit_limit', hitLimit ? 'true' : 'false');
core.setOutput('has_findings', hasFindings ? 'true' : 'false');
core.setOutput('needs_recovery', (hitLimit && hasFindings) ? 'true' : 'false');
- name: Format partial findings
if: steps.recover.outputs.needs_recovery == 'true'
id: format_recovery
uses: anthropics/claude-code-base-action@beta
with:
prompt_file: claude_md/ci_recovery_prompt.md
model: claude-sonnet-4-20250514
max_turns: "10"
allowed_tools: "View,GlobTool,GrepTool"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Build review comment
uses: actions/github-script@v7
env:
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
RECOVERY_FILE: ${{ steps.format_recovery.outputs.execution_file }}
CONCLUSION: ${{ steps.claude.outputs.conclusion }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
REVIEWER: ${{ github.event_name == 'workflow_dispatch' && github.actor || github.event.comment.user.login }}
COMMAND_TYPE: ${{ steps.command.outputs.type }}
NEEDS_RECOVERY: ${{ steps.recover.outputs.needs_recovery }}
with:
script: |
const parse = require('./.github/scripts/parse-claude-review.js');
const fs = require('fs');
const isReview = process.env.COMMAND_TYPE === 'review';
let execFile = process.env.EXECUTION_FILE;
if (process.env.NEEDS_RECOVERY === 'true') {
const rf = process.env.RECOVERY_FILE;
if (rf && fs.existsSync(rf)) {
execFile = rf;
}
}
const body = parse({
executionFile: execFile,
conclusion: process.env.CONCLUSION,
meta: {
trigger: 'manual',
headSha: process.env.HEAD_SHA,
reviewer: process.env.REVIEWER,
isQuery: !isReview,
isPartial: process.env.NEEDS_RECOVERY === 'true',
}
});
fs.writeFileSync('claude-review-comment.md', body);
- name: Save PR number
run: echo "${{ steps.pr_info.outputs.pr_number }}" > pr_number.txt
- name: Save trigger metadata
env:
REVIEWER: ${{ github.event_name == 'workflow_dispatch' && github.actor || github.event.comment.user.login }}
COMMENT_ID: ${{ github.event.comment.id }}
run: |
echo "manual" > trigger_type.txt
echo "${REVIEWER}" > reviewer.txt
echo "${COMMENT_ID}" > comment_id.txt
- name: Upload review artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: claude-review-result
path: |
claude-review-comment.md
pr_number.txt
trigger_type.txt
reviewer.txt
comment_id.txt
if-no-files-found: ignore
retention-days: 1
- name: Upload execution log
if: always()
uses: actions/upload-artifact@v4
with:
name: claude-review-execution-log
path: ${{ steps.claude.outputs.execution_file }}
retention-days: 7
if-no-files-found: warn
- name: Upload recovery execution log
if: always() && steps.recover.outputs.needs_recovery == 'true'
uses: actions/upload-artifact@v4
with:
name: claude-review-recovery-log
path: ${{ steps.format_recovery.outputs.execution_file }}
retention-days: 7
if-no-files-found: ignore
+7 -26
View File
@@ -41,41 +41,22 @@ jobs:
- uses: "./.github/actions/pre-steps"
- run: make V=1 -j32 check
- uses: "./.github/actions/post-steps"
build-linux-clang-21-asan-ubsan-with-folly:
build-linux-clang-18-asan-ubsan-with-folly:
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
env:
CC: clang-21
CXX: clang++-21
CC: clang-18
CXX: clang++-18
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- name: Build folly and dependencies
run: |
clean_path=()
IFS=: read -ra path_entries <<< "$PATH"
for entry in "${path_entries[@]}"; do
if [[ "$entry" != "/usr/lib/ccache" ]]; then
clean_path+=("$entry")
fi
done
export PATH="$(IFS=:; echo "${clean_path[*]}")"
ccache_bin="$(command -v ccache || true)"
if [[ -n "$ccache_bin" && -x "$ccache_bin" ]]; then
mv "$ccache_bin" "${ccache_bin}.disabled"
fi
export CC=gcc
export CXX=g++
export USE_CCACHE=0
export CCACHE_DISABLE=1
make build_folly
shell: bash
- uses: "./.github/actions/build-folly"
- run: LIB_MODE=static USE_CLANG=1 USE_FOLLY=1 COMPILE_WITH_UBSAN=1 COMPILE_WITH_ASAN=1 make -j32 check
- uses: "./.github/actions/post-steps"
build-linux-cmake-with-folly:
@@ -164,13 +145,13 @@ jobs:
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- name: Build rocksdb lib
run: CC=clang-21 CXX=clang++-21 USE_CLANG=1 make -j4 static_lib
run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 make -j4 static_lib
- name: Build fuzzers
run: cd fuzz && make sst_file_writer_fuzzer db_fuzzer db_map_fuzzer
- uses: "./.github/actions/post-steps"
+72 -201
View File
@@ -72,8 +72,6 @@ jobs:
run: make check-buck-targets
- name: Simple source code checks
run: make check-sources
- name: Validate GitHub Actions YAML
run: make check-workflow-yaml
- name: Sanity check check_format_compatible.sh
run: |-
export TEST_TMPDIR=/dev/shm/rocksdb
@@ -94,12 +92,7 @@ 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')
@@ -107,14 +100,11 @@ jobs:
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: cmake-mingw
- run: update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix
- name: Build cmake-mingw
run: |-
@@ -123,14 +113,12 @@ 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: 32-core-ubuntu
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
@@ -139,17 +127,12 @@ jobs:
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: make-with-folly
- uses: "./.github/actions/cache-folly"
id: cache-folly
- uses: "./.github/actions/build-folly"
with:
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
- run: USE_FOLLY=1 LIB_MODE=static V=1 make -j64 check
- uses: "./.github/actions/teardown-ccache"
if: always()
- run: USE_FOLLY=1 LIB_MODE=static V=1 make -j32 check
- uses: "./.github/actions/post-steps"
build-linux-make-with-folly-lite-no-test:
if: needs.config.outputs.only_job == 'build-linux-make-with-folly-lite-no-test' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -164,18 +147,13 @@ 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: 32-core-ubuntu
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
@@ -184,45 +162,26 @@ jobs:
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: cmake-folly-coroutines
- uses: "./.github/actions/cache-folly"
id: cache-folly
- uses: "./.github/actions/build-folly"
with:
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
- run: "(mkdir build && cd build && cmake -DUSE_COROUTINES=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 -DPORTABLE=ON .. && make VERBOSE=1 -j64 && ctest -j64)"
- uses: "./.github/actions/teardown-ccache"
if: always()
- run: "(mkdir build && cd build && cmake -DUSE_COROUTINES=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 .. && make VERBOSE=1 -j20 && ctest -j20)"
- uses: "./.github/actions/post-steps"
build-linux-cmake-with-benchmark-no-thread-status:
if: needs.config.outputs.only_job == 'build-linux-cmake-with-benchmark-no-thread-status' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3]
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: cmake-benchmark
- name: Build
run: mkdir build && cd build && cmake -DWITH_GFLAGS=1 -DWITH_BENCHMARK=1 -DPORTABLE=ON -DCMAKE_CXX_FLAGS=-DNROCKSDB_THREAD_STATUS .. && make VERBOSE=1 -j20
- name: Test shard ${{ matrix.shard }} of 4
run: cd build && ctest -j20 -I ${{ matrix.shard }},,4
- uses: "./.github/actions/teardown-ccache"
if: always()
- run: mkdir build && cd build && cmake -DWITH_GFLAGS=1 -DWITH_BENCHMARK=1 -DCMAKE_CXX_FLAGS=-DNROCKSDB_THREAD_STATUS .. && make VERBOSE=1 -j20 && ctest -j20
- uses: "./.github/actions/post-steps"
with:
artifact-prefix: "${{ github.job }}-shard${{ matrix.shard }}"
build-linux-encrypted_env-no_compression:
if: needs.config.outputs.only_job == 'build-linux-encrypted_env-no_compression' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
@@ -234,13 +193,8 @@ 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:
@@ -253,9 +207,6 @@ jobs:
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: release
- run: make V=1 -j32 LIB_MODE=shared release
- run: ls librocksdb.so
- run: "./trace_analyzer --version" # A tool dependent on gflags that can run in release build
@@ -272,8 +223,6 @@ 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')
@@ -286,36 +235,26 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang-13
# FIXME: get back to "all microbench" targets
- run: CC=clang-13 CXX=clang++-13 USE_CLANG=1 EXTRA_CXXFLAGS=-stdlib=libc++ EXTRA_LDFLAGS=-stdlib=libc++ make -j32 shared_lib
- run: make clean
# FIXME: get back to "release" target
- run: CC=clang-13 CXX=clang++-13 USE_CLANG=1 EXTRA_CXXFLAGS=-stdlib=libc++ EXTRA_LDFLAGS=-stdlib=libc++ DEBUG_LEVEL=0 make -j32 shared_lib
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-clang-21-no_test_run:
if: needs.config.outputs.only_job == 'build-linux-clang-21-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
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')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang-21
- run: CC=clang-21 CXX=clang++-21 USE_CLANG=1 make -j32 all microbench
- run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 make -j32 all microbench
- run: make clean
- run: CC=clang-21 CXX=clang++-21 USE_CLANG=1 DEBUG_LEVEL=0 make -j32 release
- uses: "./.github/actions/teardown-ccache"
if: always()
- run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 DEBUG_LEVEL=0 make -j32 release
- uses: "./.github/actions/post-steps"
build-linux-gcc-14-no_test_run:
if: needs.config.outputs.only_job == 'build-linux-gcc-14-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -323,20 +262,35 @@ jobs:
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: gcc-14
- run: CC=gcc-14 CXX=g++-14 V=1 make -j32 all microbench
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
# ======================== Linux Other Checks ======================= #
build-linux-clang18-clang-analyze:
if: 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')
@@ -349,98 +303,50 @@ 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"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: mini-crashtest
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=${{ matrix.crash_duration }} --max_key=2500000' ${{ matrix.crash_test_target }}
- uses: "./.github/actions/teardown-ccache"
if: always()
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=960 --max_key=2500000' blackbox_crash_test_with_atomic_flush
- uses: "./.github/actions/post-steps"
with:
artifact-prefix: "${{ github.job }}-${{ matrix.crash_test_target }}"
# ======================= Linux with Sanitizers ===================== #
build-linux-clang21-asan-ubsan:
if: needs.config.outputs.only_job == 'build-linux-clang21-asan-ubsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
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')
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.1
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang21-asan-ubsan
- run: COMPILE_WITH_ASAN=1 COMPILE_WITH_UBSAN=1 CC=clang-21 CXX=clang++-21 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j40 check
env:
CI_SHARD_INDEX: ${{ matrix.shard }}
CI_TOTAL_SHARDS: 3
- uses: "./.github/actions/teardown-ccache"
if: always()
- run: COMPILE_WITH_ASAN=1 COMPILE_WITH_UBSAN=1 CC=clang-18 CXX=clang++-18 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j40 check
- uses: "./.github/actions/post-steps"
with:
artifact-prefix: "${{ github.job }}-shard${{ matrix.shard }}"
build-linux-clang21-mini-tsan:
if: needs.config.outputs.only_job == 'build-linux-clang21-mini-tsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
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')
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.1
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang21-tsan
- run: COMPILE_WITH_TSAN=1 CC=clang-21 CXX=clang++-21 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j32 check
env:
CI_SHARD_INDEX: ${{ matrix.shard }}
CI_TOTAL_SHARDS: 3
- uses: "./.github/actions/teardown-ccache"
if: always()
- run: COMPILE_WITH_TSAN=1 CC=clang-18 CXX=clang++-18 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j32 check
- uses: "./.github/actions/post-steps"
with:
artifact-prefix: "${{ github.job }}-shard${{ matrix.shard }}"
build-linux-static_lib-alt_namespace-status_checked:
if: needs.config.outputs.only_job == 'build-linux-static_lib-alt_namespace-status_checked' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
@@ -452,12 +358,7 @@ 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:
@@ -471,16 +372,11 @@ 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:
@@ -495,9 +391,6 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-cmake
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/pre-steps-macos"
@@ -517,8 +410,6 @@ 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
@@ -526,28 +417,12 @@ 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')
@@ -558,20 +433,29 @@ jobs:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
# The docker image is intentionally based on an OS that has an older GLIBC version.
# That GLIBC is incompatibile with GitHub's actions/checkout. Thus we implement a manual checkout step.
# NOTE: replaced evolvedbinary/rocksjava:centos7_x64-be with ghcr.io/facebook/rocksdb_ubuntu:22.1
# until a more appropriate docker image with C++20 support is made.
- name: Checkout
env:
GH_TOKEN: ${{ github.token }}
run: |
chown `whoami` . || true
git clone --no-checkout https://oath2:$GH_TOKEN@github.com/${{ github.repository }}.git .
git -c protocol.version=2 fetch --update-head-ok --no-tags --prune --no-recurse-submodules --depth=1 origin +${{ github.sha }}:${{ github.ref }}
git checkout --progress --force ${{ github.ref }}
git log -1 --format='%H'
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: java
- name: Set Java Environment
run: |-
echo "JAVA_HOME=${JAVA_HOME}"
which java && java -version
which javac && javac -version
- name: Test RocksDBJava
# NOTE: replaced scl enable devtoolset-7 'make V=1 J=8 -j8 jtest'
run: make V=1 J=8 -j8 jtest
- uses: "./.github/actions/teardown-ccache"
if: always()
# post-steps skipped because of compatibility issues with docker image
build-linux-java-static:
if: needs.config.outputs.only_job == 'build-linux-java-static' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
@@ -581,20 +465,29 @@ jobs:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
# The docker image is intentionally based on an OS that has an older GLIBC version.
# That GLIBC is incompatibile with GitHub's actions/checkout. Thus we implement a manual checkout step.
# NOTE: replaced evolvedbinary/rocksjava:centos7_x64-be with ghcr.io/facebook/rocksdb_ubuntu:22.1
# until a more appropriate docker image with C++20 support is made.
- name: Checkout
env:
GH_TOKEN: ${{ github.token }}
run: |
chown `whoami` . || true
git clone --no-checkout https://oath2:$GH_TOKEN@github.com/${{ github.repository }}.git .
git -c protocol.version=2 fetch --update-head-ok --no-tags --prune --no-recurse-submodules --depth=1 origin +${{ github.sha }}:${{ github.ref }}
git checkout --progress --force ${{ github.ref }}
git log -1 --format='%H'
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: java-static
- name: Set Java Environment
run: |-
echo "JAVA_HOME=${JAVA_HOME}"
which java && java -version
which javac && javac -version
- name: Build RocksDBJava Static Library
# NOTE: replaced scl enable devtoolset-7 'make V=1 J=8 -j8 rocksdbjavastatic'
run: make V=1 J=8 -j8 rocksdbjavastatic
- uses: "./.github/actions/teardown-ccache"
if: always()
# post-steps skipped because of compatibility issues with docker image
build-macos-java:
if: needs.config.outputs.only_job == 'build-macos-java' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
@@ -607,9 +500,6 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-java
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/install-jdk8-on-macos"
@@ -621,8 +511,6 @@ 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')
@@ -635,9 +523,6 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-java-static
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/install-jdk8-on-macos"
@@ -649,8 +534,6 @@ 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')
@@ -663,9 +546,6 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-java-static-universal
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/install-jdk8-on-macos"
@@ -677,8 +557,6 @@ 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')
@@ -715,13 +593,6 @@ 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 ccache
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: arm
- run: sudo apt-get update && sudo apt-get install -y build-essential
- run: ROCKSDBTESTS_PLATFORM_DEPENDENT=only make V=1 J=4 -j4 all_but_some_tests check_some
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
-8
View File
@@ -102,11 +102,3 @@ cmake-build-*
third-party/folly/
.cache
*.sublime-*
# Claude Code local settings
.claude/settings.local.json
tools/__pycache__/
# Keep documentation trackable even if broader ignores match names like "*_test".
!docs/
!docs/**
-9
View File
@@ -1,9 +0,0 @@
# Agent Instructions
This repository's authoritative agent instructions live in `CLAUDE.md`.
Read and follow [`CLAUDE.md`](./CLAUDE.md) in full before making changes or
reviewing code in this checkout.
If there is any ambiguity between this file and `CLAUDE.md`, `CLAUDE.md` takes
precedence.
+8 -59
View File
@@ -32,14 +32,12 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"db/blob/blob_file_cache.cc",
"db/blob/blob_file_garbage.cc",
"db/blob/blob_file_meta.cc",
"db/blob/blob_file_partition_manager.cc",
"db/blob/blob_file_reader.cc",
"db/blob/blob_garbage_meter.cc",
"db/blob/blob_log_format.cc",
"db/blob/blob_log_sequential_reader.cc",
"db/blob/blob_log_writer.cc",
"db/blob/blob_source.cc",
"db/blob/blob_write_batch_transformer.cc",
"db/blob/prefetch_buffer_collection.cc",
"db/builder.cc",
"db/c.cc",
@@ -108,10 +106,8 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"db/version_edit.cc",
"db/version_edit_handler.cc",
"db/version_set.cc",
"db/version_util.cc",
"db/wal_edit.cc",
"db/wal_manager.cc",
"db/wide/read_path_blob_resolver.cc",
"db/wide/wide_column_serialization.cc",
"db/wide/wide_columns.cc",
"db/wide/wide_columns_helper.cc",
@@ -211,7 +207,6 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"table/block_based/hash_index_reader.cc",
"table/block_based/index_builder.cc",
"table/block_based/index_reader_common.cc",
"table/block_based/multi_scan_index_iterator.cc",
"table/block_based/parsed_full_filter_block.cc",
"table/block_based/partitioned_filter_block.cc",
"table/block_based/partitioned_index_iterator.cc",
@@ -334,7 +329,6 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"utilities/secondary_index/simple_secondary_index.cc",
"utilities/simulator_cache/cache_simulator.cc",
"utilities/simulator_cache/sim_cache.cc",
"utilities/sorted_run_builder/sorted_run_builder.cc",
"utilities/table_properties_collectors/compact_for_tiering_collector.cc",
"utilities/table_properties_collectors/compact_on_deletion_collector.cc",
"utilities/trace/file_trace_reader_writer.cc",
@@ -368,9 +362,6 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"utilities/transactions/write_prepared_txn_db.cc",
"utilities/transactions/write_unprepared_txn.cc",
"utilities/transactions/write_unprepared_txn_db.cc",
"utilities/trie_index/bitvector.cc",
"utilities/trie_index/louds_trie.cc",
"utilities/trie_index/trie_index_factory.cc",
"utilities/ttl/db_ttl_impl.cc",
"utilities/types_util.cc",
"utilities/wal_filter.cc",
@@ -378,10 +369,10 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"utilities/write_batch_with_index/write_batch_with_index_internal.cc",
], deps=[
"//folly/container:f14_hash",
"//folly/coro:blocking_wait",
"//folly/coro:collect",
"//folly/coro:coroutine",
"//folly/coro:task",
"//folly/experimental/coro:blocking_wait",
"//folly/experimental/coro:collect",
"//folly/experimental/coro:coroutine",
"//folly/experimental/coro:task",
"//folly/synchronization:distributed_mutex",
], headers=glob(["**/*.h"]), link_whole=False, extra_test_libs=False)
@@ -4808,12 +4799,6 @@ cpp_unittest_wrapper(name="db_blob_corruption_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_blob_direct_write_test",
srcs=["db/blob/db_blob_direct_write_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_blob_index_test",
srcs=["db/blob/db_blob_index_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -4952,12 +4937,6 @@ cpp_unittest_wrapper(name="db_merge_operator_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_open_with_config_test",
srcs=["db/db_open_with_config_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_options_test",
srcs=["db/db_options_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5048,12 +5027,6 @@ cpp_unittest_wrapper(name="db_wide_basic_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_wide_blob_direct_write_test",
srcs=["db/wide/db_wide_blob_direct_write_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_with_timestamp_basic_test",
srcs=["db/db_with_timestamp_basic_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5164,12 +5137,6 @@ cpp_unittest_wrapper(name="faiss_ivf_index_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="fault_injection_fs_test",
srcs=["utilities/fault_injection_fs_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="fault_injection_test",
srcs=["db/fault_injection_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5554,12 +5521,6 @@ cpp_unittest_wrapper(name="slice_transform_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="sorted_run_builder_test",
srcs=["utilities/sorted_run_builder/sorted_run_builder_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="sst_dump_test",
srcs=["tools/sst_dump_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5662,18 +5623,6 @@ cpp_unittest_wrapper(name="transaction_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="trie_index_db_test",
srcs=["utilities/trie_index/trie_index_db_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="trie_index_test",
srcs=["utilities/trie_index/trie_index_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="ttl_test",
srcs=["utilities/ttl/ttl_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5776,14 +5725,14 @@ cpp_unittest_wrapper(name="write_controller_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="write_prepared_transaction_seqno_test",
srcs=["utilities/transactions/write_prepared_transaction_seqno_test.cc"],
cpp_unittest_wrapper(name="write_prepared_transaction_test",
srcs=["utilities/transactions/write_prepared_transaction_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="write_prepared_transaction_test",
srcs=["utilities/transactions/write_prepared_transaction_test.cc"],
cpp_unittest_wrapper(name="write_prepared_transaction_test_seqno",
srcs=["utilities/transactions/write_prepared_transaction_test_seqno.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
-19
View File
@@ -227,11 +227,6 @@ 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
@@ -255,9 +250,6 @@ 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
@@ -266,14 +258,6 @@ 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
@@ -287,9 +271,6 @@ 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
+5 -78
View File
@@ -82,15 +82,10 @@ if(NOT CMAKE_BUILD_TYPE)
endif()
message(STATUS "CMAKE_BUILD_TYPE is set to ${CMAKE_BUILD_TYPE}")
# Use ccache for compilation if available. Use CMAKE_C/CXX_COMPILER_LAUNCHER
# instead of RULE_LAUNCH_COMPILE to avoid double-wrapping when ccache is also
# injected via PATH (e.g., /usr/lib/ccache or brew --prefix ccache/libexec).
# Note: we intentionally do NOT set RULE_LAUNCH_LINK because ccache cannot
# cache link operations -- it only adds overhead.
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
set(CMAKE_C_COMPILER_LAUNCHER ccache)
set(CMAKE_CXX_COMPILER_LAUNCHER ccache)
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache)
endif(CCACHE_FOUND)
option(WITH_JEMALLOC "build with JeMalloc" OFF)
@@ -520,15 +515,6 @@ elseif(CMAKE_SYSTEM_NAME MATCHES "iOS")
add_definitions(-DOS_MACOSX -DIOS_CROSS_COMPILE)
elseif(CMAKE_SYSTEM_NAME MATCHES "Linux")
add_definitions(-DOS_LINUX)
# Use lld linker if available for faster linking (12x faster than ld.bfd)
if(NOT ROCKSDB_NO_FAST_LINKER)
execute_process(COMMAND ${CMAKE_CXX_COMPILER} -fuse-ld=lld -Wl,--version
OUTPUT_VARIABLE LLD_VERSION_OUTPUT ERROR_QUIET RESULT_VARIABLE LLD_RESULT)
if(LLD_RESULT EQUAL 0 AND LLD_VERSION_OUTPUT MATCHES "LLD")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=lld")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fuse-ld=lld")
endif()
endif()
elseif(CMAKE_SYSTEM_NAME MATCHES "SunOS")
add_definitions(-DOS_SOLARIS)
elseif(CMAKE_SYSTEM_NAME MATCHES "kFreeBSD")
@@ -674,38 +660,10 @@ if(USE_FOLLY)
endif()
endif()
# Folly itself uses gflags transitively, but RocksDB tools and benchmarks
# also call gflags APIs directly, so they need an explicit link dependency.
if(WITH_GFLAGS)
if(NOT TARGET gflags::gflags AND NOT TARGET gflags_shared AND
NOT gflags_LIBRARIES)
find_package(gflags CONFIG QUIET)
if(NOT gflags_FOUND)
find_package(gflags REQUIRED)
endif()
endif()
if(TARGET gflags::gflags)
set(GFLAGS_LIB gflags::gflags)
elseif(TARGET gflags_shared)
set(GFLAGS_LIB gflags_shared)
elseif(DEFINED GFLAGS_TARGET AND TARGET ${GFLAGS_TARGET})
set(GFLAGS_LIB ${GFLAGS_TARGET})
elseif(gflags_LIBRARIES)
set(GFLAGS_LIB ${gflags_LIBRARIES})
else()
message(FATAL_ERROR
"WITH_GFLAGS is enabled, but no gflags library could be resolved")
endif()
list(APPEND THIRDPARTY_LIBS ${GFLAGS_LIB})
endif()
add_compile_definitions(USE_FOLLY FOLLY_NO_CONFIG HAVE_CXX11_ATOMIC)
list(APPEND THIRDPARTY_LIBS Folly::folly)
set(FOLLY_LIBS Folly::folly)
# --copy-dt-needed-entries is ld.bfd-specific; lld handles DT_NEEDED transitively
if(NOT CMAKE_EXE_LINKER_FLAGS MATCHES "fuse-ld=lld")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--copy-dt-needed-entries")
endif()
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--copy-dt-needed-entries")
endif()
find_package(Threads REQUIRED)
@@ -732,7 +690,6 @@ set(SOURCES
db/blob/blob_file_addition.cc
db/blob/blob_file_builder.cc
db/blob/blob_file_cache.cc
db/blob/blob_file_partition_manager.cc
db/blob/blob_file_garbage.cc
db/blob/blob_file_meta.cc
db/blob/blob_file_reader.cc
@@ -741,7 +698,6 @@ set(SOURCES
db/blob/blob_log_sequential_reader.cc
db/blob/blob_log_writer.cc
db/blob/blob_source.cc
db/blob/blob_write_batch_transformer.cc
db/blob/prefetch_buffer_collection.cc
db/builder.cc
db/c.cc
@@ -810,10 +766,8 @@ set(SOURCES
db/version_edit.cc
db/version_edit_handler.cc
db/version_set.cc
db/version_util.cc
db/wal_edit.cc
db/wal_manager.cc
db/wide/read_path_blob_resolver.cc
db/wide/wide_column_serialization.cc
db/wide/wide_columns.cc
db/wide/wide_columns_helper.cc
@@ -888,7 +842,6 @@ set(SOURCES
table/block_based/block_based_table_builder.cc
table/block_based/block_based_table_factory.cc
table/block_based/block_based_table_iterator.cc
table/block_based/multi_scan_index_iterator.cc
table/block_based/block_based_table_reader.cc
table/block_based/block_builder.cc
table/block_based/block_cache.cc
@@ -995,7 +948,6 @@ set(SOURCES
utilities/cassandra/merge_operator.cc
utilities/checkpoint/checkpoint_impl.cc
utilities/compaction_filters.cc
utilities/sorted_run_builder/sorted_run_builder.cc
utilities/compaction_filters/remove_emptyvalue_compactionfilter.cc
utilities/counted_fs.cc
utilities/debug.cc
@@ -1047,9 +999,6 @@ set(SOURCES
utilities/transactions/write_prepared_txn_db.cc
utilities/transactions/write_unprepared_txn.cc
utilities/transactions/write_unprepared_txn_db.cc
utilities/trie_index/bitvector.cc
utilities/trie_index/louds_trie.cc
utilities/trie_index/trie_index_factory.cc
utilities/types_util.cc
utilities/ttl/db_ttl_impl.cc
utilities/wal_filter.cc
@@ -1165,23 +1114,8 @@ if(USE_FOLLY_LITE)
FMT_INCLUDE_DIR)
include_directories(${FMT_INCLUDE_DIR})
exec_program(python3 ${PROJECT_SOURCE_DIR}/third-party/folly ARGS
build/fbcode_builder/getdeps.py show-inst-dir glog OUTPUT_VARIABLE
GLOG_INST_PATH)
if(EXISTS ${GLOG_INST_PATH}/lib64)
set(GLOG_LIB_DIR ${GLOG_INST_PATH}/lib64)
else()
set(GLOG_LIB_DIR ${GLOG_INST_PATH}/lib)
endif()
add_definitions(-DUSE_FOLLY -DFOLLY_NO_CONFIG)
find_library(GLOG_LIBRARY NAMES glog PATHS ${GLOG_LIB_DIR} NO_DEFAULT_PATH)
if(NOT GLOG_LIBRARY)
find_library(GLOG_LIBRARY NAMES glog)
endif()
if(GLOG_LIBRARY)
list(APPEND THIRDPARTY_LIBS ${GLOG_LIBRARY})
endif()
list(APPEND THIRDPARTY_LIBS glog)
endif()
set(ROCKSDB_STATIC_LIB rocksdb${ARTIFACT_SUFFIX})
@@ -1427,7 +1361,6 @@ if(WITH_TESTS)
db/blob/blob_garbage_meter_test.cc
db/blob/blob_source_test.cc
db/blob/db_blob_basic_test.cc
db/blob/db_blob_direct_write_test.cc
db/blob/db_blob_compaction_test.cc
db/blob/db_blob_corruption_test.cc
db/blob/db_blob_index_test.cc
@@ -1465,7 +1398,6 @@ if(WITH_TESTS)
db/db_memtable_test.cc
db/db_merge_operator_test.cc
db/db_merge_operand_test.cc
db/db_open_with_config_test.cc
db/db_options_test.cc
db/db_properties_test.cc
db/db_range_del_test.cc
@@ -1518,7 +1450,6 @@ if(WITH_TESTS)
db/wal_manager_test.cc
db/wal_edit_test.cc
db/wide/db_wide_basic_test.cc
db/wide/db_wide_blob_direct_write_test.cc
db/wide/wide_column_serialization_test.cc
db/wide/wide_columns_helper_test.cc
db/write_batch_test.cc
@@ -1599,9 +1530,7 @@ if(WITH_TESTS)
utilities/cassandra/cassandra_row_merge_test.cc
utilities/cassandra/cassandra_serialize_test.cc
utilities/checkpoint/checkpoint_test.cc
utilities/sorted_run_builder/sorted_run_builder_test.cc
utilities/env_timed_test.cc
utilities/fault_injection_fs_test.cc
utilities/memory/memory_test.cc
utilities/merge_operators/string_append/stringappend_test.cc
utilities/object_registry_test.cc
@@ -1619,12 +1548,10 @@ if(WITH_TESTS)
utilities/transactions/lock/point/point_lock_manager_stress_test.cc
utilities/transactions/write_committed_transaction_ts_test.cc
utilities/transactions/write_prepared_transaction_test.cc
utilities/transactions/write_prepared_transaction_seqno_test.cc
utilities/transactions/write_prepared_transaction_test_seqno.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
+8 -46
View File
@@ -1,55 +1,17 @@
# Rocksdb Change Log
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
## 11.2.4 (06/24/2026)
### Bug Fixes
* Fixed a bug where closing a read-only DB instance could delete live SST files created by a concurrent read-write DB sharing the same directory.
## 11.2.3 (05/07/2026)
### New Features
* Add experimental mutable DBOption `optimize_manifest_for_recovery` (default false). When enabled, RocksDB can reduce recovery work after a clean shutdown, which may lower DB::Open latency on warm reopens.
* Add experimental `reuse_manifest_on_open` DBOption (default false). When enabled, DB::Open reuses the existing MANIFEST file for append instead of creating a fresh one, avoiding the cost of serializing the entire database state into a new MANIFEST on the first post-open write.
## 11.2.2 (05/07/2026)
## 11.2.1 (04/29/2026)
### Bug Fixes
* Fix bug in range tombstone synthesis that covers live keys added during an IngestExternalFile
## 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.
## 11.0.4 (03/11/2026)
### 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.
* Re-remove deprecated DB::Open raw pointer variants in db.h
### 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.
## 11.0.3 (03/03/2026)
### Public API Changes
* Relax option sanitization for kv ratio compaction
### 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.2 (02/26/2026)
### Public API Changes
* Revert removing deprecated DB::Open raw pointer variants in db.h
## 11.0.0 (02/23/2026)
### New Features
+23 -123
View File
@@ -450,7 +450,7 @@ ifndef USE_FOLLY
endif
ifndef GTEST_THROW_ON_FAILURE
export GTEST_THROW_ON_FAILURE=0
export GTEST_THROW_ON_FAILURE=1
endif
ifndef GTEST_HAS_EXCEPTIONS
export GTEST_HAS_EXCEPTIONS=1
@@ -621,24 +621,25 @@ endif
ROCKSDBTESTS_SUBSET ?= $(TESTS)
# c_test - doesn't use gtest, can't be sharded
# Other tests previously listed here (backup_engine_test, db_bloom_filter_test,
# perf_context_test, etc.) were NON_PARALLEL due to /dev/shm memory concerns
# when the old per-test-case sharding spawned thousands of processes. With the
# new gtest-based sharding (GTEST_SHARD_SIZE=10, max NCORES*8 shards), at most
# ~4 shards run concurrently on CI (4 cores), so peak memory is manageable
# (4 * 1GB = 4GB << 16GB shm).
# c_test - doesn't use gtest
# env_test - suspicious use of test::TmpDir
# deletefile_test - serial because it generates giant temporary files in
# its various tests. Parallel can fill up your /dev/shm
# db_bloom_filter_test - serial because excessive space usage by instances
# of DBFilterConstructionReserveMemoryTestWithParam can fill up /dev/shm
NON_PARALLEL_TEST = \
c_test \
env_test \
deletefile_test \
db_bloom_filter_test \
$(PLUGIN_TESTS) \
PARALLEL_TEST = $(filter-out $(NON_PARALLEL_TEST), $(ROCKSDBTESTS_SUBSET))
PARALLEL_TEST = $(filter-out $(NON_PARALLEL_TEST), $(TESTS))
# Not necessarily well thought out or up-to-date, but matches old list
TESTS_PLATFORM_DEPENDENT := \
db_basic_test \
db_blob_basic_test \
db_blob_direct_write_test \
db_encryption_test \
external_sst_file_basic_test \
auto_roll_logger_test \
@@ -646,7 +647,6 @@ TESTS_PLATFORM_DEPENDENT := \
dynamic_bloom_test \
c_test \
checkpoint_test \
sorted_run_builder_test \
crc32c_test \
coding_test \
inlineskiplist_test \
@@ -807,18 +807,7 @@ endif # PLATFORM_SHARED_EXT
rocksdbjavastatic rocksdbjava install install-static install-shared \
uninstall analyze tools tools_lib check-headers checkout_folly clang-tidy
# Auto-configure git hooks on first build so developers do not need to run
# "make install-hooks" manually. This is a no-op if already set.
setup-hooks:
@if [ -d .git ] && [ -d githooks ]; then \
cur=$$(git config core.hooksPath 2>/dev/null); \
if [ "$$cur" != "githooks" ]; then \
git config core.hooksPath githooks; \
echo "git hooks: configured core.hooksPath = githooks"; \
fi; \
fi
all: setup-hooks $(LIBRARY) $(BENCHMARKS) tools tools_lib test_libs $(TESTS)
all: $(LIBRARY) $(BENCHMARKS) tools tools_lib test_libs $(TESTS)
all_but_some_tests: $(LIBRARY) $(BENCHMARKS) tools tools_lib test_libs $(ROCKSDBTESTS_SUBSET)
@@ -882,42 +871,21 @@ coverage: clean
parallel_tests = $(patsubst %,parallel_%,$(PARALLEL_TEST))
.PHONY: gen_parallel_tests $(parallel_tests)
# Shard size controls how many test cases run per process. The actual number
# of shards per binary is: min(ceil(test_count / GTEST_SHARD_SIZE), NCORES * 8)
# This adapts to machine size: many small shards on beefy machines, fewer
# larger shards on CI (typically 2-4 cores).
GTEST_SHARD_SIZE ?= 10
NCORES ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
$(parallel_tests):
$(AM_V_at)TEST_BINARY=$(patsubst parallel_%,%,$@); \
TEST_COUNT=` \
(./$$TEST_BINARY --gtest_list_tests 2>/dev/null || echo " list_failure") \
| grep -c '^ '`; \
if [ "$$TEST_COUNT" -le 0 ]; then TEST_COUNT=1; fi; \
MAX_SHARDS=$$(( $(NCORES) * 8 )); \
NUM_SHARDS=$$(( (TEST_COUNT + $(GTEST_SHARD_SIZE) - 1) / $(GTEST_SHARD_SIZE) )); \
if [ "$$NUM_SHARDS" -gt "$$MAX_SHARDS" ]; then NUM_SHARDS=$$MAX_SHARDS; fi; \
if [ "$$NUM_SHARDS" -le 0 ]; then NUM_SHARDS=1; fi; \
echo " Generating $$NUM_SHARDS shards for $$TEST_BINARY ($$TEST_COUNT tests)"; \
SHARD_IDX=0; \
while [ "$$SHARD_IDX" -lt "$$NUM_SHARDS" ]; do \
if [ -n "$(CI_TOTAL_SHARDS)" ] && [ $$(( $$SHARD_IDX % $(CI_TOTAL_SHARDS) )) -ne $(CI_SHARD_INDEX) ]; then \
SHARD_IDX=$$((SHARD_IDX + 1)); \
continue; \
fi; \
TEST_SCRIPT=t/run-$$TEST_BINARY-shard-$$SHARD_IDX; \
TEST_NAMES=` \
(./$$TEST_BINARY --gtest_list_tests || echo " $${TEST_BINARY}__list_tests_failure") \
| awk '/^[^ ]/ { prefix = $$1 } /^[ ]/ { print prefix $$1 }'`; \
echo " Generating parallel test scripts for $$TEST_BINARY"; \
for TEST_NAME in $$TEST_NAMES; do \
TEST_SCRIPT=t/run-$$TEST_BINARY-$${TEST_NAME//\//-}; \
printf '%s\n' \
'#!/bin/sh' \
"d=\$(TEST_TMPDIR)$$TEST_SCRIPT" \
'mkdir -p $$d' \
"TEST_TMPDIR=\$$d GTEST_TOTAL_SHARDS=$$NUM_SHARDS GTEST_SHARD_INDEX=$$SHARD_IDX $(DRIVER) ./$$TEST_BINARY" \
'test_retcode=$$?' \
'[ $$test_retcode -eq 0 ] && rm -rf $$d' \
'exit $$test_retcode' \
"TEST_TMPDIR=\$$d $(DRIVER) ./$$TEST_BINARY --gtest_filter=$$TEST_NAME" \
> $$TEST_SCRIPT; \
chmod a=rx $$TEST_SCRIPT; \
SHARD_IDX=$$((SHARD_IDX + 1)); \
done
gen_parallel_tests:
@@ -941,10 +909,8 @@ gen_parallel_tests:
# 152.120 PASS t/DBTest.FileCreationRandomFailure
# 107.816 PASS t/DBTest.EncodeDecompressedBlockSizeTest
#
# With sharded test execution, prioritize binaries known to be slow.
# These generate many shards and should start early for good load balancing.
slow_test_regexp = \
^.*block_based_table_reader_test.*$$|^.*table_test.*$$|^.*block_test.*$$|^.*write_prepared_transaction_test.*$$|^.*transaction_test.*$$|^.*external_sst_file_test.*$$|^.*db_wal_test.*$$|^.*db_with_timestamp_basic_test.*$$|^.*db_test-.*$$
^.*MySQLStyleTransactionTest.*$$|^.*SnapshotConcurrentAccessTest.*$$|^.*SeqAdvanceConcurrentTest.*$$|^t/run-table_test-HarnessTest.Randomized$$|^t/run-db_test-.*(?:FileCreationRandomFailure|EncodeDecompressedBlockSizeTest)$$|^.*RecoverFromCorruptedWALWithoutFlush$$
prioritize_long_running_tests = \
perl -pe 's,($(slow_test_regexp)),100 $$1,' \
| sort -k1,1gr \
@@ -979,13 +945,7 @@ check_0:
'To monitor subtest <duration,pass/fail,name>,' \
' run "make watch-log" in a separate window' ''; \
{ \
NON_PARALLEL_LIST="$(filter-out $(PARALLEL_TEST),$(ROCKSDBTESTS_SUBSET))"; \
if [ -n "$$NON_PARALLEL_LIST" ]; then \
printf './%s\n' $$NON_PARALLEL_LIST \
| if [ -n "$(CI_TOTAL_SHARDS)" ]; then \
awk -v s=$(CI_SHARD_INDEX) -v n=$(CI_TOTAL_SHARDS) '(NR-1)%n==s'; \
else cat; fi; \
fi; \
printf './%s\n' $(filter-out $(PARALLEL_TEST),$(TESTS)); \
find t -name 'run-*' -print; \
} \
| $(prioritize_long_running_tests) \
@@ -1041,11 +1001,6 @@ 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) | \
@@ -1061,7 +1016,6 @@ ifneq ($(PLATFORM), OS_AIX)
$(PYTHON) tools/check_all_python.py
ifndef ASSERT_STATUS_CHECKED # not yet working with these tests
$(PYTHON) tools/ldb_test.py
$(PYTHON) tools/db_crashtest_test.py
sh tools/rocksdb_dump_test.sh
endif
endif
@@ -1069,7 +1023,6 @@ ifndef SKIP_FORMAT_BUCK_CHECKS
$(MAKE) check-format
$(MAKE) check-buck-targets
$(MAKE) check-sources
$(MAKE) check-workflow-yaml
endif
# TODO add ldb_tests
@@ -1080,10 +1033,6 @@ check_some: $(ROCKSDBTESTS_SUBSET)
ldb_tests: ldb
$(PYTHON) tools/ldb_test.py
.PHONY: db_crashtest_tests
db_crashtest_tests:
$(PYTHON) tools/db_crashtest_test.py
include crash_test.mk
asan_check: clean
@@ -1267,40 +1216,12 @@ format-auto:
check-format:
build_tools/format-diff.sh -c
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
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:
@@ -1488,9 +1409,6 @@ db_basic_test: $(OBJ_DIR)/db/db_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
db_blob_basic_test: $(OBJ_DIR)/db/blob/db_blob_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_blob_direct_write_test: $(OBJ_DIR)/db/blob/db_blob_direct_write_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_blob_compaction_test: $(OBJ_DIR)/db/blob/db_blob_compaction_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1500,9 +1418,6 @@ db_readonly_with_timestamp_test: $(OBJ_DIR)/db/db_readonly_with_timestamp_test.o
db_wide_basic_test: $(OBJ_DIR)/db/wide/db_wide_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_wide_blob_direct_write_test: $(OBJ_DIR)/db/wide/db_wide_blob_direct_write_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_with_timestamp_basic_test: $(OBJ_DIR)/db/db_with_timestamp_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1512,9 +1427,6 @@ db_with_timestamp_compaction_test: db/db_with_timestamp_compaction_test.o $(TEST
db_encryption_test: $(OBJ_DIR)/db/db_encryption_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_open_with_config_test: $(OBJ_DIR)/db/db_open_with_config_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_test: $(OBJ_DIR)/db/db_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1659,9 +1571,6 @@ backup_engine_test: $(OBJ_DIR)/utilities/backup/backup_engine_test.o $(TEST_LIBR
checkpoint_test: $(OBJ_DIR)/utilities/checkpoint/checkpoint_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
sorted_run_builder_test: $(OBJ_DIR)/utilities/sorted_run_builder/sorted_run_builder_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
cache_simulator_test: $(OBJ_DIR)/utilities/simulator_cache/cache_simulator_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1680,12 +1589,6 @@ object_registry_test: $(OBJ_DIR)/utilities/object_registry_test.o $(TEST_LIBRARY
ttl_test: $(OBJ_DIR)/utilities/ttl/ttl_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
trie_index_db_test: $(OBJ_DIR)/utilities/trie_index/trie_index_db_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
trie_index_test: $(OBJ_DIR)/utilities/trie_index/trie_index_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
types_util_test: $(OBJ_DIR)/utilities/types_util_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1737,9 +1640,6 @@ io_posix_test: $(OBJ_DIR)/env/io_posix_test.o $(TEST_LIBRARY) $(LIBRARY)
fault_injection_test: $(OBJ_DIR)/db/fault_injection_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
fault_injection_fs_test: $(OBJ_DIR)/utilities/fault_injection_fs_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
rate_limiter_test: $(OBJ_DIR)/util/rate_limiter_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1938,7 +1838,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_seqno_test: $(OBJ_DIR)/utilities/transactions/write_prepared_transaction_seqno_test.o $(TEST_LIBRARY) $(LIBRARY)
write_prepared_transaction_test_seqno: $(OBJ_DIR)/utilities/transactions/write_prepared_transaction_test_seqno.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
write_unprepared_transaction_test: $(OBJ_DIR)/utilities/transactions/write_unprepared_transaction_test.o $(TEST_LIBRARY) $(LIBRARY)
@@ -2661,7 +2561,7 @@ list_all_tests:
# Remove the rules for which dependencies should not be generated and see if any are left.
#If so, include the dependencies; if not, do not include the dependency files
ROCKS_DEP_RULES=$(filter-out clean format check-format check-buck-targets check-headers check-sources check-workflow-yaml clang-tidy jclean jtest package analyze tags rocksdbjavastatic% unity.% unity_test checkout_folly, $(MAKECMDGOALS))
ROCKS_DEP_RULES=$(filter-out clean format check-format check-buck-targets check-headers check-sources clang-tidy jclean jtest package analyze tags rocksdbjavastatic% unity.% unity_test checkout_folly, $(MAKECMDGOALS))
ifneq ("$(ROCKS_DEP_RULES)", "")
-include $(DEPFILES)
endif
+4 -4
View File
@@ -146,10 +146,10 @@ def generate_buck(repo_path, deps_map):
src_mk["RANGE_TREE_SOURCES"] + src_mk["TOOL_LIB_SOURCES"],
deps=[
"//folly/container:f14_hash",
"//folly/coro:blocking_wait",
"//folly/coro:collect",
"//folly/coro:coroutine",
"//folly/coro:task",
"//folly/experimental/coro:blocking_wait",
"//folly/experimental/coro:collect",
"//folly/experimental/coro:coroutine",
"//folly/experimental/coro:task",
"//folly/synchronization:distributed_mutex",
],
headers=LiteralValue("glob([\"**/*.h\"])")
-18
View File
@@ -148,24 +148,6 @@ PLATFORM_SHARED_LDFLAGS="-Wl,--no-as-needed -shared -Wl,-soname -Wl,"
PLATFORM_SHARED_CFLAGS="-fPIC"
PLATFORM_SHARED_VERSIONED=true
# Prefer lld linker when available on Linux. lld is typically 5-10x faster
# than the default ld.bfd for large C++ projects. macOS uses ld64 (or
# ld-prime) which is already fast, so we skip lld detection there.
# Set ROCKSDB_NO_FAST_LINKER=1 to disable this auto-detection.
if [ -z "$ROCKSDB_NO_FAST_LINKER" ] && [ "$TARGET_OS" = "Linux" ]; then
if $CXX -fuse-ld=lld -L/usr/local/lib -x c++ - -o /dev/null 2>/dev/null <<EOF
int main() { return 0; }
EOF
then
PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -fuse-ld=lld"
# Ensure lld can find libraries in /usr/local/lib (lld does not
# search there by default, unlike ld.bfd)
if [ -d /usr/local/lib ]; then
PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -L/usr/local/lib"
fi
fi
fi
# generic port files (working on all platform by #ifdef) go directly in /port
GENERIC_PORT_FILES=`cd "$ROCKSDB_ROOT"; find port -name '*.cc' | tr "\n" " "`
-35
View File
@@ -1,35 +0,0 @@
#!/usr/bin/env bash
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
#
# Validate GitHub Actions workflow YAML before it reaches CI runtime.
set -euo pipefail
if ! command -v ruby >/dev/null 2>&1; then
echo "ruby is required to validate GitHub Actions workflow YAML"
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
+1 -1
View File
@@ -1913,7 +1913,7 @@ sub drain_job_queue {
}
$last_progress_time = time();
$ps_reported = 0;
} elsif (not $ps_reported and (time() - $last_progress_time) >= 300) {
} elsif (not $ps_reported and (time() - $last_progress_time) >= 60) {
# No progress in at least 60 seconds: run ps
print $Global::original_stderr "\n";
my $script_dir = ::dirname($0);
+3 -5
View File
@@ -5,8 +5,6 @@
# the docker and docker-registry groups, and logging out and back in to pick
# those up.)
#
# Meta employees: see https://fburl.com/rocksdb-docker to build this on a devvm.
#
# Follow https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic
# to login with your GitHub credentials, as in
#
@@ -17,8 +15,8 @@
# Then in the build_tools/ubuntu22_image directory, (bump minor version for
# random docker file updates, major version tracks Ubuntu release)
#
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:22.2
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:22.2
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:22.0
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:22.0
#
# Might need to change visibility to public through
# https://github.com/orgs/facebook/packages/container/rocksdb_ubuntu/settings
@@ -30,7 +28,7 @@ FROM ubuntu:22.04
RUN apt-get update
RUN apt-get upgrade -y
# install basic tools
RUN apt-get install -y vim wget curl ccache
RUN apt-get install -y vim wget curl
# install tzdata noninteractive
RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install tzdata
# install git and default compilers
+3 -9
View File
@@ -5,8 +5,6 @@
# the docker and docker-registry groups, and logging out and back in to pick
# those up.)
#
# Meta employees: see https://fburl.com/rocksdb-docker to build this on a devvm.
#
# Follow https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic
# to login with your GitHub credentials, as in
#
@@ -17,8 +15,8 @@
# Then in the build_tools/ubuntu24_image directory, (bump minor version for
# random docker file updates, major version tracks Ubuntu release)
#
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:24.1
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:24.1
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:24.0
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:24.0
#
# Might need to change visibility to public through
# https://github.com/orgs/facebook/packages/container/rocksdb_ubuntu/settings
@@ -30,15 +28,11 @@ FROM ubuntu:24.04
RUN apt-get update
RUN apt-get upgrade -y
# install basic tools
RUN apt-get install -y vim wget curl ccache
RUN apt-get install -y vim wget curl
# install tzdata noninteractive
RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install tzdata
# install git and default compilers
RUN apt-get install -y git gcc g++ clang clang-tools
# install clang-21 from LLVM snapshot repo
RUN curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/llvm.gpg && \
echo "deb https://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" > /etc/apt/sources.list.d/llvm-21.list && \
apt-get update && apt-get install -y clang-21
# install basic package
RUN apt-get install -y lsb-release software-properties-common gnupg
# install gflags, tbb
-3
View File
@@ -1,3 +0,0 @@
You are an expert C++ engineer for RocksDB.
Read CLAUDE.md in the repo root and claude_md/ files for project context.
Answer thoroughly with exact file paths and line numbers.
-14
View File
@@ -1,14 +0,0 @@
Read review-findings.md. This file contains partial code
review findings from a review session that ran out of turns before
finishing. Format them into a clean final review comment using the
output format defined in claude_md/code_review.md (Summary, Issues
Found, Cross-Component Analysis, Positive Observations).
At the top of the review, add this notice:
> **Partial review** -- the analysis was interrupted before all
> perspectives were completed. The findings below cover the
> perspectives that were finished. You can request a full re-review
> with `/claude-review`.
Write ONLY the formatted review as your final response -- no
commentary or explanation outside the review itself.
-606
View File
@@ -1,606 +0,0 @@
# Code Review Workflow — CI Prompt
## Purpose
Thorough multi-agent code review for RocksDB commits following CLAUDE.md guidelines,
with structured codebase exploration, inter-agent debate, and verification.
IMPORTANT — Incremental output: After completing EACH major phase (Setup,
Codebase Context, Initial Review, Debate, Synthesis), append your findings
to review-findings.md using the Write tool. This ensures partial results are
saved even if the review is interrupted by the turn limit. After the final
synthesis, write the complete review to review-findings.md, replacing
previous content. Also output the final review as your response text.
## Workflow Steps
### 1. Setup Phase
- Read CLAUDE.md to understand review guidelines
- Identify the commit/diff/PR to review
- The PR diff is provided below in this prompt. The repository is already
checked out — use local file search tools (Grep, Glob, Read) to explore.
- Parse the changed files and categorize them (API, core logic, tests, etc.)
- Read all changed files to build full context
### 2. Codebase Context Phase (CRITICAL — Do Not Skip)
**Why this matters**: Review agents that only see the diff miss systemic bugs.
For example, a change to an iterator's return value might look correct in
isolation but break multi-SST iteration 5 layers up the call stack. A change
to error handling might silently drop errors that a caller 3 levels up relies
on for recovery. This phase builds the context that prevents those blind spots.
Go deep. Surface-level reading leads to reviews that miss caching layers,
existing helper functions, or concurrency requirements. The most expensive
failure mode is findings that look correct in isolation but miss how the
change breaks the surrounding system.
The team lead spawns one or more research agents to perform the following
analyses using local file search tools (Grep, Glob, Read). The research
must be written to `context.md` BEFORE any review agent is spawned.
#### 2a. Subsystem Deep-Read
Read the changed files AND their surrounding subsystem **in depth** — not just
signatures, but actual logic, edge cases, data flows, locking protocols,
concurrency patterns, and interactions between components.
For each changed file, also read:
- The header that declares its interface (if the change is in a .cc file)
- The sibling implementation that does the same thing the "standard" way
(e.g., for a UDI iterator change, read how `IndexBlockIter` handles the
same scenario — it's the reference implementation)
- The wrapper/adapter layer that sits between this code and its callers
- Any existing tests to understand what behaviors are currently guaranteed
#### 2b. Caller-Chain Analysis (upstream impact)
For each changed function/method, trace the call chain UPWARD 3-5 levels to
understand who consumes the changed behavior and what invariants they rely on.
**Focus on control flow decisions** — if/else branches, while loop conditions,
early returns — that depend on the changed return values or state.
**How to do this**:
- Search for callers of each changed function (search for the function name, the
class method, virtual overrides)
- For each caller, search for ITS callers, repeating 3-5 levels up
- At each level, read the actual code to understand the control flow decision
- Stop when you reach a "terminal" caller (e.g., user-facing API, top-level
iterator) or when the propagation is clearly safe
#### 2c. Callee-Chain Analysis (downstream dependencies AND side effects)
For each changed function, trace what it calls and whether those calls have
new preconditions or changed semantics. **Critically, trace SIDE EFFECTS —
not just return values.** Many bugs hide in side effects on shared state
(counters, sequence numbers, flags) that are invisible if you only check
return values.
**For each callee, ask:**
1. What does it RETURN? (the obvious check)
2. What shared state does it MUTATE? (the non-obvious check)
3. Under what PRECONDITIONS does it operate correctly?
4. Are those preconditions met in the new calling context?
#### 2d. Sibling Implementation Comparison
Identify the "reference" or "standard" implementation that does the same thing
the changed code does, and compare their behavior. This is critical for plugin/
extension APIs where the implementation must match an implicit contract.
#### 2e. Invariant Documentation
Document the key invariants that the changed code must maintain. Read existing
comments, assertions, and test expectations to discover these invariants.
#### 2f. Related Functionality and Existing Conventions
Identify related functionality that already exists in the codebase:
- Are there existing helper functions the change should use (or is duplicating)?
- Are there existing patterns for the same kind of change (e.g., how other
Customizable subclasses handle deprecation)?
- Are there existing tests that test similar scenarios (e.g., multi-SST
iteration tests that could be extended)?
#### 2g. Cross-Component Data Consumer Analysis (CRITICAL — common blind spot)
**Why this is separate from caller-chain analysis:** Sections 2b-2c trace
who CALLS the changed functions. This section traces who CONSUMES the data
that the changed code PRODUCES. These are often completely different
subsystems.
When the change writes data to a shared structure (memtable, SST block, cache
entry, statistics counter), ask: **"Who reads this data, under what rules,
and do those rules match the writer's assumptions?"**
**How to do this:**
1. Identify every piece of data the change WRITES (e.g., a range tombstone
entry, a counter update, a metadata field)
2. For each, search the codebase for ALL readers of that data structure
3. For each reader, check:
- Does the reader apply visibility rules (seqno, read_callback, snapshot)?
- Are those rules compatible with the writer's assumptions?
- Does the reader assume invariants about the data (e.g., seqno ordering,
monotonicity) that the writer might violate?
#### 2h. Alternative Execution Contexts
The same code path may run in different contexts with different assumptions.
**Enumerate all contexts and check each one.** This is where "works in the
common case but breaks in edge configurations" bugs hide.
For RocksDB, always check whether the changed code interacts differently with:
| Context | Key difference | Common failure mode |
|---------|---------------|-------------------|
| WritePreparedTxnDB / WriteUnpreparedTxnDB | `read_callback_` controls visibility, not just seqno | Visibility bypass |
| ReadOnly DB / SecondaryInstance | No mutable memtable, writes not allowed | Null pointer or no-op needed |
| CompactionService / Remote compaction | Different process, serialized state | Serialization mismatch |
| User-defined timestamps | Extra dimension in key comparison and visibility | Wrong ordering |
| MemPurge | Memtable-to-memtable, not memtable-to-SST | Missing data |
| Column family with BlobDB | Values may be in blob files, not inline | Missing dereference |
| Snapshots (old, held long-term) | Snapshot seqno may be far behind current state | Metadata corruption |
| Concurrent writers (allow_concurrent_memtable_write) | Lock-free paths vs locked paths | Lost updates |
| FIFO / Universal compaction | Different compaction invariants than Level | Wrong assumptions |
| Prefix seek / total order seek | Different iterator behavior | Wrong iteration results |
**For each context, ask:**
1. Does the code execute in this context? (Check constructor, feature flags)
2. If yes, do the assumptions still hold?
3. If not, should the feature be disabled in this context?
#### 2i. Assumption Stress-Testing (the "logically X" audit)
When the change claims a property (e.g., "logically redundant," "no-op in
this case," "safe because X"), **systematically break it:**
1. **State the claim precisely.** E.g., "The inserted range tombstone is
logically redundant — it covers only keys already deleted by point
tombstones."
2. **List the preconditions** that must be true for the claim to hold.
3. **For each precondition, construct a counterexample** — a concrete scenario
where the precondition is violated.
4. **Verify each counterexample against the code.** Does the code guard
against it? If not, it's a bug.
**Anti-pattern: treating asserts as proofs.** When you see an assert, do NOT
conclude "the invariant holds." Instead ask: "Can I construct an input where
this assert fires?" If yes, it's a bug (the assert catches it in debug but
release builds silently corrupt). If you cannot construct a counterexample
after trying, document WHY it's impossible.
#### 2j. Write Context Document
Write the complete analysis to `context.md` in the review folder. This document
is provided to ALL review agents as part of their prompt.
**Context document template**:
```markdown
# Codebase Context for Review
## How the Relevant Subsystem Works Today
[Detailed description of the subsystem architecture, data flows,
and component interactions. Not just "what" but "how" and "why."]
## Changed Functions and Their Call Chains
### function_name() (file:line)
**What changed**: [brief description of the behavioral change]
**Upstream callers** (who depends on this):
1. CallerA::method() (file:line) — uses return value to decide X
2. CallerB::method() (file:line) — passes result to CallerC
3. CallerC::method() (file:line) — THE CRITICAL DECISION POINT: ...
**Downstream callees** (what this depends on):
1. calleeA() — behavior unchanged
2. calleeB() — NEW dependency, requires X
**Sibling implementation** (how the "standard" version handles this):
- StandardImpl::method() does Y at file:line, which ensures invariant Z
- The changed code must match or document any deviation
**Key invariants**:
- Must never return X when Y is true because CallerC will...
- Must always set Z before returning because CallerB assumes...
## System Architecture Context
[How the changed components fit into the overall system]
## Known Invariants That Must Be Preserved
1. ...
2. ...
## Existing Conventions and Related Code
- [helper functions, patterns, existing tests that are relevant]
## Cross-Component Data Consumers
For each piece of data the change WRITES to a shared structure:
### [data item] written to [structure]
**Writer assumptions**: [what the writer assumes about how this data is consumed]
**All readers**:
1. ReaderA::method() (file:line) — reads under [visibility rules]
- Compatible with writer? YES/NO. If NO, explain the mismatch.
2. ReaderB::method() (file:line) — reads under [different rules]
- Compatible with writer? YES/NO.
## Alternative Execution Contexts
| Context | Does code execute? | Assumptions hold? | Action needed? |
|---------|-------------------|-------------------|----------------|
| WritePreparedTxnDB | YES/NO | YES/NO | [disable/guard/safe] |
| Old snapshots | YES/NO | YES/NO | ... |
| User-defined timestamps | YES/NO | YES/NO | ... |
| ReadOnly DB | YES/NO | YES/NO | ... |
| ... | ... | ... | ... |
## Assumption Stress Test
### Claim: "[the design claim, e.g., logically redundant]"
**Preconditions for claim to hold:**
1. [precondition] — counterexample: [scenario]. Guarded? YES/NO.
2. [precondition] — counterexample: [scenario]. Guarded? YES/NO.
## Potential Pitfalls
- [specific scenarios where the change could interact badly with
the surrounding system, identified from the caller-chain analysis]
```
### 3. Initial Review Phase (Parallel)
Create a team and spawn 5 review agents in parallel. **Include the context
document from Phase 2 in each agent's prompt.** Each agent writes findings
to its own file and sends a summary message to the team lead.
Each agent's prompt should include:
```
## Codebase Context (READ THIS FIRST)
[paste or reference the context.md file]
You MUST consider how your findings interact with the upstream callers
and system invariants documented above. A finding that looks correct
in isolation may be a critical bug when you consider the full call chain.
```
#### Agent: Design & Approach Reviewer
- Read the commit message deeply to understand the problem being solved
- Analyze whether this is the right approach to the problem
- Consider alternative designs and trade-offs:
- Are there simpler solutions that achieve the same goal?
- Does the approach introduce unnecessary complexity?
- Would a different data structure or algorithm be more appropriate?
- Evaluate whether the change follows existing architectural patterns or
introduces new patterns that may be inconsistent
- Assess the scope: is the change too large? Should it be split into
smaller, independently reviewable pieces?
- Check if the approach has precedent in the codebase or in the academic
literature it references (e.g., SuRF paper)
#### Agent: Correctness Reviewer
- Thread safety and concurrency analysis
- Error handling and propagation via Status type
- Edge cases (empty inputs, overflow, boundary conditions)
- Data corruption scenarios
- Logic correctness of new algorithms
- **Behavioral contract changes**: Do return value semantics match what
upstream callers expect? (Use the caller-chain analysis from context.md)
- **Callee side effects**: Does the change call functions that mutate shared
state (counters, seqnos, metadata)? Are the mutations correct for the new
calling context? (Use the callee-chain analysis from context.md)
#### Agent: Cross-Component & Adversarial Reviewer
This agent exists because the most critical bugs hide at component boundaries,
not within a single component. It uses a fundamentally different methodology
than the correctness reviewer: instead of verifying "does the code do what it
intends?", it asks "what breaks when we change the assumptions?"
**Data-flow analysis:** Perform the cross-component data consumer analysis
described in section 2g. For every piece of data the change WRITES to a shared
structure, trace ALL READERS and verify their visibility rules are compatible
with the writer's assumptions. This is a DATA-FLOW question, not a
CONTROL-FLOW question — readers may be in completely different subsystems.
**Alternative execution contexts:** Use the canonical table from section 2h.
Enumerate all contexts where the changed code executes and verify assumptions
hold in each.
**Assumption stress-testing and assert-breaking:** Follow the methodology from
section 2i. Identify every design claim, enumerate preconditions, construct
counterexamples. Treat asserts as hypotheses to break, not proofs.
**Red flag words**: "logically redundant", "safe because", "no-op",
"always true", "cannot happen", "invariant holds"
**Callee side-effect audit:** Perform the callee side-effect audit described
in section 2c. For every callee, ask what it RETURNS and what it MUTATES.
Write findings to `findings-cross-component.md`.
#### Agent: Invariant Adversary
This agent does NOT read the diff in detail. It takes the feature summary and
systematically tries to break it. While other agents verify "does the code do
what it says?", this agent asks "what existing system invariants does this
feature violate?" and "under what inputs do the design claims fail?"
**Steps 1-4: Assumption stress-testing.** Follow the methodology from section
2i: extract design claims, enumerate preconditions, construct counterexamples,
and attempt to break every assert. Be exhaustive about input parameter ranges,
shared state configurations, and concurrent interleaving.
**Step 5: Callee side-effect audit.** Perform the callee side-effect audit
described in section 2c. For every function the changed code calls, list ALL
mutations to shared state (atomic CAS loops, counter increments, flag/metadata
updates, cache invalidation). A function returning `Status::OK()` does NOT
mean its side effects are correct.
Write findings to `findings-invariant-adversary.md`.
#### Agent: Caller-Context Auditor
This agent traces BACKWARD from every entry point the changed code hooks into.
While other agents read the changed code forward ("what does it do?"), this
agent enumerates WHO invokes it and WITH WHAT parameter ranges.
The key insight: a function that is correct for all *typical* inputs may be
catastrophically wrong for *atypical-but-reachable* inputs. This agent's job
is to find the atypical inputs.
**Step 1: Identify entry points.** List every function/constructor/method in
the changed code that receives external input (parameters, config values,
pointers to shared state). Include:
- Constructor parameters (e.g., `active_mem`, `read_callback`, `sequence`)
- Virtual method overrides that callers invoke polymorphically
- Functions called from multiple subsystems
**Step 2: Enumerate all callers (3-5 levels up).** For each entry point,
search the codebase for ALL callers. Do NOT stop at the first caller — trace
the full call chain. Use `Grep` and `Glob` to find:
- Direct callers of the function
- Callers of wrapper/adapter functions that forward to it
- Factory functions that construct the object
- Virtual dispatch sites (search for the base class method name)
**Step 3: Parameter range analysis.** For each caller, determine:
- What values does it pass for each parameter?
- Under what conditions does it call this function?
- Are there callers that pass unusual values? (e.g., `kMaxSequenceNumber`,
`nullptr`, old snapshot seqnos, non-null `read_callback_`)
Build a parameter range table for each entry point, listing all callers
and the values they pass for each parameter.
**Step 4: Configuration matrix.** Enumerate option/config combinations that
affect the changed code path:
- Which options enable/disable the feature?
- Which options change the execution context? (e.g.,
`allow_concurrent_memtable_write`, `use_trie_index`)
- Are there option combinations that create contradictions?
**Step 5: Cross-reference with context.md.** Compare your caller analysis
with the invariants and execution contexts documented in context.md. Flag
any caller that violates an assumed precondition.
Write findings to `findings-caller-audit.md`.
#### Agent: Performance Reviewer
- Memory allocation on hot paths
- Unnecessary copies (string, buffer)
- Cache efficiency and data locality
- Loop optimization opportunities
- Branch prediction (LIKELY/UNLIKELY)
- Zero-overhead design verification
#### Agent: API & Compatibility Reviewer
- Public API backwards compatibility
- API consistency with existing patterns
- Documentation completeness
- Deprecation policy compliance
- Forward compatibility (struct extensibility)
- **Behavioral compatibility**: Do changed semantics (e.g., kOutOfBound vs
kUnknown) break implicit contracts with callers?
#### Agent: Serialization & Deserialization Reviewer
- Format correctness and versioning
- Alignment requirements
- Integer overflow in size computations
- Bounds checking on untrusted data
- Crafted input attack vectors
#### Agent: Test Coverage Reviewer
- Edge case coverage
- Failure mode testing (corruption, truncation)
- Round-trip testing (build → serialize → deserialize → verify)
- Integration test coverage
- **System-level test coverage**: Are the caller-chain interactions tested?
(e.g., multi-SST iteration with UDI, level compaction with new behavior)
- Test quality (no flaky patterns, good assertions)
### 4. Round-Robin Debate Phase
After all 5 agents complete their initial review, the team lead orchestrates
a structured debate:
#### Step 4a: Share findings
- Team lead sends each agent a summary of ALL other agents' findings
(or instructs each agent to read the other agents' findings files)
#### Step 4b: Critique round (2 rounds)
Each agent reviews the findings from the other agents and sends messages to
challenge, support, or refine them:
- **Challenge**: "I disagree with [agent] [finding] because [evidence]."
- **Support**: "I independently found the same issue — this confirms it."
- **Refine**: "[Finding A] is actually a consequence of [Finding B].
The root cause is the same."
The debate assignment follows a round-robin pattern:
```
correctness-reviewer → critiques → invariant-adversary, serialization
cross-component-reviewer → critiques → correctness, caller-audit
invariant-adversary → critiques → correctness, cross-component
caller-audit → critiques → invariant-adversary, cross-component
performance-reviewer → critiques → api, cross-component
api-reviewer → critiques → correctness, performance
serialization-reviewer → critiques → correctness, invariant-adversary
test-reviewer → critiques → serialization, caller-audit
design-reviewer → critiques → cross-component, invariant-adversary
```
**Note:** The invariant-adversary and caller-audit agents are deliberately
cross-linked with correctness and cross-component because their findings
often reveal the same underlying bug from different angles (invariant
violation vs reachable bad input vs data-flow mismatch).
Each agent should:
1. Read the assigned agents' findings files
2. Send a critique message to each assigned agent
3. Respond to critiques received from other agents
4. Update their own findings file with any revisions (upgraded/downgraded severity,
withdrawn findings, new findings inspired by others)
#### Step 4c: Team lead collects debate results
- Read all updated findings files
- Review the debate messages
- Build consensus: findings supported by 2+ agents → HIGH confidence
- Write consensus document with final severity classifications
### 5. Consensus Phase
Team lead synthesizes the debate into a consensus document:
- Cross-reference overlapping findings across agents
- Count agreement (majority = 2+ agents independently flagging or supporting)
- Note disagreements and how they were resolved
- Classify findings as HIGH/MEDIUM/LOW severity
- Write consensus document
### 6. Summary Phase
- Compile all validated findings ordered by severity
- Include detailed bug analysis (root cause, vulnerable code paths)
- Include suggested fixes
- Note which findings were debated and the outcome
- Write the complete review to review-findings.md and output as response
**Final report quality rules:**
- The final report must be CLEAN and POLISHED. No stream-of-consciousness,
no "Wait, actually...", no "on closer re-examination". Those belong in
the working notes, not the output.
- If a finding was raised during review but later disproven during debate
or deeper analysis, REMOVE IT entirely from the final report. Do not
include it with a retraction — just drop it.
- If a finding was downgraded (e.g., Critical → Suggestion), present it
at the final severity only. Do not narrate the severity change.
- Each finding in the final report should read as a confident, verified
conclusion — not a record of the analysis process.
- The reader should never see the reviewer arguing with itself.
## Review Checklist
### Context Phase (must be completed before agents spawn)
- [ ] Subsystem deep-read — read changed files AND surrounding subsystem
in depth (logic, edge cases, data flows, locking, concurrency)
- [ ] Caller chain for every changed public/virtual method (3-5 levels up)
- [ ] Critical decision points where callers branch on changed behavior
- [ ] Callee chain with SIDE EFFECTS — downstream dependencies, new
preconditions, AND mutations to shared state (section 2c)
- [ ] Sibling implementations — how does the standard version handle same scenarios?
- [ ] Invariants that callers rely on
- [ ] Related functionality — existing helpers, patterns, conventions
- [ ] Cross-component data consumers — for every data WRITTEN, find ALL readers
and verify visibility rules match (section 2g)
- [ ] Alternative execution contexts verified (section 2h table)
- [ ] Assumption stress-test — for every design claim, list preconditions
and construct counterexamples (section 2i)
- [ ] Multi-component interactions (compaction, recovery, snapshots, iterators)
- [ ] Configuration dependencies and unexpected option combinations
- [ ] Potential pitfalls from caller-chain analysis
### Review Phase (verified by agents)
- [ ] Database semantics preserved (snapshot isolation, key ordering)
- [ ] All error cases handled
- [ ] Thread-safe with correct synchronization
- [ ] No data races or deadlocks
- [ ] Appropriate test coverage (edge cases, failure modes, system-level)
- [ ] No unnecessary allocations or copies in hot paths
- [ ] Backwards compatible
- [ ] New APIs consistent with existing patterns and documented
- [ ] Code follows RocksDB style conventions
- [ ] Behavioral contracts with upstream callers preserved
- [ ] All callers enumerated with parameter range table
## Output Structure
Write all review artifacts to the working directory root:
- `review-findings.md` — Incremental findings (appended after each phase),
then replaced with the final synthesized review at the end
- `context.md` — Codebase context (call chains, invariants)
- `findings-*.md` — Per-agent findings (design, correctness, cross-component,
invariant-adversary, caller-audit, performance, api, serialization, tests)
- `consensus.md` — Cross-review consensus (post-debate)
## Team Structure
```
Team Lead (you)
│
├── Phase 2: Codebase Context (team lead or dedicated research agent)
│ └── context-researcher (general-purpose agent)
│ ├── Trace caller chains (3-5 levels up)
│ ├── Trace callee chains (dependencies AND side effects)
│ ├── Trace data consumers (who reads what the change writes?)
│ └── Document invariants
│
├── Phase 3: Initial Review (parallel, run_in_background)
│ │ (all agents receive context.md in their prompt)
│ ├── design-reviewer (general-purpose agent)
│ ├── correctness-reviewer (general-purpose agent)
│ ├── cross-component-reviewer (general-purpose agent)
│ ├── invariant-adversary (general-purpose agent) ← NEW
│ ├── caller-audit (general-purpose agent) ← NEW
│ ├── performance-reviewer (general-purpose agent)
│ ├── api-reviewer (general-purpose agent)
│ ├── serialization-reviewer (general-purpose agent)
│ └── test-reviewer (general-purpose agent)
│
├── Phase 4: Debate (agents message each other)
│ ├── correctness ↔ invariant-adversary, serialization
│ ├── cross-component ↔ correctness, caller-audit
│ ├── invariant-adversary ↔ correctness, cross-component
│ ├── caller-audit ↔ invariant-adversary, cross-component
│ ├── performance ↔ api, cross-component
│ ├── api ↔ correctness, performance
│ ├── serialization ↔ correctness, invariant-adversary
│ ├── test-coverage ↔ serialization, caller-audit
│ └── design ↔ cross-component, invariant-adversary
│
```
## Agent Communication Protocol
### During Initial Review (Phase 3)
- Each agent writes findings to its own file
- Each agent sends a summary message to team lead when done
- Agents do NOT communicate with each other yet
### During Debate (Phase 4)
- Team lead sends each agent a message with instructions to:
1. Read the assigned agents' findings files
2. Send critique messages to those agents
3. Respond to incoming critiques
4. Update their own findings file with revisions
- Each critique message should include:
- Which finding they're addressing (e.g., "Correctness F1")
- Whether they AGREE, DISAGREE, or want to REFINE
- Their reasoning with code evidence
- Suggested severity adjustment (if any)
### Message format example
```
To: correctness-reviewer
Re: Your Finding F1
AGREE/DISAGREE/REFINE - [reasoning with code evidence].
[Suggested severity adjustment if any.]
```
## Review Anti-Patterns
These recurring failure modes lead to missed bugs. Each is detailed in the
referenced section; this table is a quick-reference checklist.
| Anti-Pattern | Fix | Reference |
|---|---|---|
| Return-Value Tunnel Vision | Trace callee MUTATIONS, not just returns | Section 2c |
| Default-Configuration Bias | Enumerate all execution contexts | Section 2h |
| Assert-as-Proof | Try to BREAK every assert | Section 2i |
| Write-Path-Only Analysis | Trace data readers, not just writers | Section 2g |
| Confirmation-Seeking Research | Use adversarial prompts ("find where X fails") | Invariant Adversary agent |
| Data-Flow vs Control-Flow Confusion | Separate who CALLS from who READS the data | Section 2g |
-104
View File
@@ -1,104 +0,0 @@
# Code Review Skill
This document defines the methodology for performing thorough, multi-perspective
code reviews on RocksDB changes. It is used both by CI (GitHub Actions) and
local review workflows.
## Prerequisites
Before starting a review:
- Read `CLAUDE.md` in the repository root for project-specific guidelines
- Read any files in `claude_md/` for architecture context
- Identify the commit/diff/PR to review and parse the changed files
## Review Methodology
Conduct the review from multiple perspectives, then synthesize findings into a
single report. Each perspective catches different classes of bugs.
### Perspective 1: Codebase Context & Call-Chain Analysis
Before reviewing the diff itself, build deep context:
- For each changed function/method, trace the call chain UPWARD 3-5 levels.
Who consumes the changed behavior? What invariants do callers rely on?
- Trace DOWNWARD into callees. What side effects do they have? What shared
state do they mutate (counters, sequence numbers, flags, metadata)?
- Identify the "sibling" or "reference" implementation that handles the same
scenario the standard way. Compare behaviors.
- For data written to shared structures (memtable, SST block, cache entry),
find ALL readers and verify their visibility rules match the writer.
### Perspective 2: Correctness & Edge Cases
- Thread safety and concurrency (lock ordering, data races)
- Error handling and Status propagation
- Edge cases: empty inputs, overflow, boundary conditions
- Data corruption scenarios
- Behavioral contract changes: do return value semantics match callers?
### Perspective 3: Cross-Component & Adversarial Analysis
Check the change in ALL execution contexts:
| Context | Key difference |
|---------|---------------|
| WritePreparedTxnDB | read_callback_ controls visibility |
| ReadOnly DB / SecondaryInstance | No mutable memtable |
| CompactionService / Remote compaction | Different process |
| User-defined timestamps | Extra key comparison dimension |
| MemPurge | Memtable-to-memtable path |
| BlobDB | Values in blob files, not inline |
| Old snapshots | Seqno far behind current |
| Concurrent writers | Lock-free vs locked paths |
| FIFO / Universal compaction | Different invariants |
| Prefix seek / total order seek | Different iterator behavior |
When the change claims a property ("logically redundant", "safe because X"),
systematically try to break it: list preconditions, construct counterexamples.
### Perspective 4: Performance
RocksDB is a high-performance storage engine.
- Memory allocation on hot paths
- Unnecessary copies (prefer move semantics, Slice, string_view)
- Cache efficiency and data locality
- Loop optimization, branch prediction (LIKELY/UNLIKELY)
### Perspective 5: API, Compatibility & Testing
- Public API backwards compatibility
- Serialization format correctness and versioning
- Test coverage for edge cases, failure modes, and system-level interactions
## How to Explore the Codebase
Use available tools to explore deeply — do NOT just review the diff in isolation.
The most critical bugs hide at component boundaries.
- Read the header files for changed implementations
- Search for callers of changed functions (3-5 levels up)
- Read existing tests to understand guaranteed behaviors
- Check related implementations for conventions and patterns
## Output Format
### Summary
Brief overall assessment (1-2 sentences).
### Issues Found
Categorize by severity:
- :red_circle: **Critical**: Must fix (correctness, data corruption, security)
- :yellow_circle: **Suggestion**: Should consider (performance, edge cases)
- :green_circle: **Nitpick**: Minor style issues
For each issue include:
- File and line reference
- Which review perspective found it
- Description with root cause analysis
- Suggested fix
### Cross-Component Analysis
Execution context checks and assumption stress-test results.
### Positive Observations
Good patterns, clever optimizations, or improvements.
-7
View File
@@ -1,7 +0,0 @@
# Removing Deprecated Options from RocksDB
### Keep in these files:
- [ ] **KEEP** entry in type_info (`options/cf_options.cc` or `options/db_options.cc`) with `OptionVerificationType::kDeprecated` for loading old option files
- [ ] **KEEP** or add test in `options/options_test.cc` `OptionsOldApiTest::GetOptionsFromMapTest` for loading old option files
### Documentation:
- [ ] Add release note to `HISTORY.md` and `unreleased_history/`
+7 -18
View File
@@ -5,8 +5,6 @@
#include "db/blob/blob_fetcher.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_index.h"
#include "db/version_set.h"
namespace ROCKSDB_NAMESPACE {
@@ -16,13 +14,10 @@ Status BlobFetcher::FetchBlob(const Slice& user_key,
FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value,
uint64_t* bytes_read) const {
BlobIndex blob_index;
Status status = blob_index.DecodeFrom(blob_index_slice);
if (status.ok()) {
status = FetchBlob(user_key, blob_index, prefetch_buffer, blob_value,
bytes_read);
}
return status;
assert(version_);
return version_->GetBlob(read_options_, user_key, blob_index_slice,
prefetch_buffer, blob_value, bytes_read);
}
Status BlobFetcher::FetchBlob(const Slice& user_key,
@@ -30,16 +25,10 @@ Status BlobFetcher::FetchBlob(const Slice& user_key,
FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value,
uint64_t* bytes_read) const {
if (!allow_write_path_fallback_) {
assert(version_);
assert(version_);
return version_->GetBlob(read_options_, user_key, blob_index,
prefetch_buffer, blob_value, bytes_read);
}
return BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
read_options_, user_key, blob_index, version_, blob_file_cache_,
prefetch_buffer, blob_value, bytes_read);
return version_->GetBlob(read_options_, user_key, blob_index, prefetch_buffer,
blob_value, bytes_read);
}
} // namespace ROCKSDB_NAMESPACE
+3 -13
View File
@@ -10,25 +10,17 @@
namespace ROCKSDB_NAMESPACE {
class BlobFileCache;
class Version;
class Slice;
class FilePrefetchBuffer;
class PinnableSlice;
class BlobIndex;
// A thin wrapper around blob retrieval. By default it reads through Version,
// and it can optionally fall back to direct-write blob files that are not yet
// manifest-visible.
// A thin wrapper around the blob retrieval functionality of Version.
class BlobFetcher {
public:
BlobFetcher(const Version* version, const ReadOptions& read_options,
BlobFileCache* blob_file_cache = nullptr,
bool allow_write_path_fallback = false)
: version_(version),
read_options_(read_options),
blob_file_cache_(blob_file_cache),
allow_write_path_fallback_(allow_write_path_fallback) {}
BlobFetcher(const Version* version, const ReadOptions& read_options)
: version_(version), read_options_(read_options) {}
Status FetchBlob(const Slice& user_key, const Slice& blob_index_slice,
FilePrefetchBuffer* prefetch_buffer,
@@ -41,7 +33,5 @@ class BlobFetcher {
private:
const Version* version_;
ReadOptions read_options_;
BlobFileCache* blob_file_cache_;
bool allow_write_path_fallback_;
};
} // namespace ROCKSDB_NAMESPACE
+4 -2
View File
@@ -67,11 +67,13 @@ BlobFileBuilder::BlobFileBuilder(
min_blob_size_(mutable_cf_options->min_blob_size),
blob_file_size_(mutable_cf_options->blob_file_size),
blob_compression_type_(mutable_cf_options->blob_compression_type),
// TODO: support most CompressionOptions with a new CF option
// blob_compression_opts
// TODO with schema change: support custom compression manager and options
// such as max_compressed_bytes_per_kb
// NOTE: returns nullptr for kNoCompression
blob_compressor_(GetBuiltinV2CompressionManager()->GetCompressor(
mutable_cf_options->blob_compression_opts, blob_compression_type_)),
CompressionOptions{}, blob_compression_type_)),
blob_compressor_wa_(blob_compressor_
? blob_compressor_->ObtainWorkingArea()
: Compressor::ManagedWorkingArea{}),
@@ -109,7 +111,7 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
assert(blob_index);
assert(blob_index->empty());
if (value.empty() || value.size() < min_blob_size_) {
if (value.size() < min_blob_size_) {
return Status::OK();
}
+3 -125
View File
@@ -9,7 +9,6 @@
#include <memory>
#include "db/blob/blob_file_reader.h"
#include "logging/logging.h"
#include "options/cf_options.h"
#include "rocksdb/cache.h"
#include "rocksdb/slice.h"
@@ -39,8 +38,7 @@ BlobFileCache::BlobFileCache(Cache* cache,
Status BlobFileCache::GetBlobFileReader(
const ReadOptions& read_options, uint64_t blob_file_number,
CacheHandleGuard<BlobFileReader>* blob_file_reader,
bool allow_footer_skip_retry) {
CacheHandleGuard<BlobFileReader>* blob_file_reader) {
assert(blob_file_reader);
assert(blob_file_reader->IsEmpty());
@@ -75,22 +73,10 @@ Status BlobFileCache::GetBlobFileReader(
{
assert(file_options_);
Status s = BlobFileReader::Create(
const Status s = BlobFileReader::Create(
*immutable_options_, read_options, *file_options_, column_family_id_,
blob_file_read_hist_, blob_file_number, io_tracer_,
/*skip_footer_validation=*/false, &reader);
if (!s.ok() && s.IsCorruption() && allow_footer_skip_retry) {
reader.reset();
s = BlobFileReader::Create(
*immutable_options_, read_options, *file_options_, column_family_id_,
blob_file_read_hist_, blob_file_number, io_tracer_,
/*skip_footer_validation=*/true, &reader);
}
blob_file_read_hist_, blob_file_number, io_tracer_, &reader);
if (!s.ok()) {
ROCKS_LOG_WARN(immutable_options_->logger,
"BlobFileCache open failed for blob file %" PRIu64
" in CF %u: %s",
blob_file_number, column_family_id_, s.ToString().c_str());
RecordTick(statistics, NO_FILE_ERRORS);
return s;
}
@@ -113,120 +99,12 @@ Status BlobFileCache::GetBlobFileReader(
return Status::OK();
}
Status BlobFileCache::OpenBlobFileReaderUncached(
const ReadOptions& read_options, uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
bool allow_footer_skip_retry) {
assert(blob_file_reader);
assert(!*blob_file_reader);
Statistics* const statistics = immutable_options_->stats;
RecordTick(statistics, NO_FILE_OPENS);
Status s = BlobFileReader::Create(
*immutable_options_, read_options, *file_options_, column_family_id_,
blob_file_read_hist_, blob_file_number, io_tracer_,
/*skip_footer_validation=*/false, blob_file_reader);
if (!s.ok() && s.IsCorruption() && allow_footer_skip_retry) {
blob_file_reader->reset();
s = BlobFileReader::Create(
*immutable_options_, read_options, *file_options_, column_family_id_,
blob_file_read_hist_, blob_file_number, io_tracer_,
/*skip_footer_validation=*/true, blob_file_reader);
}
if (!s.ok()) {
RecordTick(statistics, NO_FILE_ERRORS);
}
return s;
}
Status BlobFileCache::InsertBlobFileReader(
uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader) {
assert(blob_file_reader);
assert(*blob_file_reader);
assert(cached_blob_file_reader);
assert(cached_blob_file_reader->IsEmpty());
const Slice key = GetSliceForKey(&blob_file_number);
// Serialize refreshes for the same blob file. The cache API does not expose
// a conditional replace, so refresh is intentionally modeled as
// Lookup/optional Erase/Insert under this per-key mutex.
MutexLock lock(&mutex_.Get(key));
TypedHandle* handle = cache_.Lookup(key);
if (handle) {
*cached_blob_file_reader = cache_.Guard(handle);
blob_file_reader->reset();
return Status::OK();
}
constexpr size_t charge = 1;
Status s = cache_.Insert(key, blob_file_reader->get(), charge, &handle);
if (!s.ok()) {
RecordTick(immutable_options_->stats, NO_FILE_ERRORS);
return s;
}
// Ownership transferred to the cache.
[[maybe_unused]] BlobFileReader* released_reader =
blob_file_reader->release();
*cached_blob_file_reader = cache_.Guard(handle);
return Status::OK();
}
Status BlobFileCache::RefreshBlobFileReader(
uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader) {
assert(blob_file_reader);
assert(*blob_file_reader);
assert(cached_blob_file_reader);
assert(cached_blob_file_reader->IsEmpty());
const Slice key = GetSliceForKey(&blob_file_number);
MutexLock lock(&mutex_.Get(key));
TypedHandle* handle = cache_.Lookup(key);
if (handle) {
BlobFileReader* const cached_reader = cache_.Value(handle);
assert(cached_reader != nullptr);
// Active direct-write blob files can grow between refresh attempts. Keep
// whichever reader observed the larger on-disk size so an older refresh
// cannot overwrite a newer one that another thread already installed.
if (cached_reader->GetFileSize() >= (*blob_file_reader)->GetFileSize()) {
*cached_blob_file_reader = cache_.Guard(handle);
blob_file_reader->reset();
return Status::OK();
}
cache_.Release(handle);
cache_.get()->Erase(key);
}
constexpr size_t charge = 1;
Status s = cache_.Insert(key, blob_file_reader->get(), charge, &handle);
if (!s.ok()) {
RecordTick(immutable_options_->stats, NO_FILE_ERRORS);
return s;
}
// Ownership transferred to the cache.
[[maybe_unused]] BlobFileReader* released_reader =
blob_file_reader->release();
*cached_blob_file_reader = cache_.Guard(handle);
return Status::OK();
}
void BlobFileCache::Evict(uint64_t blob_file_number) {
// NOTE: sharing same Cache with table_cache
const Slice key = GetSliceForKey(&blob_file_number);
assert(cache_);
MutexLock lock(&mutex_.Get(key));
cache_.get()->Erase(key);
}
+1 -27
View File
@@ -32,35 +32,9 @@ class BlobFileCache {
BlobFileCache(const BlobFileCache&) = delete;
BlobFileCache& operator=(const BlobFileCache&) = delete;
// Returns a cached reader for `blob_file_number`, opening and caching it on
// miss. If `allow_footer_skip_retry` is true, a footer-validation corruption
// retries once without requiring a footer.
Status GetBlobFileReader(const ReadOptions& read_options,
uint64_t blob_file_number,
CacheHandleGuard<BlobFileReader>* blob_file_reader,
bool allow_footer_skip_retry = false);
// Opens a blob file reader without inserting it into the cache.
Status OpenBlobFileReaderUncached(
const ReadOptions& read_options, uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
bool allow_footer_skip_retry = false);
// Inserts a freshly opened uncached reader unless another thread already
// cached the same blob file.
Status InsertBlobFileReader(
uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader);
// Installs a refresh-opened reader into the cache. If another thread has
// already cached a reader opened on at least as large a file size, keep that
// reader instead so a racing refresh cannot reintroduce an older active-file
// view after the blob file grows.
Status RefreshBlobFileReader(
uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader);
CacheHandleGuard<BlobFileReader>* blob_file_reader);
// Called when a blob file is obsolete to ensure it is removed from the cache
// to avoid effectively leaking the open file and assicated memory
-96
View File
@@ -192,102 +192,6 @@ TEST_F(BlobFileCacheTest, GetBlobFileReader_Race) {
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(BlobFileCacheTest, RefreshBlobFileReaderPrefersLargestObservedFileSize) {
Options options;
options.env = mock_env_.get();
options.statistics = CreateDBStatistics();
options.cf_paths.emplace_back(
test::PerThreadDBPath(
mock_env_.get(),
"BlobFileCacheTest_"
"RefreshBlobFileReaderPrefersLargestObservedFileSize"),
0);
options.enable_blob_files = true;
constexpr uint32_t column_family_id = 1;
ImmutableOptions immutable_options(options);
constexpr uint64_t blob_file_number = 123;
const std::string blob_file_path =
BlobFileName(immutable_options.cf_paths.front().path, blob_file_number);
std::unique_ptr<FSWritableFile> file;
ASSERT_OK(NewWritableFile(immutable_options.fs.get(), blob_file_path, &file,
FileOptions()));
std::unique_ptr<WritableFileWriter> file_writer(new WritableFileWriter(
std::move(file), blob_file_path, FileOptions(), immutable_options.clock));
constexpr Statistics* statistics = nullptr;
constexpr bool use_fsync = false;
constexpr bool do_flush = false;
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
statistics, blob_file_number, use_fsync,
do_flush);
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range;
BlobLogHeader header(column_family_id, kNoCompression, has_ttl,
expiration_range);
ASSERT_OK(blob_log_writer.WriteHeader(WriteOptions(), header));
uint64_t key_offset = 0;
uint64_t blob_offset = 0;
ASSERT_OK(blob_log_writer.AddRecord(WriteOptions(), "key0", "blob0",
&key_offset, &blob_offset));
ASSERT_OK(blob_log_writer.Sync(WriteOptions()));
constexpr size_t capacity = 10;
std::shared_ptr<Cache> backing_cache = NewLRUCache(capacity);
FileOptions file_options;
constexpr HistogramImpl* blob_file_read_hist = nullptr;
BlobFileCache blob_file_cache(backing_cache.get(), &immutable_options,
&file_options, column_family_id,
blob_file_read_hist, nullptr /*IOTracer*/);
const ReadOptions read_options;
std::unique_ptr<BlobFileReader> stale_reader;
ASSERT_OK(blob_file_cache.OpenBlobFileReaderUncached(
read_options, blob_file_number, &stale_reader,
/*allow_footer_skip_retry=*/true));
std::unique_ptr<BlobFileReader> initial_cached_reader;
ASSERT_OK(blob_file_cache.OpenBlobFileReaderUncached(
read_options, blob_file_number, &initial_cached_reader,
/*allow_footer_skip_retry=*/true));
CacheHandleGuard<BlobFileReader> cached_reader;
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(
blob_file_number, &initial_cached_reader, &cached_reader));
ASSERT_NE(cached_reader.GetValue(), nullptr);
const uint64_t initial_file_size = cached_reader.GetValue()->GetFileSize();
ASSERT_OK(blob_log_writer.AddRecord(WriteOptions(), "key1", "blob1",
&key_offset, &blob_offset));
ASSERT_OK(blob_log_writer.Sync(WriteOptions()));
std::unique_ptr<BlobFileReader> fresh_reader;
ASSERT_OK(blob_file_cache.OpenBlobFileReaderUncached(
read_options, blob_file_number, &fresh_reader,
/*allow_footer_skip_retry=*/true));
ASSERT_GT(fresh_reader->GetFileSize(), initial_file_size);
CacheHandleGuard<BlobFileReader> refreshed_reader;
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(
blob_file_number, &fresh_reader, &refreshed_reader));
ASSERT_NE(refreshed_reader.GetValue(), nullptr);
ASSERT_GT(refreshed_reader.GetValue()->GetFileSize(), initial_file_size);
BlobFileReader* const largest_reader = refreshed_reader.GetValue();
CacheHandleGuard<BlobFileReader> preserved_reader;
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(
blob_file_number, &stale_reader, &preserved_reader));
ASSERT_NE(preserved_reader.GetValue(), nullptr);
ASSERT_EQ(preserved_reader.GetValue(), largest_reader);
ASSERT_EQ(stale_reader.get(), nullptr);
}
TEST_F(BlobFileCacheTest, GetBlobFileReader_IOError) {
Options options;
options.env = mock_env_.get();
-840
View File
@@ -1,840 +0,0 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "db/blob/blob_file_partition_manager.h"
#include <array>
#include <atomic>
#include <cinttypes>
#include <memory>
#include <utility>
#include "cache/cache_key.h"
#include "cache/typed_cache.h"
#include "db/blob/blob_contents.h"
#include "db/blob/blob_file_cache.h"
#include "db/blob/blob_file_completion_callback.h"
#include "db/blob/blob_file_reader.h"
#include "db/blob/blob_index.h"
#include "db/blob/blob_log_writer.h"
#include "db/version_set.h"
#include "file/filename.h"
#include "file/read_write_util.h"
#include "file/writable_file_writer.h"
#include "logging/logging.h"
#include "util/aligned_buffer.h"
#include "util/compression.h"
#include "util/mutexlock.h"
namespace ROCKSDB_NAMESPACE {
namespace {
class RoundRobinBlobFilePartitionStrategy : public BlobFilePartitionStrategy {
public:
using BlobFilePartitionStrategy::SelectPartition;
const char* Name() const override {
return "RoundRobinBlobFilePartitionStrategy";
}
uint32_t SelectPartition(uint32_t num_partitions,
uint32_t /*column_family_id*/, const Slice& /*key*/,
const Slice& /*value*/) override {
assert(num_partitions > 0);
return static_cast<uint32_t>(
next_partition_.fetch_add(1, std::memory_order_relaxed) %
static_cast<uint64_t>(num_partitions));
}
private:
std::atomic<uint64_t> next_partition_{0};
};
struct DirectWriteCompressionState {
// `working_area` must be released before its owning compressor.
std::unique_ptr<Compressor> compressor;
Compressor::ManagedWorkingArea working_area;
};
DirectWriteCompressionState& GetDirectWriteCompressionState(
CompressionType compression) {
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) {
// BDW v1 mirrors BlobFileBuilder by using the built-in compressor with the
// default CompressionOptions only. Cache it per thread so repeated writes
// do not allocate a new compressor and working area every time.
compression_state.compressor =
GetBuiltinV2CompressionManager()->GetCompressor(CompressionOptions{},
compression);
if (compression_state.compressor != nullptr) {
compression_state.working_area =
compression_state.compressor->ObtainWorkingArea();
}
}
return compression_state;
}
} // namespace
BlobFilePartitionManager::BlobFilePartitionManager(
uint32_t num_partitions,
std::shared_ptr<BlobFilePartitionStrategy> strategy,
FileNumberAllocator file_number_allocator, FileSystem* fs,
SystemClock* clock, Statistics* statistics, const FileOptions& file_options,
std::string db_path, std::string column_family_name,
uint64_t blob_file_size, bool use_fsync, BlobFileCache* blob_file_cache,
BlobFileCompletionCallback* blob_callback,
const std::vector<std::shared_ptr<EventListener>>& listeners,
FileChecksumGenFactory* file_checksum_gen_factory,
const FileTypeSet& checksum_handoff_file_types,
const std::shared_ptr<IOTracer>& io_tracer, std::string db_id,
std::string db_session_id, Logger* info_log)
: num_partitions_(num_partitions == 0 ? 1 : num_partitions),
strategy_(strategy != nullptr
? std::move(strategy)
: std::make_shared<RoundRobinBlobFilePartitionStrategy>()),
file_number_allocator_(std::move(file_number_allocator)),
fs_(fs),
clock_(clock),
statistics_(statistics),
file_options_(file_options),
db_path_(std::move(db_path)),
column_family_name_(std::move(column_family_name)),
blob_file_size_(blob_file_size),
use_fsync_(use_fsync),
blob_file_cache_(blob_file_cache),
blob_callback_(blob_callback),
listeners_(listeners),
file_checksum_gen_factory_(file_checksum_gen_factory),
checksum_handoff_file_types_(checksum_handoff_file_types),
io_tracer_(io_tracer),
db_id_(std::move(db_id)),
db_session_id_(std::move(db_session_id)),
info_log_(info_log) {
partitions_.reserve(num_partitions_);
for (uint32_t i = 0; i < num_partitions_; ++i) {
partitions_.emplace_back(new Partition());
}
}
BlobFilePartitionManager::~BlobFilePartitionManager() {
if (blob_file_cache_ != nullptr) {
std::vector<uint64_t> tracked_file_numbers;
{
ReadLock lock(&file_partition_mutex_);
tracked_file_numbers.reserve(file_to_partition_.size());
for (const auto& entry : file_to_partition_) {
tracked_file_numbers.push_back(entry.first);
}
}
for (uint64_t file_number : tracked_file_numbers) {
// Dropping the CF or closing the DB can destroy the manager while active
// direct-write readers are still cached. Evict them before the CF-owned
// blob cache goes away so Close() does not retain obsolete footer-less
// readers for files that are no longer live.
blob_file_cache_->Evict(file_number);
}
}
}
void BlobFilePartitionManager::ResetPartitionState(Partition* partition) {
partition->writer.reset();
partition->file_number = 0;
partition->file_size = 0;
partition->blob_count = 0;
partition->total_blob_bytes = 0;
partition->garbage_blob_count = 0;
partition->garbage_blob_bytes = 0;
partition->column_family_id = 0;
partition->compression = kNoCompression;
partition->sync_required = false;
}
void BlobFilePartitionManager::AddFilePartitionMapping(uint64_t file_number,
uint32_t partition_idx) {
WriteLock lock(&file_partition_mutex_);
file_to_partition_[file_number] = partition_idx;
}
void BlobFilePartitionManager::RemoveFilePartitionMapping(
uint64_t file_number) {
WriteLock lock(&file_partition_mutex_);
file_to_partition_.erase(file_number);
}
Status BlobFilePartitionManager::OpenNewBlobFile(Partition* partition,
uint32_t column_family_id,
CompressionType compression,
uint32_t partition_idx) {
assert(partition != nullptr);
assert(!partition->writer);
const uint64_t blob_file_number = file_number_allocator_();
const std::string blob_file_path = BlobFileName(db_path_, blob_file_number);
AddFilePartitionMapping(blob_file_number, partition_idx);
if (blob_callback_ != nullptr) {
blob_callback_->OnBlobFileCreationStarted(
blob_file_path, column_family_name_,
/*job_id=*/0, BlobFileCreationReason::kFlush);
}
std::unique_ptr<FSWritableFile> file;
Status s = NewWritableFile(fs_, blob_file_path, &file, file_options_);
if (!s.ok()) {
RemoveFilePartitionMapping(blob_file_number);
return s;
}
const bool perform_data_verification =
checksum_handoff_file_types_.Contains(FileType::kBlobFile);
auto file_writer = std::make_unique<WritableFileWriter>(
std::move(file), blob_file_path, file_options_, clock_, io_tracer_,
statistics_, Histograms::BLOB_DB_BLOB_FILE_WRITE_MICROS, listeners_,
file_checksum_gen_factory_, perform_data_verification);
// This only drains WritableFileWriter's buffered bytes so readers can see
// each appended record promptly. Durability still comes from SyncAllOpenFiles
// or AppendFooter(), both of which call Sync().
constexpr bool kDoFlushEachRecord = true;
auto blob_log_writer = std::make_unique<BlobLogWriter>(
std::move(file_writer), clock_, statistics_, blob_file_number, use_fsync_,
kDoFlushEachRecord);
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range{};
BlobLogHeader header(column_family_id, compression, has_ttl,
expiration_range);
s = blob_log_writer->WriteHeader(WriteOptions(), header);
if (!s.ok()) {
RemoveFilePartitionMapping(blob_file_number);
return s;
}
partition->writer = std::move(blob_log_writer);
partition->file_number = blob_file_number;
partition->file_size = BlobLogHeader::kSize;
partition->blob_count = 0;
partition->total_blob_bytes = 0;
partition->garbage_blob_count = 0;
partition->garbage_blob_bytes = 0;
partition->column_family_id = column_family_id;
partition->compression = compression;
partition->sync_required = false;
return Status::OK();
}
Status BlobFilePartitionManager::SealActiveBlobFile(
const WriteOptions& write_options, Partition* partition,
SealedFile* sealed_file) {
assert(partition != nullptr);
assert(partition->writer);
assert(sealed_file != nullptr);
const uint64_t file_number = partition->file_number;
Status s =
FinalizeBlobFile(write_options, partition->writer.get(), file_number,
partition->blob_count, partition->total_blob_bytes,
&sealed_file->addition);
if (!s.ok()) {
RemoveFilePartitionMapping(file_number);
ResetPartitionState(partition);
return s;
}
sealed_file->garbage_blob_count = partition->garbage_blob_count;
sealed_file->garbage_blob_bytes = partition->garbage_blob_bytes;
ResetPartitionState(partition);
return Status::OK();
}
Status BlobFilePartitionManager::SealDeferredFile(
const WriteOptions& write_options, DeferredFile* deferred,
SealedFile* sealed_file) {
assert(deferred != nullptr);
assert(deferred->writer);
assert(sealed_file != nullptr);
Status s = FinalizeBlobFile(
write_options, deferred->writer.get(), deferred->file_number,
deferred->blob_count, deferred->total_blob_bytes, &sealed_file->addition);
if (!s.ok()) {
RemoveFilePartitionMapping(deferred->file_number);
return s;
}
sealed_file->garbage_blob_count = deferred->garbage_blob_count;
sealed_file->garbage_blob_bytes = deferred->garbage_blob_bytes;
deferred->writer.reset();
return Status::OK();
}
Status BlobFilePartitionManager::FinalizeBlobFile(
const WriteOptions& write_options, BlobLogWriter* writer,
uint64_t file_number, uint64_t blob_count, uint64_t total_blob_bytes,
BlobFileAddition* addition) {
assert(writer != nullptr);
assert(addition != nullptr);
BlobLogFooter footer;
footer.blob_count = blob_count;
std::string checksum_method;
std::string checksum_value;
Status s = writer->AppendFooter(write_options, footer, &checksum_method,
&checksum_value);
if (!s.ok()) {
return s;
}
if (blob_callback_ != nullptr) {
const std::string blob_file_path = BlobFileName(db_path_, file_number);
s = blob_callback_->OnBlobFileCompleted(
blob_file_path, column_family_name_, /*job_id=*/0, file_number,
BlobFileCreationReason::kFlush, s, checksum_value, checksum_method,
blob_count, total_blob_bytes);
if (!s.ok()) {
return s;
}
}
*addition =
BlobFileAddition(file_number, blob_count, total_blob_bytes,
std::move(checksum_method), std::move(checksum_value));
return Status::OK();
}
void BlobFilePartitionManager::AddSealedFileGarbage(
const SealedFile& sealed_file, std::vector<BlobFileGarbage>* garbages) {
assert(garbages != nullptr);
if (sealed_file.garbage_blob_count == 0) {
assert(sealed_file.garbage_blob_bytes == 0);
return;
}
garbages->emplace_back(sealed_file.addition.GetBlobFileNumber(),
sealed_file.garbage_blob_count,
sealed_file.garbage_blob_bytes);
}
bool BlobFilePartitionManager::MarkPartitionGarbage(Partition* partition,
uint64_t file_number,
uint64_t blob_count,
uint64_t blob_bytes) {
assert(partition != nullptr);
if (!partition->writer || partition->file_number != file_number) {
return false;
}
partition->garbage_blob_count += blob_count;
partition->garbage_blob_bytes += blob_bytes;
assert(partition->garbage_blob_count <= partition->blob_count);
assert(partition->garbage_blob_bytes <= partition->total_blob_bytes);
return true;
}
bool BlobFilePartitionManager::MarkDeferredFileGarbage(DeferredFile* deferred,
uint64_t file_number,
uint64_t blob_count,
uint64_t blob_bytes) {
assert(deferred != nullptr);
if (deferred->file_number != file_number) {
return false;
}
deferred->garbage_blob_count += blob_count;
deferred->garbage_blob_bytes += blob_bytes;
assert(deferred->garbage_blob_count <= deferred->blob_count);
assert(deferred->garbage_blob_bytes <= deferred->total_blob_bytes);
return true;
}
bool BlobFilePartitionManager::MarkSealedFileGarbage(
std::vector<SealedFile>* sealed_files, uint64_t file_number,
uint64_t blob_count, uint64_t blob_bytes) {
assert(sealed_files != nullptr);
for (auto& sealed_file : *sealed_files) {
if (sealed_file.addition.GetBlobFileNumber() != file_number) {
continue;
}
sealed_file.garbage_blob_count += blob_count;
sealed_file.garbage_blob_bytes += blob_bytes;
assert(sealed_file.garbage_blob_count <=
sealed_file.addition.GetTotalBlobCount());
assert(sealed_file.garbage_blob_bytes <=
sealed_file.addition.GetTotalBlobBytes());
return true;
}
return false;
}
Status BlobFilePartitionManager::MaybePrepopulateBlobCache(
const BlobDirectWriteSettings* settings, const Slice& original_value,
uint64_t blob_file_number, uint64_t blob_offset) {
if (settings == nullptr || 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);
if (compression_state.compressor == nullptr) {
return Status::NotSupported(
"Blob direct write compression type not supported");
}
Status s = LegacyForceBuiltinCompression(*compression_state.compressor,
&compression_state.working_area,
value, &compressed_value);
if (!s.ok()) {
return s;
}
write_value = Slice(compressed_value);
}
// Partition selection is based on the logical write inputs. In particular,
// strategies that inspect value contents or size see the original
// uncompressed value rather than `write_value`. The modulo here is
// intentional so custom strategies can return arbitrary hashed or sentinel
// values without violating the partition bounds.
const uint32_t selected_partition_idx =
partition_idx != nullptr
? (*partition_idx % num_partitions_)
: (strategy_->SelectPartition(num_partitions_, column_family_id, key,
value) %
num_partitions_);
{
MutexLock lock(&mutex_);
Partition* partition = partitions_[selected_partition_idx].get();
auto seal_current_file = [&]() -> Status {
if (!partition->writer) {
return Status::OK();
}
SealedFile sealed_file;
Status s = SealActiveBlobFile(write_options, partition, &sealed_file);
if (s.ok()) {
current_generation_sealed_files_.push_back(std::move(sealed_file));
}
return s;
};
const uint64_t record_size =
BlobLogRecord::kHeaderSize + key.size() + write_value.size();
const uint64_t future_file_size =
partition->file_size + record_size + BlobLogFooter::kSize;
if (partition->writer &&
(partition->column_family_id != column_family_id ||
partition->compression != compression ||
(partition->blob_count > 0 && future_file_size > blob_file_size_))) {
Status s = seal_current_file();
if (!s.ok()) {
return s;
}
}
if (!partition->writer) {
Status s = OpenNewBlobFile(partition, column_family_id, compression,
selected_partition_idx);
if (!s.ok()) {
return s;
}
}
uint64_t key_offset = 0;
Status s = partition->writer->AddRecord(write_options, key, write_value,
&key_offset, blob_offset);
if (!s.ok()) {
return s;
}
partition->sync_required = true;
partition->blob_count += 1;
partition->total_blob_bytes += record_size;
partition->file_size = BlobLogHeader::kSize + partition->total_blob_bytes;
*blob_file_number = partition->file_number;
*blob_size = write_value.size();
}
Status prepopulate_s = MaybePrepopulateBlobCache(
settings, value, *blob_file_number, *blob_offset);
if (!prepopulate_s.ok() && info_log_ != nullptr) {
ROCKS_LOG_WARN(info_log_,
"Failed to pre-populate direct-write blob cache entry: %s",
prepopulate_s.ToString().c_str());
}
return Status::OK();
}
void BlobFilePartitionManager::RotateCurrentGeneration() {
MutexLock lock(&mutex_);
GenerationBatch batch;
batch.sealed_files = std::move(current_generation_sealed_files_);
current_generation_sealed_files_.clear();
for (auto& partition : partitions_) {
if (!partition->writer) {
continue;
}
DeferredFile deferred;
deferred.writer = std::move(partition->writer);
deferred.file_number = partition->file_number;
deferred.blob_count = partition->blob_count;
deferred.total_blob_bytes = partition->total_blob_bytes;
deferred.garbage_blob_count = partition->garbage_blob_count;
deferred.garbage_blob_bytes = partition->garbage_blob_bytes;
ResetPartitionState(partition.get());
batch.deferred_files.emplace_back(std::move(deferred));
}
pending_generations_.emplace_back(std::move(batch));
}
Status BlobFilePartitionManager::PrepareFlushAdditions(
const WriteOptions& write_options, size_t num_generations,
std::vector<BlobFileAddition>* additions,
std::vector<BlobFileGarbage>* garbages,
std::vector<std::vector<uint64_t>>* generation_blob_file_numbers) {
assert(additions != nullptr);
assert(garbages != nullptr);
additions->clear();
garbages->clear();
if (generation_blob_file_numbers != nullptr) {
generation_blob_file_numbers->clear();
}
MutexLock lock(&mutex_);
if (num_generations > pending_generations_.size()) {
return Status::Corruption(
"Missing blob direct write generation metadata for flush");
}
for (size_t i = 0; i < num_generations; ++i) {
GenerationBatch& batch = pending_generations_[i];
while (!batch.deferred_files.empty()) {
DeferredFile deferred = std::move(batch.deferred_files.front());
batch.deferred_files.pop_front();
SealedFile sealed_file;
Status s = SealDeferredFile(write_options, &deferred, &sealed_file);
if (!s.ok()) {
return s;
}
// Keep each successfully sealed file attached to the generation
// immediately. If a later seal or the flush job itself fails, retry must
// reuse these exact on-disk files instead of finalizing replacements.
batch.sealed_files.push_back(std::move(sealed_file));
}
std::vector<uint64_t>* generation_file_numbers = nullptr;
if (generation_blob_file_numbers != nullptr) {
generation_blob_file_numbers->emplace_back();
generation_file_numbers = &generation_blob_file_numbers->back();
generation_file_numbers->reserve(batch.sealed_files.size());
}
for (const auto& sealed_file : batch.sealed_files) {
additions->push_back(sealed_file.addition);
AddSealedFileGarbage(sealed_file, garbages);
if (generation_file_numbers != nullptr) {
generation_file_numbers->push_back(
sealed_file.addition.GetBlobFileNumber());
}
}
}
return Status::OK();
}
Status BlobFilePartitionManager::MarkBlobWriteAsGarbage(uint64_t file_number,
uint64_t blob_count,
uint64_t blob_bytes) {
if (blob_count == 0) {
assert(blob_bytes == 0);
return Status::OK();
}
MutexLock lock(&mutex_);
for (auto& partition : partitions_) {
if (MarkPartitionGarbage(partition.get(), file_number, blob_count,
blob_bytes)) {
return Status::OK();
}
}
if (MarkSealedFileGarbage(&current_generation_sealed_files_, file_number,
blob_count, blob_bytes)) {
return Status::OK();
}
for (auto& batch : pending_generations_) {
for (auto& deferred : batch.deferred_files) {
if (MarkDeferredFileGarbage(&deferred, file_number, blob_count,
blob_bytes)) {
return Status::OK();
}
}
if (MarkSealedFileGarbage(&batch.sealed_files, file_number, blob_count,
blob_bytes)) {
return Status::OK();
}
}
const std::string message =
"Could not match failed blob direct-write rollback for file #" +
std::to_string(file_number);
if (info_log_ != nullptr) {
ROCKS_LOG_ERROR(info_log_, "%s", message.c_str());
}
return Status::Corruption(message);
}
void BlobFilePartitionManager::CommitPreparedGenerations(
size_t num_generations) {
MutexLock lock(&mutex_);
while (num_generations-- > 0 && !pending_generations_.empty()) {
pending_generations_.pop_front();
}
}
Status BlobFilePartitionManager::SyncAllOpenFiles(
const WriteOptions& write_options) {
MutexLock lock(&mutex_);
for (const auto& partition : partitions_) {
if (!partition->writer || !partition->sync_required) {
continue;
}
Status s = partition->writer->Sync(write_options);
if (!s.ok()) {
return s;
}
partition->sync_required = false;
}
return Status::OK();
}
void BlobFilePartitionManager::GetActiveBlobFileNumbers(
UnorderedSet<uint64_t>* file_numbers) const {
assert(file_numbers != nullptr);
ReadLock lock(&file_partition_mutex_);
for (const auto& entry : file_to_partition_) {
file_numbers->insert(entry.first);
}
}
void BlobFilePartitionManager::GetProtectedBlobFileNumbers(
UnorderedSet<uint64_t>* file_numbers) const {
assert(file_numbers != nullptr);
ReadLock lock(&file_partition_mutex_);
for (const auto& entry : protected_blob_file_refs_) {
file_numbers->insert(entry.first);
}
}
bool BlobFilePartitionManager::IsTrackedBlobFileNumber(
uint64_t file_number) const {
ReadLock lock(&file_partition_mutex_);
return file_to_partition_.find(file_number) != file_to_partition_.end() ||
protected_blob_file_refs_.find(file_number) !=
protected_blob_file_refs_.end();
}
void BlobFilePartitionManager::ProtectSealedBlobFileNumbers(
const std::vector<uint64_t>& file_numbers) {
if (file_numbers.empty()) {
return;
}
WriteLock lock(&file_partition_mutex_);
for (uint64_t file_number : file_numbers) {
++protected_blob_file_refs_[file_number];
}
}
void BlobFilePartitionManager::UnprotectSealedBlobFileNumbers(
const std::vector<uint64_t>& file_numbers) {
if (file_numbers.empty()) {
return;
}
WriteLock lock(&file_partition_mutex_);
for (uint64_t file_number : file_numbers) {
auto it = protected_blob_file_refs_.find(file_number);
if (it == protected_blob_file_refs_.end()) {
if (info_log_ != nullptr) {
ROCKS_LOG_ERROR(info_log_,
"Memtable blob protection underflow for file #%" PRIu64,
file_number);
}
continue;
}
assert(it->second > 0);
--it->second;
if (it->second == 0) {
protected_blob_file_refs_.erase(it);
if (blob_file_cache_ != nullptr) {
// Once the last memtable-backed reference goes away, any cached reader
// for this file is either obsolete or can be reopened through the
// manifest-visible path. Evict it here so delayed protection does not
// leave an obsolete blob reader behind until DB close.
blob_file_cache_->Evict(file_number);
}
}
}
}
void BlobFilePartitionManager::RemoveFilePartitionMappings(
const std::vector<uint64_t>& file_numbers) {
if (file_numbers.empty()) {
return;
}
WriteLock lock(&file_partition_mutex_);
for (uint64_t file_number : file_numbers) {
file_to_partition_.erase(file_number);
if (blob_file_cache_ != nullptr) {
// In-flight direct-write reads may have cached a footer-less reader for
// this file before it was sealed. Drop it now so future manifest-visible
// reads reopen against the finalized on-disk size and footer state.
blob_file_cache_->Evict(file_number);
}
}
}
Status BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
const ReadOptions& read_options, const Slice& user_key,
const BlobIndex& blob_idx, const Version* version,
BlobFileCache* blob_file_cache, FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value, uint64_t* bytes_read) {
assert(blob_value != nullptr);
if (version != nullptr) {
// Only fall back when the blob file is still owned exclusively by the
// write path and therefore absent from Version metadata. Once Version
// knows the file number, every result from Version::GetBlob(), including
// corruption, must propagate directly.
if (blob_idx.HasTTL() || blob_idx.IsInlined() ||
version->storage_info()->GetBlobFileMetaData(blob_idx.file_number()) !=
nullptr) {
return version->GetBlob(read_options, user_key, blob_idx, prefetch_buffer,
blob_value, bytes_read);
}
}
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
-294
View File
@@ -1,294 +0,0 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#pragma once
#include <cstdint>
#include <deque>
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "db/blob/blob_file_addition.h"
#include "db/blob/blob_file_garbage.h"
#include "db/blob/blob_log_format.h"
#include "db/blob/blob_write_batch_transformer.h"
#include "port/port.h"
#include "rocksdb/compression_type.h"
#include "rocksdb/env.h"
#include "rocksdb/file_system.h"
#include "rocksdb/options.h"
#include "rocksdb/rocksdb_namespace.h"
#include "rocksdb/slice.h"
#include "rocksdb/status.h"
#include "util/hash_containers.h"
namespace ROCKSDB_NAMESPACE {
class BlobFileCache;
class BlobFileCompletionCallback;
class BlobIndex;
class BlobLogWriter;
class FilePrefetchBuffer;
class IOTracer;
class Logger;
class PinnableSlice;
class Version;
class WritableFileWriter;
struct FileOptions;
struct ReadOptions;
// Manages per-partition blob files for write-path blob direct write.
//
// The v1 design keeps each memtable switch as one FIFO generation batch.
// Direct-write files for that memtable are sealed and registered when the
// matching flush commits. The current implementation still serializes blob
// appends through one manager mutex; follow-up PRs can add independent
// per-partition locks to allow concurrent blob writes without changing the
// generation/flush contract. Follow-up PRs can also broaden compatibility as
// the feature matures.
class BlobFilePartitionManager {
public:
using FileNumberAllocator = std::function<uint64_t()>;
// Creates the per-column-family manager for write-path blob files.
BlobFilePartitionManager(
uint32_t num_partitions,
std::shared_ptr<BlobFilePartitionStrategy> strategy,
FileNumberAllocator file_number_allocator, FileSystem* fs,
SystemClock* clock, Statistics* statistics,
const FileOptions& file_options, std::string db_path,
std::string column_family_name, uint64_t blob_file_size, bool use_fsync,
BlobFileCache* blob_file_cache, BlobFileCompletionCallback* blob_callback,
const std::vector<std::shared_ptr<EventListener>>& listeners,
FileChecksumGenFactory* file_checksum_gen_factory,
const FileTypeSet& checksum_handoff_file_types,
const std::shared_ptr<IOTracer>& io_tracer, std::string db_id,
std::string db_session_id, Logger* info_log);
// Evicts cached readers for manager-owned files during shutdown.
~BlobFilePartitionManager();
// Appends one blob record to a partition file and returns the resulting
// BlobIndex location metadata.
Status WriteBlob(const WriteOptions& write_options, uint32_t column_family_id,
CompressionType compression, const Slice& key,
const Slice& value, uint64_t* blob_file_number,
uint64_t* blob_offset, uint64_t* blob_size,
const BlobDirectWriteSettings* settings = nullptr,
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);
// Optionally seeds the blob cache with the original uncompressed value.
Status MaybePrepopulateBlobCache(const BlobDirectWriteSettings* settings,
const Slice& original_value,
uint64_t blob_file_number,
uint64_t blob_offset);
// Configured partition fanout and write-selection strategy.
const uint32_t num_partitions_;
std::shared_ptr<BlobFilePartitionStrategy> strategy_;
// Shared services needed to create, track, and cache blob files.
FileNumberAllocator file_number_allocator_;
FileSystem* fs_;
SystemClock* clock_;
Statistics* statistics_;
FileOptions file_options_;
// Column-family context used for file naming and callbacks.
std::string db_path_;
std::string column_family_name_;
// File creation policy and shared blob-file infrastructure.
uint64_t blob_file_size_;
bool use_fsync_;
BlobFileCache* blob_file_cache_;
BlobFileCompletionCallback* blob_callback_;
std::vector<std::shared_ptr<EventListener>> listeners_;
FileChecksumGenFactory* file_checksum_gen_factory_;
FileTypeSet checksum_handoff_file_types_;
std::shared_ptr<IOTracer> io_tracer_;
// DB identity used when constructing cache keys.
std::string db_id_;
std::string db_session_id_;
Logger* info_log_;
// One active writer slot per configured partition.
std::vector<std::unique_ptr<Partition>> partitions_;
// Protects partition state and generation queues.
mutable port::Mutex mutex_;
// Files already sealed while the current mutable memtable was active.
std::vector<SealedFile> current_generation_sealed_files_;
// FIFO immutable memtable generations waiting to be flushed.
std::deque<GenerationBatch> pending_generations_;
// Tracks blob files still owned by active or deferred write-path state until
// MANIFEST commit publishes them.
std::unordered_map<uint64_t, uint32_t> file_to_partition_;
// Sealed direct-write files that remain reachable from live memtables, such
// as old SuperVersions serving lazy iterator reads after flush commit.
std::unordered_map<uint64_t, uint32_t> protected_blob_file_refs_;
// Protects file_to_partition_ and protected_blob_file_refs_.
mutable port::RWMutex file_partition_mutex_;
};
} // namespace ROCKSDB_NAMESPACE
+14 -36
View File
@@ -29,7 +29,7 @@ Status BlobFileReader::Create(
const ImmutableOptions& immutable_options, const ReadOptions& read_options,
const FileOptions& file_options, uint32_t column_family_id,
HistogramImpl* blob_file_read_hist, uint64_t blob_file_number,
const std::shared_ptr<IOTracer>& io_tracer, bool skip_footer_validation,
const std::shared_ptr<IOTracer>& io_tracer,
std::unique_ptr<BlobFileReader>* blob_file_reader) {
assert(blob_file_reader);
assert(!*blob_file_reader);
@@ -40,8 +40,7 @@ Status BlobFileReader::Create(
{
const Status s =
OpenFile(immutable_options, file_options, blob_file_read_hist,
blob_file_number, io_tracer, &file_size, &file_reader,
/*skip_footer_size_check=*/skip_footer_validation);
blob_file_number, io_tracer, &file_size, &file_reader);
if (!s.ok()) {
return s;
}
@@ -62,7 +61,7 @@ Status BlobFileReader::Create(
}
}
if (!skip_footer_validation) {
{
const Status s =
ReadFooter(file_reader.get(), read_options, file_size, statistics);
if (!s.ok()) {
@@ -77,10 +76,9 @@ Status BlobFileReader::Create(
compression_type);
}
blob_file_reader->reset(
new BlobFileReader(std::move(file_reader), file_size, compression_type,
std::move(decompressor), immutable_options.clock,
statistics, !skip_footer_validation));
blob_file_reader->reset(new BlobFileReader(
std::move(file_reader), file_size, compression_type,
std::move(decompressor), immutable_options.clock, statistics));
return Status::OK();
}
@@ -89,8 +87,7 @@ Status BlobFileReader::OpenFile(
const ImmutableOptions& immutable_options, const FileOptions& file_opts,
HistogramImpl* blob_file_read_hist, uint64_t blob_file_number,
const std::shared_ptr<IOTracer>& io_tracer, uint64_t* file_size,
std::unique_ptr<RandomAccessFileReader>* file_reader,
bool skip_footer_size_check) {
std::unique_ptr<RandomAccessFileReader>* file_reader) {
assert(file_size);
assert(file_reader);
@@ -115,26 +112,17 @@ Status BlobFileReader::OpenFile(
}
}
if (!skip_footer_size_check &&
*file_size < BlobLogHeader::kSize + BlobLogFooter::kSize) {
return Status::Corruption("Malformed blob file");
}
if (skip_footer_size_check && *file_size < BlobLogHeader::kSize) {
if (*file_size < BlobLogHeader::kSize + BlobLogFooter::kSize) {
return Status::Corruption("Malformed blob file");
}
std::unique_ptr<FSRandomAccessFile> file;
FileOptions reader_file_opts = file_opts;
if (skip_footer_size_check && reader_file_opts.use_direct_reads) {
reader_file_opts.use_direct_reads = false;
}
{
TEST_SYNC_POINT("BlobFileReader::OpenFile:NewRandomAccessFile");
const Status s =
fs->NewRandomAccessFile(blob_file_path, reader_file_opts, &file, dbg);
fs->NewRandomAccessFile(blob_file_path, file_opts, &file, dbg);
if (!s.ok()) {
return s;
}
@@ -303,14 +291,13 @@ BlobFileReader::BlobFileReader(
std::unique_ptr<RandomAccessFileReader>&& file_reader, uint64_t file_size,
CompressionType compression_type,
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
Statistics* statistics, bool has_footer)
Statistics* statistics)
: file_reader_(std::move(file_reader)),
file_size_(file_size),
compression_type_(compression_type),
decompressor_(std::move(decompressor)),
clock_(clock),
statistics_(statistics),
has_footer_(has_footer) {
statistics_(statistics) {
assert(file_reader_);
}
@@ -325,8 +312,7 @@ Status BlobFileReader::GetBlob(
const uint64_t key_size = user_key.size();
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
has_footer_)) {
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_)) {
return Status::Corruption("Invalid blob offset");
}
@@ -442,8 +428,7 @@ void BlobFileReader::MultiGetBlob(
const uint64_t offset = req->offset;
const uint64_t value_size = req->len;
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
has_footer_)) {
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_)) {
*req->status = Status::Corruption("Invalid blob offset");
continue;
}
@@ -469,13 +454,6 @@ void BlobFileReader::MultiGetBlob(
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, total_len);
if (read_reqs.empty()) {
if (bytes_read) {
*bytes_read = 0;
}
return;
}
Buffer buf;
AlignedBuf aligned_buf;
@@ -555,7 +533,7 @@ void BlobFileReader::MultiGetBlob(
}
// Uncompress blob if needed
Slice value_slice(record_slice.data() + adjustments[j - 1], req->len);
Slice value_slice(record_slice.data() + adjustments[i], req->len);
*req->status = UncompressBlobIfNeeded(
value_slice, compression_type_, decompressor_.get(), allocator, clock_,
statistics_, &blob_reqs[i].second);
+3 -22
View File
@@ -36,20 +36,7 @@ class BlobFileReader {
HistogramImpl* blob_file_read_hist,
uint64_t blob_file_number,
const std::shared_ptr<IOTracer>& io_tracer,
std::unique_ptr<BlobFileReader>* reader) {
return Create(immutable_options, read_options, file_options,
column_family_id, blob_file_read_hist, blob_file_number,
io_tracer, /*skip_footer_validation=*/false, reader);
}
// Allows opening in-flight direct-write blob files by optionally skipping
// footer validation.
static Status Create(
const ImmutableOptions& immutable_options,
const ReadOptions& read_options, const FileOptions& file_options,
uint32_t column_family_id, HistogramImpl* blob_file_read_hist,
uint64_t blob_file_number, const std::shared_ptr<IOTracer>& io_tracer,
bool skip_footer_validation, std::unique_ptr<BlobFileReader>* reader);
std::unique_ptr<BlobFileReader>* reader);
BlobFileReader(const BlobFileReader&) = delete;
BlobFileReader& operator=(const BlobFileReader&) = delete;
@@ -76,22 +63,18 @@ class BlobFileReader {
uint64_t GetFileSize() const { return file_size_; }
private:
// `has_footer` tracks whether offset validation should reserve footer space.
BlobFileReader(std::unique_ptr<RandomAccessFileReader>&& file_reader,
uint64_t file_size, CompressionType compression_type,
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
Statistics* statistics, bool has_footer);
Statistics* statistics);
// `skip_footer_size_check` is used for direct-write files that are still
// missing their footer at open time.
static Status OpenFile(const ImmutableOptions& immutable_options,
const FileOptions& file_opts,
HistogramImpl* blob_file_read_hist,
uint64_t blob_file_number,
const std::shared_ptr<IOTracer>& io_tracer,
uint64_t* file_size,
std::unique_ptr<RandomAccessFileReader>* file_reader,
bool skip_footer_size_check);
std::unique_ptr<RandomAccessFileReader>* file_reader);
static Status ReadHeader(const RandomAccessFileReader* file_reader,
const ReadOptions& read_options,
@@ -127,8 +110,6 @@ class BlobFileReader {
std::shared_ptr<Decompressor> decompressor_;
SystemClock* clock_;
Statistics* statistics_;
// False when the reader was opened before the blob file footer was written.
bool has_footer_;
};
} // namespace ROCKSDB_NAMESPACE
-86
View File
@@ -1011,92 +1011,6 @@ TEST_P(BlobFileReaderDecodingErrorTest, DecodingError) {
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(BlobFileReaderTest, MultiGetBlobWithFailedValidation) {
// Test that MultiGetBlob correctly handles the case where some requests
// fail validation. The adjustments vector is only populated for requests
// that pass validation, so the indices must track correctly.
Options options;
options.env = mock_env_.get();
options.cf_paths.emplace_back(
test::PerThreadDBPath(
mock_env_.get(),
"BlobFileReaderTest_MultiGetBlobWithFailedValidation"),
0);
options.enable_blob_files = true;
ImmutableOptions immutable_options(options);
constexpr uint32_t column_family_id = 1;
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range;
constexpr uint64_t blob_file_number = 1;
constexpr size_t num_blobs = 3;
const std::vector<std::string> key_strs = {"key1", "key2", "key3"};
const std::vector<std::string> blob_strs = {"blob1", "blob2", "blob3"};
const std::vector<Slice> keys = {key_strs[0], key_strs[1], key_strs[2]};
const std::vector<Slice> blobs = {blob_strs[0], blob_strs[1], blob_strs[2]};
std::vector<uint64_t> blob_offsets(keys.size());
std::vector<uint64_t> blob_sizes(keys.size());
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
expiration_range, blob_file_number, keys, blobs, kNoCompression,
blob_offsets, blob_sizes);
constexpr HistogramImpl* blob_file_read_hist = nullptr;
std::unique_ptr<BlobFileReader> reader;
ReadOptions read_options;
ASSERT_OK(BlobFileReader::Create(
immutable_options, read_options, FileOptions(), column_family_id,
blob_file_read_hist, blob_file_number, nullptr /*IOTracer*/, &reader));
// Enable checksum verification so adjustments are non-zero
read_options.verify_checksums = true;
constexpr MemoryAllocator* allocator = nullptr;
// Fail the first request by giving it an invalid offset (too small).
// This causes the first request to be skipped in the adjustments vector,
// so adjustments has only 2 entries (for blobs 1 and 2), while blob_reqs
// has 3 entries. Without the fix, adjustments[i] would use the wrong
// index for the remaining valid requests.
std::array<Status, num_blobs> statuses_buf;
std::array<BlobReadRequest, num_blobs> requests_buf;
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
blob_reqs;
// First request: invalid offset (too small, will fail validation)
requests_buf[0] = BlobReadRequest(keys[0], /*offset=*/0, blob_sizes[0],
kNoCompression, nullptr, &statuses_buf[0]);
// Second and third requests: valid
requests_buf[1] = BlobReadRequest(keys[1], blob_offsets[1], blob_sizes[1],
kNoCompression, nullptr, &statuses_buf[1]);
requests_buf[2] = BlobReadRequest(keys[2], blob_offsets[2], blob_sizes[2],
kNoCompression, nullptr, &statuses_buf[2]);
for (size_t i = 0; i < num_blobs; ++i) {
blob_reqs.emplace_back(&requests_buf[i], std::unique_ptr<BlobContents>());
}
uint64_t bytes_read = 0;
reader->MultiGetBlob(read_options, allocator, blob_reqs, &bytes_read);
// First request should fail validation
ASSERT_TRUE(statuses_buf[0].IsCorruption());
ASSERT_EQ(blob_reqs[0].second, nullptr);
// Second and third requests should succeed with correct data
ASSERT_OK(statuses_buf[1]);
ASSERT_NE(blob_reqs[1].second, nullptr);
ASSERT_EQ(blob_reqs[1].second->data(), blobs[1]);
ASSERT_OK(statuses_buf[2]);
ASSERT_NE(blob_reqs[2].second, nullptr);
ASSERT_EQ(blob_reqs[2].second->data(), blobs[2]);
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+55 -88
View File
@@ -8,48 +8,70 @@
#include "db/blob/blob_index.h"
#include "db/blob/blob_log_format.h"
#include "db/dbformat.h"
#include "db/wide/wide_column_serialization.h"
namespace ROCKSDB_NAMESPACE {
Status BlobGarbageMeter::ProcessInFlow(const Slice& key, const Slice& value) {
return ProcessFlow(key, value, /*is_inflow=*/true);
}
uint64_t blob_file_number = kInvalidBlobFileNumber;
uint64_t bytes = 0;
Status BlobGarbageMeter::ProcessOutFlow(const Slice& key, const Slice& value) {
return ProcessFlow(key, value, /*is_inflow=*/false);
}
Status BlobGarbageMeter::GetBlobReferenceDetails(const ParsedInternalKey& ikey,
const BlobIndex& blob_index,
uint64_t* blob_file_number,
uint64_t* bytes) {
assert(blob_file_number);
assert(*blob_file_number == kInvalidBlobFileNumber);
assert(bytes);
assert(*bytes == 0);
if (blob_index.IsInlined() || blob_index.HasTTL()) {
return Status::Corruption("Unexpected TTL/inlined blob index");
const Status s = Parse(key, value, &blob_file_number, &bytes);
if (!s.ok()) {
return s;
}
*blob_file_number = blob_index.file_number();
*bytes =
blob_index.size() +
BlobLogRecord::CalculateAdjustmentForRecordHeader(ikey.user_key.size());
if (blob_file_number == kInvalidBlobFileNumber) {
return Status::OK();
}
flows_[blob_file_number].AddInFlow(bytes);
return Status::OK();
}
Status BlobGarbageMeter::ParseBlobIndexReference(const ParsedInternalKey& ikey,
const Slice& value,
uint64_t* blob_file_number,
uint64_t* bytes) {
Status BlobGarbageMeter::ProcessOutFlow(const Slice& key, const Slice& value) {
uint64_t blob_file_number = kInvalidBlobFileNumber;
uint64_t bytes = 0;
const Status s = Parse(key, value, &blob_file_number, &bytes);
if (!s.ok()) {
return s;
}
if (blob_file_number == kInvalidBlobFileNumber) {
return Status::OK();
}
// Note: in order to measure the amount of additional garbage, we only need to
// track the outflow for preexisting files, i.e. those that also had inflow.
// (Newly written files would only have outflow.)
auto it = flows_.find(blob_file_number);
if (it == flows_.end()) {
return Status::OK();
}
it->second.AddOutFlow(bytes);
return Status::OK();
}
Status BlobGarbageMeter::Parse(const Slice& key, const Slice& value,
uint64_t* blob_file_number, uint64_t* bytes) {
assert(blob_file_number);
assert(*blob_file_number == kInvalidBlobFileNumber);
assert(bytes);
assert(*bytes == 0);
ParsedInternalKey ikey;
{
constexpr bool log_err_key = false;
const Status s = ParseInternalKey(key, &ikey, log_err_key);
if (!s.ok()) {
return s;
}
}
if (ikey.type != kTypeBlobIndex) {
return Status::OK();
}
@@ -63,71 +85,16 @@ Status BlobGarbageMeter::ParseBlobIndexReference(const ParsedInternalKey& ikey,
}
}
return GetBlobReferenceDetails(ikey, blob_index, blob_file_number, bytes);
}
void BlobGarbageMeter::AddFlow(uint64_t blob_file_number, uint64_t bytes,
bool is_inflow) {
if (is_inflow) {
flows_[blob_file_number].AddInFlow(bytes);
return;
if (blob_index.IsInlined() || blob_index.HasTTL()) {
return Status::Corruption("Unexpected TTL/inlined blob index");
}
// Note: in order to measure the amount of additional garbage, we only need to
// track the outflow for preexisting files, i.e. those that also had inflow.
// (Newly written files would only have outflow.)
auto it = flows_.find(blob_file_number);
if (it != flows_.end()) {
it->second.AddOutFlow(bytes);
}
}
*blob_file_number = blob_index.file_number();
*bytes =
blob_index.size() +
BlobLogRecord::CalculateAdjustmentForRecordHeader(ikey.user_key.size());
Status BlobGarbageMeter::ProcessFlow(const Slice& key, const Slice& value,
bool is_inflow) {
ParsedInternalKey ikey;
{
constexpr bool log_err_key = false;
const Status s = ParseInternalKey(key, &ikey, log_err_key);
if (!s.ok()) {
return s;
}
}
uint64_t blob_file_number = kInvalidBlobFileNumber;
uint64_t bytes = 0;
if (Status s =
ParseBlobIndexReference(ikey, value, &blob_file_number, &bytes);
!s.ok()) {
return s;
}
if (blob_file_number != kInvalidBlobFileNumber) {
AddFlow(blob_file_number, bytes, is_inflow);
return Status::OK();
}
return ProcessEntityBlobReferences(ikey, value, is_inflow);
}
Status BlobGarbageMeter::ProcessEntityBlobReferences(
const ParsedInternalKey& ikey, const Slice& value, bool is_inflow) {
if (ikey.type != kTypeWideColumnEntity) {
return Status::OK();
}
return WideColumnSerialization::ForEachBlobFileNumber(
value, [&](const BlobIndex& blob_index) -> Status {
uint64_t file_number = kInvalidBlobFileNumber;
uint64_t blob_bytes = 0;
if (const Status s = GetBlobReferenceDetails(ikey, blob_index,
&file_number, &blob_bytes);
!s.ok()) {
return s;
}
AddFlow(file_number, blob_bytes, is_inflow);
return Status::OK();
});
return Status::OK();
}
} // namespace ROCKSDB_NAMESPACE
+2 -15
View File
@@ -15,8 +15,6 @@
namespace ROCKSDB_NAMESPACE {
class BlobIndex;
struct ParsedInternalKey;
class Slice;
// A class that can be used to compute the amount of additional garbage
@@ -95,19 +93,8 @@ class BlobGarbageMeter {
}
private:
static Status GetBlobReferenceDetails(const ParsedInternalKey& ikey,
const BlobIndex& blob_index,
uint64_t* blob_file_number,
uint64_t* bytes);
static Status ParseBlobIndexReference(const ParsedInternalKey& ikey,
const Slice& value,
uint64_t* blob_file_number,
uint64_t* bytes);
void AddFlow(uint64_t blob_file_number, uint64_t bytes, bool is_inflow);
Status ProcessFlow(const Slice& key, const Slice& value, bool is_inflow);
Status ProcessEntityBlobReferences(const ParsedInternalKey& ikey,
const Slice& value, bool is_inflow);
static Status Parse(const Slice& key, const Slice& value,
uint64_t* blob_file_number, uint64_t* bytes);
std::unordered_map<uint64_t, BlobInOutFlow> flows_;
};
-74
View File
@@ -11,23 +11,10 @@
#include "db/blob/blob_index.h"
#include "db/blob/blob_log_format.h"
#include "db/dbformat.h"
#include "db/wide/wide_column_serialization.h"
#include "test_util/testharness.h"
namespace ROCKSDB_NAMESPACE {
namespace {
BlobIndex MakeBlobIndex(uint64_t file_number, uint64_t offset, uint64_t size) {
std::string encoded;
BlobIndex::EncodeBlob(&encoded, file_number, offset, size, kNoCompression);
BlobIndex blob_index;
EXPECT_OK(blob_index.DecodeFrom(encoded));
return blob_index;
}
} // namespace
TEST(BlobGarbageMeterTest, MeasureGarbage) {
BlobGarbageMeter blob_garbage_meter;
@@ -201,67 +188,6 @@ TEST(BlobGarbageMeterTest, InlinedTTLBlobIndex) {
ASSERT_NOK(blob_garbage_meter.ProcessOutFlow(key_slice, value_slice));
}
TEST(BlobGarbageMeterTest, WideColumnEntity) {
constexpr char user_key[] = "entity_key";
constexpr SequenceNumber seq = 123;
const InternalKey key(user_key, seq, kTypeWideColumnEntity);
const Slice key_slice = key.Encode();
const std::vector<std::pair<std::string, std::string>> columns = {
{"", "default_inline"},
{"blob_attr", "blob_inline"},
{"ttl", "00000001"},
};
const BlobIndex default_blob = MakeBlobIndex(4, 123, 555);
const BlobIndex attr_blob = MakeBlobIndex(5, 456, 777);
std::string inflow_value;
ASSERT_OK(WideColumnSerialization::SerializeV2(
columns, {{0, default_blob}, {1, attr_blob}}, inflow_value));
std::string outflow_value;
ASSERT_OK(WideColumnSerialization::SerializeV2(columns, {{0, default_blob}},
outflow_value));
BlobGarbageMeter blob_garbage_meter;
ASSERT_OK(blob_garbage_meter.ProcessInFlow(key_slice, inflow_value));
ASSERT_OK(blob_garbage_meter.ProcessOutFlow(key_slice, outflow_value));
const auto& flows = blob_garbage_meter.flows();
ASSERT_EQ(flows.size(), 2);
constexpr size_t kUserKeySize = sizeof(user_key) - 1;
const uint64_t default_bytes =
default_blob.size() +
BlobLogRecord::CalculateAdjustmentForRecordHeader(kUserKeySize);
const uint64_t attr_bytes =
attr_blob.size() +
BlobLogRecord::CalculateAdjustmentForRecordHeader(kUserKeySize);
{
const auto it = flows.find(default_blob.file_number());
ASSERT_NE(it, flows.end());
ASSERT_EQ(it->second.GetInFlow().GetCount(), 1);
ASSERT_EQ(it->second.GetInFlow().GetBytes(), default_bytes);
ASSERT_EQ(it->second.GetOutFlow().GetCount(), 1);
ASSERT_EQ(it->second.GetOutFlow().GetBytes(), default_bytes);
ASSERT_FALSE(it->second.HasGarbage());
}
{
const auto it = flows.find(attr_blob.file_number());
ASSERT_NE(it, flows.end());
ASSERT_EQ(it->second.GetInFlow().GetCount(), 1);
ASSERT_EQ(it->second.GetInFlow().GetBytes(), attr_bytes);
ASSERT_EQ(it->second.GetOutFlow().GetCount(), 0);
ASSERT_EQ(it->second.GetOutFlow().GetBytes(), 0);
ASSERT_TRUE(it->second.HasGarbage());
ASSERT_EQ(it->second.GetGarbageCount(), 1);
ASSERT_EQ(it->second.GetGarbageBytes(), attr_bytes);
}
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+4 -17
View File
@@ -148,26 +148,13 @@ struct BlobLogRecord {
// Checks whether a blob offset is potentially valid or not.
inline bool IsValidBlobOffset(uint64_t value_offset, uint64_t key_size,
uint64_t value_size, uint64_t file_size,
bool has_footer = true) {
constexpr uint64_t kMinPrefix =
BlobLogHeader::kSize + BlobLogRecord::kHeaderSize;
if (value_offset < kMinPrefix) {
return false;
}
if (value_offset - kMinPrefix < key_size) {
uint64_t value_size, uint64_t file_size) {
if (value_offset <
BlobLogHeader::kSize + BlobLogRecord::kHeaderSize + key_size) {
return false;
}
const uint64_t footer_size = has_footer ? BlobLogFooter::kSize : 0;
if (file_size < footer_size) {
return false;
}
const uint64_t max_value_end = file_size - footer_size;
if (value_size > max_value_end) {
return false;
}
if (value_offset > max_value_end - value_size) {
if (value_offset + value_size + BlobLogFooter::kSize > file_size) {
return false;
}
-130
View File
@@ -20,21 +20,6 @@
namespace ROCKSDB_NAMESPACE {
namespace {
Status AppendBlobRefreshRetryFailure(const Status& stale_status,
const Status& retry_status) {
assert(stale_status.IsCorruption());
assert(!retry_status.ok());
if (retry_status.IsCorruption()) {
return retry_status;
}
return Status::CopyAppendMessage(
stale_status, "; refresh retry failed: ", retry_status.ToString());
}
} // namespace
BlobSource::BlobSource(const ImmutableOptions& immutable_options,
const MutableCFOptions& mutable_cf_options,
const std::string& db_id,
@@ -246,38 +231,6 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
s = blob_file_reader.GetValue()->GetBlob(
read_options, user_key, offset, value_size, compression_type,
prefetch_buffer, allocator, &blob_contents, &read_size);
if (s.IsCorruption()) {
const Status stale_status = s;
blob_file_reader.Reset();
blob_file_cache_->Evict(file_number);
std::unique_ptr<BlobFileReader> fresh_reader;
s = blob_file_cache_->OpenBlobFileReaderUncached(
read_options, file_number, &fresh_reader,
/*allow_footer_skip_retry=*/false);
if (!s.ok()) {
return AppendBlobRefreshRetryFailure(stale_status, s);
}
if (compression_type != fresh_reader->GetCompressionType()) {
return Status::Corruption(
"Compression type mismatch when reading blob");
}
blob_contents.reset();
read_size = 0;
s = fresh_reader->GetBlob(read_options, user_key, offset, value_size,
compression_type, prefetch_buffer, allocator,
&blob_contents, &read_size);
if (!s.ok()) {
s = AppendBlobRefreshRetryFailure(stale_status, s);
} else {
CacheHandleGuard<BlobFileReader> ignored_reader;
blob_file_cache_
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
.PermitUncheckedError();
}
}
if (!s.ok()) {
return s;
}
@@ -444,89 +397,6 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
blob_file_reader.GetValue()->MultiGetBlob(read_options, allocator,
_blob_reqs, &_bytes_read);
bool needs_reader_refresh = false;
for (const auto& blob_req : _blob_reqs) {
BlobReadRequest* const req = blob_req.first;
assert(req != nullptr);
assert(req->status != nullptr);
if (req->status->IsCorruption()) {
needs_reader_refresh = true;
break;
}
}
if (needs_reader_refresh) {
blob_file_reader.Reset();
blob_file_cache_->Evict(file_number);
std::unique_ptr<BlobFileReader> fresh_reader;
s = blob_file_cache_->OpenBlobFileReaderUncached(
read_options, file_number, &fresh_reader,
/*allow_footer_skip_retry=*/false);
if (!s.ok()) {
for (const auto& blob_req : _blob_reqs) {
BlobReadRequest* const req = blob_req.first;
assert(req != nullptr);
assert(req->status != nullptr);
if (req->status->IsCorruption()) {
*req->status = AppendBlobRefreshRetryFailure(*req->status, s);
}
}
return;
}
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
retry_blob_reqs;
autovector<Status> stale_statuses;
for (auto& blob_req : _blob_reqs) {
BlobReadRequest* const req = blob_req.first;
assert(req != nullptr);
assert(req->status != nullptr);
if (!req->status->IsCorruption()) {
continue;
}
stale_statuses.emplace_back(*req->status);
*req->status = Status::OK();
blob_req.second.reset();
retry_blob_reqs.emplace_back(req, std::unique_ptr<BlobContents>());
}
uint64_t refreshed_bytes_read = 0;
fresh_reader->MultiGetBlob(read_options, allocator, retry_blob_reqs,
&refreshed_bytes_read);
_bytes_read += refreshed_bytes_read;
bool install_fresh_reader = false;
for (size_t i = 0; i < retry_blob_reqs.size(); ++i) {
auto& retried_blob_req = retry_blob_reqs[i];
BlobReadRequest* const retried_req = retried_blob_req.first;
assert(retried_req != nullptr);
if (retried_req->status->ok()) {
install_fresh_reader = true;
} else {
*retried_req->status = AppendBlobRefreshRetryFailure(
stale_statuses[i], *retried_req->status);
}
for (auto& blob_req : _blob_reqs) {
if (blob_req.first != retried_req) {
continue;
}
blob_req.second = std::move(retried_blob_req.second);
break;
}
}
if (install_fresh_reader) {
CacheHandleGuard<BlobFileReader> ignored_reader;
blob_file_cache_
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
.PermitUncheckedError();
}
}
if (blob_cache_ && read_options.fill_cache) {
// If filling cache is allowed and a cache is configured, try to put
// the blob(s) to the cache.
-150
View File
@@ -1044,156 +1044,6 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromCache) {
}
}
TEST_F(BlobSourceTest, GetBlobPreservesCorruptionDetailsWhenRefreshOpenFails) {
options_.cf_paths.emplace_back(
test::PerThreadDBPath(env_,
"BlobSourceTest_GetBlobPreservesCorruptionDetails"),
0);
DestroyAndReopen(options_);
ImmutableOptions immutable_options(options_);
MutableCFOptions mutable_cf_options(options_);
constexpr uint32_t column_family_id = 1;
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range;
constexpr uint64_t blob_file_number = 1;
const std::string key_str = "key0";
const std::string blob_str = "blob0";
std::vector<Slice> keys{Slice(key_str)};
std::vector<Slice> blobs{Slice(blob_str)};
std::vector<uint64_t> blob_offsets(1);
std::vector<uint64_t> blob_sizes(1);
const uint64_t file_size = BlobLogHeader::kSize + BlobLogRecord::kHeaderSize +
key_str.size() + blob_str.size() +
BlobLogFooter::kSize;
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
expiration_range, blob_file_number, keys, blobs, kNoCompression,
blob_offsets, blob_sizes);
constexpr size_t capacity = 1024;
std::shared_ptr<Cache> backing_cache = NewLRUCache(capacity);
FileOptions file_options;
std::unique_ptr<BlobFileCache> blob_file_cache =
std::make_unique<BlobFileCache>(
backing_cache.get(), &immutable_options, &file_options,
column_family_id, nullptr /*HistogramImpl*/, nullptr /*IOTracer*/);
BlobSource blob_source(immutable_options, mutable_cf_options, db_id_,
db_session_id_, blob_file_cache.get());
ReadOptions read_options;
read_options.verify_checksums = true;
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
PinnableSlice value;
uint64_t bytes_read = 0;
ASSERT_OK(blob_source.GetBlob(
read_options, keys[0], blob_file_number, blob_offsets[0], file_size,
blob_sizes[0], kNoCompression, prefetch_buffer, &value, &bytes_read));
ASSERT_EQ(value, blobs[0]);
const std::string blob_file_path =
BlobFileName(immutable_options.cf_paths.front().path, blob_file_number);
ASSERT_OK(env_->DeleteFile(blob_file_path));
PinnableSlice invalid_value;
bytes_read = 0;
Status s =
blob_source.GetBlob(read_options, keys[0], blob_file_number, file_size,
file_size, blob_sizes[0], kNoCompression,
prefetch_buffer, &invalid_value, &bytes_read);
ASSERT_TRUE(s.IsCorruption());
ASSERT_EQ(bytes_read, 0);
const std::string status_str = s.ToString();
ASSERT_NE(status_str.find("Invalid blob offset"), std::string::npos);
ASSERT_NE(status_str.find("refresh retry failed"), std::string::npos);
ASSERT_NE(status_str.find("IO error"), std::string::npos);
}
TEST_F(BlobSourceTest,
MultiGetBlobPreservesCorruptionDetailsWhenRefreshOpenFails) {
options_.cf_paths.emplace_back(
test::PerThreadDBPath(
env_, "BlobSourceTest_MultiGetBlobPreservesCorruptionDetails"),
0);
DestroyAndReopen(options_);
ImmutableOptions immutable_options(options_);
MutableCFOptions mutable_cf_options(options_);
constexpr uint32_t column_family_id = 1;
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range;
constexpr uint64_t blob_file_number = 1;
const std::string key_str = "key0";
const std::string blob_str = "blob0";
std::vector<Slice> keys{Slice(key_str)};
std::vector<Slice> blobs{Slice(blob_str)};
std::vector<uint64_t> blob_offsets(1);
std::vector<uint64_t> blob_sizes(1);
const uint64_t file_size = BlobLogHeader::kSize + BlobLogRecord::kHeaderSize +
key_str.size() + blob_str.size() +
BlobLogFooter::kSize;
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
expiration_range, blob_file_number, keys, blobs, kNoCompression,
blob_offsets, blob_sizes);
constexpr size_t capacity = 1024;
std::shared_ptr<Cache> backing_cache = NewLRUCache(capacity);
FileOptions file_options;
std::unique_ptr<BlobFileCache> blob_file_cache =
std::make_unique<BlobFileCache>(
backing_cache.get(), &immutable_options, &file_options,
column_family_id, nullptr /*HistogramImpl*/, nullptr /*IOTracer*/);
BlobSource blob_source(immutable_options, mutable_cf_options, db_id_,
db_session_id_, blob_file_cache.get());
ReadOptions read_options;
read_options.verify_checksums = true;
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
PinnableSlice value;
uint64_t bytes_read = 0;
ASSERT_OK(blob_source.GetBlob(
read_options, keys[0], blob_file_number, blob_offsets[0], file_size,
blob_sizes[0], kNoCompression, prefetch_buffer, &value, &bytes_read));
ASSERT_EQ(value, blobs[0]);
const std::string blob_file_path =
BlobFileName(immutable_options.cf_paths.front().path, blob_file_number);
ASSERT_OK(env_->DeleteFile(blob_file_path));
std::array<Status, 1> statuses;
std::array<PinnableSlice, 1> values;
autovector<BlobReadRequest> blob_reqs;
blob_reqs.emplace_back(keys[0], file_size, blob_sizes[0], kNoCompression,
&values[0], &statuses[0]);
bytes_read = 0;
blob_source.MultiGetBlobFromOneFile(read_options, blob_file_number, file_size,
blob_reqs, &bytes_read);
ASSERT_TRUE(statuses[0].IsCorruption());
ASSERT_EQ(bytes_read, 0);
const std::string status_str = statuses[0].ToString();
ASSERT_NE(status_str.find("Invalid blob offset"), std::string::npos);
ASSERT_NE(status_str.find("refresh retry failed"), std::string::npos);
ASSERT_NE(status_str.find("IO error"), std::string::npos);
}
class BlobSecondaryCacheTest : public DBTestBase {
protected:
public:
-316
View File
@@ -1,316 +0,0 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "db/blob/blob_write_batch_transformer.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_index.h"
#include "db/blob/blob_log_format.h"
#include "db/wide/wide_column_serialization.h"
#include "db/write_batch_internal.h"
#include "port/likely.h"
namespace ROCKSDB_NAMESPACE {
BlobWriteBatchTransformer::BlobWriteBatchTransformer(
const BlobPartitionManagerProvider& partition_mgr_provider,
WriteBatch* output_batch,
const BlobDirectWriteSettingsProvider& settings_provider,
const WriteOptions& write_options)
: partition_mgr_provider_(partition_mgr_provider),
output_batch_(output_batch),
settings_provider_(settings_provider),
write_options_(write_options) {
assert(partition_mgr_provider_);
assert(output_batch_);
assert(settings_provider_);
}
Status BlobWriteBatchTransformer::TransformBatch(
const WriteOptions& write_options, WriteBatch* input_batch,
WriteBatch* output_batch,
const BlobPartitionManagerProvider& partition_mgr_provider,
const BlobDirectWriteSettingsProvider& settings_provider, bool* transformed,
std::vector<BlobFilePartitionManager*>* used_managers,
std::vector<RollbackInfo>* rollback_infos) {
assert(input_batch);
assert(output_batch);
assert(transformed);
output_batch->Clear();
*transformed = false;
BlobWriteBatchTransformer transformer(partition_mgr_provider, output_batch,
settings_provider, write_options);
Status s = input_batch->Iterate(&transformer);
*transformed = transformer.HasTransformed();
if (used_managers) {
used_managers->assign(transformer.used_managers_.begin(),
transformer.used_managers_.end());
}
if (rollback_infos) {
*rollback_infos = std::move(transformer.rollback_infos_);
}
return s;
}
Status BlobWriteBatchTransformer::MaybePreprocessWideColumns(
const WriteOptions& write_options, uint32_t column_family_id,
const Slice& key, const WideColumns& columns,
BlobFilePartitionManager* partition_mgr,
const BlobDirectWriteSettings& settings, bool serialize_inline_entity,
std::string* entity, bool* transformed,
std::vector<RollbackInfo>* rollback_infos) {
assert(entity != nullptr);
assert(transformed != nullptr);
*transformed = false;
if (partition_mgr == nullptr || !settings.enable_blob_direct_write) {
if (!serialize_inline_entity) {
return Status::OK();
}
entity->clear();
return WideColumnSerialization::Serialize(columns, *entity);
}
std::vector<std::pair<size_t, BlobIndex>> new_blob_columns;
new_blob_columns.reserve(columns.size());
std::string blob_index_buf;
bool has_entity_partition = false;
uint32_t entity_partition = 0;
for (size_t column_idx = 0; column_idx < columns.size(); ++column_idx) {
const Slice& column_value = columns[column_idx].value();
if (column_value.size() < settings.min_blob_size) {
continue;
}
if (!has_entity_partition) {
entity_partition = partition_mgr->SelectWideColumnPartition(
column_family_id, key, columns);
has_entity_partition = true;
}
uint64_t blob_file_number = 0;
uint64_t blob_offset = 0;
uint64_t blob_size = 0;
Status s = partition_mgr->WriteBlob(
write_options, column_family_id, settings.compression_type, key,
column_value, &blob_file_number, &blob_offset, &blob_size, &settings,
&entity_partition);
if (!s.ok()) {
return s;
}
if (rollback_infos != nullptr) {
const uint64_t record_bytes =
BlobLogRecord::kHeaderSize + key.size() + blob_size;
rollback_infos->push_back(
{partition_mgr, blob_file_number, /*count=*/1, record_bytes});
}
BlobIndex blob_index;
BlobIndex::EncodeBlob(&blob_index_buf, blob_file_number, blob_offset,
blob_size, settings.compression_type);
s = blob_index.DecodeFrom(blob_index_buf);
if (!s.ok()) {
return s;
}
new_blob_columns.emplace_back(column_idx, blob_index);
}
if (new_blob_columns.empty()) {
if (!serialize_inline_entity) {
return Status::OK();
}
entity->clear();
return WideColumnSerialization::Serialize(columns, *entity);
}
entity->clear();
Status s =
WideColumnSerialization::SerializeV2(columns, new_blob_columns, *entity);
if (s.ok()) {
*transformed = true;
}
return s;
}
Status BlobWriteBatchTransformer::PutCF(uint32_t column_family_id,
const Slice& key, const Slice& value) {
// Use cached settings/manager for the same CF to avoid per-entry lookup.
if (column_family_id != cached_cf_id_) {
cached_settings_ = settings_provider_(column_family_id);
cached_partition_mgr_ = partition_mgr_provider_(column_family_id);
cached_cf_id_ = column_family_id;
}
const auto& settings = cached_settings_;
if (!cached_partition_mgr_ || !settings.enable_blob_direct_write ||
value.size() < settings.min_blob_size) {
return WriteBatchInternal::Put(output_batch_, column_family_id, key, value);
}
uint64_t blob_file_number = 0;
uint64_t blob_offset = 0;
uint64_t blob_size = 0;
Status s = cached_partition_mgr_->WriteBlob(
write_options_, column_family_id, settings.compression_type, key, value,
&blob_file_number, &blob_offset, &blob_size, &settings);
if (!s.ok()) {
return s;
}
used_managers_.insert(cached_partition_mgr_);
// Track the exact file so stale transformed attempts can rollback
// per-file rather than smearing bytes across all partitions at seal time.
const uint64_t record_bytes =
BlobLogRecord::kHeaderSize + key.size() + blob_size;
rollback_infos_.push_back(
{cached_partition_mgr_, blob_file_number, /*count=*/1, record_bytes});
BlobIndex::EncodeBlob(&blob_index_buf_, blob_file_number, blob_offset,
blob_size, settings.compression_type);
has_transformed_ = true;
return WriteBatchInternal::PutBlobIndex(output_batch_, column_family_id, key,
blob_index_buf_);
}
Status BlobWriteBatchTransformer::TimedPutCF(uint32_t column_family_id,
const Slice& key,
const Slice& value,
uint64_t write_time) {
// TimedPut: pass through without blob separation for now.
return WriteBatchInternal::TimedPut(output_batch_, column_family_id, key,
value, write_time);
}
Status BlobWriteBatchTransformer::PutEntityCF(uint32_t column_family_id,
const Slice& key,
const Slice& entity) {
if (column_family_id != cached_cf_id_) {
cached_settings_ = settings_provider_(column_family_id);
cached_partition_mgr_ = partition_mgr_provider_(column_family_id);
cached_cf_id_ = column_family_id;
}
const auto& settings = cached_settings_;
if (!cached_partition_mgr_ || !settings.enable_blob_direct_write) {
return WriteBatchInternal::PutEntitySerialized(
output_batch_, column_family_id, key, entity);
}
Slice entity_ref = entity;
std::vector<WideColumn> columns;
std::vector<std::pair<size_t, BlobIndex>> existing_blob_columns;
Status status = WideColumnSerialization::DeserializeV2(entity_ref, columns,
existing_blob_columns);
if (status.ok()) {
// Reject pre-serialized entities that already contain blob references.
// Passing them through would preserve references to blob files outside this
// batch's direct-write lifetime tracking, which can later turn into stale
// reads after blob GC.
if (UNLIKELY(!existing_blob_columns.empty())) {
return Status::NotSupported(
"Blob direct write does not support pre-serialized wide entities "
"with blob references");
}
std::string rewritten_entity;
bool transformed = false;
status = MaybePreprocessWideColumns(
write_options_, column_family_id, key, columns, cached_partition_mgr_,
settings, /*serialize_inline_entity=*/false, &rewritten_entity,
&transformed, &rollback_infos_);
if (status.ok()) {
if (!transformed) {
return WriteBatchInternal::PutEntitySerialized(
output_batch_, column_family_id, key, entity);
}
has_transformed_ = true;
used_managers_.insert(cached_partition_mgr_);
return WriteBatchInternal::PutEntitySerialized(
output_batch_, column_family_id, key, rewritten_entity);
}
}
return status;
}
Status BlobWriteBatchTransformer::DeleteCF(uint32_t column_family_id,
const Slice& key) {
return WriteBatchInternal::Delete(output_batch_, column_family_id, key);
}
Status BlobWriteBatchTransformer::SingleDeleteCF(uint32_t column_family_id,
const Slice& key) {
return WriteBatchInternal::SingleDelete(output_batch_, column_family_id, key);
}
Status BlobWriteBatchTransformer::DeleteRangeCF(uint32_t column_family_id,
const Slice& begin_key,
const Slice& end_key) {
return WriteBatchInternal::DeleteRange(output_batch_, column_family_id,
begin_key, end_key);
}
Status BlobWriteBatchTransformer::MergeCF(uint32_t column_family_id,
const Slice& key,
const Slice& value) {
return WriteBatchInternal::Merge(output_batch_, column_family_id, key, value);
}
Status BlobWriteBatchTransformer::PutBlobIndexCF(uint32_t column_family_id,
const Slice& key,
const Slice& value) {
// Already a blob index — pass through unchanged.
return WriteBatchInternal::PutBlobIndex(output_batch_, column_family_id, key,
value);
}
void BlobWriteBatchTransformer::LogData(const Slice& blob) {
output_batch_->PutLogData(blob).PermitUncheckedError();
}
Status BlobWriteBatchTransformer::MarkBeginPrepare(bool unprepared) {
return WriteBatchInternal::InsertBeginPrepare(
output_batch_, !unprepared /* write_after_commit */, unprepared);
}
Status BlobWriteBatchTransformer::MarkEndPrepare(const Slice& xid) {
return WriteBatchInternal::InsertEndPrepare(output_batch_, xid);
}
Status BlobWriteBatchTransformer::MarkCommit(const Slice& xid) {
return WriteBatchInternal::MarkCommit(output_batch_, xid);
}
Status BlobWriteBatchTransformer::MarkCommitWithTimestamp(const Slice& xid,
const Slice& ts) {
return WriteBatchInternal::MarkCommitWithTimestamp(output_batch_, xid, ts);
}
Status BlobWriteBatchTransformer::MarkRollback(const Slice& xid) {
return WriteBatchInternal::MarkRollback(output_batch_, xid);
}
Status BlobWriteBatchTransformer::MarkNoop(bool /*empty_batch*/) {
return WriteBatchInternal::InsertNoop(output_batch_);
}
} // namespace ROCKSDB_NAMESPACE
-174
View File
@@ -1,174 +0,0 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#pragma once
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "rocksdb/advanced_options.h"
#include "rocksdb/compression_type.h"
#include "rocksdb/options.h"
#include "rocksdb/rocksdb_namespace.h"
#include "rocksdb/slice.h"
#include "rocksdb/status.h"
#include "rocksdb/write_batch.h"
#include "util/hash_containers.h"
namespace ROCKSDB_NAMESPACE {
class BlobFilePartitionManager;
class Cache;
// Callback to look up per-CF blob settings.
struct BlobDirectWriteSettings {
// Whether this column family should attempt direct-write blob separation.
bool enable_blob_direct_write = false;
// Minimum value size eligible for direct-write blob separation.
uint64_t min_blob_size = 0;
// Compression to use for newly written blob records.
CompressionType compression_type = kNoCompression;
// 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
-181
View File
@@ -3,29 +3,17 @@
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include <algorithm>
#include <array>
#include <set>
#include <sstream>
#include <string>
#include "cache/compressed_secondary_cache.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_index.h"
#include "db/blob/blob_log_format.h"
#include "db/blob/blob_log_sequential_reader.h"
#include "db/column_family.h"
#include "db/db_test_util.h"
#include "db/db_with_timestamp_test_util.h"
#include "file/filename.h"
#include "file/random_access_file_reader.h"
#include "port/stack_trace.h"
#include "rocksdb/convenience.h"
#include "rocksdb/trace_reader_writer.h"
#include "rocksdb/trace_record.h"
#include "rocksdb/utilities/replayer.h"
#include "test_util/sync_point.h"
#include "util/compression.h"
#include "utilities/fault_injection_env.h"
namespace ROCKSDB_NAMESPACE {
@@ -63,43 +51,6 @@ TEST_F(DBBlobBasicTest, GetBlob) {
.IsIncomplete());
}
TEST_F(DBBlobBasicTest, EmptyValueNotStoredAsBlob) {
// Regression test for crash when empty blob value is evicted to
// CompressedSecondaryCache (T261142690). Empty values should always be
// stored inline in the SST, never as blobs, even with min_blob_size=0.
// A BlobIndex for an empty value is strictly larger than the value itself,
// so storing it as a blob is pure overhead.
Options options = GetDefaultOptions();
options.enable_blob_files = true;
options.min_blob_size = 0;
options.disable_auto_compactions = true;
Reopen(options);
// Write an empty value and a non-empty value.
ASSERT_OK(Put("empty_key", ""));
constexpr char blob_value[] = "blob_value";
ASSERT_OK(Put("nonempty_key", blob_value));
ASSERT_OK(Flush());
// Both values should be readable.
ASSERT_EQ(Get("empty_key"), "");
ASSERT_EQ(Get("nonempty_key"), blob_value);
// The empty value should be stored inline (readable from block cache
// without blob file I/O), while the non-empty value requires blob I/O.
ReadOptions ro;
ro.read_tier = kBlockCacheTier;
PinnableSlice result;
ASSERT_OK(db_->Get(ro, db_->DefaultColumnFamily(), "empty_key", &result));
ASSERT_EQ(result, "");
result.Reset();
ASSERT_TRUE(db_->Get(ro, db_->DefaultColumnFamily(), "nonempty_key", &result)
.IsIncomplete());
}
TEST_F(DBBlobBasicTest, GetBlobFromCache) {
Options options = GetDefaultOptions();
@@ -2508,138 +2459,6 @@ TEST_F(DBBlobWithTimestampTest, IterateBlobs) {
}
}
TEST_F(DBBlobBasicTest, GetApproximateSizesIncludingBlobFiles) {
Options options = GetDefaultOptions();
options.enable_blob_files = true;
options.min_blob_size = 0;
Reopen(options);
// Write some key-value pairs with blob values and flush to create blob files.
constexpr int kNumKeys = 1000;
constexpr int kValueSize = 1024;
Random rnd(301);
for (int i = 0; i < kNumKeys; ++i) {
ASSERT_OK(Put(Key(i), rnd.RandomString(kValueSize)));
}
ASSERT_OK(Flush());
// Verify blob files exist.
std::vector<std::string> files;
ASSERT_OK(env_->GetChildren(dbname_, &files));
bool has_blob_files = false;
for (const auto& f : files) {
if (f.size() > 5 && f.substr(f.size() - 5) == ".blob") {
has_blob_files = true;
break;
}
}
ASSERT_TRUE(has_blob_files);
// Query the full range - all keys are covered.
std::string start = Key(0);
std::string end = Key(kNumKeys);
Range r(start, end);
// Without include_blob_files (default behavior): should not include blob
// file sizes.
uint64_t size_without_blobs = 0;
{
SizeApproximationOptions size_approx_options;
size_approx_options.include_files = true;
size_approx_options.include_blob_files = false;
ASSERT_OK(db_->GetApproximateSizes(size_approx_options,
db_->DefaultColumnFamily(), &r, 1,
&size_without_blobs));
ASSERT_GT(size_without_blobs, 0);
}
// With include_blob_files: should be strictly larger.
{
SizeApproximationOptions size_approx_options;
size_approx_options.include_files = true;
size_approx_options.include_blob_files = true;
uint64_t size_with_blobs = 0;
ASSERT_OK(db_->GetApproximateSizes(size_approx_options,
db_->DefaultColumnFamily(), &r, 1,
&size_with_blobs));
ASSERT_GT(size_with_blobs, size_without_blobs);
}
// Range that doesn't overlap any data should return 0.
{
std::string no_start = Key(kNumKeys + 100);
std::string no_end = Key(kNumKeys + 200);
Range no_r(no_start, no_end);
SizeApproximationOptions size_approx_options;
size_approx_options.include_files = true;
size_approx_options.include_blob_files = true;
uint64_t no_size = 0;
ASSERT_OK(db_->GetApproximateSizes(
size_approx_options, db_->DefaultColumnFamily(), &no_r, 1, &no_size));
ASSERT_EQ(no_size, 0);
}
// Partial range should return proportionally less blob size than full range.
{
SizeApproximationOptions size_approx_options;
size_approx_options.include_files = true;
size_approx_options.include_blob_files = true;
uint64_t full_size = 0;
ASSERT_OK(db_->GetApproximateSizes(
size_approx_options, db_->DefaultColumnFamily(), &r, 1, &full_size));
// Query roughly the first half of keys.
std::string half_end = Key(kNumKeys / 2);
Range half_r(start, half_end);
uint64_t half_size = 0;
ASSERT_OK(db_->GetApproximateSizes(size_approx_options,
db_->DefaultColumnFamily(), &half_r, 1,
&half_size));
ASSERT_GT(half_size, 0);
ASSERT_LT(half_size, full_size);
}
// Via SizeApproximationFlags API.
{
uint64_t size_flags = 0;
ASSERT_OK(db_->GetApproximateSizes(
db_->DefaultColumnFamily(), &r, 1, &size_flags,
DB::SizeApproximationFlags::INCLUDE_FILES |
DB::SizeApproximationFlags::INCLUDE_BLOB_FILES));
ASSERT_GT(size_flags, size_without_blobs);
}
// Multi-range query: two non-overlapping sub-ranges should sum to
// approximately the full-range result.
{
SizeApproximationOptions size_approx_options;
size_approx_options.include_files = true;
size_approx_options.include_blob_files = true;
std::string mid = Key(kNumKeys / 2);
std::string r1_start = Key(0);
std::string r1_end = mid;
std::string r2_start = mid;
std::string r2_end = Key(kNumKeys);
Range ranges[2] = {Range(r1_start, r1_end), Range(r2_start, r2_end)};
uint64_t sizes[2] = {0, 0};
ASSERT_OK(db_->GetApproximateSizes(
size_approx_options, db_->DefaultColumnFamily(), ranges, 2, sizes));
// Each sub-range should return a positive size.
ASSERT_GT(sizes[0], 0);
ASSERT_GT(sizes[1], 0);
// Sum of sub-ranges should be close to the full-range result.
uint64_t full_size = 0;
ASSERT_OK(db_->GetApproximateSizes(
size_approx_options, db_->DefaultColumnFamily(), &r, 1, &full_size));
ASSERT_NEAR(static_cast<double>(sizes[0] + sizes[1]),
static_cast<double>(full_size), full_size * 0.1);
}
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+8 -74
View File
@@ -13,10 +13,7 @@
#include <deque>
#include <vector>
#include "db/blob/blob_counting_iterator.h"
#include "db/blob/blob_file_builder.h"
#include "db/blob/blob_garbage_meter.h"
#include "db/column_family.h"
#include "db/compaction/compaction_iterator.h"
#include "db/dbformat.h"
#include "db/event_helpers.h"
@@ -90,8 +87,7 @@ Status BuildTable(
Env::WriteLifeTimeHint write_hint, const std::string* full_history_ts_low,
BlobFileCompletionCallback* blob_callback, Version* version,
uint64_t* memtable_payload_bytes, uint64_t* memtable_garbage_bytes,
InternalStats::CompactionStats* flush_stats,
std::vector<BlobFileGarbage>* blob_file_garbages, bool fast_sst_open) {
InternalStats::CompactionStats* flush_stats) {
assert((tboptions.column_family_id ==
TablePropertiesCollectorFactory::Context::kUnknownColumnFamily) ==
tboptions.column_family_name.empty());
@@ -103,24 +99,7 @@ Status BuildTable(
/*enable_hash=*/paranoid_file_checks);
Status s;
meta->fd.file_size = 0;
if (blob_file_garbages != nullptr) {
blob_file_garbages->clear();
}
InternalIterator* input = iter;
std::unique_ptr<BlobGarbageMeter> blob_garbage_meter;
std::unique_ptr<BlobCountingIterator> blob_counting_iter;
if (blob_file_garbages != nullptr) {
// Flush can drop blob-backed records via overwrite elision and flush-time
// compaction filters. Track input and surviving output refs just like the
// compaction path so the manifest learns about the new garbage.
blob_garbage_meter.reset(new BlobGarbageMeter());
blob_counting_iter.reset(
new BlobCountingIterator(iter, blob_garbage_meter.get()));
input = blob_counting_iter.get();
}
input->SeekToFirst();
iter->SeekToFirst();
std::unique_ptr<CompactionRangeDelAggregator> range_del_agg(
new CompactionRangeDelAggregator(&tboptions.internal_comparator,
snapshots, full_history_ts_low));
@@ -149,7 +128,7 @@ Status BuildTable(
TableProperties tp;
bool table_file_created = false;
if (input->Valid() || !range_del_agg->IsEmpty()) {
if (iter->Valid() || !range_del_agg->IsEmpty()) {
std::unique_ptr<CompactionFilter> compaction_filter;
if (ioptions.compaction_filter_factory != nullptr &&
ioptions.compaction_filter_factory->ShouldFilterTableFileCreation(
@@ -232,7 +211,7 @@ Status BuildTable(
const std::atomic<bool> kManualCompactionCanceledFalse{false};
CompactionIterator c_iter(
input, ucmp, &merge, kMaxSequenceNumber, &snapshots, earliest_snapshot,
iter, ucmp, &merge, kMaxSequenceNumber, &snapshots, earliest_snapshot,
earliest_write_conflict_snapshot, job_snapshot, snapshot_checker, env,
ShouldReportDetailedTime(env, ioptions.stats), range_del_agg.get(),
blob_file_builder.get(), ioptions.allow_data_in_errors,
@@ -240,17 +219,7 @@ Status BuildTable(
/*manual_compaction_canceled=*/kManualCompactionCanceledFalse,
true /* must_count_input_entries */,
/*compaction=*/nullptr, compaction_filter.get(),
/*shutting_down=*/nullptr, db_options.info_log, full_history_ts_low,
std::nullopt, version, tboptions.read_options.io_activity);
if (version != nullptr) {
ColumnFamilyData* const cfd = version->cfd();
c_iter.SetBlobFetcher(
version, cfd != nullptr ? cfd->blob_file_cache() : nullptr,
tboptions.read_options.io_activity,
tboptions.reason == TableFileCreationReason::kFlush ||
tboptions.reason == TableFileCreationReason::kRecovery);
}
/*shutting_down=*/nullptr, db_options.info_log, full_history_ts_low);
SequenceNumber smallest_preferred_seqno = kMaxSequenceNumber;
std::string key_after_flush_buf;
@@ -299,14 +268,6 @@ Status BuildTable(
flush_stats->num_output_records++;
}
if (blob_garbage_meter != nullptr) {
s = blob_garbage_meter->ProcessOutFlow(key_after_flush,
value_after_flush);
if (!s.ok()) {
break;
}
}
s = meta->UpdateBoundaries(key_after_flush, value_after_flush,
ikey.sequence, ikey.type);
if (!s.ok()) {
@@ -361,8 +322,6 @@ Status BuildTable(
}
TEST_SYNC_POINT("BuildTable:BeforeFinishBuildTable");
TEST_SYNC_POINT_CALLBACK("BuildTable:BeforeCheckEmpty",
static_cast<TableBuilder*>(builder));
const bool empty = builder->IsEmpty();
if (flush_stats) {
assert(c_iter.HasNumInputEntryScanned());
@@ -371,14 +330,6 @@ Status BuildTable(
}
if (!s.ok() || empty) {
builder->Abandon();
// Propagate the builder's error when the builder is empty due to an
// internal error (e.g., write fault injection causing all Add() calls to
// return early). Without this, `s` would remain OK and the downstream
// key count validation in flush_job.cc would produce a misleading
// Corruption error instead of the actual builder error.
if (s.ok() && !builder->status().ok()) {
s = builder->status();
}
} else {
SeqnoToTimeMapping relevant_mapping;
if (seqno_to_time_mapping) {
@@ -496,8 +447,6 @@ Status BuildTable(
// here because this is a special case after we finish the table building.
// No matter whether use_direct_io_for_flush_and_compaction is true,
// the goal is to cache it here for further user reads.
std::string* file_open_metadata_ptr =
fast_sst_open ? &meta->file_open_metadata : nullptr;
std::unique_ptr<InternalIterator> it(table_cache->NewIterator(
tboptions.read_options, file_options, tboptions.internal_comparator,
*meta, nullptr /* range_del_agg */, mutable_cf_options, nullptr,
@@ -508,10 +457,7 @@ Status BuildTable(
MaxFileSizeForL0MetaPin(mutable_cf_options),
/*smallest_compaction_key=*/nullptr,
/*largest_compaction_key*/ nullptr,
/*allow_unprepared_value*/ false,
/*range_del_read_seqno=*/nullptr,
/*range_del_iter=*/nullptr,
/*maybe_pin_table_handle=*/false, file_open_metadata_ptr));
/*allow_unprepared_value*/ false));
s = it->status();
if (s.ok() && paranoid_file_checks) {
OutputValidator file_validator(tboptions.internal_comparator,
@@ -529,20 +475,8 @@ Status BuildTable(
}
// Check for input iterator errors
if (!input->status().ok()) {
s = input->status();
}
if (s.ok() && blob_file_garbages != nullptr &&
blob_garbage_meter != nullptr) {
for (const auto& pair : blob_garbage_meter->flows()) {
const uint64_t blob_file_number = pair.first;
const BlobGarbageMeter::BlobInOutFlow& flow = pair.second;
if (flow.HasGarbage()) {
blob_file_garbages->emplace_back(
blob_file_number, flow.GetGarbageCount(), flow.GetGarbageBytes());
}
}
if (!iter->status().ok()) {
s = iter->status();
}
if (!s.ok() || meta->fd.GetFileSize() == 0) {
+1 -4
View File
@@ -31,7 +31,6 @@ struct FileMetaData;
class VersionSet;
class BlobFileAddition;
class BlobFileGarbage;
class SnapshotChecker;
class TableCache;
class TableBuilder;
@@ -80,8 +79,6 @@ Status BuildTable(
BlobFileCompletionCallback* blob_callback = nullptr,
Version* version = nullptr, uint64_t* memtable_payload_bytes = nullptr,
uint64_t* memtable_garbage_bytes = nullptr,
InternalStats::CompactionStats* flush_stats = nullptr,
std::vector<BlobFileGarbage>* blob_file_garbages = nullptr,
bool fast_sst_open = false);
InternalStats::CompactionStats* flush_stats = nullptr);
} // namespace ROCKSDB_NAMESPACE
+6 -70
View File
@@ -1759,13 +1759,9 @@ rocksdb_column_family_handle_t* rocksdb_create_column_family(
rocksdb_t* db, const rocksdb_options_t* column_family_options,
const char* column_family_name, char** errptr) {
rocksdb_column_family_handle_t* handle = new rocksdb_column_family_handle_t;
handle->rep = nullptr;
if (SaveError(errptr, db->rep->CreateColumnFamily(
ColumnFamilyOptions(column_family_options->rep),
std::string(column_family_name), &(handle->rep)))) {
delete handle;
return nullptr;
}
SaveError(errptr, db->rep->CreateColumnFamily(
ColumnFamilyOptions(column_family_options->rep),
std::string(column_family_name), &(handle->rep)));
handle->immortal = false;
return handle;
}
@@ -3056,7 +3052,6 @@ 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 {
@@ -3090,7 +3085,6 @@ 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,
@@ -4357,15 +4351,6 @@ unsigned char rocksdb_options_get_paranoid_checks(rocksdb_options_t* opt) {
return opt->rep.paranoid_checks;
}
void rocksdb_options_set_open_files_async(rocksdb_options_t* opt,
unsigned char v) {
opt->rep.open_files_async = v;
}
unsigned char rocksdb_options_get_open_files_async(rocksdb_options_t* opt) {
return opt->rep.open_files_async;
}
void rocksdb_options_set_db_paths(rocksdb_options_t* opt,
const rocksdb_dbpath_t** dbpath_values,
size_t num_paths) {
@@ -4650,16 +4635,6 @@ uint32_t rocksdb_options_get_memtable_avg_op_scan_flush_trigger(
return opt->rep.memtable_avg_op_scan_flush_trigger;
}
void rocksdb_options_set_min_tombstones_for_range_conversion(
rocksdb_options_t* opt, uint32_t n) {
opt->rep.min_tombstones_for_range_conversion = n;
}
uint32_t rocksdb_options_get_min_tombstones_for_range_conversion(
rocksdb_options_t* opt) {
return opt->rep.min_tombstones_for_range_conversion;
}
void rocksdb_options_enable_statistics(rocksdb_options_t* opt) {
opt->rep.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
}
@@ -4759,26 +4734,6 @@ double rocksdb_options_get_blob_gc_force_threshold(rocksdb_options_t* opt) {
return opt->rep.blob_garbage_collection_force_threshold;
}
void rocksdb_options_set_read_triggered_compaction_threshold(
rocksdb_options_t* opt, double val) {
opt->rep.read_triggered_compaction_threshold = val;
}
double rocksdb_options_get_read_triggered_compaction_threshold(
rocksdb_options_t* opt) {
return opt->rep.read_triggered_compaction_threshold;
}
void rocksdb_options_set_max_compaction_trigger_wakeup_seconds(
rocksdb_options_t* opt, uint64_t val) {
opt->rep.max_compaction_trigger_wakeup_seconds = val;
}
uint64_t rocksdb_options_get_max_compaction_trigger_wakeup_seconds(
rocksdb_options_t* opt) {
return opt->rep.max_compaction_trigger_wakeup_seconds;
}
void rocksdb_options_set_blob_compaction_readahead_size(rocksdb_options_t* opt,
uint64_t val) {
opt->rep.blob_compaction_readahead_size = val;
@@ -5834,16 +5789,6 @@ uint64_t rocksdb_perfcontext_metric(rocksdb_perfcontext_t* context,
return rep->internal_range_del_reseek_count;
case rocksdb_block_read_cpu_time:
return rep->block_read_cpu_time;
case rocksdb_data_block_read_byte:
return rep->data_block_read_byte;
case rocksdb_index_block_read_byte:
return rep->index_block_read_byte;
case rocksdb_filter_block_read_byte:
return rep->filter_block_read_byte;
case rocksdb_compression_dict_block_read_byte:
return rep->compression_dict_block_read_byte;
case rocksdb_metadata_block_read_byte:
return rep->metadata_block_read_byte;
default:
break;
}
@@ -7580,13 +7525,9 @@ rocksdb_column_family_handle_t* rocksdb_transactiondb_create_column_family(
const rocksdb_options_t* column_family_options,
const char* column_family_name, char** errptr) {
rocksdb_column_family_handle_t* handle = new rocksdb_column_family_handle_t;
handle->rep = nullptr;
if (SaveError(errptr, txn_db->rep->CreateColumnFamily(
ColumnFamilyOptions(column_family_options->rep),
std::string(column_family_name), &(handle->rep)))) {
delete handle;
return nullptr;
}
SaveError(errptr, txn_db->rep->CreateColumnFamily(
ColumnFamilyOptions(column_family_options->rep),
std::string(column_family_name), &(handle->rep)));
handle->immortal = false;
return handle;
}
@@ -8316,11 +8257,6 @@ void rocksdb_transaction_delete_cf(
SaveError(errptr, txn->rep->Delete(column_family->rep, Slice(key, klen)));
}
void rocksdb_transaction_put_log_data(rocksdb_transaction_t* txn,
const char* blob, size_t len) {
txn->rep->PutLogData(Slice(blob, len));
}
// Delete a key outside a transaction
void rocksdb_transactiondb_delete(rocksdb_transactiondb_t* txn_db,
const rocksdb_writeoptions_t* options,
-79
View File
@@ -205,27 +205,6 @@ static void CheckDel(void* ptr, const char* k, size_t klen) {
(*state)++;
}
static void NopPut(void* ptr, const char* k, size_t klen, const char* v,
size_t vlen) {
(void)ptr;
(void)k;
(void)klen;
(void)v;
(void)vlen;
}
static void NopDel(void* ptr, const char* k, size_t klen) {
(void)ptr;
(void)k;
(void)klen;
}
static void CheckLogData(void* ptr, const char* blob, size_t blen) {
CheckEqual("log_blob", blob, blen);
int* found = (int*)ptr;
*found = 1;
}
// Callback from rocksdb_writebatch_iterate_cf()
static void CheckPutCF(void* ptr, uint32_t cfid, const char* k, size_t klen,
const char* v, size_t vlen) {
@@ -2664,10 +2643,6 @@ int main(int argc, char** argv) {
CheckCondition(150 ==
rocksdb_options_get_memtable_avg_op_scan_flush_trigger(o));
rocksdb_options_set_min_tombstones_for_range_conversion(o, 200);
CheckCondition(200 ==
rocksdb_options_get_min_tombstones_for_range_conversion(o));
rocksdb_options_set_ttl(o, 5000);
CheckCondition(5000 == rocksdb_options_get_ttl(o));
@@ -2892,14 +2867,6 @@ int main(int argc, char** argv) {
rocksdb_options_set_blob_gc_force_threshold(o, 0.75);
CheckCondition(0.75 == rocksdb_options_get_blob_gc_force_threshold(o));
rocksdb_options_set_read_triggered_compaction_threshold(o, 0.001);
CheckCondition(0.001 ==
rocksdb_options_get_read_triggered_compaction_threshold(o));
rocksdb_options_set_max_compaction_trigger_wakeup_seconds(o, 3600);
CheckCondition(
3600 == rocksdb_options_get_max_compaction_trigger_wakeup_seconds(o));
rocksdb_options_set_blob_compaction_readahead_size(o, 262144);
CheckCondition(262144 ==
rocksdb_options_get_blob_compaction_readahead_size(o));
@@ -3119,12 +3086,6 @@ int main(int argc, char** argv) {
CheckCondition(150 ==
rocksdb_options_get_memtable_avg_op_scan_flush_trigger(o));
rocksdb_options_set_min_tombstones_for_range_conversion(copy, 1000);
CheckCondition(
1000 == rocksdb_options_get_min_tombstones_for_range_conversion(copy));
CheckCondition(200 ==
rocksdb_options_get_min_tombstones_for_range_conversion(o));
rocksdb_options_set_ttl(copy, 8000);
CheckCondition(8000 == rocksdb_options_get_ttl(copy));
CheckCondition(5000 == rocksdb_options_get_ttl(o));
@@ -3889,35 +3850,10 @@ int main(int argc, char** argv) {
CheckMultiGetValues(3, vals, vals_sizes, errs, expected);
}
rocksdb_transaction_put_log_data(txn, "log_blob", 8);
// record sequence number before commit so we can scan the WAL after
rocksdb_t* base_db_ld = rocksdb_transactiondb_get_base_db(txn_db);
uint64_t seq_before = rocksdb_get_latest_sequence_number(base_db_ld);
// commit
rocksdb_transaction_commit(txn, &err);
CheckNoError(err);
// verify log data was written to WAL by scanning batches since seq_before
{
rocksdb_wal_iterator_t* wal_iter =
rocksdb_get_updates_since(base_db_ld, seq_before, NULL, &err);
CheckNoError(err);
int log_found = 0;
for (; rocksdb_wal_iter_valid(wal_iter) && !log_found;
rocksdb_wal_iter_next(wal_iter)) {
uint64_t seq;
rocksdb_writebatch_t* wal_wb =
rocksdb_wal_iter_get_batch(wal_iter, &seq);
rocksdb_writebatch_iterate_ld(wal_wb, &log_found, NopPut, NopDel,
CheckLogData);
rocksdb_writebatch_destroy(wal_wb);
}
CheckCondition(log_found == 1);
rocksdb_wal_iter_destroy(wal_iter);
}
rocksdb_transactiondb_close_base_db(base_db_ld);
// read from outside transaction, after commit
CheckTxnDBGet(txn_db, roptions, "foo", "hello");
CheckTxnDBPinGet(txn_db, roptions, "foo", "hello");
@@ -4944,21 +4880,6 @@ int main(int argc, char** argv) {
rocksdb_sst_file_manager_destroy(sst_file_manager);
}
StartPhase("create_column_family_error_returns_null");
{
// Creating a column family with a name that already exists should fail
// and return NULL. Without the fix, the handle is leaked and a non-NULL
// pointer with an indeterminate rep field is returned.
char* cf_err = NULL;
rocksdb_column_family_handle_t* cf_handle =
rocksdb_create_column_family(db, options, "default", &cf_err);
// Should have an error since "default" already exists
CheckCondition(cf_err != NULL);
// The handle should be NULL on error (this is the bug fix)
CheckCondition(cf_handle == NULL);
free(cf_err);
}
StartPhase("cancel_all_background_work");
rocksdb_cancel_all_background_work(db, 1);
+2 -12
View File
@@ -12,7 +12,6 @@ namespace ROCKSDB_NAMESPACE {
void CoalescingIterator::Coalesce(
const autovector<MultiCfIteratorInfo>& items) {
assert(wide_columns_.empty());
assert(owned_columns_.empty());
MinHeap heap;
for (const auto& item : items) {
assert(item.iterator);
@@ -23,22 +22,13 @@ void CoalescingIterator::Coalesce(
if (heap.empty()) {
return;
}
// Own the coalesced bytes so the result stays valid even if a child iterator
// refreshes or reuses its internal buffers.
owned_columns_.reserve(heap.size());
wide_columns_.reserve(heap.size());
auto add_column = [this](const WideColumn& column) {
owned_columns_.emplace_back(column.name().ToString(),
column.value().ToString());
const auto& owned = owned_columns_.back();
wide_columns_.emplace_back(owned.first, owned.second);
};
auto current = heap.top();
heap.pop();
while (!heap.empty()) {
int comparison = current.column->name().compare(heap.top().column->name());
if (comparison < 0) {
add_column(*current.column);
wide_columns_.push_back(*current.column);
} else if (comparison > 0) {
// Shouldn't reach here.
// Current item in the heap is greater than the top item in the min heap
@@ -47,7 +37,7 @@ void CoalescingIterator::Coalesce(
current = heap.top();
heap.pop();
}
add_column(*current.column);
wide_columns_.push_back(*current.column);
if (WideColumnsHelper::HasDefaultColumn(wide_columns_)) {
value_ = WideColumnsHelper::GetDefaultColumn(wide_columns_);
-6
View File
@@ -5,10 +5,6 @@
#pragma once
#include <string>
#include <utility>
#include <vector>
#include "db/multi_cf_iterator_impl.h"
namespace ROCKSDB_NAMESPACE {
@@ -49,7 +45,6 @@ class CoalescingIterator : public Iterator {
void Reset() {
value_.clear();
wide_columns_.clear();
owned_columns_.clear();
}
bool PrepareValue() override { return impl_.PrepareValue(); }
@@ -84,7 +79,6 @@ class CoalescingIterator : public Iterator {
MultiCfIteratorImpl<ResetFunc, PopulateFunc> impl_;
Slice value_;
WideColumns wide_columns_;
std::vector<std::pair<std::string, std::string>> owned_columns_;
struct WideColumnWithOrder {
const WideColumn* column;
+14 -85
View File
@@ -11,14 +11,12 @@
#include <algorithm>
#include <cinttypes>
#include <cmath>
#include <limits>
#include <sstream>
#include <string>
#include <vector>
#include "db/blob/blob_file_cache.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_source.h"
#include "db/compaction/compaction_picker.h"
#include "db/compaction/compaction_picker_fifo.h"
@@ -497,15 +495,6 @@ ColumnFamilyOptions SanitizeCfOptions(const ImmutableDBOptions& db_options,
result.memtable_avg_op_scan_flush_trigger = 0;
}
}
if (result.enable_blob_direct_write && !result.enable_blob_files) {
ROCKS_LOG_WARN(db_options.info_log.get(),
"enable_blob_direct_write requires enable_blob_files=true. "
"Disabling blob direct write.");
result.enable_blob_direct_write = false;
}
if (result.blob_direct_write_partitions == 0) {
result.blob_direct_write_partitions = 1;
}
return result;
}
@@ -612,7 +601,7 @@ ColumnFamilyData::ColumnFamilyData(
const FileOptions* file_options, ColumnFamilySet* column_family_set,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
const std::string& db_session_id, bool read_only, bool fast_sst_open)
const std::string& db_session_id, bool read_only)
: id_(id),
name_(name),
dummy_versions_(_dummy_versions),
@@ -672,7 +661,7 @@ ColumnFamilyData::ColumnFamilyData(
new InternalStats(ioptions_.num_levels, ioptions_.clock, this));
table_cache_.reset(new TableCache(ioptions_, file_options, _table_cache,
block_cache_tracer, io_tracer,
db_session_id, fast_sst_open));
db_session_id));
blob_file_cache_.reset(
new BlobFileCache(_table_cache, &ioptions(), soptions(), id_,
internal_stats_->GetBlobFileReadHist(), io_tracer));
@@ -793,11 +782,6 @@ ColumnFamilyData::~ColumnFamilyData() {
}
}
void ColumnFamilyData::SetBlobPartitionManager(
std::shared_ptr<BlobFilePartitionManager> mgr) {
blob_partition_manager_ = std::move(mgr);
}
bool ColumnFamilyData::UnrefAndTryDelete() {
int old_refs = refs_.fetch_sub(1);
assert(old_refs > 0);
@@ -1539,32 +1523,6 @@ Status ColumnFamilyData::ValidateOptions(
const auto* ucmp = cf_options.comparator;
assert(ucmp);
if (cf_options.enable_blob_direct_write) {
if (db_options.enable_pipelined_write) {
return Status::NotSupported(
"Blob direct write v1 does not support pipelined writes.");
}
if (db_options.allow_concurrent_memtable_write) {
return Status::NotSupported(
"Blob direct write v1 does not support concurrent memtable writes.");
}
if (db_options.unordered_write) {
return Status::NotSupported(
"Blob direct write v1 does not support unordered writes.");
}
if (db_options.two_write_queues) {
return Status::NotSupported(
"Blob direct write v1 does not support two write queues.");
}
if (cf_options.experimental_mempurge_threshold > 0.0) {
return Status::NotSupported(
"Blob direct write does not support MemPurge.");
}
if (ucmp->timestamp_size() > 0) {
return Status::NotSupported(
"Blob direct write does not support user-defined timestamps.");
}
}
if (ucmp->timestamp_size() > 0 &&
!cf_options.persist_user_defined_timestamps) {
if (db_options.atomic_flush) {
@@ -1605,42 +1563,12 @@ Status ColumnFamilyData::ValidateOptions(
}
}
if (cf_options.read_triggered_compaction_threshold < 0.0 ||
std::isnan(cf_options.read_triggered_compaction_threshold) ||
std::isinf(cf_options.read_triggered_compaction_threshold)) {
return Status::InvalidArgument(
"read_triggered_compaction_threshold must be >= 0.0 and finite. "
"Use 0.0 to disable.");
}
if (cf_options.compaction_style == kCompactionStyleFIFO &&
db_options.max_open_files != -1 && cf_options.ttl > 0) {
return Status::NotSupported(
"FIFO compaction only supported with max_open_files = -1.");
}
if (db_options.open_files_async) {
// FIFO TTL picker relies on reading the files table properties inside a
// DB mutex. This can be slow if files are opened asynchronously, so we
// disable it.
//
// TODO: consider blocking fifo compaction until async file open task
// completes.
if (cf_options.compaction_style == kCompactionStyleFIFO) {
return Status::NotSupported(
"FIFO compaction is not supported with open_files_async = true.");
}
// Open files async is not useful if skip_stats_update_on_db_open=true
// because DB open will still block on IO.
//
// TODO: consider moving stats update inside async file open background
// task.
if (!db_options.skip_stats_update_on_db_open) {
return Status::InvalidArgument(
"open_files_async requires skip_stats_update_on_db_open = true.");
}
}
std::vector<uint32_t> supported{0, 1, 2, 4, 8};
if (std::find(supported.begin(), supported.end(),
cf_options.memtable_protection_bytes_per_key) ==
@@ -1809,14 +1737,16 @@ void ColumnFamilyData::RecoverEpochNumbers() {
vstorage->RecoverEpochNumbers(this);
}
ColumnFamilySet::ColumnFamilySet(
const std::string& dbname, const ImmutableDBOptions* db_options,
const FileOptions& file_options, Cache* table_cache,
WriteBufferManager* _write_buffer_manager,
WriteController* _write_controller,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
const std::string& db_session_id, bool fast_sst_open)
ColumnFamilySet::ColumnFamilySet(const std::string& dbname,
const ImmutableDBOptions* db_options,
const FileOptions& file_options,
Cache* table_cache,
WriteBufferManager* _write_buffer_manager,
WriteController* _write_controller,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer,
const std::string& db_id,
const std::string& db_session_id)
: max_column_family_(0),
file_options_(file_options),
dummy_cfd_(new ColumnFamilyData(
@@ -1833,8 +1763,7 @@ ColumnFamilySet::ColumnFamilySet(
block_cache_tracer_(block_cache_tracer),
io_tracer_(io_tracer),
db_id_(db_id),
db_session_id_(db_session_id),
fast_sst_open_(fast_sst_open) {
db_session_id_(db_session_id) {
// initialize linked list
dummy_cfd_->prev_ = dummy_cfd_;
dummy_cfd_->next_ = dummy_cfd_;
@@ -1901,7 +1830,7 @@ ColumnFamilyData* ColumnFamilySet::CreateColumnFamily(
ColumnFamilyData* new_cfd = new ColumnFamilyData(
id, name, dummy_versions, table_cache_, write_buffer_manager_, options,
*db_options_, &file_options_, this, block_cache_tracer_, io_tracer_,
db_id_, db_session_id_, read_only, fast_sst_open_);
db_id_, db_session_id_, read_only);
column_families_.insert({name, id});
column_family_data_.insert({id, new_cfd});
auto ucmp = new_cfd->user_comparator();
+9 -41
View File
@@ -49,7 +49,6 @@ class InstrumentedMutex;
class InstrumentedMutexLock;
struct SuperVersionContext;
class BlobFileCache;
class BlobFilePartitionManager;
class BlobSource;
extern const double kIncSlowdownRatio;
@@ -416,19 +415,6 @@ class ColumnFamilyData {
TableCache* table_cache() const { return table_cache_.get(); }
BlobFileCache* blob_file_cache() const { return blob_file_cache_.get(); }
BlobSource* blob_source() const { return blob_source_.get(); }
// Returns the write-path blob partition manager for this CF, or null if BDW
// is disabled.
BlobFilePartitionManager* blob_partition_manager() const {
return blob_partition_manager_.get();
}
// Returns a shared ownership handle for cold paths that must keep the
// manager alive beyond the lifetime of this ColumnFamilyData.
std::shared_ptr<BlobFilePartitionManager> blob_partition_manager_handle()
const {
return blob_partition_manager_;
}
// Installs the write-path blob partition manager for this CF.
void SetBlobPartitionManager(std::shared_ptr<BlobFilePartitionManager> mgr);
// See documentation in compaction_picker.h
// REQUIRES: DB mutex held
@@ -619,23 +605,16 @@ class ColumnFamilyData {
return ioptions_.cf_allow_ingest_behind || ioptions_.allow_ingest_behind;
}
// Per-CF reader-writer lock that serializes IngestExternalFiles with range
// tombstone conversion.
port::RWMutex& GetIngestSstLock() { return ingest_sst_lock_; }
private:
friend class ColumnFamilySet;
ColumnFamilyData(uint32_t id, const std::string& name,
Version* dummy_versions, Cache* table_cache,
WriteBufferManager* write_buffer_manager,
const ColumnFamilyOptions& options,
const ImmutableDBOptions& db_options,
const FileOptions* file_options,
ColumnFamilySet* column_family_set,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer,
const std::string& db_id, const std::string& db_session_id,
bool read_only, bool fast_sst_open = false);
ColumnFamilyData(
uint32_t id, const std::string& name, Version* dummy_versions,
Cache* table_cache, WriteBufferManager* write_buffer_manager,
const ColumnFamilyOptions& options, const ImmutableDBOptions& db_options,
const FileOptions* file_options, ColumnFamilySet* column_family_set,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
const std::string& db_session_id, bool read_only);
std::vector<std::string> GetDbPaths() const;
@@ -669,8 +648,6 @@ class ColumnFamilyData {
std::unique_ptr<TableCache> table_cache_;
std::unique_ptr<BlobFileCache> blob_file_cache_;
std::unique_ptr<BlobSource> blob_source_;
// Per-CF manager for write-path blob direct-write files.
std::shared_ptr<BlobFilePartitionManager> blob_partition_manager_;
std::unique_ptr<InternalStats> internal_stats_;
@@ -736,10 +713,6 @@ class ColumnFamilyData {
bool mempurge_used_;
std::atomic<uint64_t> next_epoch_number_;
// Used to synchronize IngestExternalFile with range tombstone conversion. See
// also Memtable::ingest_seqno_barrier_.
port::RWMutex ingest_sst_lock_;
};
// ColumnFamilySet has interesting thread-safety requirements
@@ -785,8 +758,7 @@ class ColumnFamilySet {
WriteController* _write_controller,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer,
const std::string& db_id, const std::string& db_session_id,
bool fast_sst_open = false);
const std::string& db_id, const std::string& db_session_id);
~ColumnFamilySet();
ColumnFamilyData* GetDefault() const;
@@ -822,9 +794,6 @@ class ColumnFamilySet {
Cache* get_table_cache() { return table_cache_; }
bool GetFastSstOpen() const { return fast_sst_open_; }
void SetFastSstOpen(bool v) { fast_sst_open_ = v; }
WriteBufferManager* write_buffer_manager() { return write_buffer_manager_; }
WriteController* write_controller() { return write_controller_; }
@@ -873,7 +842,6 @@ class ColumnFamilySet {
std::shared_ptr<IOTracer> io_tracer_;
const std::string& db_id_;
std::string db_session_id_;
bool fast_sst_open_;
};
// A wrapper for ColumnFamilySet that supports releasing DB mutex during each
+34 -664
View File
@@ -7,14 +7,12 @@
#include <iterator>
#include <limits>
#include <memory>
#include "db/blob/blob_fetcher.h"
#include "db/blob/blob_file_builder.h"
#include "db/blob/blob_index.h"
#include "db/blob/prefetch_buffer_collection.h"
#include "db/snapshot_checker.h"
#include "db/wide/blob_column_resolver_util.h"
#include "db/wide/wide_column_serialization.h"
#include "db/wide/wide_columns_helper.h"
#include "logging/logging.h"
@@ -22,106 +20,8 @@
#include "rocksdb/listener.h"
#include "table/internal_iterator.h"
#include "test_util/sync_point.h"
#include "util/autovector.h"
namespace ROCKSDB_NAMESPACE {
// CompactionBlobResolver implementation
void CompactionBlobResolver::Init(BlobFetcher* blob_fetcher,
PrefetchBufferCollection* prefetch_buffers,
CompactionIterationStats* iter_stats) {
blob_fetcher_ = blob_fetcher;
prefetch_buffers_ = prefetch_buffers;
iter_stats_ = iter_stats;
}
void CompactionBlobResolver::Reset(
const Slice& user_key, const std::vector<WideColumn>* columns,
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns,
bool track_resolve_error) {
user_key_ = user_key;
columns_ = columns;
blob_columns_ = blob_columns;
track_resolve_error_ = track_resolve_error;
resolved_cache_.clear();
resolve_error_.reset();
}
Status CompactionBlobResolver::ResolveColumn(size_t column_index,
Slice* resolved_value) {
Status status = Status::OK();
if (columns_ == nullptr || column_index >= columns_->size()) {
status = Status::InvalidArgument("Column index out of bounds");
} else {
const BlobIndex* blob_index_ptr =
blob_resolver_util::FindBlobColumn(blob_columns_, column_index);
if (blob_index_ptr == nullptr) {
// Not a blob column - return the inline value directly
*resolved_value = (*columns_)[column_index].value();
} else {
// Check if we've already resolved this column
PinnableSlice* cached =
blob_resolver_util::FindInCache(resolved_cache_, column_index);
if (cached != nullptr) {
*resolved_value = Slice(*cached);
} else if (blob_fetcher_ == nullptr) {
status = Status::NotSupported("Blob fetcher not available");
} else {
const BlobIndex& blob_index = *blob_index_ptr;
// Handle inlined blobs (e.g., from legacy stacked BlobDB with TTL)
if (blob_index.IsInlined()) {
*resolved_value = blob_resolver_util::CacheInlinedBlob(
resolved_cache_, column_index, blob_index);
} else {
FilePrefetchBuffer* prefetch_buffer =
prefetch_buffers_ ? prefetch_buffers_->GetOrCreatePrefetchBuffer(
blob_index.file_number())
: nullptr;
uint64_t bytes_read = 0;
resolved_cache_.emplace_back(column_index,
std::make_unique<PinnableSlice>());
auto& new_entry = resolved_cache_.back();
status =
blob_fetcher_->FetchBlob(user_key_, blob_index, prefetch_buffer,
new_entry.second.get(), &bytes_read);
if (!status.ok()) {
resolved_cache_.pop_back();
} else {
if (iter_stats_ != nullptr) {
++iter_stats_->num_blobs_read;
iter_stats_->total_blob_bytes_read += bytes_read;
}
*resolved_value = Slice(*new_entry.second);
}
}
}
}
}
if (!status.ok() && track_resolve_error_ && !resolve_error_.has_value()) {
resolve_error_.emplace(status);
}
return status;
}
bool CompactionBlobResolver::IsBlobColumn(size_t column_index) const {
if (columns_ == nullptr || column_index >= columns_->size()) {
return false;
}
return blob_resolver_util::IsBlobColumnIndex(blob_columns_, column_index);
}
size_t CompactionBlobResolver::NumColumns() const {
if (columns_ == nullptr) {
return 0;
}
return columns_->size();
}
CompactionIterator::CompactionIterator(
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
SequenceNumber last_sequence, std::vector<SequenceNumber>* snapshots,
@@ -138,8 +38,7 @@ CompactionIterator::CompactionIterator(
const std::atomic<bool>* shutting_down,
const std::shared_ptr<Logger> info_log,
const std::string* full_history_ts_low,
std::optional<SequenceNumber> preserve_seqno_min,
const Version* input_version, Env::IOActivity blob_read_io_activity)
std::optional<SequenceNumber> preserve_seqno_min)
: CompactionIterator(
input, cmp, merge_helper, last_sequence, snapshots, earliest_snapshot,
earliest_write_conflict_snapshot, job_snapshot, snapshot_checker, env,
@@ -148,8 +47,7 @@ CompactionIterator::CompactionIterator(
manual_compaction_canceled,
compaction ? std::make_unique<RealCompaction>(compaction) : nullptr,
must_count_input_entries, compaction_filter, shutting_down, info_log,
full_history_ts_low, preserve_seqno_min, input_version,
blob_read_io_activity) {}
full_history_ts_low, preserve_seqno_min) {}
CompactionIterator::CompactionIterator(
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
@@ -167,8 +65,7 @@ CompactionIterator::CompactionIterator(
const std::atomic<bool>* shutting_down,
const std::shared_ptr<Logger> info_log,
const std::string* full_history_ts_low,
std::optional<SequenceNumber> preserve_seqno_min,
const Version* input_version, Env::IOActivity blob_read_io_activity)
std::optional<SequenceNumber> preserve_seqno_min)
: input_(input, cmp, must_count_input_entries),
cmp_(cmp),
merge_helper_(merge_helper),
@@ -183,8 +80,6 @@ CompactionIterator::CompactionIterator(
blob_file_builder_(blob_file_builder),
compaction_(std::move(compaction)),
compaction_filter_(compaction_filter),
filter_supports_v4_(
compaction_filter_ ? compaction_filter_->SupportsFilterV4() : false),
shutting_down_(shutting_down),
manual_compaction_canceled_(manual_compaction_canceled),
bottommost_level_(compaction_ && compaction_->bottommost_level() &&
@@ -203,8 +98,7 @@ CompactionIterator::CompactionIterator(
merge_out_iter_(merge_helper_),
blob_garbage_collection_cutoff_file_number_(
ComputeBlobGarbageCollectionCutoffFileNumber(compaction_.get())),
blob_fetcher_(CreateBlobFetcherIfNeeded(compaction_.get(), input_version,
blob_read_io_activity)),
blob_fetcher_(CreateBlobFetcherIfNeeded(compaction_.get())),
prefetch_buffers_(
CreatePrefetchBufferCollectionIfNeeded(compaction_.get())),
current_key_committed_(false),
@@ -226,8 +120,6 @@ CompactionIterator::CompactionIterator(
timestamp_size_ == full_history_ts_low_->size());
#endif
input_.SetPinnedItersMgr(&pinned_iters_mgr_);
blob_resolver_.Init(blob_fetcher_.get(), prefetch_buffers_.get(),
&iter_stats_);
// The default `merge_until_status_` does not need to be checked since it is
// overwritten as soon as `MergeUntil()` is called
merge_until_status_.PermitUncheckedError();
@@ -239,24 +131,6 @@ CompactionIterator::~CompactionIterator() {
input_.SetPinnedItersMgr(nullptr);
}
void CompactionIterator::SetBlobFetcher(const Version* version,
BlobFileCache* blob_file_cache,
Env::IOActivity io_activity,
bool allow_write_path_fallback) {
if (blob_fetcher_ != nullptr || version == nullptr) {
return;
}
ReadOptions read_options;
read_options.io_activity = io_activity;
read_options.fill_cache = false;
blob_fetcher_ = std::make_unique<BlobFetcher>(
version, read_options, blob_file_cache, allow_write_path_fallback);
blob_resolver_.Init(blob_fetcher_.get(), prefetch_buffers_.get(),
&iter_stats_);
}
void CompactionIterator::ResetRecordCounts() {
iter_stats_.num_record_drop_user = 0;
iter_stats_.num_record_drop_hidden = 0;
@@ -384,10 +258,9 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
compaction_filter_skip_until_.rep());
if (decision == CompactionFilter::Decision::kUndetermined &&
!compaction_filter_->IsStackedBlobDbInternalCompactionFilter()) {
if (!blob_fetcher_) {
status_ = Status::NotSupported(
"Blob-backed value filtering requires blob read support in the "
"current table-file creation context");
if (!compaction_) {
status_ =
Status::Corruption("Unexpected blob index outside of compaction");
validity_info_.Invalidate();
return false;
}
@@ -414,6 +287,8 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
uint64_t bytes_read = 0;
assert(blob_fetcher_);
s = blob_fetcher_->FetchBlob(ikey_.user_key, blob_index,
prefetch_buffer, &blob_value_,
&bytes_read);
@@ -434,19 +309,7 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
const Slice* existing_val = nullptr;
const WideColumns* existing_col = nullptr;
// Reuse member variable to avoid per-key heap allocation.
filter_existing_columns_.clear();
WideColumnBlobResolver* blob_resolver_ptr = nullptr;
// Use member variables for entity column storage to avoid per-key
// heap allocations. Cleared at start of each use.
entity_columns_.clear();
entity_blob_columns_.clear();
// Storage for eagerly resolved blob values used by the FilterV3
// compatibility path. Keep this in the same scope as the FilterV4()
// invocation below because filter_existing_columns_ stores Slices into
// these buffers.
autovector<PinnableSlice, 4> resolved_blobs_storage;
WideColumns existing_columns;
if (ikey_.type != kTypeWideColumnEntity) {
if (!blob_value_.empty()) {
@@ -455,125 +318,23 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
existing_val = &value_;
}
} else {
// Check if entity has blob columns that need resolution
bool has_blob_columns = false;
{
Status s_hbc =
WideColumnSerialization::HasBlobColumns(value_, has_blob_columns);
if (!s_hbc.ok()) {
status_ = s_hbc;
validity_info_.Invalidate();
return false;
}
}
if (UNLIKELY(has_blob_columns)) {
// Entity has blob columns. DeserializeV2() populates
// entity_columns_ with the full column set; blob-backed entries still
// carry the serialized BlobIndex bytes in value(). The companion
// entity_blob_columns_ side list identifies which column indexes are
// blob references and provides the decoded BlobIndex objects.
Slice input_copy = value_;
Status s = WideColumnSerialization::DeserializeV2(
input_copy, entity_columns_, entity_blob_columns_);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return false;
}
// Mark as deserialized so PrepareOutput can skip re-deserialization
// if the filter returns kKeep.
entity_deserialized_ = true;
Slice value_copy = value_;
const Status s =
WideColumnSerialization::Deserialize(value_copy, existing_columns);
// Build filter_existing_columns_ with raw values (blob columns
// have blob index as value, not the actual blob data)
for (size_t i = 0; i < entity_columns_.size(); ++i) {
filter_existing_columns_.emplace_back(entity_columns_[i].name(),
entity_columns_[i].value());
}
if (filter_supports_v4_) {
// Reset member blob resolver for this entity - filter can call
// resolver->ResolveColumn() to fetch blob values on-demand
blob_resolver_.Reset(ikey_.user_key, &entity_columns_,
&entity_blob_columns_,
/*track_resolve_error=*/true);
blob_resolver_ptr = &blob_resolver_;
} else {
// FilterV3 compatibility: eagerly resolve all blob columns so
// the filter sees actual values (not raw BlobIndex bytes).
// resolved_blobs_storage is in the outer scope so the data
// outlives filter_existing_columns_ (which holds Slices into
// it).
resolved_blobs_storage.resize(entity_blob_columns_.size());
for (size_t bi = 0; bi < entity_blob_columns_.size(); ++bi) {
const size_t col_idx = entity_blob_columns_[bi].first;
const BlobIndex& blob_idx = entity_blob_columns_[bi].second;
if (blob_idx.IsInlined()) {
resolved_blobs_storage[bi].PinSelf(blob_idx.value());
} else {
FilePrefetchBuffer* prefetch_buffer =
prefetch_buffers_
? prefetch_buffers_->GetOrCreatePrefetchBuffer(
blob_idx.file_number())
: nullptr;
uint64_t bytes_read = 0;
assert(blob_fetcher_);
Status s_fetch = blob_fetcher_->FetchBlob(
ikey_.user_key, blob_idx, prefetch_buffer,
&resolved_blobs_storage[bi], &bytes_read);
if (!s_fetch.ok()) {
status_ = s_fetch;
validity_info_.Invalidate();
return false;
}
++iter_stats_.num_blobs_read;
iter_stats_.total_blob_bytes_read += bytes_read;
}
// Replace the blob index value in filter_existing_columns_
// with the resolved blob value
filter_existing_columns_[col_idx] =
WideColumn(entity_columns_[col_idx].name(),
Slice(resolved_blobs_storage[bi]));
}
}
} else {
// No blob columns, use fast path
Slice value_copy = value_;
const Status s = WideColumnSerialization::Deserialize(
value_copy, filter_existing_columns_);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return false;
}
}
existing_col = &filter_existing_columns_;
}
// existing_val / existing_col point into value_, blob_value_,
// entity_columns_, and resolved_blobs_storage, all of which stay alive
// until this call returns.
decision = compaction_filter_->FilterV4(
level_, filter_key, value_type, existing_val, existing_col,
&compaction_filter_value_, &new_columns,
compaction_filter_skip_until_.rep(), blob_resolver_ptr);
if (blob_resolver_ptr != nullptr) {
Status resolve_status = blob_resolver_.resolve_status();
if (!resolve_status.ok()) {
// Keep lazy FilterV4 failure semantics aligned with the eager
// FilterV3 compatibility path: if blob resolution fails while the
// filter is inspecting the entry, fail compaction even if the
// filter returned kKeep after noticing the error.
status_ = std::move(resolve_status);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return false;
}
existing_col = &existing_columns;
}
decision = compaction_filter_->FilterV3(
level_, filter_key, value_type, existing_val, existing_col,
&compaction_filter_value_, &new_columns,
compaction_filter_skip_until_.rep());
}
iter_stats_.total_filter_time +=
@@ -581,10 +342,10 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
}
if (decision == CompactionFilter::Decision::kUndetermined) {
// Should not reach here, since FilterV2/V3/V4 should never return
// Should not reach here, since FilterV2/FilterV3 should never return
// kUndetermined.
status_ = Status::NotSupported(
"FilterV2/V3/V4 should never return kUndetermined");
"FilterV2/FilterV3 should never return kUndetermined");
validity_info_.Invalidate();
return false;
}
@@ -593,7 +354,7 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
cmp_->Compare(*compaction_filter_skip_until_.rep(), ikey_.user_key) <=
0) {
// Can't skip to a key smaller than the current one.
// Keep the key as per FilterV2/V3/V4 documentation.
// Keep the key as per FilterV2/FilterV3 documentation.
decision = CompactionFilter::Decision::kKeep;
}
@@ -681,16 +442,6 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
}
value_ = compaction_filter_value_;
// Value changed, force re-deserialization in PrepareOutput
entity_deserialized_ = false;
}
if (UNLIKELY(compaction_filter_value_.size() >
std::numeric_limits<uint32_t>::max())) {
status_ = Status::Corruption(
"CompactionFilter result value size exceeds 4GB limit");
validity_info_.Invalidate();
return false;
}
return true;
@@ -699,14 +450,9 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
void CompactionIterator::NextFromInput() {
at_next_ = false;
validity_info_.Invalidate();
entity_deserialized_ = false;
while (!Valid() && input_.Valid() && !IsPausingManualCompaction() &&
!IsShuttingDown()) {
// A filtered-out key can advance directly to the next input record without
// returning to PrepareOutput(), so clear the per-record deserialization
// cache at the start of each loop iteration.
entity_deserialized_ = false;
key_ = input_.key();
value_ = input_.value();
blob_value_.Reset();
@@ -1520,340 +1266,6 @@ void CompactionIterator::GarbageCollectBlobIfNeeded() {
}
}
// Wide-column entity compaction flow after InvokeFilterIfNeeded():
// 1. PrepareOutput() is the only caller of the helper block below.
// 2. PrepareOutput() ensures entity_columns_/entity_blob_columns_ describe
// the current entity, either by reusing the filter-time deserialization or
// by deserializing once itself.
// 3. PrepareOutput() then picks exactly one follow-up path:
// - inline-only entity:
// ExtractLargeColumnValuesIfNeeded()
// This is the flush/compaction extraction path. It may rewrite large
// inline columns into blob references.
// - entity already containing blob references:
// GarbageCollectEntityBlobsIfNeeded()
// This is the blob-GC path. It only fetches / relocates references
// that fall below the current GC cutoff.
// Both helpers leave value_ unchanged when no rewrite is needed.
void CompactionIterator::ExtractLargeColumnValuesIfNeeded() {
assert(ikey_.type == kTypeWideColumnEntity);
// Check if blob extraction is enabled
if (!blob_file_builder_) {
return;
}
// Deserialize using member variables (already populated by PrepareOutput
// for the combined deserialization path). If not yet populated, deserialize
// here.
if (entity_columns_.empty()) {
Slice entity_slice = value_;
entity_columns_.clear();
entity_blob_columns_.clear();
Status s = WideColumnSerialization::DeserializeV2(
entity_slice, entity_columns_, entity_blob_columns_);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return;
}
}
// IMPORTANT: If there are already blob columns, we skip extraction entirely.
// This means new large inline columns added to such entities (e.g., via
// PutEntity after a previous PutEntity that already had blob columns) will
// NOT be extracted to blob files until the entity undergoes a full rewrite
// (e.g., via compaction that GCs all existing blob refs).
//
// This is a deliberate simplification: supporting partial extraction
// (extracting only new large inline columns while preserving existing
// blob refs) would require merging old and new blob column sets during
// serialization, and the benefit is marginal since full rewrites happen
// naturally during blob GC.
if (UNLIKELY(!entity_blob_columns_.empty())) {
return;
}
// Try to extract large column values to blob files
// Track which columns were extracted and their blob indices
std::vector<std::pair<size_t, BlobIndex>> new_blob_columns;
std::string temp_blob_index;
for (size_t i = 0; i < entity_columns_.size(); ++i) {
const Slice& col_value = entity_columns_[i].value();
// Clear the temporary buffer for each column
temp_blob_index.clear();
// Try to add the column value to blob file
// blob_file_builder_->Add() will check min_blob_size internally
// and only write to blob file if the value is large enough
const Status add_status =
blob_file_builder_->Add(user_key(), col_value, &temp_blob_index);
if (!add_status.ok()) {
status_ = add_status;
validity_info_.Invalidate();
return;
}
// If blob_index is not empty, the value was extracted to a blob file
if (!temp_blob_index.empty()) {
BlobIndex blob_idx;
const Status decode_status = blob_idx.DecodeFrom(temp_blob_index);
if (!decode_status.ok()) {
status_ = decode_status;
validity_info_.Invalidate();
return;
}
new_blob_columns.emplace_back(i, blob_idx);
}
}
// If no columns were extracted, nothing to do
if (new_blob_columns.empty()) {
return;
}
// Re-serialize the entity with blob indices using the Slice-based overload
// to avoid copying column names and values to strings.
entity_wide_columns_.clear();
entity_wide_columns_.reserve(entity_columns_.size());
for (const auto& col : entity_columns_) {
entity_wide_columns_.emplace_back(col.name(), col.value());
}
rewritten_entity_.clear();
const Status serialize_status = WideColumnSerialization::SerializeV2(
entity_wide_columns_, new_blob_columns, rewritten_entity_);
if (!serialize_status.ok()) {
status_ = serialize_status;
validity_info_.Invalidate();
return;
}
// Update value_ to point to the rewritten entity
value_ = rewritten_entity_;
}
bool CompactionIterator::FetchBlobsNeedingGC(
const std::vector<WideColumn>& /*columns*/,
const std::vector<std::pair<size_t, BlobIndex>>& blob_columns,
std::vector<std::pair<size_t, PinnableSlice>>* fetched_blob_values) {
assert(fetched_blob_values != nullptr);
for (const auto& blob_col : blob_columns) {
const size_t col_idx = blob_col.first;
const BlobIndex& blob_index = blob_col.second;
// Skip inlined blobs - they don't need GC
if (blob_index.IsInlined()) {
continue;
}
// Check if this blob file needs garbage collection
if (blob_index.file_number() >=
blob_garbage_collection_cutoff_file_number_) {
continue;
}
// This blob needs to be relocated - fetch its value
FilePrefetchBuffer* prefetch_buffer =
prefetch_buffers_ ? prefetch_buffers_->GetOrCreatePrefetchBuffer(
blob_index.file_number())
: nullptr;
uint64_t bytes_read = 0;
assert(blob_fetcher_);
PinnableSlice blob_value;
Status s = blob_fetcher_->FetchBlob(user_key(), blob_index, prefetch_buffer,
&blob_value, &bytes_read);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return false;
}
++iter_stats_.num_blobs_read;
iter_stats_.total_blob_bytes_read += bytes_read;
fetched_blob_values->emplace_back(col_idx, std::move(blob_value));
}
return !fetched_blob_values->empty();
}
std::vector<std::pair<size_t, BlobIndex>>
CompactionIterator::RelocateBlobValues(
const std::vector<std::pair<size_t, PinnableSlice>>& fetched_blob_values) {
std::vector<std::pair<size_t, BlobIndex>> new_blob_columns;
if (blob_file_builder_) {
for (const auto& fv : fetched_blob_values) {
const size_t col_idx = fv.first;
const Slice col_value(fv.second);
std::string temp_blob_index;
Status status =
blob_file_builder_->Add(user_key(), col_value, &temp_blob_index);
if (!status.ok()) {
status_ = status;
validity_info_.Invalidate();
new_blob_columns.clear();
break;
}
if (!temp_blob_index.empty()) {
BlobIndex blob_idx;
status = blob_idx.DecodeFrom(temp_blob_index);
if (!status.ok()) {
status_ = status;
validity_info_.Invalidate();
new_blob_columns.clear();
break;
}
new_blob_columns.emplace_back(col_idx, blob_idx);
// Track relocation stats here (after successful relocation)
++iter_stats_.num_blobs_relocated;
iter_stats_.total_blob_bytes_relocated += col_value.size();
}
// If temp_blob_index is empty, the value will be inlined
}
}
return new_blob_columns;
}
void CompactionIterator::SerializeEntityAfterGC(
const std::vector<WideColumn>& columns,
const std::vector<std::pair<size_t, BlobIndex>>& original_blob_columns,
const std::vector<std::pair<size_t, PinnableSlice>>& fetched_blob_values,
const std::vector<std::pair<size_t, BlobIndex>>& new_blob_columns) {
// Collect all blob columns for final serialization: include non-GC'd
// originals and newly relocated. Use linear scans since these
// collections are typically small (<5 elements).
std::vector<std::pair<size_t, BlobIndex>> final_blob_columns;
for (const auto& blob_col : original_blob_columns) {
// Check if this column was fetched (GC'd) via linear scan
bool was_fetched = false;
for (const auto& fv : fetched_blob_values) {
if (fv.first == blob_col.first) {
was_fetched = true;
break;
}
}
if (!was_fetched) {
// This blob column wasn't GC'd - keep original
final_blob_columns.push_back(blob_col);
}
}
// Add newly relocated blobs
for (const auto& nb : new_blob_columns) {
final_blob_columns.push_back(nb);
}
// Build WideColumns for serialization, replacing fetched blob values
// with their resolved inline values.
WideColumns wide_columns;
wide_columns.reserve(columns.size());
for (size_t i = 0; i < columns.size(); ++i) {
// Linear scan to find if this column was fetched
const PinnableSlice* fetched_value = nullptr;
for (const auto& fv : fetched_blob_values) {
if (fv.first == i) {
fetched_value = &fv.second;
break;
}
}
if (fetched_value != nullptr) {
// This blob column was fetched for GC; use the resolved inline value.
wide_columns.emplace_back(columns[i].name(), Slice(*fetched_value));
} else {
// For inline columns, this is the actual value. For non-fetched blob
// columns, this contains the raw serialized blob index, which is fine
// because SerializeV2() re-encodes from the BlobIndex
// object in final_blob_columns, ignoring this value for blob-type
// columns.
wide_columns.emplace_back(columns[i].name(), columns[i].value());
}
}
// Serialize the entity
rewritten_entity_.clear();
Status s;
if (final_blob_columns.empty()) {
s = WideColumnSerialization::Serialize(wide_columns, rewritten_entity_);
} else {
s = WideColumnSerialization::SerializeV2(wide_columns, final_blob_columns,
rewritten_entity_);
}
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return;
}
value_ = rewritten_entity_;
}
void CompactionIterator::GarbageCollectEntityBlobsIfNeeded() {
assert(ikey_.type == kTypeWideColumnEntity);
if (!compaction_ || !compaction_->enable_blob_garbage_collection()) {
return;
}
// This is the second branch in the flow above: the entity already has V2
// blob references, so compaction considers only relocating the subset of
// referenced blob files that are old enough for blob GC.
// Use member variables already populated by PrepareOutput's combined
// deserialization path. If not yet populated, deserialize here.
if (entity_columns_.empty()) {
Slice entity_slice = value_;
entity_columns_.clear();
entity_blob_columns_.clear();
Status s = WideColumnSerialization::DeserializeV2(
entity_slice, entity_columns_, entity_blob_columns_);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return;
}
}
// The caller (PrepareOutput) already verified blob columns exist.
assert(!entity_blob_columns_.empty());
if (entity_blob_columns_.empty()) {
return;
}
// Fetch blobs that need GC
std::vector<std::pair<size_t, PinnableSlice>> fetched_blob_values;
if (!FetchBlobsNeedingGC(entity_columns_, entity_blob_columns_,
&fetched_blob_values)) {
// Either no blobs needed GC (status_ is OK) or an error occurred
// (status_ is already set by FetchBlobsNeedingGC). In both cases,
// we return without modifying the entity.
return;
}
// Relocate fetched values to new blob files
std::vector<std::pair<size_t, BlobIndex>> new_blob_columns =
RelocateBlobValues(fetched_blob_values);
if (!status_.ok()) {
return; // Error occurred during relocation
}
// Serialize the final entity
SerializeEntityAfterGC(entity_columns_, entity_blob_columns_,
fetched_blob_values, new_blob_columns);
}
void CompactionIterator::PrepareOutput() {
if (Valid()) {
if (LIKELY(!is_range_del_)) {
@@ -1861,37 +1273,6 @@ void CompactionIterator::PrepareOutput() {
ExtractLargeValueIfNeeded();
} else if (ikey_.type == kTypeBlobIndex) {
GarbageCollectBlobIfNeeded();
} else if (ikey_.type == kTypeWideColumnEntity) {
// If entity was already deserialized (e.g., by InvokeFilterIfNeeded)
// and the filter returned kKeep, skip redundant re-deserialization.
if (!entity_deserialized_) {
TEST_SYNC_POINT(
"CompactionIterator::PrepareOutput:DeserializeEntity");
// Deserialize entity once into member variables, then decide between
// blob GC and extraction based on whether blob columns exist.
// This avoids the double parse of HasBlobColumns() + DeserializeV2().
entity_columns_.clear();
entity_blob_columns_.clear();
Slice entity_slice = value_;
{
Status s_deser = WideColumnSerialization::DeserializeV2(
entity_slice, entity_columns_, entity_blob_columns_);
if (!s_deser.ok()) {
status_ = s_deser;
validity_info_.Invalidate();
return;
}
}
} else {
TEST_SYNC_POINT(
"CompactionIterator::PrepareOutput:SkipDeserializeEntity");
}
entity_deserialized_ = false; // Reset for next iteration
if (UNLIKELY(!entity_blob_columns_.empty())) {
GarbageCollectEntityBlobsIfNeeded();
} else {
ExtractLargeColumnValuesIfNeeded();
}
}
}
@@ -2040,32 +1421,21 @@ uint64_t CompactionIterator::ComputeBlobGarbageCollectionCutoffFileNumber(
}
std::unique_ptr<BlobFetcher> CompactionIterator::CreateBlobFetcherIfNeeded(
const CompactionProxy* compaction, const Version* input_version,
Env::IOActivity blob_read_io_activity) {
const Version* const version =
compaction != nullptr ? compaction->input_version() : input_version;
if (version == nullptr) {
const CompactionProxy* compaction) {
if (!compaction) {
return nullptr;
}
const Version* const version = compaction->input_version();
if (!version) {
return nullptr;
}
ReadOptions read_options;
read_options.io_activity = compaction != nullptr
? Env::IOActivity::kCompaction
: blob_read_io_activity;
read_options.io_activity = Env::IOActivity::kCompaction;
read_options.fill_cache = false;
BlobFileCache* blob_file_cache = nullptr;
bool allow_write_path_fallback = false;
if (compaction == nullptr) {
allow_write_path_fallback = true;
auto* cfd = version->cfd();
if (cfd != nullptr) {
blob_file_cache = cfd->blob_file_cache();
}
}
return std::unique_ptr<BlobFetcher>(new BlobFetcher(
version, read_options, blob_file_cache, allow_write_path_fallback));
return std::unique_ptr<BlobFetcher>(new BlobFetcher(version, read_options));
}
std::unique_ptr<PrefetchBufferCollection>
+23 -158
View File
@@ -7,12 +7,10 @@
#include <algorithm>
#include <cinttypes>
#include <deque>
#include <memory>
#include <optional>
#include <string>
#include <unordered_set>
#include <vector>
#include "db/blob/blob_index.h"
#include "db/compaction/compaction.h"
#include "db/compaction/compaction_iteration_stats.h"
#include "db/merge_helper.h"
@@ -21,69 +19,12 @@
#include "db/snapshot_checker.h"
#include "options/cf_options.h"
#include "rocksdb/compaction_filter.h"
#include "rocksdb/slice.h"
namespace ROCKSDB_NAMESPACE {
class BlobFileBuilder;
class BlobFileCache;
class BlobFetcher;
class Version;
class PrefetchBufferCollection;
class FilePrefetchBuffer;
class Version;
// Internal implementation of WideColumnBlobResolver for compaction.
// This class enables lazy loading of blob column values during compaction
// filter processing.
class CompactionBlobResolver : public WideColumnBlobResolver {
public:
CompactionBlobResolver()
: columns_(nullptr),
blob_columns_(nullptr),
blob_fetcher_(nullptr),
prefetch_buffers_(nullptr),
iter_stats_(nullptr) {}
// Set the fixed context (blob fetcher, prefetch buffers, stats) once.
void Init(BlobFetcher* blob_fetcher,
PrefetchBufferCollection* prefetch_buffers,
CompactionIterationStats* iter_stats);
Status ResolveColumn(size_t column_index, Slice* resolved_value) override;
bool IsBlobColumn(size_t column_index) const override;
size_t NumColumns() const override;
Status resolve_status() const {
if (!resolve_error_.has_value()) {
return Status::OK();
}
return *resolve_error_;
}
// Reset the resolver for a new entity. Clears resolved cache.
void Reset(const Slice& user_key, const std::vector<WideColumn>* columns,
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns,
bool track_resolve_error = false);
private:
Slice user_key_;
const std::vector<WideColumn>* columns_;
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns_;
BlobFetcher* blob_fetcher_;
PrefetchBufferCollection* prefetch_buffers_;
CompactionIterationStats* iter_stats_;
bool track_resolve_error_ = false;
// Cache for resolved blob values to avoid re-fetching. Uses a vector of
// (column_index, PinnableSlice) pairs — typical entities have few blob
// columns (<5), making linear scan cheaper than hash map overhead.
std::vector<std::pair<size_t, std::unique_ptr<PinnableSlice>>>
resolved_cache_;
// Sticky resolver error for the current entity. This is enabled only for the
// lazy FilterV4 path so compaction can still fail even if the filter
// notices the error and returns kKeep.
std::optional<Status> resolve_error_;
};
// A wrapper of internal iterator whose purpose is to count how
// many entries there are in the iterator.
@@ -272,43 +213,34 @@ class CompactionIterator {
const std::atomic<bool>* shutting_down = nullptr,
const std::shared_ptr<Logger> info_log = nullptr,
const std::string* full_history_ts_low = nullptr,
std::optional<SequenceNumber> preserve_seqno_min = {},
const Version* input_version = nullptr,
Env::IOActivity blob_read_io_activity = Env::IOActivity::kCompaction);
std::optional<SequenceNumber> preserve_seqno_min = {});
// Constructor with custom CompactionProxy, used for tests.
CompactionIterator(
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
SequenceNumber last_sequence, std::vector<SequenceNumber>* snapshots,
SequenceNumber earliest_snapshot,
SequenceNumber earliest_write_conflict_snapshot,
SequenceNumber job_snapshot, const SnapshotChecker* snapshot_checker,
Env* env, bool report_detailed_time,
CompactionRangeDelAggregator* range_del_agg,
BlobFileBuilder* blob_file_builder, bool allow_data_in_errors,
bool enforce_single_del_contracts,
const std::atomic<bool>& manual_compaction_canceled,
std::unique_ptr<CompactionProxy> compaction,
bool must_count_input_entries,
const CompactionFilter* compaction_filter = nullptr,
const std::atomic<bool>* shutting_down = nullptr,
const std::shared_ptr<Logger> info_log = nullptr,
const std::string* full_history_ts_low = nullptr,
std::optional<SequenceNumber> preserve_seqno_min = {},
const Version* input_version = nullptr,
Env::IOActivity blob_read_io_activity = Env::IOActivity::kCompaction);
CompactionIterator(InternalIterator* input, const Comparator* cmp,
MergeHelper* merge_helper, SequenceNumber last_sequence,
std::vector<SequenceNumber>* snapshots,
SequenceNumber earliest_snapshot,
SequenceNumber earliest_write_conflict_snapshot,
SequenceNumber job_snapshot,
const SnapshotChecker* snapshot_checker, Env* env,
bool report_detailed_time,
CompactionRangeDelAggregator* range_del_agg,
BlobFileBuilder* blob_file_builder,
bool allow_data_in_errors,
bool enforce_single_del_contracts,
const std::atomic<bool>& manual_compaction_canceled,
std::unique_ptr<CompactionProxy> compaction,
bool must_count_input_entries,
const CompactionFilter* compaction_filter = nullptr,
const std::atomic<bool>* shutting_down = nullptr,
const std::shared_ptr<Logger> info_log = nullptr,
const std::string* full_history_ts_low = nullptr,
std::optional<SequenceNumber> preserve_seqno_min = {});
~CompactionIterator();
void ResetRecordCounts();
// Non-compaction table-file creation (e.g., flush/recovery) can still need
// blob resolution when direct-write wide entities already contain blob
// references. Plumb a fetcher explicitly in those cases.
void SetBlobFetcher(const Version* version, BlobFileCache* blob_file_cache,
Env::IOActivity io_activity,
bool allow_write_path_fallback);
// Seek to the beginning of the compaction iterator output.
//
// REQUIRED: Call only once.
@@ -370,12 +302,6 @@ class CompactionIterator {
// for regular values (kTypeValue).
void ExtractLargeValueIfNeeded();
// Extracts large column values from wide column entities to blob files.
// For each column whose value size >= min_blob_size, the value is written
// to a blob file and replaced with a blob index reference. Should only be
// called for wide column entities (kTypeWideColumnEntity).
void ExtractLargeColumnValuesIfNeeded();
// Relocates valid blobs residing in the oldest blob files if garbage
// collection is enabled. Relocated blobs are written to new blob files or
// inlined in the LSM tree depending on the current settings (i.e.
@@ -386,39 +312,6 @@ class CompactionIterator {
// algorithm is also called from here.
void GarbageCollectBlobIfNeeded();
// Garbage collects blob references within wide column entities. For entities
// that contain blob column references, this method checks if any of the
// referenced blob files need garbage collection. If so, the blob values are
// fetched and either relocated to new blob files or inlined based on
// current settings. Should only be called for wide column entities
// (kTypeWideColumnEntity) that have blob columns.
void GarbageCollectEntityBlobsIfNeeded();
// Helper for GarbageCollectEntityBlobsIfNeeded: Fetches blob values that need
// garbage collection based on the cutoff file number.
// Returns true if any blobs were fetched, false otherwise.
// On error, sets status_ and returns false.
bool FetchBlobsNeedingGC(
const std::vector<WideColumn>& columns,
const std::vector<std::pair<size_t, BlobIndex>>& blob_columns,
std::vector<std::pair<size_t, PinnableSlice>>* fetched_blob_values);
// Helper for GarbageCollectEntityBlobsIfNeeded: Relocates fetched blob
// values to new blob files if blob_file_builder_ is available.
// Returns the new blob column indices. On error, sets status_ and returns
// empty.
std::vector<std::pair<size_t, BlobIndex>> RelocateBlobValues(
const std::vector<std::pair<size_t, PinnableSlice>>& fetched_blob_values);
// Helper for GarbageCollectEntityBlobsIfNeeded: Serializes the entity after
// garbage collection, combining original columns with fetched values and
// new blob indices. On error, sets status_.
void SerializeEntityAfterGC(
const std::vector<WideColumn>& columns,
const std::vector<std::pair<size_t, BlobIndex>>& original_blob_columns,
const std::vector<std::pair<size_t, PinnableSlice>>& fetched_blob_values,
const std::vector<std::pair<size_t, BlobIndex>>& new_blob_columns);
// Invoke compaction filter if needed.
// Return true on success, false on failures (e.g.: kIOError).
bool InvokeFilterIfNeeded(bool* need_skip, Slice* skip_until);
@@ -459,8 +352,7 @@ class CompactionIterator {
static uint64_t ComputeBlobGarbageCollectionCutoffFileNumber(
const CompactionProxy* compaction);
static std::unique_ptr<BlobFetcher> CreateBlobFetcherIfNeeded(
const CompactionProxy* compaction, const Version* input_version,
Env::IOActivity blob_read_io_activity);
const CompactionProxy* compaction);
static std::unique_ptr<PrefetchBufferCollection>
CreatePrefetchBufferCollectionIfNeeded(const CompactionProxy* compaction);
@@ -484,7 +376,6 @@ class CompactionIterator {
BlobFileBuilder* blob_file_builder_;
std::unique_ptr<CompactionProxy> compaction_;
const CompactionFilter* compaction_filter_;
const bool filter_supports_v4_;
const std::atomic<bool>* shutting_down_;
const std::atomic<bool>& manual_compaction_canceled_;
const bool bottommost_level_;
@@ -588,32 +479,6 @@ class CompactionIterator {
PinnableSlice blob_value_;
std::string compaction_filter_value_;
InternalKey compaction_filter_skip_until_;
// Buffer for storing rewritten entity with blob references after
// extracting large column values to blob files.
std::string rewritten_entity_;
// Cached deserialized columns for the current wide-column entity, reused
// across iterations to avoid per-key heap allocations. This is the full
// sorted column set. For blob-backed columns, value() initially contains the
// serialized BlobIndex bytes from the entity.
std::vector<WideColumn> entity_columns_;
// Side list for the blob-backed subset of entity_columns_. Each pair is
// (column_index_in_entity_columns_, decoded_blob_index), so callers can tell
// which entries in entity_columns_ are blob references without reparsing.
std::vector<std::pair<size_t, BlobIndex>> entity_blob_columns_;
// Temporary Slice-based view over entity_columns_ used when reserializing an
// entity after blob extraction, to avoid copying names/values into strings.
WideColumns entity_wide_columns_;
// Reusable storage for existing_columns in InvokeFilterIfNeeded,
// avoiding per-key heap allocations.
WideColumns filter_existing_columns_;
// Reusable blob resolver for compaction filter, avoiding per-key heap
// allocation. Init() is called once; Reset() is called per entity.
CompactionBlobResolver blob_resolver_;
// True when entity_columns_/entity_blob_columns_ already describe the
// current input record (e.g., because InvokeFilterIfNeeded() deserialized
// it) and PrepareOutput() can skip redundant work. Reset for each candidate
// input record and whenever the filter rewrites the entity value.
bool entity_deserialized_{false};
// "level_ptrs" holds indices that remember which file of an associated
// level we were last checking during the last call to compaction->
// KeyNotExistsBeyondOutputLevel(). This allows future calls to the function
File diff suppressed because it is too large. Load diff
+15 -105
View File
@@ -33,7 +33,6 @@
#include "db/range_del_aggregator.h"
#include "db/version_edit.h"
#include "db/version_set.h"
#include "file/file_util.h"
#include "file/filename.h"
#include "file/read_write_util.h"
#include "file/sst_file_manager_impl.h"
@@ -58,7 +57,6 @@
#include "table/table_builder.h"
#include "table/unique_id_impl.h"
#include "test_util/sync_point.h"
#include "util/hash_containers.h"
#include "util/stop_watch.h"
namespace ROCKSDB_NAMESPACE {
@@ -105,8 +103,6 @@ const char* GetCompactionReasonString(CompactionReason compaction_reason) {
return "RoundRobinTtl";
case CompactionReason::kRefitLevel:
return "RefitLevel";
case CompactionReason::kReadTriggered:
return "ReadTriggered";
case CompactionReason::kNumOfReasons:
// fall through
default:
@@ -899,10 +895,6 @@ Status CompactionJob::VerifyOutputFiles() {
}
auto verify_table = [&](SubcompactionState& subcompaction_state) {
// Collect file open metadata during verification when fast_sst_open
// is enabled, keyed by file number.
UnorderedMap<uint64_t, std::string> file_open_metadata_map;
for (const auto& output_file : subcompaction_state.GetOutputs()) {
// Verify that the table is usable
// We set for_compaction to false and don't
@@ -911,21 +903,17 @@ Status CompactionJob::VerifyOutputFiles() {
// use_direct_io_for_flush_and_compaction is true, we will regard this
// verification as user reads since the goal is to cache it here for
// further user reads
ReadOptions verification_read_options(Env::IOActivity::kCompaction);
verification_read_options.verify_checksums = true;
verification_read_options.readahead_size =
ReadOptions verify_table_read_options(Env::IOActivity::kCompaction);
verify_table_read_options.verify_checksums = true;
verify_table_read_options.readahead_size =
file_options_for_read_.compaction_readahead_size;
std::unique_ptr<TableReader> table_reader_guard;
TableReader* table_reader_ptr = table_reader_guard.get();
verification_read_options.rate_limiter_priority =
verify_table_read_options.rate_limiter_priority =
GetRateLimiterPriority();
std::string file_open_metadata;
std::string* file_open_metadata_ptr =
mutable_db_options_copy_.fast_sst_open ? &file_open_metadata
: nullptr;
InternalIterator* iter = cfd->table_cache()->NewIterator(
verification_read_options, file_options_, cfd->internal_comparator(),
verify_table_read_options, file_options_, cfd->internal_comparator(),
output_file.meta,
/*range_del_agg=*/nullptr, compact_->compaction->mutable_cf_options(),
/*table_reader_ptr=*/&table_reader_ptr,
@@ -936,10 +924,7 @@ Status CompactionJob::VerifyOutputFiles() {
MaxFileSizeForL0MetaPin(compact_->compaction->mutable_cf_options()),
/*smallest_compaction_key=*/nullptr,
/*largest_compaction_key=*/nullptr,
/*allow_unprepared_value=*/false,
/*range_del_read_seqno=*/nullptr,
/*range_del_iter=*/nullptr,
/*maybe_pin_table_handle=*/false, file_open_metadata_ptr);
/*allow_unprepared_value=*/false);
auto s = iter->status();
if (s.ok()) {
// Check for remote/local compaction and verify_output_flags flags
@@ -956,17 +941,12 @@ Status CompactionJob::VerifyOutputFiles() {
!!(verify_output_flags & VerifyOutputFlags::kVerifyBlockChecksum);
const bool should_verify_iteration =
!!(verify_output_flags & VerifyOutputFlags::kVerifyIteration);
const bool should_verify_file_checksum =
!!(verify_output_flags &
VerifyOutputFlags::kVerifyFileChecksum) &&
db_options_.file_checksum_gen_factory != nullptr &&
output_file.meta.file_checksum != kUnknownFileChecksum;
if (should_verify_block_checksum) {
assert(table_reader_ptr != nullptr);
// If verifying iteration as well, verify meta blocks here only to
// avoid redundant checks on data blocks
s = table_reader_ptr->VerifyChecksum(
verification_read_options, TableReaderCaller::kCompaction,
verify_table_read_options, TableReaderCaller::kCompaction,
/*meta_blocks_only=*/should_verify_iteration);
}
if (s.ok() && should_verify_iteration) {
@@ -987,24 +967,6 @@ Status CompactionJob::VerifyOutputFiles() {
"was computed when written");
}
}
if (s.ok() && should_verify_file_checksum) {
std::string file_checksum;
std::string file_checksum_func_name;
std::string fname =
GetTableFileName(output_file.meta.fd.GetNumber());
s = GenerateOneFileChecksum(
fs_.get(), fname, db_options_.file_checksum_gen_factory.get(),
output_file.meta.file_checksum_func_name, &file_checksum,
&file_checksum_func_name,
verification_read_options.readahead_size,
db_options_.allow_mmap_reads, io_tracer_,
db_options_.rate_limiter.get(), verification_read_options,
stats_, db_options_.clock, file_options_for_read_);
if (s.ok() && file_checksum != output_file.meta.file_checksum) {
s = Status::Corruption(
"File checksum mismatch for compaction output file " + fname);
}
}
}
}
@@ -1014,27 +976,6 @@ Status CompactionJob::VerifyOutputFiles() {
subcompaction_state.status = s;
break;
}
if (!file_open_metadata.empty()) {
file_open_metadata_map[output_file.meta.fd.GetNumber()] =
std::move(file_open_metadata);
}
}
// Apply collected file open metadata to mutable outputs
if (!file_open_metadata_map.empty()) {
auto apply_metadata =
[&file_open_metadata_map](
std::vector<CompactionOutputs::Output>& outputs) {
for (auto& output : outputs) {
auto it = file_open_metadata_map.find(output.meta.fd.GetNumber());
if (it != file_open_metadata_map.end()) {
output.meta.file_open_metadata = std::move(it->second);
}
}
};
apply_metadata(subcompaction_state.GetMutableCompactionOutputs());
apply_metadata(subcompaction_state.GetMutableProximalOutputs());
}
};
for (size_t i = 1; i < compact_->sub_compact_states.size(); i++) {
@@ -1151,9 +1092,6 @@ Status CompactionJob::Run() {
status = VerifyOutputFiles();
}
TEST_SYNC_POINT_CALLBACK("CompactionJob::Run():AfterVerifyOutputFiles",
&status);
if (status.ok()) {
SetOutputTableProperties();
}
@@ -1340,16 +1278,6 @@ Status CompactionJob::Install(bool* compaction_released) {
<< pl_stats.bytes_written_blob;
}
// Propagate Install failure to compact_->status so that
// CleanupCompaction() -> SubcompactionState::Cleanup() sees the failure and
// calls ReleaseObsolete on output files' table cache entries. Without this,
// if Run() succeeds but InstallCompactionResults() fails, Cleanup would see
// overall_status = OK and skip ReleaseObsolete, leaking entries for output
// files that were never installed into any Version.
if (!status.ok() && compact_->status.ok()) {
compact_->status = status;
}
CleanupCompaction();
return status;
}
@@ -2398,16 +2326,11 @@ Status CompactionJob::InstallCompactionResults(bool* compaction_released) {
*compaction_released = true;
};
Status s;
TEST_SYNC_POINT_CALLBACK(
"CompactionJob::InstallCompactionResults:BeforeLogAndApply", &s);
if (s.ok()) {
s = versions_->LogAndApply(compaction->column_family_data(), read_options,
write_options, edit, db_mutex_, db_directory_,
/*new_descriptor_log=*/false,
/*column_family_options=*/nullptr, manifest_wcb);
}
return s;
return versions_->LogAndApply(compaction->column_family_data(), read_options,
write_options, edit, db_mutex_, db_directory_,
/*new_descriptor_log=*/false,
/*column_family_options=*/nullptr,
manifest_wcb);
}
void CompactionJob::RecordCompactionIOStats() {
@@ -2547,16 +2470,8 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
return s;
}
// Enable hash computation if paranoid_file_checks is on or if
// verify_output_flags includes kVerifyIteration, so that
// VerifyOutputFiles() can compare the hash of the written data
// against a re-read of the output file.
bool enable_output_hash =
paranoid_file_checks_ ||
!!(sub_compact->compaction->mutable_cf_options().verify_output_flags &
VerifyOutputFlags::kVerifyIteration);
outputs.AddOutput(std::move(meta), cfd->internal_comparator(),
enable_output_hash);
paranoid_file_checks_);
}
writable_file->SetIOPriority(GetRateLimiterPriority());
@@ -2597,7 +2512,7 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
void CompactionJob::CleanupCompaction() {
for (SubcompactionState& sub_compact : compact_->sub_compact_states) {
sub_compact.Cleanup(table_cache_.get(), compact_->status);
sub_compact.Cleanup(table_cache_.get());
}
delete compact_;
compact_ = nullptr;
@@ -2989,17 +2904,12 @@ void CompactionJob::RestoreCompactionOutputs(
const auto& output_files = subcompaction_progress_per_level.GetOutputFiles();
const bool enable_output_hash =
paranoid_file_checks_ ||
!!(compact_->compaction->mutable_cf_options().verify_output_flags &
VerifyOutputFlags::kVerifyIteration);
for (size_t i = 0; i < output_files.size(); i++) {
FileMetaData file_copy = output_files[i];
outputs_to_restore->AddOutput(std::move(file_copy),
cfd->internal_comparator(),
enable_output_hash, true /* finished */);
paranoid_file_checks_, true /* finished */);
outputs_to_restore->UpdateTableProperties(
*output_files_table_properties[i]);
-1
View File
@@ -60,7 +60,6 @@ class CompactionOutputs {
}
const std::vector<Output>& GetOutputs() const { return outputs_; }
std::vector<Output>& GetMutableOutputs() { return outputs_; }
// Set new table builder for the current output
void NewBuilder(const TableBuilderOptions& tboptions);
+5 -6
View File
@@ -102,10 +102,10 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
for (auto ritr = level_files.rbegin(); ritr != level_files.rend(); ++ritr) {
FileMetaData* f = *ritr;
assert(f);
TableReader* reader = f->fd.pinned_reader.Get();
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
if (f->fd.table_reader && f->fd.table_reader->GetTableProperties()) {
uint64_t newest_key_time = f->TryGetNewestKeyTime();
uint64_t creation_time = reader->GetTableProperties()->creation_time;
uint64_t creation_time =
f->fd.table_reader->GetTableProperties()->creation_time;
uint64_t est_newest_key_time = newest_key_time == kUnknownNewestKeyTime
? creation_time
: newest_key_time;
@@ -166,9 +166,8 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
assert(f);
uint64_t newest_key_time = f->TryGetNewestKeyTime();
uint64_t creation_time = 0;
TableReader* reader = f->fd.pinned_reader.Get();
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
creation_time = reader->GetTableProperties()->creation_time;
if (f->fd.table_reader && f->fd.table_reader->GetTableProperties()) {
creation_time = f->fd.table_reader->GetTableProperties()->creation_time;
}
uint64_t est_newest_key_time = newest_key_time == kUnknownNewestKeyTime
? creation_time
+1 -20
View File
@@ -36,9 +36,6 @@ 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;
@@ -328,14 +325,6 @@ 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() {
@@ -415,15 +404,7 @@ 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)
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++) {
for (size_t i = start_index + 1; i < level_files.size(); i++) {
auto* f = level_files[i];
if (f->being_compacted) {
// Constraint 1a
+3 -185
View File
@@ -178,8 +178,7 @@ class CompactionPickerTestBase : public testing::Test {
}
TableProperties tp;
tp.newest_key_time = newest_key_time;
f->fd.pinned_reader.TEST_SetReader(
new mock::MockTableReader(mock::KVVector{}, tp));
f->fd.table_reader = new mock::MockTableReader(mock::KVVector{}, tp);
vstorage->AddFile(level, f);
files_.emplace_back(f);
@@ -273,9 +272,8 @@ class CompactionPickerTestBase : public testing::Test {
void ClearFiles() {
for (auto& file : files_) {
TableReader* t = file->fd.pinned_reader.Get();
if (t != nullptr) {
delete t;
if (file->fd.table_reader != nullptr) {
delete file->fd.table_reader;
}
}
files_.clear();
@@ -984,186 +982,6 @@ 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;
+101 -178
View File
@@ -275,21 +275,6 @@ 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);
@@ -307,14 +292,6 @@ 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.
@@ -617,9 +594,6 @@ bool UniversalCompactionPicker::NeedsCompaction(
if (!vstorage->FilesMarkedForCompaction().empty()) {
return true;
}
if (!vstorage->ReadTriggeredCompactionFiles().empty()) {
return true;
}
return false;
}
@@ -785,7 +759,6 @@ 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());
@@ -809,7 +782,6 @@ Compaction* UniversalCompactionBuilder::PickCompaction() {
c = MaybePickCompactionToReduceSortedRuns(c, file_num_compaction_trigger,
ratio);
c = MaybePickDeleteTriggeredCompaction(c);
c = MaybePickReadTriggeredCompaction(c);
if (c == nullptr) {
TEST_SYNC_POINT_CALLBACK(
@@ -1489,6 +1461,9 @@ 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
@@ -1499,6 +1474,7 @@ 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];
@@ -1527,9 +1503,13 @@ 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
@@ -1542,10 +1522,100 @@ 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);
}
}
return BuildCompactionToNextLevel(
start_level_inputs, CompactionReason::kFilesMarkedForCompaction);
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);
}
Compaction* UniversalCompactionBuilder::PickCompactionToOldest(
@@ -1712,153 +1782,6 @@ 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();
+5 -10
View File
@@ -846,16 +846,11 @@ static std::unordered_map<std::string, OptionTypeInfo>
{offsetof(struct InternalStats::CompactionStats, count),
OptionType::kUInt64T, OptionVerificationType::kNormal,
OptionTypeFlags::kNone}},
{"counts",
OptionTypeInfo::Array<
/* In release 11.2, a new compaction reason was added. This broken
* the reader and writer. To unblock release 11.1, we temporarily
* reduce the count array size to the old one. TODO add a proper
* serialization and deserialization method. */
int, static_cast<int>(CompactionReason::kNumOfReasons) - 1>(
offsetof(struct InternalStats::CompactionStats, counts),
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
{0, OptionType::kInt})},
{"counts", OptionTypeInfo::Array<
int, static_cast<int>(CompactionReason::kNumOfReasons)>(
offsetof(struct InternalStats::CompactionStats, counts),
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
{0, OptionType::kInt})},
};
static std::unordered_map<std::string, OptionTypeInfo>
+1 -35
View File
@@ -409,34 +409,6 @@ 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;
@@ -1141,8 +1113,6 @@ TEST_F(CompactionServiceTest, CorruptedOutputVerifyOutputFlags) {
VerifyOutputFlags::kVerifyBlockChecksum,
VerifyOutputFlags::kEnableForRemoteCompaction |
VerifyOutputFlags::kVerifyIteration,
VerifyOutputFlags::kEnableForRemoteCompaction |
VerifyOutputFlags::kVerifyFileChecksum,
VerifyOutputFlags::kVerifyAll}) {
SCOPED_TRACE(
"verify_output_flags=" +
@@ -1154,7 +1124,6 @@ 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();
@@ -1190,13 +1159,10 @@ 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_file_checksum)) {
(should_verify_block_checksum || should_verify_iteration)) {
ASSERT_NOK(s);
ASSERT_TRUE(s.IsCorruption());
} else {
+2 -2
View File
@@ -38,11 +38,11 @@ OutputIterator SubcompactionState::GetOutputs() const {
compaction_outputs_.outputs_);
}
void SubcompactionState::Cleanup(Cache* cache, const Status& overall_status) {
void SubcompactionState::Cleanup(Cache* cache) {
proximal_level_outputs_.Cleanup();
compaction_outputs_.Cleanup();
if (!status.ok() || !overall_status.ok()) {
if (!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
+1 -10
View File
@@ -81,15 +81,6 @@ 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(
@@ -179,7 +170,7 @@ class SubcompactionState {
}
}
void Cleanup(Cache* cache, const Status& overall_status);
void Cleanup(Cache* cache);
void AggregateCompactionOutputStats(
InternalStats::CompactionStatsFull& internal_stats) const;
-1064
View File
File diff suppressed because it is too large. Load diff
-114
View File
@@ -466,90 +466,6 @@ 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> {
@@ -704,36 +620,6 @@ 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)
+10 -518
View File
@@ -14,13 +14,11 @@
#include "db/db_test_util.h"
#include "db/dbformat.h"
#include "env/mock_env.h"
#include "file/filename.h"
#include "port/port.h"
#include "port/stack_trace.h"
#include "rocksdb/advanced_options.h"
#include "rocksdb/concurrent_task_limiter.h"
#include "rocksdb/experimental.h"
#include "rocksdb/file_checksum.h"
#include "rocksdb/iostats_context.h"
#include "rocksdb/sst_file_writer.h"
#include "test_util/mock_time_env.h"
@@ -119,17 +117,6 @@ class DBCompactionTest : public DBTestBase {
DBCompactionTest()
: DBTestBase("db_compaction_test", /*env_do_fsync=*/false) {}
void TearDown() override {
// Reset Env::Default() thread pool state that tests may have modified.
// Under sharded execution multiple tests share a process, so leaked
// thread-pool sizes (especially BOTTOM) cause later tests to see
// unexpected compaction scheduling (e.g. ForwardToBottomPriPool).
Env::Default()->SetBackgroundThreads(0, Env::Priority::BOTTOM);
Env::Default()->SetBackgroundThreads(1, Env::Priority::LOW);
Env::Default()->SetBackgroundThreads(1, Env::Priority::HIGH);
DBTestBase::TearDown();
}
protected:
/*
* Verifies compaction stats of cfd are valid.
@@ -810,13 +797,6 @@ TEST_F(DBCompactionTest, CompactRangeBottomPri) {
// and one compact to L2 in bottom pri pool.
int low_pri_count = 0;
int bottom_pri_count = 0;
bool bottom_running_seen = false;
SyncPoint::GetInstance()->SetCallBack(
"BackgroundCallCompaction:1", [&](void*) {
if (dbfull()->TEST_NumRunningBottomCompactions() > 0) {
bottom_running_seen = true;
}
});
SyncPoint::GetInstance()->SetCallBack(
"ThreadPoolImpl::Impl::BGThread:BeforeRun", [&](void* arg) {
Env::Priority* pri = static_cast<Env::Priority*>(arg);
@@ -835,7 +815,6 @@ TEST_F(DBCompactionTest, CompactRangeBottomPri) {
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_EQ(1, low_pri_count);
ASSERT_EQ(1, bottom_pri_count);
ASSERT_TRUE(bottom_running_seen);
ASSERT_EQ("0,0,2", FilesPerLevel(0));
// Recompact bottom most level uses bottom pool
@@ -11509,24 +11488,20 @@ TEST_F(DBCompactionTest, RecordNewestKeyTimeForTtlCompaction) {
// Check that we are populating newest_key_time on flush
std::vector<FileMetaData*> file_metadatas = GetLevelFileMetadatas(0);
ASSERT_EQ(file_metadatas.size(), 4);
uint64_t first_newest_key_time = file_metadatas[0]
->fd.pinned_reader.Get()
->GetTableProperties()
->newest_key_time;
uint64_t first_newest_key_time =
file_metadatas[0]->fd.table_reader->GetTableProperties()->newest_key_time;
ASSERT_NE(first_newest_key_time, kUnknownNewestKeyTime);
// Check that the newest_key_times are in expected ordering
uint64_t prev_newest_key_time = first_newest_key_time;
for (size_t idx = 1; idx < file_metadatas.size(); idx++) {
uint64_t newest_key_time = file_metadatas[idx]
->fd.pinned_reader.Get()
->GetTableProperties()
->fd.table_reader->GetTableProperties()
->newest_key_time;
ASSERT_LT(newest_key_time, prev_newest_key_time);
prev_newest_key_time = newest_key_time;
ASSERT_EQ(newest_key_time, file_metadatas[idx]
->fd.pinned_reader.Get()
->GetTableProperties()
->fd.table_reader->GetTableProperties()
->creation_time);
}
// The delta between the first and last newest_key_times is 15s
@@ -11541,18 +11516,14 @@ TEST_F(DBCompactionTest, RecordNewestKeyTimeForTtlCompaction) {
ASSERT_EQ(NumTableFilesAtLevel(0), 1);
file_metadatas = GetLevelFileMetadatas(0);
ASSERT_EQ(file_metadatas.size(), 1);
ASSERT_EQ(file_metadatas[0]
->fd.pinned_reader.Get()
->GetTableProperties()
->newest_key_time,
first_newest_key_time);
ASSERT_EQ(
file_metadatas[0]->fd.table_reader->GetTableProperties()->newest_key_time,
first_newest_key_time);
// Contrast newest_key_time with creation_time, which records the oldest
// ancestor time (15s older than newest_key_time)
ASSERT_EQ(file_metadatas[0]
->fd.pinned_reader.Get()
->GetTableProperties()
->creation_time,
last_newest_key_time);
ASSERT_EQ(
file_metadatas[0]->fd.table_reader->GetTableProperties()->creation_time,
last_newest_key_time);
ASSERT_EQ(file_metadatas[0]->oldest_ancester_time, last_newest_key_time);
// Make sure TTL of 5s causes compaction
@@ -11692,485 +11663,6 @@ TEST_F(DBCompactionTest, PeriodicTask) {
ASSERT_EQ(listener->num_periodic_compactions, 1);
Close();
}
TEST_F(DBCompactionTest, ReadTriggeredCompaction) {
Options options = CurrentOptions();
options.num_levels = 3;
options.level0_file_num_compaction_trigger = 10;
options.read_triggered_compaction_threshold = 0.001;
options.disable_auto_compactions = true;
DestroyAndReopen(options);
// Write data at L2 first so L1 is not the bottommost level
Random rnd(301);
for (int i = 0; i < 5; ++i) {
ASSERT_OK(Put(Key(i), rnd.RandomString(1024)));
}
ASSERT_OK(Flush());
MoveFilesToLevel(2);
ASSERT_EQ("0,0,1", FilesPerLevel());
// Write more data and move to L1 (the hot level)
for (int i = 5; i < 10; ++i) {
ASSERT_OK(Put(Key(i), rnd.RandomString(1024)));
}
ASSERT_OK(Flush());
MoveFilesToLevel(1);
ASSERT_EQ("0,1,1", FilesPerLevel());
ColumnFamilyMetaData cf_meta;
dbfull()->GetColumnFamilyMetaData(dbfull()->DefaultColumnFamily(), &cf_meta);
ASSERT_EQ(cf_meta.levels[1].files.size(), 1);
uint64_t file_size = cf_meta.levels[1].files[0].size;
// Set reads high enough to exceed threshold (0.001 * file_size)
uint64_t reads_needed =
static_cast<uint64_t>(0.002 * static_cast<double>(file_size));
{
auto* cfd = static_cast_with_check<ColumnFamilyHandleImpl>(
dbfull()->DefaultColumnFamily())
->cfd();
auto* vstorage = cfd->current()->storage_info();
for (auto* f : vstorage->LevelFiles(1)) {
f->stats.num_collapsible_entry_reads_sampled.store(reads_needed);
}
}
std::atomic<int> read_triggered_compactions{0};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"LevelCompactionPicker::PickCompaction:Return", [&](void* arg) {
Compaction* compaction = static_cast<Compaction*>(arg);
if (compaction->compaction_reason() ==
CompactionReason::kReadTriggered) {
read_triggered_compactions.fetch_add(1, std::memory_order_relaxed);
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Enable auto compactions to trigger
ASSERT_OK(dbfull()->SetOptions({{"disable_auto_compactions", "false"}}));
ASSERT_OK(dbfull()->TEST_WaitForCompact());
ASSERT_GE(read_triggered_compactions, 1);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
// Verify the L1 file was compacted down (L1 should be empty now)
ASSERT_EQ(0, NumTableFilesAtLevel(1));
// Verify data integrity: all keys are still readable
for (int i = 0; i < 10; ++i) {
ASSERT_NE(Get(Key(i)), "NOT_FOUND");
}
}
// Regression test for a bug in SetupOtherFilesWithRoundRobinExpansion where
// duplicate files are added to the compaction input, corrupting
// ExpandInputsToCleanCut and violating the clean-cut invariant. The bug
// requires: (1) kRoundRobin compaction priority, (2) files at a non-L0 level
// with shared user key boundaries (adjacent files whose boundary keys share
// the same user key), and (3) ExpandInputsToCleanCut expanding the initially
// picked file to include multiple adjacent files in PickFileToCompact.
TEST_F(DBCompactionTest, RoundRobinCleanCutWithSharedBoundary) {
Options options = CurrentOptions();
options.compaction_style = kCompactionStyleLevel;
options.compaction_pri = kRoundRobin;
options.level_compaction_dynamic_level_bytes = false;
options.max_bytes_for_level_base = 100;
options.disable_auto_compactions = true;
DestroyAndReopen(options);
std::vector<const Snapshot*> snapshots;
for (int v = 0; v < 5; v++) {
for (int k = 0; k < 3; k++) {
ASSERT_OK(Put("key" + std::to_string(k), "v" + std::to_string(v)));
}
snapshots.push_back(db_->GetSnapshot());
ASSERT_OK(Flush());
}
// Force L0->L1 compaction output to split every 3 keys. With 3 keys x 5
// versions (15 KVs) sorted by (user_key asc, seq desc), splitting every 3
// creates 5 files where adjacent files share boundaries across different
// user keys (e.g., File0 ends with key0, File1 starts with key0 and ends
// with key1, etc.). This chain of 4+ shared boundaries across 3 different
// user keys is needed so that ExpandInputsToCleanCut expands the picked
// file to multiple files, and the duplicate in the round-robin loop causes
// GetRange to return a truncated range that drops files from the set.
std::atomic<int> key_count{0};
SyncPoint::GetInstance()->SetCallBack(
"CompactionOutputs::ShouldStopBefore::manual_decision", [&](void* arg) {
auto* p = static_cast<std::pair<bool*, const Slice>*>(arg);
int n = key_count.fetch_add(1);
if (n > 0 && n % 3 == 0) {
*(p->first) = true;
}
});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(dbfull()->TEST_CompactRange(0, nullptr, nullptr));
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ColumnFamilyMetaData cf_meta;
db_->GetColumnFamilyMetaData(&cf_meta);
ASSERT_EQ(cf_meta.levels[1].files.size(), 5U);
for (auto s : snapshots) {
db_->ReleaseSnapshot(s);
}
ASSERT_OK(dbfull()->SetOptions({{"disable_auto_compactions", "false"}}));
ASSERT_OK(dbfull()->TEST_WaitForCompact());
for (int k = 0; k < 3; k++) {
ASSERT_EQ(Get("key" + std::to_string(k)), "v4");
}
}
// Regression test:
// 1. Compaction succeeds at subcompaction level, VerifyOutputFiles adds cache
// entries for output files via table_cache()->NewIterator().
// 2. A post-verification step fails (injected here via sync point), setting
// compact_->status to error while each subcompaction's status stays OK.
// 3. SubcompactionState::Cleanup checks individual status (OK) and skips
// ReleaseObsolete — the cache entries leak.
// 4. FaultInjectionTestFS injects metadata read errors, causing GetChildren
// to fail in FindObsoleteFiles.
// 5. Close()'s FindObsoleteFiles also fails to find the orphan for the same
// reason. TEST_VerifyNoObsoleteFilesCached finds the leaked entry.
TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
auto fault_fs = std::make_shared<FaultInjectionTestFS>(env_->GetFileSystem());
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
Options options = CurrentOptions();
options.env = fault_env.get();
options.paranoid_file_checks = true;
options.level0_file_num_compaction_trigger = 2;
options.disable_auto_compactions = true;
options.num_levels = 3;
DestroyAndReopen(options);
// Write overlapping data to force a real (non-trivial) compaction.
ASSERT_OK(Put("a", std::string(1024, 'x')));
ASSERT_OK(Put("z", std::string(1024, 'x')));
ASSERT_OK(Flush());
ASSERT_OK(Put("a", std::string(1024, 'y')));
ASSERT_OK(Put("z", std::string(1024, 'y')));
ASSERT_OK(Flush());
ASSERT_EQ(NumTableFilesAtLevel(0), 2);
// After VerifyOutputFiles succeeds (cache entries created), inject error
// and deactivate the filesystem. The error makes the overall compaction
// fail while individual subcompaction statuses stay OK (so Cleanup skips
// ReleaseObsolete). The filesystem deactivation makes GetChildren fail
// in FindObsoleteFiles, preventing the backstop from evicting the leaked
// cache entries — matching the crash test's metadata read fault injection.
std::atomic<bool> inject_error{true};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"CompactionJob::Run():AfterVerifyOutputFiles", [&](void* arg) {
if (inject_error.exchange(false)) {
*static_cast<Status*>(arg) = Status::Corruption("injected");
}
});
// Enable metadata read fault injection on the bg compaction thread after
// the compaction job finishes but before FindObsoleteFiles runs. This
// makes GetChildren fail (metadata read), matching crash test's
// --open_metadata_read_fault_one_in=8. Only metadata reads fail —
// logging and other IO operations continue normally.
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"BackgroundCallCompaction:1", [&](void*) {
fault_fs->SetThreadLocalErrorContext(
FaultInjectionIOType::kMetadataRead, /*seed=*/0, /*one_in=*/1,
/*retryable=*/false, /*has_data_loss=*/false);
fault_fs->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Trigger compaction — fails after VerifyOutputFiles.
Status s = dbfull()->TEST_CompactRange(0, nullptr, nullptr);
ASSERT_NOK(s);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
// Enable metadata read fault injection on the main thread too, so
// Close()'s FindObsoleteFiles also fails to find the orphan file.
fault_fs->SetThreadLocalErrorContext(
FaultInjectionIOType::kMetadataRead, /*seed=*/0, /*one_in=*/1,
/*retryable=*/false, /*has_data_loss=*/false);
fault_fs->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
// TEST_VerifyNoObsoleteFilesCached asserted within Close on ASAN builds
s = db_->Close();
ASSERT_OK(s);
// Release DB before fault_env goes out of scope to avoid use-after-free.
db_ = nullptr;
}
// Regression test for table cache leak when InstallCompactionResults fails.
// Similar to LeakedTableCacheEntryOnCompactionFailure but the failure occurs
// during Install() (MANIFEST write) rather than during Run().
// 1. Compaction Run() succeeds: all subcompactions OK, VerifyOutputFiles
// inserts output files into the table cache, compact_->status = OK.
// 2. Install() calls InstallCompactionResults() -> LogAndApply(), which
// fails (injected MANIFEST write error).
// 3. Install()'s local status captures the error, but compact_->status
// was never updated (THE BUG). CleanupCompaction passes compact_->status
// (OK) to Cleanup(), which skips ReleaseObsolete.
// 4. FaultInjection prevents FindObsoleteFiles backstop from working.
// 5. Close()'s TEST_VerifyNoObsoleteFilesCached finds the leaked entry.
TEST_F(DBCompactionTest, LeakedTableCacheEntryOnInstallFailure) {
auto fault_fs = std::make_shared<FaultInjectionTestFS>(env_->GetFileSystem());
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
Options options = CurrentOptions();
options.env = fault_env.get();
options.paranoid_file_checks = true;
options.level0_file_num_compaction_trigger = 2;
options.disable_auto_compactions = true;
options.num_levels = 3;
DestroyAndReopen(options);
// Write overlapping data to force a real (non-trivial) compaction.
ASSERT_OK(Put("a", std::string(1024, 'x')));
ASSERT_OK(Put("z", std::string(1024, 'x')));
ASSERT_OK(Flush());
ASSERT_OK(Put("a", std::string(1024, 'y')));
ASSERT_OK(Put("z", std::string(1024, 'y')));
ASSERT_OK(Flush());
ASSERT_EQ(NumTableFilesAtLevel(0), 2);
// After VerifyOutputFiles succeeds (cache entries created) and Run()
// completes successfully, inject an error into InstallCompactionResults()
// so that Install() fails while compact_->status remains OK.
// Also enable metadata read faults to prevent the FindObsoleteFiles
// backstop from evicting the leaked cache entries.
std::atomic<bool> inject_error{true};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"CompactionJob::InstallCompactionResults:BeforeLogAndApply",
[&](void* arg) {
if (inject_error.exchange(false)) {
*static_cast<Status*>(arg) = Status::IOError("injected");
}
});
// After the compaction job finishes (including Install), enable metadata
// read faults so FindObsoleteFiles backstop cannot scan the directory.
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"BackgroundCallCompaction:1", [&](void*) {
fault_fs->SetThreadLocalErrorContext(
FaultInjectionIOType::kMetadataRead, /*seed=*/0, /*one_in=*/1,
/*retryable=*/false, /*has_data_loss=*/false);
fault_fs->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Trigger compaction — Run() succeeds but Install() fails.
Status s = dbfull()->TEST_CompactRange(0, nullptr, nullptr);
ASSERT_NOK(s);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
// Enable metadata read fault injection on the main thread too, so
// Close()'s FindObsoleteFiles also fails to find the orphan file.
fault_fs->SetThreadLocalErrorContext(
FaultInjectionIOType::kMetadataRead, /*seed=*/0, /*one_in=*/1,
/*retryable=*/false, /*has_data_loss=*/false);
fault_fs->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
// TEST_VerifyNoObsoleteFilesCached asserted within Close on ASAN builds
s = db_->Close();
ASSERT_OK(s);
// Release DB before fault_env goes out of scope to avoid use-after-free.
db_ = nullptr;
}
TEST_F(DBCompactionTest, VerifyFileChecksumOnCompactionOutput) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
options.verify_output_flags = VerifyOutputFlags::kVerifyFileChecksum |
VerifyOutputFlags::kEnableForLocalCompaction;
DestroyAndReopen(options);
// Create 2 L0 files to trigger compaction
for (int i = 0; i < 10; i++) {
ASSERT_OK(Put(Key(i), "value" + std::to_string(i)));
}
ASSERT_OK(Flush());
for (int i = 5; i < 15; i++) {
ASSERT_OK(Put(Key(i), "value2_" + std::to_string(i)));
}
ASSERT_OK(Flush());
// Corrupt output files right before verification
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"CompactionJob::Run:BeforeVerify", [&](void* /*arg*/) {
// Find and corrupt the newest SST file (compaction output)
std::vector<std::string> filenames;
ASSERT_OK(env_->GetChildren(dbname_, &filenames));
uint64_t max_number = 0;
std::string target_fname;
for (const auto& f : filenames) {
uint64_t number;
FileType type;
if (ParseFileName(f, &number, &type) && type == kTableFile &&
number > max_number) {
max_number = number;
target_fname = dbname_ + "/" + f;
}
}
ASSERT_FALSE(target_fname.empty());
ASSERT_OK(test::CorruptFile(env_, target_fname, 0, 1,
false /* verifyChecksum */));
});
SyncPoint::GetInstance()->EnableProcessing();
Status s = db_->CompactRange(CompactRangeOptions(), nullptr, nullptr);
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
ASSERT_TRUE(
std::strstr(s.getState(), "File checksum mismatch for compaction output"))
<< s.ToString();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
// Regression test: verify_output_flags with kVerifyIteration should work
// correctly even when paranoid_file_checks is false. Before the fix, the
// OutputValidator hash was only computed during writing when
// paranoid_file_checks was true, but the verification always computed the
// hash, leading to a false positive "Key-value checksum of compaction output
// doesn't match" error.
TEST_F(DBCompactionTest, VerifyIterationWithoutParanoidFileChecks) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.paranoid_file_checks = false;
options.verify_output_flags = VerifyOutputFlags::kVerifyIteration |
VerifyOutputFlags::kEnableForLocalCompaction;
DestroyAndReopen(options);
// Create 2 L0 files to trigger compaction
for (int i = 0; i < 10; i++) {
ASSERT_OK(Put(Key(i), "value" + std::to_string(i)));
}
ASSERT_OK(Flush());
for (int i = 5; i < 15; i++) {
ASSERT_OK(Put(Key(i), "value2_" + std::to_string(i)));
}
ASSERT_OK(Flush());
// Compaction should succeed without false corruption errors
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
// Verify data is intact
for (int i = 0; i < 15; i++) {
std::string expected;
if (i >= 5) {
expected = "value2_" + std::to_string(i);
} else {
expected = "value" + std::to_string(i);
}
ASSERT_EQ(Get(Key(i)), expected);
}
}
// Also test all verification types combined without paranoid_file_checks
TEST_F(DBCompactionTest, VerifyAllOutputFlagsWithoutParanoidFileChecks) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.paranoid_file_checks = false;
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
options.verify_output_flags = VerifyOutputFlags::kVerifyBlockChecksum |
VerifyOutputFlags::kVerifyIteration |
VerifyOutputFlags::kVerifyFileChecksum |
VerifyOutputFlags::kEnableForLocalCompaction;
DestroyAndReopen(options);
for (int i = 0; i < 10; i++) {
ASSERT_OK(Put(Key(i), "value" + std::to_string(i)));
}
ASSERT_OK(Flush());
for (int i = 5; i < 15; i++) {
ASSERT_OK(Put(Key(i), "value2_" + std::to_string(i)));
}
ASSERT_OK(Flush());
// Compaction should succeed with all verification types enabled
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
for (int i = 0; i < 15; i++) {
std::string expected;
if (i >= 5) {
expected = "value2_" + std::to_string(i);
} else {
expected = "value" + std::to_string(i);
}
ASSERT_EQ(Get(Key(i)), expected);
}
}
// Requires ~8GB+ RAM because the compaction filter internally creates a ~4GB
// value via std::string::assign().
TEST_F(DBCompactionTest, CompactionFilterLargeValueRejected) {
if (!test::HasBigMem()) {
ROCKSDB_GTEST_BYPASS("insufficient memory for reliable continuous testing");
return;
}
// A compaction filter that inflates every value to a configurable size
class InflatingFilter : public CompactionFilter {
public:
std::atomic<size_t> target_size{0};
Decision FilterV2(int /*level*/, const Slice& /*key*/,
ValueType /*value_type*/, const Slice& /*existing_value*/,
std::string* new_value,
std::string* /*skip_until*/) const override {
new_value->assign(target_size.load(), 'X');
return Decision::kChangeValue;
}
const char* Name() const override { return "InflatingFilter"; }
};
InflatingFilter inflating_filter;
Options options = CurrentOptions();
options.compaction_filter = &inflating_filter;
options.disable_auto_compactions = true;
DestroyAndReopen(options);
// Control: value at exactly 4GB - 1 should be accepted
inflating_filter.target_size = size_t{std::numeric_limits<uint32_t>::max()};
ASSERT_OK(Put("key", "small_value"));
ASSERT_OK(Flush());
ASSERT_OK(Put("key", "small_value2"));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
// Now test rejection: value at 4GB should be rejected
inflating_filter.target_size =
size_t{std::numeric_limits<uint32_t>::max()} + 1;
DestroyAndReopen(options);
ASSERT_OK(Put("key", "small_value"));
ASSERT_OK(Flush());
ASSERT_OK(Put("key", "small_value2"));
ASSERT_OK(Flush());
Status s = db_->CompactRange(CompactRangeOptions(), nullptr, nullptr);
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
ASSERT_TRUE(s.ToString().find("4GB") != std::string::npos) << s.ToString();
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+22 -35
View File
@@ -28,10 +28,9 @@
namespace ROCKSDB_NAMESPACE {
Status DBImpl::FlushForGetLiveFiles(bool force_atomic_flush) {
FlushOptions flush_opts;
flush_opts.force_atomic_flush = force_atomic_flush;
return DBImpl::FlushAllColumnFamilies(flush_opts, FlushReason::kGetLiveFiles);
Status DBImpl::FlushForGetLiveFiles() {
return DBImpl::FlushAllColumnFamilies(FlushOptions(),
FlushReason::kGetLiveFiles);
}
Status DBImpl::GetLiveFiles(std::vector<std::string>& ret,
@@ -205,6 +204,7 @@ Status DBImpl::GetLiveFilesStorageInfo(
// NOTE: This implementation was largely migrated from Checkpoint.
Status s;
VectorWalPtr live_wal_files;
bool flush_memtable = true;
if (!immutable_db_options_.allow_2pc) {
@@ -216,12 +216,12 @@ Status DBImpl::GetLiveFilesStorageInfo(
// Don't take archived log size into account when calculating wal
// size for flush, and don't need to verify consistency with manifest
// here & now.
Status wal_s = wal_manager_.GetSortedWalFiles(live_wal_files,
/* need_seqnos */ false,
/*include_archived*/ false);
s = wal_manager_.GetSortedWalFiles(live_wal_files,
/* need_seqnos */ false,
/*include_archived*/ false);
if (!wal_s.ok()) {
return wal_s;
if (!s.ok()) {
return s;
}
// Don't flush column families if total log size is smaller than
@@ -242,28 +242,13 @@ Status DBImpl::GetLiveFilesStorageInfo(
// This is a modified version of GetLiveFiles, to get access to more
// metadata.
mutex_.Lock();
bool wal_locked = false;
const bool needs_blob_direct_write_flush =
HasInFlightBlobDirectWriteFilesWithLockHeld();
if (needs_blob_direct_write_flush && !flush_memtable) {
mutex_.Unlock();
return Status::NotSupported(
"Blob direct write requires flushing active blob files before "
"capturing live files. Retry with flush enabled.");
}
if (flush_memtable) {
wal_locked = lock_wal_count_ > 0;
bool wal_locked = lock_wal_count_ > 0;
if (wal_locked) {
if (needs_blob_direct_write_flush) {
mutex_.Unlock();
return Status::NotSupported(
"Blob direct write requires flushing active blob files before "
"capturing live files. Retry with WAL unlocked.");
}
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"Can't FlushForGetLiveFiles while WAL is locked");
} else {
Status status = FlushForGetLiveFiles(opts.atomic_flush);
Status status = FlushForGetLiveFiles();
if (!status.ok()) {
mutex_.Unlock();
ROCKS_LOG_ERROR(immutable_db_options_.info_log,
@@ -407,15 +392,17 @@ Status DBImpl::GetLiveFilesStorageInfo(
TEST_SYNC_POINT("CheckpointImpl::CreateCheckpoint:SavedLiveFiles1");
TEST_SYNC_POINT("CheckpointImpl::CreateCheckpoint:SavedLiveFiles2");
// FlushWAL is required to ensure we can physically copy everything
// logically written to the WAL. (Sync not strictly required for
// active WAL to be copied rather than hard linked, even when
// Checkpoint guarantees that the copied-to file is sync-ed. Plus we can't
// help track_and_verify_wals_in_manifest after manifest_size is
// already determined.)
Status s = FlushWAL(/*sync=*/false);
if (s.IsNotSupported()) { // read-only DB or similar
s = Status::OK();
if (s.ok()) {
// FlushWAL is required to ensure we can physically copy everything
// logically written to the WAL. (Sync not strictly required for
// active WAL to be copied rather than hard linked, even when
// Checkpoint guarantees that the copied-to file is sync-ed. Plus we can't
// help track_and_verify_wals_in_manifest after manifest_size is
// already determined.)
s = FlushWAL(/*sync=*/false);
if (s.IsNotSupported()) { // read-only DB or similar
s = Status::OK();
}
}
TEST_SYNC_POINT("CheckpointImpl::CreateCustomCheckpoint:AfterGetLive1");
-262
View File
@@ -17,7 +17,6 @@
#include "port/port.h"
#include "port/stack_trace.h"
#include "rocksdb/utilities/transaction_db.h"
#include "table/block_based/block_based_table_builder.h"
#include "test_util/sync_point.h"
#include "test_util/testutil.h"
#include "util/cast_util.h"
@@ -34,17 +33,6 @@ static std::string NEW_VALUE = "NewValue";
class DBFlushTest : public DBTestBase {
public:
DBFlushTest() : DBTestBase("db_flush_test", /*env_do_fsync=*/true) {}
// Wait for all background flush callbacks to complete before Close().
// Close() calls db_.reset() which nulls the unique_ptr before the destructor
// runs, so a concurrent OnFlushCompleted reading test_->db_ would race.
// WaitForCompact() waits for bg_flush_scheduled_==0 (which is decremented
// after listener callbacks return) without setting shutting_down_ (so
// callbacks are not skipped) and without requiring NumNotFlushed()==0 (which
// would hang when mempurge leaves a memtable in imm()).
Status WaitForFlushCallbacks() {
return db_->WaitForCompact(WaitForCompactOptions());
}
};
class DBFlushDirectIOTest : public DBFlushTest,
@@ -1067,7 +1055,6 @@ TEST_F(DBFlushTest, MemPurgeBasic) {
ASSERT_EQ(Get(RNDKEY2), p_rv2);
ASSERT_EQ(Get(RNDKEY3), p_rv3);
ASSERT_OK(WaitForFlushCallbacks());
Close();
}
@@ -1182,7 +1169,6 @@ TEST_F(DBFlushTest, MemPurgeBasicToggle) {
// We expect no mempurge at all.
EXPECT_EQ(mempurge_count.exchange(0), ZERO);
ASSERT_OK(WaitForFlushCallbacks());
Close();
}
// End of MemPurgeBasicToggle, which is not
@@ -1439,7 +1425,6 @@ TEST_F(DBFlushTest, MemPurgeDeleteAndDeleteRange) {
delete iter;
}
ASSERT_OK(WaitForFlushCallbacks());
Close();
}
@@ -1894,85 +1879,6 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
Close();
}
// Reproduction for MemPurge memtable ID ordering bug.
// When MemPurge runs, it releases db_mutex_. During that window, new
// memtables can be switched to the immutable list with higher IDs. When
// MemPurge re-acquires the mutex and adds its output memtable using the
// stale ID from mems_.back(), the ordering assertion fires.
TEST_F(DBFlushTest, MemPurgeIdOrdering) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.compression = kNoCompression;
options.inplace_update_support = false;
options.allow_concurrent_memtable_write = true;
options.write_buffer_size = 1 << 20; // 1MB
// Allow enough immutable memtables so writes don't stall while the
// flush thread is paused in the sync point.
options.max_write_buffer_number = 8;
// Always attempt MemPurge on flush.
options.experimental_mempurge_threshold = 1.0;
ASSERT_OK(TryReopen(options));
// Coordinate via LoadDependency:
// 1. Flush thread hits BeforeReacquireMutex -> foreground unblocks
// 2. Foreground writes data, triggers memtable switch
// 3. Foreground hits SwitchDone -> flush thread unblocks at
// AfterWaitForTest
// 4. Flush thread re-acquires mutex, tries AddMemTable with stale ID
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"FlushJob::MemPurge:BeforeReacquireMutex",
"DBFlushTest::MemPurgeIdOrdering:StartWriting"},
{"DBFlushTest::MemPurgeIdOrdering:SwitchDone",
"FlushJob::MemPurge:AfterWaitForTest"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
const int kValueSize = 10240; // 10KB values like MemPurgeBasic
const int kNumKeys = 30;
Random rnd(301);
// Fill the memtable with overwrites of the same keys so MemPurge
// can compact them down (high garbage ratio). Each round is ~300KB,
// need ~3 rounds to approach the 1MB write_buffer_size.
for (int round = 0; round < 3; round++) {
for (int i = 0; i < kNumKeys; i++) {
ASSERT_OK(Put("key" + std::to_string(i), rnd.RandomString(kValueSize)));
}
}
// One more round should trigger a flush with MemPurge in the background.
for (int i = 0; i < kNumKeys; i++) {
ASSERT_OK(Put("key" + std::to_string(i), rnd.RandomString(kValueSize)));
}
// Block until MemPurge reaches the sync point (db_mutex_ released).
TEST_SYNC_POINT("DBFlushTest::MemPurgeIdOrdering:StartWriting");
// Now MemPurge is paused with db_mutex_ released. Write enough data
// to fill another memtable and trigger a switch. This creates an
// immutable memtable with a higher ID than the one being mempurged.
for (int i = 0; i < kNumKeys * 4; i++) {
ASSERT_OK(Put("newkey" + std::to_string(i), rnd.RandomString(kValueSize)));
}
// Let MemPurge continue -- it will re-acquire the mutex and try to
// add the output memtable with the stale ID.
TEST_SYNC_POINT("DBFlushTest::MemPurgeIdOrdering:SwitchDone");
// Allow background work to finish.
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
// If the bug is present, the assertion in AddMemTable fires before
// we reach here.
for (int i = 0; i < kNumKeys; i++) {
ASSERT_NE(Get("key" + std::to_string(i)), "NOT_FOUND");
}
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
Close();
}
TEST_P(DBFlushDirectIOTest, DirectIO) {
Options options;
options.create_if_missing = true;
@@ -3800,174 +3706,6 @@ INSTANTIATE_TEST_CASE_P(
// larger than the max allowed padding size
testing::Values(4, kLowSpaceOverheadRatio)));
// Test that when the table builder's io_status becomes bad during flush
// (simulating write fault injection), BuildTable properly propagates the
// builder's IO error instead of producing a misleading Corruption from the
// num_entries mismatch check.
TEST_F(DBFlushTest, BuilderWriteFaultPropagationDuringFlush) {
Options options = CurrentOptions();
options.flush_verify_memtable_count = true;
options.table_factory.reset(
NewBlockBasedTableFactory(BlockBasedTableOptions()));
DestroyAndReopen(options);
for (int i = 0; i < 100; i++) {
ASSERT_OK(Put("key" + std::to_string(i),
std::string(100, 'v') + std::to_string(i)));
}
// Skip all Add() calls to simulate entries not being committed (builder
// stays empty), as happens when fault injection causes early returns.
SyncPoint::GetInstance()->SetCallBack(
"BlockBasedTableBuilder::Add::skip",
[&](void* skip) { *(bool*)skip = true; });
// Inject an IOError into the builder's status before the empty check.
// This simulates the scenario where write fault injection puts the builder
// in an error state, causing all Add() calls to return early (ok() is false)
// and leaving the builder empty.
SyncPoint::GetInstance()->SetCallBack(
"BuildTable:BeforeCheckEmpty", [&](void* arg) {
auto* builder = static_cast<BlockBasedTableBuilder*>(
static_cast<TableBuilder*>(arg));
builder->TEST_InjectIOError();
});
SyncPoint::GetInstance()->EnableProcessing();
Status s = Flush();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// With the fix, the builder's IOError is propagated instead of the
// misleading Corruption from the key count mismatch check.
ASSERT_NOK(s);
ASSERT_TRUE(s.IsIOError())
<< "Expected IOError from builder error propagation, got: "
<< s.ToString();
Close();
}
// Regression test for table cache leak when flush install fails.
// BuildTable caches the flush output file (for user reads) but if the
// subsequent TryInstallMemtableFlushResults (LogAndApply) fails, the
// cache entry was never evicted. The FindObsoleteFiles backstop normally
// catches this, but fails under metadata read fault injection
// (open_metadata_read_fault_one_in), causing
// TEST_VerifyNoObsoleteFilesCached to fire during Close().
TEST_F(DBFlushTest, LeakedTableCacheEntryOnFlushInstallFailure) {
auto fault_fs = std::make_shared<FaultInjectionTestFS>(env_->GetFileSystem());
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
Options options = CurrentOptions();
options.env = fault_env.get();
options.paranoid_file_checks = true;
// Disable auto-recovery so it doesn't clean up the orphan on a separate
// thread where metadata faults aren't enabled.
options.max_bgerror_resume_count = 0;
DestroyAndReopen(options);
// Write data so flush produces a non-empty file.
ASSERT_OK(Put("a", std::string(1024, 'x')));
// After BuildTable succeeds (caching the output file), inject a failure
// before the install-or-rollback decision. This simulates scenarios like
// CF dropped or shutdown after BuildTable. Also deactivate the filesystem
// so the post-flush FindObsoleteFiles backstop cannot scan the directory.
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"FlushJob::Run:PostBuildTable", [&](void* arg) {
*static_cast<Status*>(arg) = Status::IOError("injected");
fault_fs->SetFilesystemActive(false);
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Trigger flush — BuildTable succeeds but LogAndApply fails.
Status s = Flush();
ASSERT_NOK(s);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
// With the fix, the leaked cache entry was already evicted by
// ReleaseObsolete in FlushJob::Run(). Without the fix, it persists.
// We can verify by counting non-live cache entries. The quarantine
// includes the file (BG error is active), but that's only a workaround --
// the cache entry should not exist at all with the fix.
//
// Check: with the fix, the table cache should be empty (the flush output
// was never installed and the fix evicted it). Without the fix, there's
// a leaked entry.
int cache_entries = 0;
auto count_fn = [&cache_entries](const Slice&, Cache::ObjectPtr, size_t,
const Cache::CacheItemHelper*) {
cache_entries++;
};
dbfull()->TEST_table_cache()->ApplyToAllEntries(count_fn, {});
ASSERT_EQ(cache_entries, 0)
<< "Leaked table cache entry for flush output that was never installed";
// Reactivate filesystem for clean shutdown.
fault_fs->SetFilesystemActive(true);
db_ = nullptr; // destructor handles Close
}
// Verify that ConstructFragmentedRangeTombstones runs after MarkImmutable
// in SwitchMemtable. A range tombstone inserted between construction and
// immutability would cause a flush entry count mismatch.
TEST_F(DBFlushTest, FlushAfterReadPathRangeTombstoneInsertion) {
Options options = CurrentOptions();
options.min_tombstones_for_range_conversion = 2;
options.flush_verify_memtable_count = true;
options.statistics = CreateDBStatistics();
DestroyAndReopen(options);
// Write base data and flush to L0.
for (char c = 'a'; c <= 'h'; c++) {
ASSERT_OK(Put(std::string(1, c), std::string("v") + c));
}
ASSERT_OK(Flush());
// Delete contiguous keys in the memtable.
for (const auto& key : {"b", "c", "d", "e"}) {
ASSERT_OK(Delete(key));
}
// When SwitchMemtable hits the sync point after
// ConstructFragmentedRangeTombstones, directly call
// AddLogicallyRedundantRangeTombstone on the memtable being switched.
// We can't create an iterator here (would deadlock on the DB mutex).
auto* cfh = static_cast<ColumnFamilyHandleImpl*>(db_->DefaultColumnFamily());
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::SwitchMemtable:AfterConstructFragmentedRangeTombstones",
[&](void*) {
// Try to insert a range tombstone. With the fix (Construct after
// MarkImmutable), this returns false because the memtable is
// already immutable. Without the fix, this succeeds and creates
// an entry count mismatch.
// Try to insert a range tombstone. With the fix (Construct after
// MarkImmutable), this returns false because the memtable is
// already immutable. Without the fix, this succeeds and creates
// an entry count mismatch.
cfh->cfd()->mem()->AddLogicallyRedundantRangeTombstone(
1 /* seq */, "b", "f", cfh->cfd()->GetIngestSstLock());
});
SyncPoint::GetInstance()->EnableProcessing();
Status s = Flush();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_OK(s);
ASSERT_EQ(Get("a"), "va");
ASSERT_EQ(Get("b"), "NOT_FOUND");
ASSERT_EQ(Get("d"), "NOT_FOUND");
ASSERT_EQ(Get("f"), "vf");
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+34 -62
View File
@@ -5,7 +5,6 @@
#include "db/db_impl/compacted_db_impl.h"
#include "db/blob/blob_fetcher.h"
#include "db/db_impl/db_impl.h"
#include "db/version_set.h"
#include "logging/logging.h"
@@ -76,16 +75,13 @@ Status CompactedDBImpl::Get(const ReadOptions& _read_options,
}
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
BlobFetcher blob_fetcher(version_, read_options);
std::string* ts =
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr;
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
GetContext get_context(user_comparator_, nullptr, nullptr, nullptr,
GetContext::kNotFound, lkey.user_key(), value,
/*columns=*/nullptr, ts, nullptr, nullptr, true,
nullptr, nullptr, nullptr, nullptr, &read_cb,
nullptr /* is_blob_index */, 0 /* tracing_get_id */,
&blob_fetcher);
nullptr, nullptr, nullptr, nullptr, &read_cb);
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
if (user_comparator_->CompareWithoutTimestamp(
@@ -95,21 +91,8 @@ Status CompactedDBImpl::Get(const ReadOptions& _read_options,
/*b_has_ts=*/false) < 0) {
return Status::NotFound();
}
TableReader* t = nullptr;
TableCache::TypedHandle* handle = nullptr;
Status s = cfd_->table_cache()->FindTable(
read_options, cfd_->table_cache()->file_options(),
cfd_->internal_comparator(), *f.file_metadata, &handle,
version_->GetMutableCFOptions(), &t, false /* no_io */,
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
true /* prefetch_index_and_filter_in_cache */,
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
true /* pin_table_handle */);
if (s.ok()) {
assert(handle == nullptr);
// Use TableReader directly to avoid extra table_cache->Get() overheads
s = t->Get(read_options, lkey.internal_key(), &get_context, nullptr);
}
Status s = f.fd.table_reader->Get(read_options, lkey.internal_key(),
&get_context, nullptr);
if (!s.ok() && !s.IsNotFound()) {
return s;
}
@@ -169,7 +152,7 @@ void CompactedDBImpl::MultiGet(const ReadOptions& _read_options,
}
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
BlobFetcher blob_fetcher(version_, read_options);
autovector<TableReader*, 16> reader_list;
for (size_t i = 0; i < num_keys; ++i) {
const Slice& key = keys[i];
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
@@ -179,41 +162,39 @@ void CompactedDBImpl::MultiGet(const ReadOptions& _read_options,
ExtractUserKeyAndStripTimestamp(f.smallest_key,
user_comparator_->timestamp_size()),
/*b_has_ts=*/false) < 0) {
statuses[i] = Status::NotFound();
continue;
}
PinnableSlice& pinnable_val = values[i];
std::string* timestamp = timestamps ? &timestamps[i] : nullptr;
GetContext get_context(
user_comparator_, nullptr, nullptr, nullptr, GetContext::kNotFound,
lkey.user_key(), &pinnable_val, /*columns=*/nullptr,
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr, nullptr,
nullptr, true, nullptr, nullptr, nullptr, nullptr, &read_cb,
nullptr /* is_blob_index */, 0 /* tracing_get_id */, &blob_fetcher);
TableReader* t = nullptr;
TableCache::TypedHandle* handle = nullptr;
Status status = cfd_->table_cache()->FindTable(
read_options, cfd_->table_cache()->file_options(),
cfd_->internal_comparator(), *f.file_metadata, &handle,
version_->GetMutableCFOptions(), &t, false /* no_io */,
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
true /* prefetch_index_and_filter_in_cache */,
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
true /* pin_table_handle */);
if (status.ok()) {
assert(handle == nullptr);
// Use TableReader directly to avoid extra table_cache->Get() overheads
status = t->Get(read_options, lkey.internal_key(), &get_context, nullptr);
}
if (!status.ok() && !status.IsNotFound()) {
statuses[i] = status;
} else if (get_context.State() == GetContext::kFound) {
statuses[i] = Status::OK();
reader_list.push_back(nullptr);
} else {
statuses[i] = Status::NotFound();
f.fd.table_reader->Prepare(lkey.internal_key());
reader_list.push_back(f.fd.table_reader);
}
}
for (size_t i = 0; i < num_keys; ++i) {
statuses[i] = Status::NotFound();
}
int idx = 0;
for (auto* r : reader_list) {
if (r != nullptr) {
PinnableSlice& pinnable_val = values[idx];
LookupKey lkey(keys[idx], kMaxSequenceNumber, read_options.timestamp);
std::string* timestamp = timestamps ? &timestamps[idx] : nullptr;
GetContext get_context(
user_comparator_, nullptr, nullptr, nullptr, GetContext::kNotFound,
lkey.user_key(), &pinnable_val, /*columns=*/nullptr,
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr, nullptr,
nullptr, true, nullptr, nullptr, nullptr, nullptr, &read_cb);
Status status =
r->Get(read_options, lkey.internal_key(), &get_context, nullptr);
assert(static_cast<size_t>(idx) < num_keys);
if (!status.ok() && !status.IsNotFound()) {
statuses[idx] = status;
} else {
if (get_context.State() == GetContext::kFound) {
statuses[idx] = Status::OK();
}
}
}
++idx;
}
}
Status CompactedDBImpl::Init(const Options& options) {
@@ -249,7 +230,6 @@ Status CompactedDBImpl::Init(const Options& options) {
return Status::NotSupported("Both L0 and other level contain files");
}
files_ = l0;
files_level_ = 0;
return Status::OK();
}
@@ -262,7 +242,6 @@ Status CompactedDBImpl::Init(const Options& options) {
int level = vstorage->num_non_empty_levels() - 1;
if (vstorage->LevelFilesBrief(level).num_files > 0) {
files_ = vstorage->LevelFilesBrief(level);
files_level_ = level;
return Status::OK();
}
return Status::NotSupported("no file exists");
@@ -282,13 +261,6 @@ Status CompactedDBImpl::Open(const Options& options, const std::string& dbname,
std::unique_ptr<CompactedDBImpl> db(new CompactedDBImpl(db_options, dbname));
Status s = db->Init(options);
if (s.ok()) {
{
InstrumentedMutexLock l(&db->mutex_);
db->opened_successfully_ = true;
if (db->immutable_db_options_.open_files_async) {
db->ScheduleAsyncFileOpening();
}
}
s = db->StartPeriodicTaskScheduler();
}
if (s.ok()) {
+1 -2
View File
@@ -129,7 +129,7 @@ class CompactedDBImpl : public DBImpl {
// Share with DBImplReadOnly?
protected:
Status FlushForGetLiveFiles(bool /*force_atomic_flush*/) override {
Status FlushForGetLiveFiles() override {
// No-op for read-only DB
return Status::OK();
}
@@ -143,6 +143,5 @@ class CompactedDBImpl : public DBImpl {
Version* version_;
const Comparator* user_comparator_;
LevelFilesBrief files_;
int files_level_{0};
};
} // namespace ROCKSDB_NAMESPACE
+51 -734
View File
File diff suppressed because it is too large. Load diff
+19 -250
View File
@@ -260,10 +260,6 @@ class DBImpl : public DB {
const WriteOptions& options,
std::shared_ptr<WriteBatchWithIndex> wbwi) override;
// Returns true if any live column family currently has blob direct write
// enabled.
bool HasAnyBlobDirectWriteColumnFamily();
using DB::Get;
Status Get(const ReadOptions& _read_options,
ColumnFamilyHandle* column_family, const Slice& key,
@@ -1252,7 +1248,6 @@ class DBImpl : public DB {
int TEST_BGCompactionsAllowed() const;
int TEST_BGFlushesAllowed() const;
int TEST_NumRunningBottomCompactions() const;
size_t TEST_GetWalPreallocateBlockSize(uint64_t write_buffer_size) const;
void TEST_WaitForPeriodicTaskRun(std::function<void()> callback) const;
SeqnoToTimeMapping TEST_GetSeqnoToTimeMapping() const;
@@ -1371,7 +1366,6 @@ class DBImpl : public DB {
protected:
const std::string dbname_;
const bool read_only_;
// TODO(peterd): unify with VersionSet::db_id_
std::string db_id_;
// db_session_id_ is an identifier that gets reset
@@ -1536,7 +1530,7 @@ class DBImpl : public DB {
Status FlushAllColumnFamilies(const FlushOptions& flush_options,
FlushReason flush_reason);
virtual Status FlushForGetLiveFiles(bool force_atomic_flush = false);
virtual Status FlushForGetLiveFiles();
void NewThreadStatusCfInfo(ColumnFamilyData* cfd) const;
@@ -1556,17 +1550,10 @@ class DBImpl : public DB {
// @param memtable_updated Whether the same write that ingests wbwi has
// updated memtable. This is useful for determining whether to set bg
// error when IngestWBWIAsMemtable fails.
// @param ingest_wbwi_for_commit Whether wbwi ingestion is publishing the
// committed data of a prepared transaction. This means a failure can leave
// committed data durable in WAL but not published in memtables.
// @param ignore_missing_cf If true, skip column families not found in the DB
// instead of returning an error.
Status IngestWBWIAsMemtable(std::shared_ptr<WriteBatchWithIndex> wbwi,
const WBWIMemTable::SeqnoRange& assigned_seqno,
uint64_t min_prep_log, SequenceNumber last_seqno,
bool memtable_updated,
bool ingest_wbwi_for_commit,
bool ignore_missing_cf);
bool memtable_updated, bool ignore_missing_cf);
// If disable_memtable is set the application logic must guarantee that the
// batch will still be skipped from memtable during the recovery. An excption
@@ -1589,131 +1576,21 @@ class DBImpl : public DB {
// See more in comment above PreReleaseCallback::Callback().
// post_memtable_callback is called after memtable write but before publishing
// the sequence number to readers.
// `trace_batch_override`, when non-null, supplies the logical batch to trace
// instead of `updates`. Fast paths may rewrite or rebuild `updates` before
// apply, but tracing should still record the original user-visible batch.
// `skip_blob_direct_write_transform` indicates the caller has already
// performed any needed batch-level blob direct-write preprocessing.
// `deferred_put_entities`, when non-null, carries structured `PutEntity()`
// ops that should be materialized into `updates` after `PreprocessWrite()`
// has selected the target memtable/blob generation.
//
// The main write queue. This is the only write queue that updates
// LastSequence. When using one write queue, the same sequence also indicates
// the last published sequence.
struct DeferredPutEntityBatch;
Status WriteImpl(
const WriteOptions& options, WriteBatch* updates,
WriteCallback* callback = nullptr,
UserWriteCallback* user_write_cb = nullptr, uint64_t* wal_used = nullptr,
uint64_t log_ref = 0, bool disable_memtable = false,
uint64_t* seq_used = nullptr, size_t batch_cnt = 0,
PreReleaseCallback* pre_release_callback = nullptr,
PostMemTableCallback* post_memtable_callback = nullptr,
std::shared_ptr<WriteBatchWithIndex> wbwi = nullptr,
WriteBatch* trace_batch_override = nullptr,
bool skip_blob_direct_write_transform = false,
const DeferredPutEntityBatch* deferred_put_entities = nullptr);
// Per-WriteImpl state that keeps BDW column families pinned through
// referenced SuperVersions until the transformed write either commits or
// rolls back.
struct BlobDirectWriteContext;
// High-level `PutEntity()` fast-path flow:
//
// non-BDW target CF:
// PutEntityFastPath()
// -> AppendPreprocessedPutEntityToBatch()
// -> WritePreprocessedPutEntityBatch()
// -> WriteImpl(skip_blob_direct_write_transform = true)
//
// BDW-enabled target CF:
// PutEntityFastPath()
// -> sort columns and stash DeferredPutEntityBatch::Op
// -> WritePreprocessedPutEntityBatch()
// -> WriteImpl(..., deferred_put_entities)
// -> after PreprocessWrite() picks the memtable/blob generation:
// AppendSortedPutEntityToBatch()
//
// The BDW branch defers final serialization until `WriteImpl()` knows the
// target memtable/blob generation, avoiding the old
// serialize -> deserialize -> serialize cycle on the write hot path.
// Staging buffer for the `PutEntity()` fast path when blob direct write may
// need to rewrite wide-column values. `PutEntityFastPath()` records the
// logical operations here instead of serializing them into a `WriteBatch`
// immediately, then passes this struct to `WriteImpl()`. Once
// `PreprocessWrite()` has selected the memtable/blob-file generation,
// `WriteImpl()` materializes each op directly into the final batch with blob
// direct-write preprocessing applied. This avoids the old
// serialize-deserialize-serialize cycle on the write hot path.
struct DeferredPutEntityBatch {
// The fast path sorts columns up front so `WriteImpl()` can append the
// final serialized entity without redoing that work.
struct Op {
// Target column family for the logical `PutEntity()` op.
uint32_t column_family_id = 0;
// Owned key bytes kept alive until `WriteImpl()` rebuilds the batch.
std::string key;
// Pre-sorted columns ready for final serialization and blob
// direct-write preprocessing.
WideColumns sorted_columns;
};
// Logical `PutEntity()` ops collected by the fast path and replayed into
// the final `WriteBatch` inside `WriteImpl()`.
std::vector<Op> ops;
};
// Rewrites a write batch for blob direct write when the current DB and batch
// shape are compatible, recording touched managers and rollback metadata in
// `blob_direct_write_ctx`.
Status MaybeTransformBatchForBlobDirectWrite(
const WriteOptions& write_options, WriteBatch** batch,
bool should_write_to_memtable, bool lookup_from_write_thread,
WriteBatch* transformed_storage,
BlobDirectWriteContext* blob_direct_write_ctx);
// Sorts and preprocesses one `PutEntity()` op, then appends the final
// serialized form to `batch`.
Status AppendPreprocessedPutEntityToBatch(
const WriteOptions& write_options, WriteBatch* batch,
ColumnFamilyHandle* column_family, const Slice& key,
const WideColumns& columns,
BlobDirectWriteContext* blob_direct_write_ctx);
// Appends a `PutEntity()` op whose columns are already sorted, allowing the
// fast path to avoid re-sorting before final serialization.
Status AppendSortedPutEntityToBatch(
const WriteOptions& write_options, WriteBatch* batch,
uint32_t column_family_id, const Slice& key,
const WideColumns& sorted_columns,
BlobDirectWriteContext* blob_direct_write_ctx);
// Specialized `PutEntity()` write path for a single column family. This
// avoids building an intermediate serialized entity when blob direct write
// needs to rewrite the final batch inside `WriteImpl()`.
Status PutEntityFastPath(const WriteOptions& write_options,
ColumnFamilyHandle* column_family, const Slice& key,
const WideColumns& columns);
// Specialized `PutEntity(AttributeGroups)` path that defers final batch
// materialization until `WriteImpl()` when any target CF may use blob direct
// write.
Status PutEntityFastPath(const WriteOptions& write_options, const Slice& key,
const AttributeGroups& attribute_groups);
// Writes a batch prepared by the `PutEntity()` fast path. When
// `deferred_put_entities` is present, `WriteImpl()` rebuilds `batch`
// internally after `PreprocessWrite()` and disables normal write batching to
// keep the staging state local to this write. `trace_batch` stays separate
// so tracing can still emit the logical `PutEntity()` batch even when the
// applied batch is materialized later inside `WriteImpl()`.
Status WritePreprocessedPutEntityBatch(
const WriteOptions& write_options, WriteBatch* batch,
WriteBatch* trace_batch,
const DeferredPutEntityBatch* deferred_put_entities = nullptr);
// Flushes or syncs all blob direct-write managers touched by the current
// transformed write before the write can proceed.
Status SyncBlobDirectWriteManagers(
const WriteOptions& write_options,
const BlobDirectWriteContext& blob_direct_write_ctx);
Status WriteImpl(const WriteOptions& options, WriteBatch* updates,
WriteCallback* callback = nullptr,
UserWriteCallback* user_write_cb = nullptr,
uint64_t* wal_used = nullptr, uint64_t log_ref = 0,
bool disable_memtable = false, uint64_t* seq_used = nullptr,
size_t batch_cnt = 0,
PreReleaseCallback* pre_release_callback = nullptr,
PostMemTableCallback* post_memtable_callback = nullptr,
std::shared_ptr<WriteBatchWithIndex> wbwi = nullptr);
Status PipelinedWriteImpl(const WriteOptions& options, WriteBatch* updates,
WriteBatch* trace_batch,
WriteCallback* callback = nullptr,
UserWriteCallback* user_write_cb = nullptr,
uint64_t* wal_used = nullptr, uint64_t log_ref = 0,
@@ -1742,7 +1619,7 @@ class DBImpl : public DB {
// marks start of a new sub-batch.
Status WriteImplWALOnly(
WriteThread* write_thread, const WriteOptions& options,
WriteBatch* updates, WriteBatch* trace_batch, WriteCallback* callback,
WriteBatch* updates, WriteCallback* callback,
UserWriteCallback* user_write_cb, uint64_t* wal_used,
const uint64_t log_ref, uint64_t* seq_used, const size_t sub_batch_cnt,
PreReleaseCallback* pre_release_callback, const AssignOrder assign_order,
@@ -1822,9 +1699,6 @@ class DBImpl : public DB {
Status FailIfCfHasTs(const ColumnFamilyHandle* column_family) const;
Status FailIfTsMismatchCf(ColumnFamilyHandle* column_family,
const Slice& ts) const;
Status FailIfTableFilterWithRangeConversion(
const ReadOptions& read_options,
const MutableCFOptions& mutable_cf_options) const;
// Check that the read timestamp `ts` is at or above the `full_history_ts_low`
// timestamp in a `SuperVersion`. It's necessary to do this check after
@@ -1844,17 +1718,6 @@ class DBImpl : public DB {
// recovery.
Status LogAndApplyForRecovery(const RecoveryContext& recovery_ctx);
// Schedule background work to open and validate SST files asynchronously.
// Called when open_files_async is enabled.
void ScheduleAsyncFileOpening();
// Mark async file opening as not needed. Used by subclasses that load
// table files through a different mechanism (e.g., ReactiveVersionSet).
void MarkAsyncFileOpenNotNeeded();
// Background work function for async file opening.
static void BGWorkAsyncFileOpen(void* arg);
void InvokeWalFilterIfNeededOnColumnFamilyToWalNumberMap();
// Return true to proceed with current WAL record whose content is stored in
@@ -1865,12 +1728,8 @@ class DBImpl : public DB {
Status& status, bool& stop_replay,
WriteBatch& batch);
// Indicate DB was opened successfully
bool opened_successfully_ = false;
private:
friend class DB;
friend class DBImplSecondary;
friend class ErrorHandler;
friend class InternalStats;
friend class PessimisticTransaction;
@@ -1903,21 +1762,6 @@ class DBImpl : public DB {
friend class CompactionServiceTest_PreservedOptionsRemoteCompaction_Test;
#endif
// Same as HasAnyBlobDirectWriteColumnFamily(), but requires `mutex_` held.
bool HasAnyBlobDirectWriteColumnFamilyWithLockHeld();
// Returns true if any BDW column family still owns blob files that have not
// yet been made visible through MANIFEST. Requires `mutex_` held.
bool HasInFlightBlobDirectWriteFilesWithLockHeld();
// Creates and attaches the per-CF blob direct-write partition manager when
// the column family is opened with the feature enabled.
void MaybeInitBlobDirectWriteColumnFamily(
ColumnFamilyData* cfd, const ColumnFamilyOptions& cf_options,
const std::string& column_family_name);
// Increments the DB-level count of live blob direct-write column families.
void RegisterBlobDirectWriteColumnFamily();
// Decrements the DB-level count of live blob direct-write column families.
void UnregisterBlobDirectWriteColumnFamily();
struct CompactionState;
struct PrepickedCompaction;
struct PurgeFileInfo;
@@ -2055,12 +1899,11 @@ class DBImpl : public DB {
flush_reason_(FlushReason::kOthers) {}
BGFlushArg(ColumnFamilyData* cfd, uint64_t max_memtable_id,
SuperVersionContext* superversion_context,
FlushReason flush_reason, bool atomic_flush)
FlushReason flush_reason)
: cfd_(cfd),
max_memtable_id_(max_memtable_id),
superversion_context_(superversion_context),
flush_reason_(flush_reason),
atomic_flush_(atomic_flush) {}
flush_reason_(flush_reason) {}
// Column family to flush.
ColumnFamilyData* cfd_;
@@ -2072,8 +1915,6 @@ class DBImpl : public DB {
// requires a SuperVersionContext object (currently embedded in JobContext).
SuperVersionContext* superversion_context_;
FlushReason flush_reason_;
// Whether this flush should use atomic flush code path.
bool atomic_flush_ = false;
};
// Argument passed to flush thread.
@@ -2193,10 +2034,6 @@ class DBImpl : public DB {
void DeleteObsoleteFileImpl(int job_id, const std::string& fname,
const std::string& path_to_sync, FileType type,
uint64_t number);
// Returns true when a blob file must be preserved because it is still
// tracked by blob direct write or is still footer-less on disk.
bool ShouldKeepBlobFileDuringPurge(uint64_t number,
const std::string& blob_file_path);
// Background process needs to call
// auto x = CaptureCurrentFileNumberInPendingOutputs()
@@ -2313,7 +2150,7 @@ class DBImpl : public DB {
Status InsertLogRecordToMemtable(WriteBatch* batch_to_use,
uint64_t wal_number,
SequenceNumber* next_sequence,
bool* has_valid_writes, bool read_only);
bool* has_valid_writes);
Status MaybeWriteLevel0TableForRecovery(
bool has_valid_writes, bool read_only, uint64_t wal_number, int job_id,
@@ -2579,13 +2416,8 @@ class DBImpl : public DB {
void MaybeScheduleFlushOrCompaction();
BackgroundJobPressure CaptureBackgroundJobPressure() const;
void NotifyOnBackgroundJobPressureChanged();
struct FlushRequest {
FlushReason flush_reason;
// Whether this flush request should use the atomic flush code path.
bool atomic_flush = false;
// A map from column family to flush to largest memtable id to persist for
// each column family. Once all the memtables whose IDs are smaller than or
// equal to this per-column-family specified value, this flush request is
@@ -2674,13 +2506,6 @@ class DBImpl : public DB {
// Schedule background tasks
Status StartPeriodicTaskScheduler();
// Compute the repeat period for the kTriggerCompaction task, which ensures
// compactions not dependent on writes (flushes) are eventually triggered when
// there are no writes (flushes). NOT thread safe; only called during DB open
// (StartPeriodicTaskScheduler). KNOWN LIMITATION: doesn't get updated with
// dynamic option updates. (Probably not worth the extra complexity.)
uint64_t ComputeTriggerCompactionPeriod();
// Cancel scheduled periodic tasks
Status CancelPeriodicTaskScheduler();
@@ -2744,8 +2569,6 @@ class DBImpl : public DB {
Status MaybeReleaseTimestampedSnapshotsAndCheck();
Status MaybeWriteWalMarkersToManifestOnClose();
Status CloseHelper();
void WaitForBackgroundWork();
@@ -2923,30 +2746,6 @@ class DBImpl : public DB {
std::string ts_low);
bool ShouldReferenceSuperVersion(const MergeContext& merge_context);
// Resolves a plain value whose current payload is an encoded direct-write
// blob index, either through `value` directly or through the default-column
// view layered on `columns`.
static Status ResolveDirectWritePlainValue(const ReadOptions& read_options,
const Slice& key,
const Version* current,
ColumnFamilyData* cfd,
PinnableSlice* value,
PinnableWideColumns* columns);
// Resolves each unresolved direct-write blob-valued column in `columns` and
// rebuilds the serialized wide-column entity in place.
static Status ResolveDirectWriteWideColumns(const ReadOptions& read_options,
const Slice& key,
const Version* current,
ColumnFamilyData* cfd,
PinnableWideColumns* columns);
// Dispatches between plain-value and wide-column direct-write resolution for
// read results observed before flush makes the corresponding blob file
// visible through normal Version metadata.
static bool MaybeResolveDirectWriteValue(
const ReadOptions& read_options, const Slice& key,
bool resolve_direct_write_value, const Version* current,
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns,
Status* s, bool* is_blob_index, bool* value_found = nullptr);
template <typename IterType, typename ImplType,
typename ErrorIteratorFuncType>
@@ -3252,9 +3051,6 @@ class DBImpl : public DB {
// stores the number of compactions are currently running
int num_running_compactions_ = 0;
// stores the number of BOTTOM-priority compactions currently running
int num_running_bottom_compactions_ = 0;
// number of background memtable flush jobs, submitted to the HIGH pool
int bg_flush_scheduled_ = 0;
@@ -3264,19 +3060,6 @@ class DBImpl : public DB {
// number of background obsolete file purge jobs, submitted to the HIGH pool
int bg_purge_scheduled_ = 0;
// number of pressure callbacks currently in progress (for destructor safety)
int bg_pressure_callback_in_progress_ = 0;
enum class AsyncFileOpenState : uint8_t {
kNotScheduled = 0, // Async file opening has not been scheduled.
kScheduled, // Async file opening is in-flight in the HIGH pool.
kComplete, // Async file opening has finished (or was not needed).
};
// Tracks whether background async file opening has been scheduled/completed.
AsyncFileOpenState bg_async_file_open_state_ =
AsyncFileOpenState::kNotScheduled;
std::deque<ManualCompactionState*> manual_compaction_dequeue_;
// shall we disable deletion of obsolete files
@@ -3308,11 +3091,6 @@ class DBImpl : public DB {
// Used when disableWAL is true.
std::atomic<bool> has_unpersisted_data_{false};
// Number of live column families with an active blob direct write partition
// manager. This keeps the read/write fast paths at O(1) when the feature is
// completely disabled for the DB.
std::atomic<uint32_t> blob_direct_write_cf_count_{0};
// if an attempt was made to flush all column families that
// the oldest log depends on but uncommitted data in the oldest
// log prevents the log from being released.
@@ -3337,6 +3115,9 @@ class DBImpl : public DB {
// Guard against multiple concurrent refitting
bool refitting_level_ = false;
// Indicate DB was opened successfully
bool opened_successfully_ = false;
// The min threshold to triggere bottommost compaction for removing
// garbages, among all column families.
SequenceNumber bottommost_files_mark_threshold_ = kMaxSequenceNumber;
@@ -3404,7 +3185,7 @@ class DBImpl : public DB {
// installed to MANIFEST first.
InstrumentedCondVar atomic_flush_install_cv_;
bool wal_in_db_path_ = false;
bool wal_in_db_path_;
std::atomic<uint64_t> max_total_wal_size_;
BlobFileCompletionCallback blob_callback_;
@@ -3541,18 +3322,6 @@ inline Status DBImpl::FailIfTsMismatchCf(ColumnFamilyHandle* column_family,
return Status::OK();
}
inline Status DBImpl::FailIfTableFilterWithRangeConversion(
const ReadOptions& read_options,
const MutableCFOptions& mutable_cf_options) const {
if (read_options.table_filter &&
mutable_cf_options.min_tombstones_for_range_conversion > 0) {
return Status::InvalidArgument(
"ReadOptions::table_filter is not supported when "
"min_tombstones_for_range_conversion > 0");
}
return Status::OK();
}
inline Status DBImpl::FailIfReadCollapsedHistory(const ColumnFamilyData* cfd,
const SuperVersion* sv,
const Slice& ts) const {
+18 -267
View File
@@ -8,9 +8,7 @@
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include <cinttypes>
#include <deque>
#include <unordered_map>
#include "db/blob/blob_file_partition_manager.h"
#include "db/builder.h"
#include "db/db_impl/db_impl.h"
#include "db/error_handler.h"
@@ -217,8 +215,7 @@ Status DBImpl::FlushMemTableToOutputFile(
&event_logger_, mutable_cf_options.report_bg_io_stats,
true /* sync_output_directory */, true /* write_manifest */, thread_pri,
io_tracer_, cfd->GetSuperVersion()->ShareSeqnoToTimeMapping(), db_id_,
db_session_id_, cfd->GetFullHistoryTsLow(), &blob_callback_,
mutable_db_options_.fast_sst_open);
db_session_id_, cfd->GetFullHistoryTsLow(), &blob_callback_);
FileMetaData file_meta;
Status s;
@@ -282,49 +279,12 @@ Status DBImpl::FlushMemTableToOutputFile(
flush_reason);
bool switched_to_mempurge = false;
size_t prepared_blob_generations = 0;
std::vector<uint64_t> sealed_blob_numbers;
// Within flush_job.Run, rocksdb may call event listener to notify
// file creation and deletion.
//
// Note that flush_job.Run will unlock and lock the db_mutex,
// and EventListener callback will be called when the db_mutex
// is unlocked by the current thread.
if (s.ok()) {
auto mgr_handle = cfd->blob_partition_manager_handle();
auto* mgr = mgr_handle.get();
if (mgr != nullptr) {
prepared_blob_generations = flush_job.GetMemTables().size();
std::vector<BlobFileAddition> write_path_additions;
std::vector<BlobFileGarbage> write_path_garbages;
std::vector<std::vector<uint64_t>> generation_blob_file_numbers;
mutex_.Unlock();
s = mgr->PrepareFlushAdditions(
write_options, prepared_blob_generations, &write_path_additions,
&write_path_garbages, &generation_blob_file_numbers);
mutex_.Lock();
if (!s.ok()) {
prepared_blob_generations = 0;
} else if (!write_path_additions.empty()) {
const auto& memtables = flush_job.GetMemTables();
assert(generation_blob_file_numbers.size() == memtables.size());
for (size_t i = 0; i < generation_blob_file_numbers.size(); ++i) {
// Old SuperVersions can keep these memtables alive after the flush
// commits, so keep their sealed direct-write blob files protected
// from obsolete-file purge until the memtable is finally released.
memtables[i]->ProtectSealedBlobFiles(mgr_handle,
generation_blob_file_numbers[i]);
}
for (const auto& addition : write_path_additions) {
sealed_blob_numbers.push_back(addition.GetBlobFileNumber());
}
flush_job.AddExternalBlobFileAdditions(std::move(write_path_additions));
flush_job.AddExternalBlobFileGarbages(std::move(write_path_garbages));
} else if (!write_path_garbages.empty()) {
flush_job.AddExternalBlobFileGarbages(std::move(write_path_garbages));
}
}
}
if (s.ok()) {
s = flush_job.Run(&logs_with_prep_tracker_, &file_meta,
&switched_to_mempurge, &skip_set_bg_error,
@@ -332,33 +292,10 @@ Status DBImpl::FlushMemTableToOutputFile(
need_cancel = false;
}
if (cfd->blob_partition_manager() != nullptr &&
prepared_blob_generations > 0) {
auto unconsumed_additions = flush_job.TakeExternalBlobFileAdditions();
auto unconsumed_garbages = flush_job.TakeExternalBlobFileGarbages();
if (!s.ok() || !unconsumed_additions.empty() ||
!unconsumed_garbages.empty()) {
// Skip committing this attempt, but keep the prepared generations queued
// in pending_generations_. The same immutable memtables still reference
// these sealed blob files, so a later flush retry must publish the exact
// same additions rather than abandoning them here.
prepared_blob_generations = 0;
sealed_blob_numbers.clear();
}
}
if (!s.ok() && need_cancel) {
flush_job.Cancel();
}
if (s.ok() && prepared_blob_generations > 0) {
auto* mgr = cfd->blob_partition_manager();
mgr->CommitPreparedGenerations(prepared_blob_generations);
if (!sealed_blob_numbers.empty()) {
mgr->RemoveFilePartitionMappings(sealed_blob_numbers);
}
}
if (s.ok()) {
InstallSuperVersionAndScheduleWork(cfd, superversion_context);
if (made_progress) {
@@ -451,12 +388,7 @@ Status DBImpl::FlushMemTableToOutputFile(
Status DBImpl::FlushMemTablesToOutputFiles(
const autovector<BGFlushArg>& bg_flush_args, bool* made_progress,
JobContext* job_context, LogBuffer* log_buffer, Env::Priority thread_pri) {
#ifndef NDEBUG
for (const auto& bg_flush_arg : bg_flush_args) {
assert(bg_flush_arg.atomic_flush_ == bg_flush_args[0].atomic_flush_);
}
#endif /* !NDEBUG */
if (bg_flush_args[0].atomic_flush_) {
if (immutable_db_options_.atomic_flush) {
return AtomicFlushMemTablesToOutputFiles(
bg_flush_args, made_progress, job_context, log_buffer, thread_pri);
}
@@ -549,8 +481,7 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
false /* sync_output_directory */, false /* write_manifest */,
thread_pri, io_tracer_,
cfd->GetSuperVersion()->ShareSeqnoToTimeMapping(), db_id_,
db_session_id_, cfd->GetFullHistoryTsLow(), &blob_callback_,
mutable_db_options_.fast_sst_open));
db_session_id_, cfd->GetFullHistoryTsLow(), &blob_callback_));
}
std::vector<FileMetaData> file_meta(num_cfs);
@@ -599,13 +530,11 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
// <bool /* executed */, Status /* status code */>
autovector<std::pair<bool, Status>> exec_status;
std::vector<bool> pick_status;
std::vector<size_t> prepared_blob_generations(num_cfs, 0);
for (int i = 0; i != num_cfs; ++i) {
// Initially all jobs are not executed, with status OK.
exec_status.emplace_back(false, Status::OK());
pick_status.push_back(false);
}
std::unordered_map<int, std::vector<uint64_t>> sealed_blob_numbers_by_cf;
bool flush_for_recovery =
bg_flush_args[0].flush_reason_ == FlushReason::kErrorRecovery ||
@@ -629,46 +558,6 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
}
}
if (s.ok()) {
for (int i = 0; i != num_cfs; ++i) {
auto mgr_handle = cfds[i]->blob_partition_manager_handle();
auto* mgr = mgr_handle.get();
if (mgr == nullptr) {
continue;
}
prepared_blob_generations[i] = jobs[i]->GetMemTables().size();
std::vector<BlobFileAddition> write_path_additions;
std::vector<BlobFileGarbage> write_path_garbages;
std::vector<std::vector<uint64_t>> generation_blob_file_numbers;
mutex_.Unlock();
s = mgr->PrepareFlushAdditions(
write_options, prepared_blob_generations[i], &write_path_additions,
&write_path_garbages, &generation_blob_file_numbers);
mutex_.Lock();
if (!s.ok()) {
prepared_blob_generations[i] = 0;
break;
}
if (!write_path_additions.empty()) {
const auto& memtables = jobs[i]->GetMemTables();
assert(generation_blob_file_numbers.size() == memtables.size());
for (size_t j = 0; j < generation_blob_file_numbers.size(); ++j) {
memtables[j]->ProtectSealedBlobFiles(mgr_handle,
generation_blob_file_numbers[j]);
}
auto& sealed_blob_numbers = sealed_blob_numbers_by_cf[i];
for (const auto& addition : write_path_additions) {
sealed_blob_numbers.push_back(addition.GetBlobFileNumber());
}
jobs[i]->AddExternalBlobFileAdditions(std::move(write_path_additions));
}
if (!write_path_garbages.empty()) {
jobs[i]->AddExternalBlobFileGarbages(std::move(write_path_garbages));
}
}
}
if (s.ok()) {
assert(switched_to_mempurge.size() ==
static_cast<long unsigned int>(num_cfs));
@@ -874,30 +763,6 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
directories_.GetDbDir(), log_buffer);
}
for (int i = 0; i != num_cfs; ++i) {
auto* mgr = cfds[i]->blob_partition_manager();
if (mgr == nullptr || prepared_blob_generations[i] == 0) {
continue;
}
auto unconsumed_additions = jobs[i]->TakeExternalBlobFileAdditions();
auto unconsumed_garbages = jobs[i]->TakeExternalBlobFileGarbages();
if (!s.ok() || !unconsumed_additions.empty() ||
!unconsumed_garbages.empty()) {
// Same retry semantics as the single-CF flush path above: do not
// consume these prepared generations yet, because the rolled-back
// memtables still need to reuse the same sealed blob files on retry.
prepared_blob_generations[i] = 0;
continue;
}
mgr->CommitPreparedGenerations(prepared_blob_generations[i]);
auto it = sealed_blob_numbers_by_cf.find(i);
if (it != sealed_blob_numbers_by_cf.end() && !it->second.empty()) {
mgr->RemoveFilePartitionMappings(it->second);
}
}
if (s.ok()) {
assert(num_cfs ==
static_cast<int>(job_context->superversion_contexts.size()));
@@ -2172,7 +2037,7 @@ Status DBImpl::FlushAllColumnFamilies(const FlushOptions& flush_options,
FlushReason flush_reason) {
mutex_.AssertHeld();
Status status;
if (immutable_db_options_.atomic_flush || flush_options.force_atomic_flush) {
if (immutable_db_options_.atomic_flush) {
mutex_.Unlock();
status = AtomicFlushMemTables(flush_options, flush_reason);
if (status.IsColumnFamilyDropped()) {
@@ -2205,7 +2070,7 @@ Status DBImpl::Flush(const FlushOptions& flush_options,
ROCKS_LOG_INFO(immutable_db_options_.info_log, "[%s] Manual flush start.",
cfh->GetName().c_str());
Status s;
if (immutable_db_options_.atomic_flush || flush_options.force_atomic_flush) {
if (immutable_db_options_.atomic_flush) {
s = AtomicFlushMemTables(flush_options, FlushReason::kManualFlush,
{cfh->cfd()});
} else {
@@ -2221,8 +2086,7 @@ Status DBImpl::Flush(const FlushOptions& flush_options,
Status DBImpl::Flush(const FlushOptions& flush_options,
const std::vector<ColumnFamilyHandle*>& column_families) {
Status s;
if (!immutable_db_options_.atomic_flush &&
!flush_options.force_atomic_flush) {
if (!immutable_db_options_.atomic_flush) {
for (auto cfh : column_families) {
s = Flush(flush_options, cfh);
if (!s.ok()) {
@@ -2517,7 +2381,7 @@ void DBImpl::GenerateFlushRequest(const autovector<ColumnFamilyData*>& cfds,
continue;
}
uint64_t max_memtable_id = cfd->imm()->GetLatestMemTableID(
req->atomic_flush /* for_atomic_flush */);
immutable_db_options_.atomic_flush /* for_atomic_flush */);
req->cfd_to_max_mem_id_to_persist.emplace(cfd, max_memtable_id);
}
}
@@ -2587,8 +2451,7 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
if (cfd->imm()->NumNotFlushed() != 0 || !cfd->mem()->IsEmpty() ||
!cached_recoverable_state_empty_.load() ||
IsRecoveryFlush(flush_reason)) {
FlushRequest req{
flush_reason, false /* atomic_flush */, {{cfd, flush_memtable_id}}};
FlushRequest req{flush_reason, {{cfd, flush_memtable_id}}};
flush_reqs.emplace_back(std::move(req));
memtable_ids_to_wait.emplace_back(
cfd->imm()->GetLatestMemTableID(false /* for_atomic_flush */));
@@ -2616,9 +2479,7 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
"to avoid holding old logs",
cfd->GetName().c_str());
s = SwitchMemtable(cfd_stats, &context);
FlushRequest req{flush_reason,
false /* atomic_flush */,
{{cfd_stats, flush_memtable_id}}};
FlushRequest req{flush_reason, {{cfd_stats, flush_memtable_id}}};
flush_reqs.emplace_back(std::move(req));
memtable_ids_to_wait.emplace_back(
cfd_stats->imm()->GetLatestMemTableID(
@@ -2697,6 +2558,7 @@ Status DBImpl::AtomicFlushMemTables(
const FlushOptions& flush_options, FlushReason flush_reason,
const autovector<ColumnFamilyData*>& provided_candidate_cfds,
bool entered_write_thread) {
assert(immutable_db_options_.atomic_flush);
if (!flush_options.wait && write_controller_.IsStopped()) {
std::ostringstream oss;
oss << "Writes have been stopped, thus unable to perform manual flush. "
@@ -2751,7 +2613,6 @@ Status DBImpl::AtomicFlushMemTables(
}
const bool needs_to_join_write_thread = !entered_write_thread;
FlushRequest flush_req;
flush_req.atomic_flush = true;
autovector<ColumnFamilyData*> cfds;
{
WriteContext context;
@@ -2859,7 +2720,6 @@ Status DBImpl::RetryFlushesForErrorRecovery(FlushReason flush_reason,
autovector<uint64_t> flush_memtable_ids;
if (immutable_db_options_.atomic_flush) {
FlushRequest flush_req;
flush_req.atomic_flush = true;
GenerateFlushRequest(cfds, flush_reason, &flush_req);
EnqueuePendingFlush(flush_req);
for (auto& iter : flush_req.cfd_to_max_mem_id_to_persist) {
@@ -2873,7 +2733,6 @@ Status DBImpl::RetryFlushesForErrorRecovery(FlushReason flush_reason,
// reason to do so outside of atomic flush.
FlushRequest flush_req{
flush_reason,
false /* atomic_flush */,
{{cfd,
std::numeric_limits<uint64_t>::max() /* max_mem_id_to_persist */}}};
if (EnqueuePendingFlush(flush_req)) {
@@ -3162,7 +3021,7 @@ void DBImpl::ResumeAllCompactions() {
void DBImpl::MaybeScheduleFlushOrCompaction() {
mutex_.AssertHeld();
TEST_SYNC_POINT("DBImpl::MaybeScheduleFlushOrCompaction:Start");
if (!opened_successfully_ || read_only_) {
if (!opened_successfully_) {
// Compaction may introduce data race to DB open
return;
}
@@ -3286,93 +3145,6 @@ DBImpl::BGJobLimits DBImpl::GetBGJobLimits(int max_background_flushes,
return res;
}
BackgroundJobPressure DBImpl::CaptureBackgroundJobPressure() const {
mutex_.AssertHeld();
BackgroundJobPressure snapshot;
// Compaction: LOW + BOTTOM combined (matches RocksDB's scheduling check)
snapshot.compaction_scheduled =
bg_compaction_scheduled_ + bg_bottom_compaction_scheduled_;
snapshot.compaction_running = num_running_compactions_;
// Per-priority breakdown for pool-specific expansion
assert(num_running_compactions_ >= num_running_bottom_compactions_);
snapshot.compaction_low_scheduled = bg_compaction_scheduled_;
snapshot.compaction_low_running =
std::max(0, num_running_compactions_ - num_running_bottom_compactions_);
snapshot.compaction_bottom_scheduled = bg_bottom_compaction_scheduled_;
snapshot.compaction_bottom_running = num_running_bottom_compactions_;
// Flush
snapshot.flush_scheduled = bg_flush_scheduled_;
snapshot.flush_running = num_running_flushes_;
snapshot.compaction_speedup_active =
write_controller_.NeedSpeedupCompaction();
// Compute write_stall_proximity_pct: max across all CFs.
// Uses the same inputs as RecalculateWriteStallConditions() in
// column_family.cc:
// - l0_delay_trigger_count(): sorted run count used for write stall
// decisions. Equals NumLevelFiles(0) for level compaction; for universal
// compaction, also counts non-empty levels from L1+ as sorted runs.
// - estimated_compaction_needed_bytes(): pending compaction bytes.
// Prefers slowdown/soft triggers (gradual); falls back to stop/hard triggers.
// level0_slowdown_writes_trigger is sanitized to always be > 0;
// soft_pending_compaction_bytes_limit can be 0 (disabled).
int max_proximity = 0;
for (auto cfd : *versions_->GetColumnFamilySet()) {
if (cfd->IsDropped() || !cfd->initialized()) {
continue;
}
const auto* vstorage = cfd->current()->storage_info();
const auto& mutable_cf_options = cfd->GetCurrentMutableCFOptions();
int l0_proximity = 0;
int l0_trigger = mutable_cf_options.level0_slowdown_writes_trigger > 0
? mutable_cf_options.level0_slowdown_writes_trigger
: mutable_cf_options.level0_stop_writes_trigger;
if (l0_trigger > 0) {
l0_proximity = static_cast<int>(
100.0 * vstorage->l0_delay_trigger_count() / l0_trigger);
}
int bytes_proximity = 0;
uint64_t bytes_limit =
mutable_cf_options.soft_pending_compaction_bytes_limit > 0
? mutable_cf_options.soft_pending_compaction_bytes_limit
: mutable_cf_options.hard_pending_compaction_bytes_limit;
if (bytes_limit > 0) {
bytes_proximity = static_cast<int>(
100.0 *
static_cast<double>(vstorage->estimated_compaction_needed_bytes()) /
static_cast<double>(bytes_limit));
}
max_proximity =
std::max(max_proximity, std::max(l0_proximity, bytes_proximity));
}
snapshot.write_stall_proximity_pct = max_proximity;
return snapshot;
}
void DBImpl::NotifyOnBackgroundJobPressureChanged() {
mutex_.AssertHeld();
if (immutable_db_options_.listeners.empty()) {
return;
}
BackgroundJobPressure pressure = CaptureBackgroundJobPressure();
bg_pressure_callback_in_progress_++;
mutex_.Unlock();
for (const auto& listener : immutable_db_options_.listeners) {
listener->OnBackgroundJobPressureChanged(this, pressure);
}
mutex_.Lock();
bg_pressure_callback_in_progress_--;
}
void DBImpl::AddToCompactionQueue(ColumnFamilyData* cfd) {
assert(!cfd->queued_for_compaction());
cfd->Ref();
@@ -3394,11 +3166,11 @@ DBImpl::FlushRequest DBImpl::PopFirstFromFlushQueue() {
assert(!flush_queue_.empty());
FlushRequest flush_req = std::move(flush_queue_.front());
flush_queue_.pop_front();
if (!flush_req.atomic_flush) {
if (!immutable_db_options_.atomic_flush) {
assert(flush_req.cfd_to_max_mem_id_to_persist.size() == 1);
}
for (const auto& elem : flush_req.cfd_to_max_mem_id_to_persist) {
if (!flush_req.atomic_flush) {
if (!immutable_db_options_.atomic_flush) {
ColumnFamilyData* cfd = elem.first;
assert(cfd);
assert(cfd->queued_for_flush());
@@ -3443,7 +3215,7 @@ bool DBImpl::EnqueuePendingFlush(const FlushRequest& flush_req) {
if (flush_req.cfd_to_max_mem_id_to_persist.empty()) {
return enqueued;
}
if (!flush_req.atomic_flush) {
if (!immutable_db_options_.atomic_flush) {
// For the non-atomic flush case, we never schedule multiple column
// families in the same flush request.
assert(flush_req.cfd_to_max_mem_id_to_persist.size() == 1);
@@ -3459,11 +3231,6 @@ bool DBImpl::EnqueuePendingFlush(const FlushRequest& flush_req) {
enqueued = true;
}
} else {
// Atomic flush requests bypass the queued_for_flush() deduplication guard.
// This means an atomic request and a non-atomic request for the same CF
// can coexist in the flush queue. This is safe because PickMemtablesToFlush
// uses flush_in_progress_ to prevent double-picking of memtables, and
// BackgroundFlush filters out CFs where !IsFlushPending().
for (auto& iter : flush_req.cfd_to_max_mem_id_to_persist) {
ColumnFamilyData* cfd = iter.first;
cfd->Ref();
@@ -3627,7 +3394,7 @@ Status DBImpl::BackgroundFlush(bool* made_progress, JobContext* job_context,
}
return status;
}
if (!flush_req.atomic_flush &&
if (!immutable_db_options_.atomic_flush &&
ShouldRescheduleFlushRequestToRetainUDT(flush_req)) {
assert(flush_req.cfd_to_max_mem_id_to_persist.size() == 1);
ColumnFamilyData* cfd =
@@ -3676,8 +3443,7 @@ Status DBImpl::BackgroundFlush(bool* made_progress, JobContext* job_context,
}
superversion_contexts.emplace_back(true);
bg_flush_args.emplace_back(cfd, max_memtable_id,
&(superversion_contexts.back()), flush_reason,
flush_req.atomic_flush);
&(superversion_contexts.back()), flush_reason);
}
// `MaybeScheduleFlushOrCompaction` schedules as many `BackgroundCallFlush`
// jobs as the number of `FlushRequest` in the `flush_queue_`, a.k.a
@@ -3814,9 +3580,6 @@ void DBImpl::BackgroundCallFlush(Env::Priority thread_pri) {
bg_flush_scheduled_--;
// See if there's more work to be done
MaybeScheduleFlushOrCompaction();
NotifyOnBackgroundJobPressureChanged();
atomic_flush_install_cv_.SignalAll();
bg_cv_.SignalAll();
// IMPORTANT: there should be no code after calling SignalAll. This call may
@@ -3841,9 +3604,6 @@ void DBImpl::BackgroundCallCompaction(PrepickedCompaction* prepicked_compaction,
InstrumentedMutexLock l(&mutex_);
num_running_compactions_++;
if (bg_thread_pri == Env::Priority::BOTTOM) {
num_running_bottom_compactions_++;
}
std::unique_ptr<std::list<uint64_t>::iterator>
pending_outputs_inserted_elem(new std::list<uint64_t>::iterator(
@@ -3926,15 +3686,11 @@ void DBImpl::BackgroundCallCompaction(PrepickedCompaction* prepicked_compaction,
} else {
assert(bg_thread_pri == Env::Priority::BOTTOM);
bg_bottom_compaction_scheduled_--;
assert(num_running_bottom_compactions_ > 0);
num_running_bottom_compactions_--;
}
// See if there's more work to be done
MaybeScheduleFlushOrCompaction();
NotifyOnBackgroundJobPressureChanged();
if (prepicked_compaction != nullptr &&
prepicked_compaction->task_token != nullptr) {
// Releasing task tokens affects (and asserts on) the DB state, so
@@ -5155,10 +4911,6 @@ void DBImpl::InitSnapshotContext(JobContext* job_context) {
Status DBImpl::WaitForCompact(
const WaitForCompactOptions& wait_for_compact_options) {
InstrumentedMutexLock l(&mutex_);
// `close_db=true` needs the same direct-write cleanup guarantee as DB close:
// register live blob files before relying on the DB being reopenable.
const bool flush_for_blob_direct_write =
HasAnyBlobDirectWriteColumnFamilyWithLockHeld();
if (wait_for_compact_options.flush) {
Status s = DBImpl::FlushAllColumnFamilies(FlushOptions(),
FlushReason::kManualFlush);
@@ -5166,9 +4918,8 @@ Status DBImpl::WaitForCompact(
return s;
}
} else if (wait_for_compact_options.close_db &&
(flush_for_blob_direct_write ||
(has_unpersisted_data_.load(std::memory_order_relaxed) &&
!mutable_db_options_.avoid_flush_during_shutdown))) {
has_unpersisted_data_.load(std::memory_order_relaxed) &&
!mutable_db_options_.avoid_flush_during_shutdown) {
Status s =
DBImpl::FlushAllColumnFamilies(FlushOptions(), FlushReason::kShutDown);
if (!s.ok()) {
+1 -13
View File
@@ -8,17 +8,14 @@
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef NDEBUG
#include <iostream>
#include <unordered_set>
#include "db/blob/blob_file_cache.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/column_family.h"
#include "db/db_impl/db_impl.h"
#include "db/error_handler.h"
#include "db/periodic_task_scheduler.h"
#include "monitoring/thread_status_updater.h"
#include "util/cast_util.h"
#include "util/hash_containers.h"
namespace ROCKSDB_NAMESPACE {
uint64_t DBImpl::TEST_GetLevel0TotalSize() {
@@ -285,11 +282,6 @@ int DBImpl::TEST_BGFlushesAllowed() const {
return GetBGJobLimits().max_flushes;
}
int DBImpl::TEST_NumRunningBottomCompactions() const {
mutex_.AssertHeld();
return num_running_bottom_compactions_;
}
SequenceNumber DBImpl::TEST_GetLastVisibleSequence() const {
if (last_seq_same_as_publish_seq_) {
return versions_->LastSequence();
@@ -360,15 +352,11 @@ void DBImpl::TEST_VerifyNoObsoleteFilesCached(
}
// Live and "quarantined" files are allowed to be open in table cache
UnorderedSet<uint64_t> live_and_quar_files;
std::set<uint64_t> live_and_quar_files;
for (auto cfd : *versions_->GetColumnFamilySet()) {
if (cfd->IsDropped()) {
continue;
}
if (auto* mgr = cfd->blob_partition_manager(); mgr != nullptr) {
mgr->GetActiveBlobFileNumbers(&live_and_quar_files);
mgr->GetProtectedBlobFileNumbers(&live_and_quar_files);
}
// Iterate over live versions
Version* current = cfd->current();
Version* ver = current;
+15 -165
View File
@@ -6,13 +6,10 @@
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include <array>
#include <cinttypes>
#include <set>
#include <unordered_set>
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_log_format.h"
#include "db/db_impl/db_impl.h"
#include "db/event_helpers.h"
#include "db/memtable_list.h"
@@ -20,7 +17,6 @@
#include "file/filename.h"
#include "file/sst_file_manager_impl.h"
#include "logging/logging.h"
#include "monitoring/thread_status_util.h"
#include "port/port.h"
#include "rocksdb/options.h"
#include "util/autovector.h"
@@ -28,91 +24,6 @@
namespace ROCKSDB_NAMESPACE {
namespace {
Env::IOActivity GetCurrentThreadIOActivityForMetadataRead() {
switch (ThreadStatusUtil::GetThreadOperation()) {
case ThreadStatus::OperationType::OP_FLUSH:
return Env::IOActivity::kFlush;
case ThreadStatus::OperationType::OP_COMPACTION:
return Env::IOActivity::kCompaction;
case ThreadStatus::OperationType::OP_DBOPEN:
return Env::IOActivity::kDBOpen;
case ThreadStatus::OperationType::OP_GET:
return Env::IOActivity::kGet;
case ThreadStatus::OperationType::OP_MULTIGET:
return Env::IOActivity::kMultiGet;
case ThreadStatus::OperationType::OP_DBITERATOR:
return Env::IOActivity::kDBIterator;
case ThreadStatus::OperationType::OP_VERIFY_DB_CHECKSUM:
return Env::IOActivity::kVerifyDBChecksum;
case ThreadStatus::OperationType::OP_VERIFY_FILE_CHECKSUMS:
return Env::IOActivity::kVerifyFileChecksums;
case ThreadStatus::OperationType::OP_GETENTITY:
return Env::IOActivity::kGetEntity;
case ThreadStatus::OperationType::OP_MULTIGETENTITY:
return Env::IOActivity::kMultiGetEntity;
case ThreadStatus::OperationType::
OP_GET_FILE_CHECKSUMS_FROM_CURRENT_MANIFEST:
return Env::IOActivity::kGetFileChecksumsFromCurrentManifest;
default:
return Env::IOActivity::kUnknown;
}
}
// A full-scan obsolete-file purge can observe a newly created direct-write
// blob file before it is added to the manager's active-file set. Those
// in-flight files are still missing their footer, so conservatively keep any
// footer-less blob file here rather than risk deleting a live write-path file.
bool ShouldKeepFooterlessBlobFile(FileSystem* fs,
const FileOptions& file_options,
const std::string& blob_file_path) {
assert(fs != nullptr);
constexpr IODebugContext* dbg = nullptr;
// This purge path can run from DB open, flush/compaction cleanup, or
// iterator-triggered obsolete-file cleanup. Tag the probe with the current
// thread activity so db_stress keeps validating the read against the active
// operation instead of seeing an unexpected kUnknown metadata read.
IOOptions io_options;
io_options.io_activity = GetCurrentThreadIOActivityForMetadataRead();
uint64_t file_size = 0;
IOStatus io_s = fs->GetFileSize(blob_file_path, io_options, &file_size, dbg);
if (!io_s.ok()) {
return !io_s.IsPathNotFound();
}
if (file_size < BlobLogHeader::kSize + BlobLogFooter::kSize) {
return true;
}
FileOptions read_file_options = file_options;
read_file_options.use_direct_reads = false;
std::unique_ptr<FSRandomAccessFile> file;
io_s = fs->NewRandomAccessFile(blob_file_path, read_file_options, &file, dbg);
if (!io_s.ok()) {
return !io_s.IsPathNotFound();
}
std::array<char, BlobLogFooter::kSize> scratch{};
Slice footer_slice;
io_s = file->Read(file_size - BlobLogFooter::kSize, BlobLogFooter::kSize,
io_options, &footer_slice, scratch.data(), dbg);
if (!io_s.ok()) {
return !io_s.IsPathNotFound();
}
if (footer_slice.size() != BlobLogFooter::kSize) {
return true;
}
BlobLogFooter footer;
return !footer.DecodeFrom(footer_slice).ok();
}
} // namespace
uint64_t DBImpl::MinLogNumberToKeep() {
return versions_->min_log_number_to_keep();
}
@@ -245,17 +156,6 @@ void DBImpl::FindObsoleteFiles(JobContext* job_context, bool force,
job_context->min_pending_output = MinObsoleteSstNumberToKeep();
job_context->files_to_quarantine = error_handler_.GetFilesToQuarantine();
job_context->min_options_file_number = MinOptionsFileNumberToKeep();
job_context->min_blob_file_number_to_keep =
versions_->current_next_file_number();
for (auto* cfd : *versions_->GetColumnFamilySet()) {
auto* mgr = cfd->blob_partition_manager();
if (mgr != nullptr) {
mgr->GetActiveBlobFileNumbers(
&job_context->active_blob_direct_write_files);
mgr->GetProtectedBlobFileNumbers(
&job_context->active_blob_direct_write_files);
}
}
// Get obsolete files. This function will also update the list of
// pending files in VersionSet().
@@ -546,7 +446,7 @@ void DBImpl::PurgeObsoleteFiles(JobContext& state, bool schedule_only) {
state.manifest_delete_files.size());
// We may ignore the dbname when generating the file names.
for (auto& file : state.sst_delete_files) {
auto* handle = file.metadata->fd.pinned_reader.GetCacheHandle();
auto* handle = file.metadata->table_reader_handle;
if (file.only_delete_metadata) {
if (handle) {
// Simply release handle of file that is not being deleted
@@ -687,24 +587,13 @@ void DBImpl::PurgeObsoleteFiles(JobContext& state, bool schedule_only) {
files_to_del.insert(number);
}
break;
case kBlobFile: {
case kBlobFile:
keep = number >= state.min_pending_output ||
number >= state.min_blob_file_number_to_keep ||
(blob_live_set.find(number) != blob_live_set.end()) ||
(state.active_blob_direct_write_files.find(number) !=
state.active_blob_direct_write_files.end());
(blob_live_set.find(number) != blob_live_set.end());
if (!keep) {
const std::string blob_file_path =
BlobFileName(candidate_file.file_path, number);
if (ShouldKeepBlobFileDuringPurge(number, blob_file_path)) {
keep = true;
break;
}
TableCache::Evict(table_cache_.get(), number);
files_to_del.insert(number);
}
break;
}
case kTempFile:
// Any temp files that are currently being written to must
// be recorded in pending_outputs_, which is inserted into "live".
@@ -861,21 +750,6 @@ void DBImpl::DeleteObsoleteFiles() {
mutex_.Lock();
}
bool DBImpl::ShouldKeepBlobFileDuringPurge(uint64_t number,
const std::string& blob_file_path) {
{
InstrumentedMutexLock lock(&mutex_);
for (auto* cfd : *versions_->GetColumnFamilySet()) {
auto* mgr = cfd->blob_partition_manager();
if (mgr != nullptr && mgr->IsTrackedBlobFileNumber(number)) {
return true;
}
}
}
return ShouldKeepFooterlessBlobFile(fs_.get(), file_options_, blob_file_path);
}
VersionEdit GetDBRecoveryEditForObsoletingMemTables(
VersionSet* vset, const ColumnFamilyData& cfd,
const autovector<VersionEdit*>& edit_list,
@@ -1186,9 +1060,8 @@ std::set<std::string> DBImpl::CollectAllDBPaths() {
Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
mutex_.AssertHeld();
const uint64_t prev_next_file_number = versions_->current_next_file_number();
uint64_t largest_file_number = prev_next_file_number;
bool on_disk_file_advanced_counter = false;
uint64_t next_file_number = versions_->current_next_file_number();
uint64_t largest_file_number = next_file_number;
Status s;
for (const auto& path : CollectAllDBPaths()) {
std::vector<std::string> files;
@@ -1202,15 +1075,7 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
if (!ParseFileName(fname, &number, &type)) {
continue;
}
std::string normalized_fpath = path;
normalized_fpath += kFilePathSeparator;
normalized_fpath.append(fname);
// Use >= so a crashed-mid-allocation file at exactly
// prev_next_file_number triggers the advance — otherwise the next
// NewFileNumber() would collide with it.
if (number >= prev_next_file_number) {
on_disk_file_advanced_counter = true;
}
const std::string normalized_fpath = path + kFilePathSeparator + fname;
largest_file_number = std::max(largest_file_number, number);
if ((type == kTableFile || type == kBlobFile)) {
recovery_ctx->existing_data_files_.push_back(normalized_fpath);
@@ -1221,31 +1086,16 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
return s;
}
// Legacy bug: initializing largest_file_number from prev_next_file_number
// made the post-loop check trivially true and emitted a noop SetNextFile
// on every recovery.
// Preserved verbatim by default (some tests pin file numbers); skipped
// only when the option is on AND we're not in salvage mode (which
// requires the MANIFEST rewrite this emit triggers).
const bool optimize_manifest_for_recovery =
mutable_db_options_.optimize_manifest_for_recovery &&
!immutable_db_options_.best_efforts_recovery;
if (!optimize_manifest_for_recovery || on_disk_file_advanced_counter) {
if (largest_file_number >= prev_next_file_number) {
versions_->next_file_number_.store(largest_file_number + 1);
}
TEST_SYNC_POINT("DBImpl::MaybeUpdateNextFileNumber:EmitEdit");
VersionEdit edit;
edit.SetNextFile(versions_->next_file_number_.load());
assert(versions_->GetColumnFamilySet());
ColumnFamilyData* default_cfd =
versions_->GetColumnFamilySet()->GetDefault();
assert(default_cfd);
recovery_ctx->UpdateVersionEdits(default_cfd, edit);
} else {
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber");
if (largest_file_number >= next_file_number) {
versions_->next_file_number_.store(largest_file_number + 1);
}
VersionEdit edit;
edit.SetNextFile(versions_->next_file_number_.load());
assert(versions_->GetColumnFamilySet());
ColumnFamilyData* default_cfd = versions_->GetColumnFamilySet()->GetDefault();
assert(default_cfd);
recovery_ctx->UpdateVersionEdits(default_cfd, edit);
return s;
}
} // namespace ROCKSDB_NAMESPACE
-1
View File
@@ -321,7 +321,6 @@ Status DB::OpenAsFollower(
sv_context.NewSuperVersion();
cfd->InstallSuperVersion(&sv_context, &impl->mutex_);
}
impl->MarkAsyncFileOpenNotNeeded();
}
impl->mutex_.Unlock();
sv_context.Clean();
+14 -218
View File
@@ -8,12 +8,10 @@
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include <cinttypes>
#include "db/blob/blob_file_partition_manager.h"
#include "db/builder.h"
#include "db/db_impl/db_impl.h"
#include "db/error_handler.h"
#include "db/periodic_task_scheduler.h"
#include "db/version_util.h"
#include "env/composite_env_wrapper.h"
#include "file/filename.h"
#include "file/read_write_util.h"
@@ -680,18 +678,8 @@ Status DBImpl::Recover(
} else if (immutable_db_options_.write_dbid_to_manifest && recovery_ctx) {
VersionEdit edit;
s = SetupDBId(write_options, read_only, is_new_db, is_retry, &edit);
// best_efforts_recovery rebuilds CURRENT/MANIFEST as the side-effect
// of LogAndApplyForRecovery emitting an edit, so the optimization is
// disabled there (see optimize_manifest_for_recovery doc).
const bool optimize_manifest_for_recovery =
mutable_db_options_.optimize_manifest_for_recovery &&
!immutable_db_options_.best_efforts_recovery;
if (!optimize_manifest_for_recovery || edit.HasDbId()) {
recovery_ctx->UpdateVersionEdits(
versions_->GetColumnFamilySet()->GetDefault(), edit);
} else {
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:SetupDBId");
}
recovery_ctx->UpdateVersionEdits(
versions_->GetColumnFamilySet()->GetDefault(), edit);
} else {
s = SetupDBId(write_options, read_only, is_new_db, is_retry, nullptr);
}
@@ -780,10 +768,6 @@ Status DBImpl::Recover(
// otherwise, in the future, if WAL tracking is enabled again,
// since the WALs deleted when WAL tracking is disabled are not persisted
// into MANIFEST, WAL check may fail.
//
// Intentionally NOT gated by optimize_manifest_for_recovery:
// this edit is safety-critical for a future re-enable of WAL tracking,
// even though it can look noop-ish under the current session.
VersionEdit edit;
WalNumber max_wal_number =
versions_->GetWalSet().GetWals().rbegin()->first;
@@ -997,11 +981,7 @@ Status DBImpl::InitPersistStatsColumnFamily() {
Status DBImpl::LogAndApplyForRecovery(const RecoveryContext& recovery_ctx) {
mutex_.AssertHeld();
// descriptor_log_ is normally null after Recover, but when
// reuse_manifest_on_open is set VersionSet::Recover may have already
// bound a log::Writer to the existing MANIFEST for append.
assert(versions_->descriptor_log_ == nullptr ||
immutable_db_options_.reuse_manifest_on_open);
assert(versions_->descriptor_log_ == nullptr);
const ReadOptions read_options(Env::IOActivity::kDBOpen);
const WriteOptions write_options(Env::IOActivity::kDBOpen);
@@ -1512,8 +1492,8 @@ Status DBImpl::ProcessLogRecord(
}
assert(process_status.ok());
process_status = InsertLogRecordToMemtable(
batch_to_use, wal_number, next_sequence, &has_valid_writes, read_only);
process_status = InsertLogRecordToMemtable(batch_to_use, wal_number,
next_sequence, &has_valid_writes);
MaybeIgnoreError(&process_status);
// We are treating this as a failure while reading since we read valid
// blocks that do not form coherent data
@@ -1591,8 +1571,7 @@ void DBImpl::MaybeReviseStopReplayForCorruption(
Status DBImpl::InsertLogRecordToMemtable(WriteBatch* batch_to_use,
uint64_t wal_number,
SequenceNumber* next_sequence,
bool* has_valid_writes,
bool read_only) {
bool* has_valid_writes) {
// If column family was not found, it might mean that the WAL write
// batch references to the column family that was dropped after the
// insert. We don't want to fail the whole write batch in that case --
@@ -1605,34 +1584,6 @@ Status DBImpl::InsertLogRecordToMemtable(WriteBatch* batch_to_use,
&trim_history_scheduler_, true, wal_number, this,
false /* concurrent_memtable_writes */, next_sequence, has_valid_writes,
seq_per_batch_, batch_per_txn_);
// Check WriteBufferManager global limit during recovery.
// When multiple RocksDB instances share a WriteBufferManager, a recovering
// instance could exceed the global memory limit. Schedule flushes when needed
// to prevent OOM during WAL recovery.
//
// Skip scheduling in read-only mode since flushes cannot be performed and
// the scheduler would never be drained, causing assertion failures on
// duplicate ScheduleWork() calls.
//
// TODO: Currently we schedule all CFs with non-empty memtables for flush
// (similar to the atomic_flush=false path in the normal write flow). This
// may produce more, smaller L0 files in some CFs. A future improvement
// could flush only the oldest CF or pick CFs more selectively to reduce
// unnecessary L0 file creation.
if (status.ok() && *has_valid_writes && !read_only &&
immutable_db_options_.enforce_write_buffer_manager_during_recovery &&
write_buffer_manager_ != nullptr &&
write_buffer_manager_->ShouldFlush()) {
for (auto cfd : *versions_->GetColumnFamilySet()) {
if (cfd->mem() != nullptr && cfd->mem()->GetFirstSequenceNumber() != 0 &&
!cfd->mem()->HasFlushScheduled()) {
cfd->mem()->MarkFlushScheduled();
flush_scheduler_.ScheduleWork(cfd);
}
}
}
return status;
}
@@ -1888,47 +1839,25 @@ Status DBImpl::MaybeFlushFinalMemtableOrRestoreActiveLogFiles(
versions_->MarkFileNumberUsed(max_wal_number + 1);
assert(recovery_ctx != nullptr);
const bool optimize_manifest_for_recovery =
mutable_db_options_.optimize_manifest_for_recovery &&
!immutable_db_options_.best_efforts_recovery;
for (auto* cfd : *versions_->GetColumnFamilySet()) {
auto iter = version_edits->find(cfd->GetID());
assert(iter != version_edits->end());
const VersionEdit& cf_edit = iter->second;
if (!optimize_manifest_for_recovery ||
cf_edit.ShouldEmitPerColumnFamilyRecoveryEdit(
cfd->GetLogNumber())) {
recovery_ctx->UpdateVersionEdits(cfd, cf_edit);
} else {
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:PerCF");
}
recovery_ctx->UpdateVersionEdits(cfd, iter->second);
}
if (flushed || !data_seen) {
const uint64_t new_min_log = max_wal_number + 1;
VersionEdit wal_deletion;
bool emit_wal_deletion = false;
if (immutable_db_options_.track_and_verify_wals_in_manifest) {
// Determining whether DeleteWalsBefore actually shrinks WalSet
// membership requires WalSet state outside this site, so emit
// unconditionally (pre-existing behavior).
wal_deletion.DeleteWalsBefore(new_min_log);
emit_wal_deletion = true;
wal_deletion.DeleteWalsBefore(max_wal_number + 1);
}
const bool min_log_advances =
new_min_log > versions_->min_log_number_to_keep();
if (!allow_2pc() &&
(!optimize_manifest_for_recovery || min_log_advances)) {
wal_deletion.SetMinLogNumberToKeep(new_min_log);
emit_wal_deletion = true;
if (!allow_2pc()) {
// In non-2pc mode, flushing the memtables of the column families
// means we can advance min_log_number_to_keep.
wal_deletion.SetMinLogNumberToKeep(max_wal_number + 1);
}
assert(versions_->GetColumnFamilySet() != nullptr);
if (!optimize_manifest_for_recovery || emit_wal_deletion) {
recovery_ctx->UpdateVersionEdits(
versions_->GetColumnFamilySet()->GetDefault(), wal_deletion);
} else {
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:WalDeletion");
}
recovery_ctx->UpdateVersionEdits(
versions_->GetColumnFamilySet()->GetDefault(), wal_deletion);
}
}
}
@@ -2647,14 +2576,6 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
}
}
if (s.ok()) {
for (size_t i = 0; i < column_families.size(); ++i) {
const auto& cf = column_families[i];
auto* cfd = static_cast<ColumnFamilyHandleImpl*>((*handles)[i])->cfd();
impl->MaybeInitBlobDirectWriteColumnFamily(cfd, cf.options, cf.name);
}
}
if (s.ok() && impl->immutable_db_options_.persist_stats_to_disk) {
// Install SuperVersion for hidden column family
assert(impl->persist_stats_cf_handle_);
@@ -2736,10 +2657,6 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
impl->DeleteObsoleteFiles();
TEST_SYNC_POINT("DBImpl::Open:AfterDeleteFiles");
impl->MaybeScheduleFlushOrCompaction();
if (impl->immutable_db_options_.open_files_async) {
impl->ScheduleAsyncFileOpening();
}
impl->mutex_.Unlock();
}
@@ -2788,125 +2705,4 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
}
return s;
}
struct AsyncFileOpenContext {
DBImpl* db = nullptr;
FileOptions file_options;
std::vector<Version*> versions;
AsyncFileOpenContext() = default;
AsyncFileOpenContext(const AsyncFileOpenContext&) = delete;
AsyncFileOpenContext& operator=(const AsyncFileOpenContext&) = delete;
AsyncFileOpenContext(AsyncFileOpenContext&&) = delete;
AsyncFileOpenContext& operator=(AsyncFileOpenContext&&) = delete;
~AsyncFileOpenContext() {
db->mutex()->AssertHeld();
for (auto* v : versions) {
// must unref version before cfd
ColumnFamilyData* cfd = v->cfd();
v->Unref();
cfd->UnrefAndTryDelete();
}
}
};
void DBImpl::ScheduleAsyncFileOpening() {
mutex_.AssertHeld();
auto* ctx = new AsyncFileOpenContext();
ctx->db = this;
ctx->file_options = versions_->file_options();
for (auto cfd : *versions_->GetColumnFamilySet()) {
assert(!cfd->IsDropped());
Version* current = cfd->current();
VersionStorageInfo* vstorage = current->storage_info();
bool has_files = false;
for (int level = 0; level < vstorage->num_levels() && !has_files; level++) {
has_files = !vstorage->LevelFiles(level).empty();
}
if (has_files) {
cfd->Ref();
current->Ref();
ctx->versions.push_back(current);
}
}
bg_async_file_open_state_ = AsyncFileOpenState::kScheduled;
// since this is a one time job, best to schedule it with high priority
env_->Schedule(&DBImpl::BGWorkAsyncFileOpen, ctx, Env::Priority::HIGH,
nullptr);
}
void DBImpl::MarkAsyncFileOpenNotNeeded() {
mutex_.AssertHeld();
assert(bg_async_file_open_state_ == AsyncFileOpenState::kNotScheduled);
bg_async_file_open_state_ = AsyncFileOpenState::kComplete;
}
void DBImpl::BGWorkAsyncFileOpen(void* arg) {
TEST_SYNC_POINT("DBImpl::BGWorkAsyncFileOpen::Start");
AsyncFileOpenContext* raw_ctx = static_cast<AsyncFileOpenContext*>(arg);
DBImpl* db = raw_ctx->db;
auto deleter = [](AsyncFileOpenContext* p) {
auto* dbPtr = p->db;
InstrumentedMutexLock l(&dbPtr->mutex_);
delete p;
dbPtr->bg_async_file_open_state_ = AsyncFileOpenState::kComplete;
dbPtr->bg_cv_.SignalAll();
};
std::unique_ptr<AsyncFileOpenContext, decltype(deleter)> ctx(raw_ctx,
deleter);
ReadOptions ro;
for (size_t i = 0; i < ctx->versions.size(); i++) {
auto* version = ctx->versions[i];
ColumnFamilyData* cfd = version->cfd();
// Skip column families that were dropped after scheduling
if (cfd->IsDropped()) {
continue;
}
VersionStorageInfo* vstorage = version->storage_info();
MutableCFOptions mutable_cf_options;
{
InstrumentedMutexLock l(&db->mutex_);
mutable_cf_options = cfd->GetLatestMutableCFOptions();
}
size_t max_file_size_for_l0_meta_pin =
MaxFileSizeForL0MetaPin(mutable_cf_options);
std::vector<std::pair<FileMetaData*, int>> files_meta;
for (int level = 0; level < vstorage->num_levels(); level++) {
for (FileMetaData* file_meta : vstorage->LevelFiles(level)) {
files_meta.emplace_back(file_meta, level);
}
}
Status s = LoadTableHandlersHelper(
files_meta, cfd->table_cache(), ctx->file_options,
*vstorage->InternalComparator(), cfd->internal_stats(),
db->immutable_db_options_.max_file_opening_threads,
false /* prefetch_index_and_filter_in_cache */, mutable_cf_options,
max_file_size_for_l0_meta_pin, ro, &db->shutting_down_);
if (!s.ok()) {
ROCKS_LOG_ERROR(
db->immutable_db_options_.info_log,
"BGWorkAsyncFileOpen: LoadTableHandlers failed for CF %s: "
"%s",
cfd->GetName().c_str(), s.ToString().c_str());
InstrumentedMutexLock l(&db->mutex_);
db->error_handler_.SetBGError(s, BackgroundErrorReason::kAsyncFileOpen);
break;
}
}
TEST_SYNC_POINT("DBImpl::BGWorkAsyncFileOpen:Done");
}
} // namespace ROCKSDB_NAMESPACE
-6
View File
@@ -357,12 +357,6 @@ Status DBImplReadOnly::OpenForReadOnlyWithoutCheck(
sv_context.NewSuperVersion();
cfd->InstallSuperVersion(&sv_context, &impl->mutex_);
}
impl->opened_successfully_ = true;
if (db_options.open_files_async) {
impl->ScheduleAsyncFileOpening();
}
}
impl->mutex_.Unlock();
sv_context.Clean();
+1 -1
View File
@@ -186,7 +186,7 @@ class DBImplReadOnly : public DBImpl {
// FIXME: some missing overrides for more "write" functions
protected:
Status FlushForGetLiveFiles(bool /*force_atomic_flush*/) override {
Status FlushForGetLiveFiles() override {
// No-op for read-only DB
return Status::OK();
}
+19 -36
View File
@@ -43,6 +43,7 @@ Status DBImplSecondary::Recover(
RecoveryContext* /*recovery_ctx*/, bool* /*can_retry*/) {
mutex_.AssertHeld();
JobContext job_context(0);
Status s;
s = static_cast<ReactiveVersionSet*>(versions_.get())
->Recover(column_families, &manifest_reader_, &manifest_reporter_,
@@ -60,12 +61,24 @@ Status DBImplSecondary::Recover(
max_total_in_memory_state_ += mutable_cf_options.write_buffer_size *
mutable_cf_options.max_write_buffer_number;
}
default_cf_handle_ = new ColumnFamilyHandleImpl(
versions_->GetColumnFamilySet()->GetDefault(), this, &mutex_);
default_cf_internal_stats_ = default_cf_handle_->cfd()->internal_stats();
if (s.ok()) {
default_cf_handle_ = new ColumnFamilyHandleImpl(
versions_->GetColumnFamilySet()->GetDefault(), this, &mutex_);
default_cf_internal_stats_ = default_cf_handle_->cfd()->internal_stats();
std::unordered_set<ColumnFamilyData*> cfds_changed;
s = FindAndRecoverLogFiles(&cfds_changed, &job_context);
}
if (s.IsPathNotFound()) {
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"Secondary tries to read WAL, but WAL file(s) have already "
"been purged by primary.");
s = Status::OK();
}
// TODO: update options_file_number_ needed?
job_context.Clean();
return s;
}
@@ -75,7 +88,6 @@ Status DBImplSecondary::FindAndRecoverLogFiles(
JobContext* job_context) {
assert(nullptr != cfds_changed);
assert(nullptr != job_context);
TEST_SYNC_POINT("DBImplSecondary::FindAndRecoverLogFiles:Begin");
Status s;
std::vector<uint64_t> logs;
s = FindNewLogNumbers(&logs);
@@ -728,17 +740,6 @@ Status DB::OpenAsSecondary(
const std::string& secondary_path,
const std::vector<ColumnFamilyDescriptor>& column_families,
std::vector<ColumnFamilyHandle*>* handles, std::unique_ptr<DB>* dbptr) {
return DBImplSecondary::OpenAsSecondaryImpl(
db_options, dbname, secondary_path, column_families, handles, dbptr,
/*recover_wal=*/true);
}
Status DBImplSecondary::OpenAsSecondaryImpl(
const DBOptions& db_options, const std::string& dbname,
const std::string& secondary_path,
const std::vector<ColumnFamilyDescriptor>& column_families,
std::vector<ColumnFamilyHandle*>* handles, std::unique_ptr<DB>* dbptr,
bool recover_wal) {
*dbptr = nullptr;
DBOptions tmp_opts(db_options);
@@ -783,21 +784,7 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
impl->mutex_.Lock();
JobContext job_context(0);
s = impl->Recover(column_families, true, false, false);
// WAL recovery is optional: DB::OpenAsSecondary() needs it to replay
// memtable data, while DB::OpenAndCompact() skips it since remote
// compaction only needs LSM state from MANIFEST.
if (s.ok() && recover_wal) {
std::unordered_set<ColumnFamilyData*> cfds_changed;
s = impl->FindAndRecoverLogFiles(&cfds_changed, &job_context);
if (s.IsPathNotFound()) {
ROCKS_LOG_INFO(impl->immutable_db_options_.info_log,
"Secondary tries to read WAL, but WAL file(s) have "
"already been purged by primary.");
s = Status::OK();
}
}
if (s.ok()) {
for (const auto& cf : column_families) {
auto cfd =
@@ -815,11 +802,9 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
sv_context.NewSuperVersion();
cfd->InstallSuperVersion(&sv_context, &impl->mutex_);
}
impl->MarkAsyncFileOpenNotNeeded();
}
impl->mutex_.Unlock();
sv_context.Clean();
job_context.Clean();
if (s.ok()) {
dbptr->reset(impl);
for (auto h : *handles) {
@@ -1553,13 +1538,11 @@ Status DB::OpenAndCompact(
}
}
// 5. Open db As Secondary (skip WAL recovery — remote compaction only
// needs LSM state from MANIFEST, not memtable data from WAL replay)
// 5. Open db As Secondary
std::unique_ptr<DB> db;
std::vector<ColumnFamilyHandle*> handles;
s = DBImplSecondary::OpenAsSecondaryImpl(db_options, name, output_directory,
column_families, &handles, &db,
/*recover_wal=*/false);
s = DB::OpenAsSecondary(db_options, name, output_directory, column_families,
&handles, &db);
if (!s.ok()) {
return s;
}
+7 -20
View File
@@ -78,9 +78,8 @@ class DBImplSecondary : public DBImpl {
std::string secondary_path);
~DBImplSecondary() override;
// Recover by replaying MANIFEST only. Also initialize manifest_reader_
// to facilitate future operations. WAL recovery, if needed, is done
// separately after opening (see DB::OpenAsSecondary).
// Recover by replaying MANIFEST and WAL. Also initialize manifest_reader_
// and log_readers_ to facilitate future operations.
Status Recover(const std::vector<ColumnFamilyDescriptor>& column_families,
bool read_only, bool error_if_wal_file_exists,
bool error_if_data_exists_in_wals, bool is_retry = false,
@@ -195,11 +194,10 @@ class DBImplSecondary : public DBImpl {
return Status::NotSupported("Not supported operation in secondary mode.");
}
Status GetLiveFiles(std::vector<std::string>& ret,
uint64_t* manifest_file_size,
bool /*flush_memtable*/) override {
return DBImpl::GetLiveFiles(ret, manifest_file_size,
false /* flush_memtable */);
Status GetLiveFiles(std::vector<std::string>&,
uint64_t* /*manifest_file_size*/,
bool /*flush_memtable*/ = true) override {
return Status::NotSupported("Not supported operation in secondary mode.");
}
using DBImpl::Flush;
@@ -260,7 +258,7 @@ class DBImplSecondary : public DBImpl {
#endif // NDEBUG
protected:
Status FlushForGetLiveFiles(bool /*force_atomic_flush*/) override {
Status FlushForGetLiveFiles() override {
// No-op for read-only DB
return Status::OK();
}
@@ -386,17 +384,6 @@ class DBImplSecondary : public DBImpl {
uint64_t CalculateResumedCompactionBytes(
const CompactionProgress& compaction_progress) const;
// Internal helper for opening a secondary instance. Recover() replays
// MANIFEST only. When recover_wal is true, WAL files are also replayed
// (needed by DB::OpenAsSecondary). When false, WAL replay is skipped
// (used by DB::OpenAndCompact which only needs LSM state).
static Status OpenAsSecondaryImpl(
const DBOptions& db_options, const std::string& dbname,
const std::string& secondary_path,
const std::vector<ColumnFamilyDescriptor>& column_families,
std::vector<ColumnFamilyHandle*>* handles, std::unique_ptr<DB>* dbptr,
bool recover_wal);
// Cache log readers for each log number, used for continue WAL replay
// after recovery
std::map<uint64_t, std::unique_ptr<LogReaderContainer>> log_readers_;
+47 -664
View File
@@ -7,81 +7,18 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include <cinttypes>
#include <optional>
#include <unordered_map>
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_write_batch_transformer.h"
#include "db/db_impl/db_impl.h"
#include "db/error_handler.h"
#include "db/event_helpers.h"
#include "db/wide/wide_columns_helper.h"
#include "logging/logging.h"
#include "memtable/wbwi_memtable.h"
#include "monitoring/perf_context_imp.h"
#include "options/options_helper.h"
#include "test_util/sync_point.h"
#include "util/cast_util.h"
#include "util/hash_containers.h"
namespace ROCKSDB_NAMESPACE {
namespace {
class BlobWriteRollbackGuard {
public:
explicit BlobWriteRollbackGuard(
const std::vector<BlobWriteBatchTransformer::RollbackInfo>*
rollback_infos,
Logger* info_log)
: rollback_infos_(rollback_infos), info_log_(info_log) {}
BlobWriteRollbackGuard(const BlobWriteRollbackGuard&) = delete;
BlobWriteRollbackGuard& operator=(const BlobWriteRollbackGuard&) = delete;
BlobWriteRollbackGuard(BlobWriteRollbackGuard&&) = delete;
BlobWriteRollbackGuard& operator=(BlobWriteRollbackGuard&&) = delete;
~BlobWriteRollbackGuard() {
if (!active_ || rollback_infos_ == nullptr) {
return;
}
for (const auto& rollback_info : *rollback_infos_) {
if (rollback_info.partition_mgr == nullptr) {
continue;
}
Status rollback_s = rollback_info.partition_mgr->MarkBlobWriteAsGarbage(
rollback_info.file_number, rollback_info.count, rollback_info.bytes);
if (!rollback_s.ok()) {
if (info_log_ != nullptr) {
ROCKS_LOG_ERROR(
info_log_,
"Failed to rollback blob direct-write garbage accounting for "
"file #%" PRIu64 " (%" PRIu64 " blobs, %" PRIu64 " bytes): %s",
rollback_info.file_number, rollback_info.count,
rollback_info.bytes, rollback_s.ToString().c_str());
}
rollback_s.PermitUncheckedError();
}
}
}
void Dismiss() { active_ = false; }
private:
const std::vector<BlobWriteBatchTransformer::RollbackInfo>* rollback_infos_;
Logger* info_log_;
bool active_ = true;
};
class PutEntityFastPathWriteCallback final : public WriteCallback {
public:
Status Callback(DB* /*db*/) override { return Status::OK(); }
bool AllowWriteBatching() override { return false; }
};
} // namespace
// Convenience methods
Status DBImpl::Put(const WriteOptions& o, ColumnFamilyHandle* column_family,
const Slice& key, const Slice& val) {
@@ -109,23 +46,18 @@ Status DBImpl::PutEntity(const WriteOptions& options,
return s;
}
return PutEntityFastPath(options, column_family, key, columns);
return DB::PutEntity(options, column_family, key, columns);
}
Status DBImpl::PutEntity(const WriteOptions& options, const Slice& key,
const AttributeGroups& attribute_groups) {
if (attribute_groups.empty()) {
return Status::InvalidArgument(
"Cannot call this method with empty attribute groups");
}
for (const AttributeGroup& ag : attribute_groups) {
const Status s = FailIfCfHasTs(ag.column_family());
if (!s.ok()) {
return s;
}
}
return PutEntityFastPath(options, key, attribute_groups);
return DB::PutEntity(options, key, attribute_groups);
}
Status DBImpl::Merge(const WriteOptions& o, ColumnFamilyHandle* column_family,
@@ -268,11 +200,6 @@ Status DBImpl::IngestWriteBatchWithIndex(
return Status::NotSupported(
"IngestWriteBatchWithIndex does not support disableWAL=true");
}
if (HasAnyBlobDirectWriteColumnFamily()) {
return Status::NotSupported(
"IngestWriteBatchWithIndex is not supported with "
"enable_blob_direct_write");
}
Status s;
if (write_options.protection_bytes_per_key > 0) {
s = WriteBatchInternal::UpdateProtectionInfo(
@@ -294,7 +221,7 @@ Status DBImpl::IngestWBWIAsMemtable(
std::shared_ptr<WriteBatchWithIndex> wbwi,
const WBWIMemTable::SeqnoRange& assigned_seqno, uint64_t min_prep_log,
SequenceNumber last_seqno_after_ingest, bool memtable_updated,
bool ingest_wbwi_for_commit, bool ignore_missing_cf) {
bool ignore_missing_cf) {
// Keys in new memtable have seqno > last_seqno_after_ingest >= keys in wbwi.
assert(assigned_seqno.upper_bound <= last_seqno_after_ingest);
// Keys in the current memtable have seqno <= LastSequence() < keys in wbwi.
@@ -323,16 +250,8 @@ Status DBImpl::IngestWBWIAsMemtable(
std::to_string(cf_id));
if (memtable_updated) {
s = Status::Corruption(
"Part of the write batch is applied. Memtable is in an "
"inconsistent state due to invalid column family. " +
s.ToString());
error_handler_.SetBGError(s, BackgroundErrorReason::kMemTable);
} else if (ingest_wbwi_for_commit) {
s = Status::Corruption(
"Commit marker is durable in WAL, but publishing committed WBWI "
"data to memtable failed due to invalid column family. This DB "
"instance cannot safely resume; close and reopen the DB for WAL "
"recovery. " +
"Part of the write batch is applied. Memtable is in a inconsistent "
"state. " +
s.ToString());
error_handler_.SetBGError(s, BackgroundErrorReason::kMemTable);
}
@@ -397,16 +316,8 @@ Status DBImpl::IngestWBWIAsMemtable(
if (i != 0 || memtable_updated) {
// escalate error to non-recoverable
s = Status::Corruption(
"Part of the write batch is applied. Memtable is in an "
"inconsistent state due to SwitchMemtable failure. " +
s.ToString());
error_handler_.SetBGError(s, BackgroundErrorReason::kMemTable);
} else if (ingest_wbwi_for_commit) {
s = Status::Corruption(
"Commit marker is durable in WAL, but publishing committed WBWI "
"data to memtable failed due to SwitchMemtable failure. This DB "
"instance cannot safely resume; close and reopen the DB for WAL "
"recovery. " +
"Part of the write batch is applied. Memtable is in a inconsistent "
"state. " +
s.ToString());
error_handler_.SetBGError(s, BackgroundErrorReason::kMemTable);
} else {
@@ -447,7 +358,6 @@ Status DBImpl::IngestWBWIAsMemtable(
if (immutable_db_options_.atomic_flush) {
AssignAtomicFlushSeq(cfds);
FlushRequest flush_req;
flush_req.atomic_flush = true;
GenerateFlushRequest(cfds, FlushReason::kExternalFileIngestion,
&flush_req);
EnqueuePendingFlush(flush_req);
@@ -457,421 +367,14 @@ Status DBImpl::IngestWBWIAsMemtable(
return s;
}
struct DBImpl::BlobDirectWriteContext {
struct Entry {
// Hold a referenced SuperVersion so BDW uses the same published mutable
// option snapshot as the rest of the DB while keeping the CFD alive.
SuperVersion* super_version = nullptr;
BlobFilePartitionManager* partition_mgr = nullptr;
BlobDirectWriteSettings settings;
};
explicit BlobDirectWriteContext(DBImpl* db_impl) : db_impl_(db_impl) {}
BlobDirectWriteContext(const BlobDirectWriteContext&) = delete;
BlobDirectWriteContext& operator=(const BlobDirectWriteContext&) = delete;
BlobDirectWriteContext(BlobDirectWriteContext&&) = delete;
BlobDirectWriteContext& operator=(BlobDirectWriteContext&&) = delete;
~BlobDirectWriteContext() { Release(); }
BlobDirectWriteSettings GetSettings(uint32_t cf_id,
bool lookup_from_write_thread) {
return GetOrCreate(cf_id, lookup_from_write_thread).settings;
}
BlobFilePartitionManager* GetPartitionManager(uint32_t cf_id,
bool lookup_from_write_thread) {
return GetOrCreate(cf_id, lookup_from_write_thread).partition_mgr;
}
void Release() {
if (entries_.empty()) {
return;
}
for (auto& entry : entries_) {
if (entry.second.super_version != nullptr) {
db_impl_->CleanupSuperVersion(entry.second.super_version);
}
}
entries_.clear();
}
UnorderedSet<BlobFilePartitionManager*> touched_managers;
std::vector<BlobWriteBatchTransformer::RollbackInfo> rollback_infos;
private:
static BlobDirectWriteSettings BuildSettings(const SuperVersion* sv) {
assert(sv != nullptr);
BlobDirectWriteSettings settings;
settings.enable_blob_direct_write =
sv->cfd->ioptions().enable_blob_direct_write;
settings.min_blob_size = sv->mutable_cf_options.min_blob_size;
settings.compression_type = sv->mutable_cf_options.blob_compression_type;
settings.blob_cache = sv->cfd->ioptions().blob_cache.get();
settings.prepopulate_blob_cache =
sv->mutable_cf_options.prepopulate_blob_cache;
return settings;
}
SuperVersion* AcquireReferencedSuperVersion(uint32_t cf_id,
bool lookup_from_write_thread) {
if (lookup_from_write_thread) {
auto* cfd =
db_impl_->versions_->GetColumnFamilySet()->GetColumnFamily(cf_id);
if (cfd != nullptr) {
return cfd->GetReferencedSuperVersion(db_impl_);
}
return nullptr;
}
ColumnFamilyData* cfd = nullptr;
{
InstrumentedMutexLock lock(&db_impl_->mutex_);
cfd = db_impl_->versions_->GetColumnFamilySet()->GetColumnFamily(cf_id);
if (cfd != nullptr) {
// Hold the CFD long enough to acquire a referenced SuperVersion after
// dropping DB mutex. This reuses the existing SuperVersion publication
// path for mutable options rather than maintaining a separate BDW
// settings snapshot. A future attached-WriteBatch path could pin the
// needed CFDs up front and avoid this DB mutex lookup entirely.
cfd->Ref();
}
}
if (cfd == nullptr) {
return nullptr;
}
SuperVersion* sv = cfd->GetReferencedSuperVersion(db_impl_);
{
InstrumentedMutexLock lock(&db_impl_->mutex_);
cfd->UnrefAndTryDelete();
}
return sv;
}
Entry& GetOrCreate(uint32_t cf_id, bool lookup_from_write_thread) {
auto entry_it = entries_.find(cf_id);
if (entry_it != entries_.end()) {
return entry_it->second;
}
Entry entry;
if (SuperVersion* sv =
AcquireReferencedSuperVersion(cf_id, lookup_from_write_thread)) {
entry.super_version = sv;
entry.partition_mgr = sv->cfd->blob_partition_manager();
if (entry.partition_mgr != nullptr) {
entry.settings = BuildSettings(sv);
}
}
return entries_.emplace(cf_id, std::move(entry)).first->second;
}
DBImpl* db_impl_;
std::unordered_map<uint32_t, Entry> entries_;
};
Status DBImpl::MaybeTransformBatchForBlobDirectWrite(
const WriteOptions& write_options, WriteBatch** batch,
bool should_write_to_memtable, bool lookup_from_write_thread,
WriteBatch* transformed_storage,
BlobDirectWriteContext* blob_direct_write_ctx) {
assert(batch != nullptr);
assert(transformed_storage != nullptr);
assert(blob_direct_write_ctx != nullptr);
if (!should_write_to_memtable || *batch == nullptr ||
(!(*batch)->HasPut() && !(*batch)->HasPutEntity()) ||
(*batch)->HasMerge()) {
return Status::OK();
}
// Quick check: skip the (potentially expensive) batch iteration when no
// column family has blob direct write enabled. This also avoids Iterate()
// returning an error on a corrupted batch before the WAL verification path
// has a chance to detect it.
if (!HasAnyBlobDirectWriteColumnFamily()) {
return Status::OK();
}
std::vector<BlobFilePartitionManager*> batch_managers;
std::vector<BlobWriteBatchTransformer::RollbackInfo> batch_rollbacks;
bool transformed = false;
transformed_storage->Clear();
auto settings_provider = [&](uint32_t cf_id) -> BlobDirectWriteSettings {
return blob_direct_write_ctx->GetSettings(cf_id, lookup_from_write_thread);
};
auto partition_mgr_provider =
[&](uint32_t cf_id) -> BlobFilePartitionManager* {
return blob_direct_write_ctx->GetPartitionManager(cf_id,
lookup_from_write_thread);
};
Status s = BlobWriteBatchTransformer::TransformBatch(
write_options, *batch, transformed_storage, partition_mgr_provider,
settings_provider, &transformed, &batch_managers, &batch_rollbacks);
if (!s.ok()) {
return s;
}
if (transformed) {
*batch = transformed_storage;
blob_direct_write_ctx->touched_managers.insert(batch_managers.begin(),
batch_managers.end());
blob_direct_write_ctx->rollback_infos.insert(
blob_direct_write_ctx->rollback_infos.end(), batch_rollbacks.begin(),
batch_rollbacks.end());
}
return Status::OK();
}
Status DBImpl::AppendPreprocessedPutEntityToBatch(
const WriteOptions& write_options, WriteBatch* batch,
ColumnFamilyHandle* column_family, const Slice& key,
const WideColumns& columns, BlobDirectWriteContext* blob_direct_write_ctx) {
assert(batch != nullptr);
Status s;
uint32_t cf_id = 0;
size_t ts_sz = 0;
std::tie(s, cf_id, ts_sz) =
WriteBatchInternal::GetColumnFamilyIdAndTimestampSize(batch,
column_family);
if (s.ok()) {
assert(ts_sz == 0);
WideColumns sorted_columns(columns);
WideColumnsHelper::SortColumns(sorted_columns);
return AppendSortedPutEntityToBatch(write_options, batch, cf_id, key,
sorted_columns, blob_direct_write_ctx);
}
return s;
}
Status DBImpl::AppendSortedPutEntityToBatch(
const WriteOptions& write_options, WriteBatch* batch, uint32_t cf_id,
const Slice& key, const WideColumns& sorted_columns,
BlobDirectWriteContext* blob_direct_write_ctx) {
assert(batch != nullptr);
Status s;
BlobDirectWriteSettings settings;
BlobFilePartitionManager* partition_mgr = nullptr;
std::vector<BlobWriteBatchTransformer::RollbackInfo>* rollback_infos =
nullptr;
if (blob_direct_write_ctx != nullptr) {
settings = blob_direct_write_ctx->GetSettings(
cf_id, /*lookup_from_write_thread=*/false);
partition_mgr = blob_direct_write_ctx->GetPartitionManager(
cf_id, /*lookup_from_write_thread=*/false);
rollback_infos = &blob_direct_write_ctx->rollback_infos;
}
std::string entity;
bool transformed = false;
s = BlobWriteBatchTransformer::MaybePreprocessWideColumns(
write_options, cf_id, key, sorted_columns, partition_mgr, settings,
/*serialize_inline_entity=*/true, &entity, &transformed, rollback_infos);
if (s.ok()) {
if (transformed && blob_direct_write_ctx != nullptr) {
blob_direct_write_ctx->touched_managers.insert(partition_mgr);
}
return WriteBatchInternal::PutEntitySerialized(batch, cf_id, key, entity);
}
return s;
}
Status DBImpl::PutEntityFastPath(const WriteOptions& write_options,
ColumnFamilyHandle* column_family,
const Slice& key, const WideColumns& columns) {
const ColumnFamilyHandle* const default_cf = DefaultColumnFamily();
assert(default_cf);
const Comparator* const default_cf_ucmp = default_cf->GetComparator();
assert(default_cf_ucmp);
WriteBatch batch(/*reserved_bytes=*/0, /*max_bytes=*/0,
write_options.protection_bytes_per_key,
default_cf_ucmp->timestamp_size());
std::unique_ptr<WriteBatch> trace_batch;
bool needs_trace_batch = false;
{
InstrumentedMutexLock lock(&trace_mutex_);
needs_trace_batch = tracer_ != nullptr;
}
if (needs_trace_batch) {
// Keep tracing on the original logical `PutEntity()` op. The applied batch
// can stay empty here and be materialized later inside `WriteImpl()`.
trace_batch = std::make_unique<WriteBatch>(
/*reserved_bytes=*/0, /*max_bytes=*/0,
/*protection_bytes_per_key=*/0, default_cf_ucmp->timestamp_size());
Status trace_s = trace_batch->PutEntity(column_family, key, columns);
if (!trace_s.ok()) {
return trace_s;
}
}
auto* cfh = static_cast_with_check<ColumnFamilyHandleImpl>(column_family);
const bool needs_deferred_transform =
cfh->cfd()->blob_partition_manager() != nullptr;
std::optional<DeferredPutEntityBatch> deferred_put_entities;
if (needs_deferred_transform) {
WideColumns sorted_columns(columns);
WideColumnsHelper::SortColumns(sorted_columns);
deferred_put_entities.emplace();
deferred_put_entities->ops.push_back(
{cfh->cfd()->GetID(), key.ToString(), std::move(sorted_columns)});
} else {
Status append_s = AppendPreprocessedPutEntityToBatch(
write_options, &batch, column_family, key, columns,
/*blob_direct_write_ctx=*/nullptr);
if (!append_s.ok()) {
return append_s;
}
}
Status write_s = WritePreprocessedPutEntityBatch(
write_options, &batch, trace_batch ? trace_batch.get() : nullptr,
deferred_put_entities ? &deferred_put_entities.value() : nullptr);
return write_s;
}
Status DBImpl::PutEntityFastPath(const WriteOptions& write_options,
const Slice& key,
const AttributeGroups& attribute_groups) {
ColumnFamilyHandle* default_cf = DefaultColumnFamily();
assert(default_cf);
const Comparator* const default_cf_ucmp = default_cf->GetComparator();
assert(default_cf_ucmp);
WriteBatch batch(/*reserved_bytes=*/0, /*max_bytes=*/0,
write_options.protection_bytes_per_key,
default_cf_ucmp->timestamp_size());
std::unique_ptr<WriteBatch> trace_batch;
bool needs_trace_batch = false;
{
InstrumentedMutexLock lock(&trace_mutex_);
needs_trace_batch = tracer_ != nullptr;
}
if (needs_trace_batch) {
// Keep tracing on the original logical `PutEntity()` op. The applied batch
// can stay empty here and be materialized later inside `WriteImpl()`.
trace_batch = std::make_unique<WriteBatch>(
/*reserved_bytes=*/0, /*max_bytes=*/0,
/*protection_bytes_per_key=*/0, default_cf_ucmp->timestamp_size());
Status trace_s = trace_batch->PutEntity(key, attribute_groups);
if (!trace_s.ok()) {
return trace_s;
}
}
bool needs_deferred_transform = false;
for (const AttributeGroup& ag : attribute_groups) {
auto* cfh =
static_cast_with_check<ColumnFamilyHandleImpl>(ag.column_family());
if (cfh->cfd()->blob_partition_manager() != nullptr) {
needs_deferred_transform = true;
break;
}
}
std::optional<DeferredPutEntityBatch> deferred_put_entities;
if (needs_deferred_transform) {
deferred_put_entities.emplace();
}
for (const AttributeGroup& ag : attribute_groups) {
auto* cfh =
static_cast_with_check<ColumnFamilyHandleImpl>(ag.column_family());
if (deferred_put_entities.has_value()) {
WideColumns sorted_columns(ag.columns());
WideColumnsHelper::SortColumns(sorted_columns);
deferred_put_entities->ops.push_back(
{cfh->cfd()->GetID(), key.ToString(), std::move(sorted_columns)});
continue;
}
Status append_s = AppendPreprocessedPutEntityToBatch(
write_options, &batch, ag.column_family(), key, ag.columns(),
/*blob_direct_write_ctx=*/nullptr);
if (!append_s.ok()) {
return append_s;
}
}
Status write_s = WritePreprocessedPutEntityBatch(
write_options, &batch, trace_batch ? trace_batch.get() : nullptr,
deferred_put_entities ? &deferred_put_entities.value() : nullptr);
return write_s;
}
Status DBImpl::WritePreprocessedPutEntityBatch(
const WriteOptions& write_options, WriteBatch* batch,
WriteBatch* trace_batch,
const DeferredPutEntityBatch* deferred_put_entities) {
Status s;
if (deferred_put_entities == nullptr &&
write_options.protection_bytes_per_key > 0) {
s = WriteBatchInternal::UpdateProtectionInfo(
batch, write_options.protection_bytes_per_key);
}
if (s.ok()) {
PutEntityFastPathWriteCallback no_batching_callback;
WriteCallback* callback =
deferred_put_entities != nullptr ? &no_batching_callback : nullptr;
// Pass the logical trace batch separately since `batch` may be rebuilt in
// `WriteImpl()` after `PreprocessWrite()` selects the target generation.
s = WriteImpl(write_options, batch, callback,
/*user_write_cb=*/nullptr,
/*wal_used=*/nullptr, /*log_ref=*/0,
/*disable_memtable=*/false, /*seq_used=*/nullptr,
/*batch_cnt=*/0, /*pre_release_callback=*/nullptr,
/*post_memtable_callback=*/nullptr,
/*wbwi=*/nullptr, trace_batch,
/*skip_blob_direct_write_transform=*/true,
deferred_put_entities);
}
return s;
}
Status DBImpl::SyncBlobDirectWriteManagers(
const WriteOptions& write_options,
const BlobDirectWriteContext& blob_direct_write_ctx) {
for (auto* mgr : blob_direct_write_ctx.touched_managers) {
Status blob_s = write_options.sync ? mgr->SyncAllOpenFiles(write_options)
: mgr->FlushAllOpenFiles(write_options);
if (!blob_s.ok()) {
return blob_s;
}
}
if (!blob_direct_write_ctx.touched_managers.empty()) {
// Tests can inject a post-transform failure after blob bytes are durable
// enough to require manifest-time garbage accounting.
Status blob_s;
TEST_SYNC_POINT_CALLBACK("DBImpl::WriteImpl:AfterBlobDirectWrite", &blob_s);
if (!blob_s.ok()) {
return blob_s;
}
}
return Status::OK();
}
Status DBImpl::WriteImpl(
const WriteOptions& write_options, WriteBatch* my_batch,
WriteCallback* callback, UserWriteCallback* user_write_cb,
uint64_t* wal_used, uint64_t log_ref, bool disable_memtable,
uint64_t* seq_used, size_t batch_cnt,
PreReleaseCallback* pre_release_callback,
PostMemTableCallback* post_memtable_callback,
std::shared_ptr<WriteBatchWithIndex> wbwi, WriteBatch* trace_batch_override,
bool skip_blob_direct_write_transform,
const DBImpl::DeferredPutEntityBatch* deferred_put_entities) {
Status DBImpl::WriteImpl(const WriteOptions& write_options,
WriteBatch* my_batch, WriteCallback* callback,
UserWriteCallback* user_write_cb, uint64_t* wal_used,
uint64_t log_ref, bool disable_memtable,
uint64_t* seq_used, size_t batch_cnt,
PreReleaseCallback* pre_release_callback,
PostMemTableCallback* post_memtable_callback,
std::shared_ptr<WriteBatchWithIndex> wbwi) {
assert(!seq_per_batch_ || batch_cnt != 0);
assert(my_batch == nullptr || my_batch->Count() == 0 ||
write_options.protection_bytes_per_key == 0 ||
@@ -918,12 +421,6 @@ Status DBImpl::WriteImpl(
"WriteOptions::disableWAL option is not supported if "
"DBOptions::recycle_log_file_num > 0");
}
WriteBatch* trace_batch =
trace_batch_override != nullptr ? trace_batch_override : my_batch;
// Non-preserved-order tracing runs before the write joins a group, so choose
// the logical batch here. Preserved-order tracing later uses
// `Writer::trace_batch` on each grouped writer.
// TODO: this use of operator bool on `tracer_` can avoid unnecessary lock
// grabs but does not seem thread-safe.
if (tracer_) {
@@ -933,7 +430,7 @@ Status DBImpl::WriteImpl(
// efficient to trace here than to add latency to a phase of the log/apply
// pipeline.
// TODO: maybe handle the tracing status?
tracer_->Write(trace_batch).PermitUncheckedError();
tracer_->Write(my_batch).PermitUncheckedError();
}
}
if (write_options.sync && write_options.disableWAL) {
@@ -1007,50 +504,10 @@ Status DBImpl::WriteImpl(
seq_per_batch_ ? kDoAssignOrder : kDontAssignOrder;
// Otherwise it is WAL-only Prepare batches in WriteCommitted policy and
// they don't consume sequence.
return WriteImplWALOnly(&nonmem_write_thread_, write_options, my_batch,
my_batch, callback, user_write_cb, wal_used,
log_ref, seq_used, batch_cnt, pre_release_callback,
assign_order, kDontPublishLastSeq,
disable_memtable);
}
const bool maybe_use_blob_direct_write = wbwi == nullptr &&
!skip_blob_direct_write_transform &&
HasAnyBlobDirectWriteColumnFamily();
const bool maybe_use_deferred_put_entity =
deferred_put_entities != nullptr && wbwi == nullptr &&
HasAnyBlobDirectWriteColumnFamily();
std::optional<WriteBatch> transformed_batch_storage;
std::optional<std::vector<WriteBatch>> transformed_write_group_batches;
std::optional<BlobDirectWriteContext> blob_direct_write_ctx;
std::optional<BlobWriteRollbackGuard> blob_write_rollback_guard;
if (maybe_use_blob_direct_write || maybe_use_deferred_put_entity) {
blob_direct_write_ctx.emplace(this);
blob_write_rollback_guard.emplace(&blob_direct_write_ctx->rollback_infos,
immutable_db_options_.info_log.get());
}
auto finish_write = [&](Status s) {
if (s.ok() && blob_write_rollback_guard.has_value()) {
blob_write_rollback_guard->Dismiss();
}
return s;
};
if (UNLIKELY(maybe_use_blob_direct_write) &&
(immutable_db_options_.unordered_write ||
immutable_db_options_.enable_pipelined_write)) {
transformed_batch_storage.emplace();
Status blob_s = MaybeTransformBatchForBlobDirectWrite(
write_options, &my_batch, !disable_memtable,
/*lookup_from_write_thread=*/false, &transformed_batch_storage.value(),
&blob_direct_write_ctx.value());
if (!blob_s.ok()) {
return blob_s;
}
blob_s = SyncBlobDirectWriteManagers(write_options,
blob_direct_write_ctx.value());
if (!blob_s.ok()) {
return blob_s;
}
return WriteImplWALOnly(
&nonmem_write_thread_, write_options, my_batch, callback, user_write_cb,
wal_used, log_ref, seq_used, batch_cnt, pre_release_callback,
assign_order, kDontPublishLastSeq, disable_memtable);
}
if (immutable_db_options_.unordered_write) {
@@ -1061,11 +518,10 @@ Status DBImpl::WriteImpl(
uint64_t seq = 0;
// Use a write thread to i) optimize for WAL write, ii) publish last
// sequence in in increasing order, iii) call pre_release_callback serially
Status status =
WriteImplWALOnly(&write_thread_, write_options, my_batch, trace_batch,
callback, user_write_cb, wal_used, log_ref, &seq,
sub_batch_cnt, pre_release_callback, kDoAssignOrder,
kDoPublishLastSeq, disable_memtable);
Status status = WriteImplWALOnly(
&write_thread_, write_options, my_batch, callback, user_write_cb,
wal_used, log_ref, &seq, sub_batch_cnt, pre_release_callback,
kDoAssignOrder, kDoPublishLastSeq, disable_memtable);
TEST_SYNC_POINT("DBImpl::WriteImpl:UnorderedWriteAfterWriteWAL");
if (!status.ok()) {
return status;
@@ -1078,20 +534,19 @@ Status DBImpl::WriteImpl(
status = UnorderedWriteMemtable(write_options, my_batch, callback,
log_ref, seq, sub_batch_cnt);
}
return finish_write(status);
return status;
}
if (immutable_db_options_.enable_pipelined_write) {
return finish_write(PipelinedWriteImpl(
write_options, my_batch, trace_batch, callback, user_write_cb, wal_used,
log_ref, disable_memtable, seq_used));
return PipelinedWriteImpl(write_options, my_batch, callback, user_write_cb,
wal_used, log_ref, disable_memtable, seq_used);
}
PERF_TIMER_GUARD(write_pre_and_post_process_time);
WriteThread::Writer w(write_options, my_batch, callback, user_write_cb,
log_ref, disable_memtable, batch_cnt,
pre_release_callback, post_memtable_callback,
/*_ingest_wbwi=*/wbwi != nullptr, trace_batch);
/*_ingest_wbwi=*/wbwi != nullptr);
StopWatch write_sw(immutable_db_options_.clock, stats_, DB_WRITE);
write_thread_.JoinBatchGroup(&w);
@@ -1149,7 +604,7 @@ Status DBImpl::WriteImpl(
*seq_used = w.sequence;
}
// write is complete and leader has updated sequence
return finish_write(w.FinalStatus());
return w.FinalStatus();
}
// else we are the leader of the write batch group
assert(w.state == WriteThread::STATE_GROUP_LEADER);
@@ -1199,59 +654,6 @@ Status DBImpl::WriteImpl(
IOStatus io_s;
Status pre_release_cb_status;
size_t seq_inc = 0;
if (status.ok()) {
if (deferred_put_entities != nullptr) {
my_batch->Clear();
for (const auto& op : deferred_put_entities->ops) {
status = AppendSortedPutEntityToBatch(
write_options, my_batch, op.column_family_id, op.key,
op.sorted_columns,
blob_direct_write_ctx ? &blob_direct_write_ctx.value() : nullptr);
if (!status.ok()) {
break;
}
}
if (status.ok() && write_options.protection_bytes_per_key > 0) {
status = WriteBatchInternal::UpdateProtectionInfo(
my_batch, write_options.protection_bytes_per_key);
}
if (status.ok() && blob_direct_write_ctx.has_value()) {
status = SyncBlobDirectWriteManagers(write_options,
blob_direct_write_ctx.value());
}
}
if (UNLIKELY(maybe_use_blob_direct_write) &&
!immutable_db_options_.unordered_write &&
!immutable_db_options_.enable_pipelined_write) {
// PreprocessWrite() may switch memtables in order to schedule pending
// flushes. Defer the blob direct-write transformation until after that
// point so the blob file generation matches the memtable that will store
// the transformed blob index.
transformed_write_group_batches.emplace();
transformed_write_group_batches->reserve(write_group.size);
for (auto* writer : write_group) {
assert(writer != nullptr);
if (!writer->ShouldWriteToMemtable()) {
continue;
}
transformed_write_group_batches->emplace_back();
status = MaybeTransformBatchForBlobDirectWrite(
write_options, &writer->batch, /*should_write_to_memtable=*/true,
/*lookup_from_write_thread=*/true,
&transformed_write_group_batches->back(),
&blob_direct_write_ctx.value());
if (!status.ok()) {
break;
}
}
if (status.ok()) {
status = SyncBlobDirectWriteManagers(write_options,
blob_direct_write_ctx.value());
}
}
}
if (status.ok()) {
// Rules for when we can update the memtable concurrently
// 1. supported by memtable
@@ -1300,7 +702,7 @@ Status DBImpl::WriteImpl(
// is in writer->batch
tracer_->Write(wbwi->GetWriteBatch()).PermitUncheckedError();
} else {
tracer_->Write(writer->trace_batch).PermitUncheckedError();
tracer_->Write(writer->batch).PermitUncheckedError();
}
}
}
@@ -1521,11 +923,11 @@ Status DBImpl::WriteImpl(
if (two_write_queues_) {
assert(ub <= versions_->LastAllocatedSequence());
}
status = IngestWBWIAsMemtable(
wbwi, {/*lower_bound=*/lb, /*upper_bound=*/ub},
/*min_prep_log=*/log_ref, last_sequence,
/*memtable_updated=*/memtable_update_count > 0,
ingest_wbwi_for_commit, write_options.ignore_missing_column_families);
status =
IngestWBWIAsMemtable(wbwi, {/*lower_bound=*/lb, /*upper_bound=*/ub},
/*min_prep_log=*/log_ref, last_sequence,
/*memtable_updated=*/memtable_update_count > 0,
write_options.ignore_missing_column_families);
RecordTick(stats_, NUMBER_WBWI_INGEST);
}
}
@@ -1563,12 +965,11 @@ Status DBImpl::WriteImpl(
if (status.ok()) {
status = w.FinalStatus();
}
return finish_write(status);
return status;
}
Status DBImpl::PipelinedWriteImpl(const WriteOptions& write_options,
WriteBatch* my_batch, WriteBatch* trace_batch,
WriteCallback* callback,
WriteBatch* my_batch, WriteCallback* callback,
UserWriteCallback* user_write_cb,
uint64_t* wal_used, uint64_t log_ref,
bool disable_memtable, uint64_t* seq_used) {
@@ -1579,9 +980,7 @@ Status DBImpl::PipelinedWriteImpl(const WriteOptions& write_options,
WriteThread::Writer w(write_options, my_batch, callback, user_write_cb,
log_ref, disable_memtable, /*_batch_cnt=*/0,
/*_pre_release_callback=*/nullptr,
/*_post_memtable_callback=*/nullptr,
/*_ingest_wbwi=*/false, trace_batch);
/*_pre_release_callback=*/nullptr);
write_thread_.JoinBatchGroup(&w);
TEST_SYNC_POINT("DBImplWrite::PipelinedWriteImpl:AfterJoinBatchGroup");
if (w.state == WriteThread::STATE_GROUP_LEADER) {
@@ -1630,7 +1029,7 @@ Status DBImpl::PipelinedWriteImpl(const WriteOptions& write_options,
continue;
}
// TODO: maybe handle the tracing status?
tracer_->Write(writer->trace_batch).PermitUncheckedError();
tracer_->Write(writer->batch).PermitUncheckedError();
}
}
}
@@ -1825,7 +1224,7 @@ Status DBImpl::UnorderedWriteMemtable(const WriteOptions& write_options,
// applicable in a two-queue setting.
Status DBImpl::WriteImplWALOnly(
WriteThread* write_thread, const WriteOptions& write_options,
WriteBatch* my_batch, WriteBatch* trace_batch, WriteCallback* callback,
WriteBatch* my_batch, WriteCallback* callback,
UserWriteCallback* user_write_cb, uint64_t* wal_used,
const uint64_t log_ref, uint64_t* seq_used, const size_t sub_batch_cnt,
PreReleaseCallback* pre_release_callback, const AssignOrder assign_order,
@@ -1833,9 +1232,7 @@ Status DBImpl::WriteImplWALOnly(
PERF_TIMER_GUARD(write_pre_and_post_process_time);
WriteThread::Writer w(write_options, my_batch, callback, user_write_cb,
log_ref, disable_memtable, sub_batch_cnt,
pre_release_callback,
/*_post_memtable_callback=*/nullptr,
/*_ingest_wbwi=*/false, trace_batch);
pre_release_callback);
StopWatch write_sw(immutable_db_options_.clock, stats_, DB_WRITE);
write_thread->JoinBatchGroup(&w);
@@ -1915,7 +1312,7 @@ Status DBImpl::WriteImplWALOnly(
continue;
}
// TODO: maybe handle the tracing status?
tracer_->Write(writer->trace_batch).PermitUncheckedError();
tracer_->Write(writer->batch).PermitUncheckedError();
}
}
}
@@ -2588,6 +1985,7 @@ void DBImpl::SelectColumnFamiliesForAtomicFlush(
// Assign sequence number for atomic flush.
void DBImpl::AssignAtomicFlushSeq(const autovector<ColumnFamilyData*>& cfds) {
assert(immutable_db_options_.atomic_flush);
auto seq = versions_->LastSequence();
for (auto cfd : cfds) {
// cfd can be nullptr, see ScheduleFlushes()
@@ -2691,7 +2089,6 @@ Status DBImpl::SwitchWAL(WriteContext* write_context) {
}
if (immutable_db_options_.atomic_flush) {
FlushRequest flush_req;
flush_req.atomic_flush = true;
GenerateFlushRequest(cfds, FlushReason::kWalFull, &flush_req);
EnqueuePendingFlush(flush_req);
}
@@ -2784,7 +2181,6 @@ Status DBImpl::HandleWriteBufferManagerFlush(WriteContext* write_context) {
}
if (immutable_db_options_.atomic_flush) {
FlushRequest flush_req;
flush_req.atomic_flush = true;
GenerateFlushRequest(cfds, FlushReason::kWriteBufferManager, &flush_req);
EnqueuePendingFlush(flush_req);
}
@@ -3061,7 +2457,6 @@ Status DBImpl::ScheduleFlushes(WriteContext* context) {
if (immutable_db_options_.atomic_flush) {
AssignAtomicFlushSeq(cfds);
FlushRequest flush_req;
flush_req.atomic_flush = true;
GenerateFlushRequest(cfds, FlushReason::kWriteBufferFull, &flush_req);
EnqueuePendingFlush(flush_req);
} else {
@@ -3145,7 +2540,7 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
memtable_info.num_deletes = cfd->mem()->NumDeletion();
if (!cfd->ioptions().persist_user_defined_timestamps &&
cfd->user_comparator()->timestamp_size() > 0) {
Slice newest_udt = cfd->mem()->GetNewestUDT();
const Slice& newest_udt = cfd->mem()->GetNewestUDT();
memtable_info.newest_udt.assign(newest_udt.data(), newest_udt.size());
}
// Log this later after lock release. It may be outdated, e.g., if background
@@ -3168,7 +2563,6 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
// of mutable_cf_options.write_buffer_size.
io_s = CreateWAL(write_options, new_log_number, recycle_log_number,
preallocate_block_size, info, &new_log);
TEST_SYNC_POINT_CALLBACK("DBImpl::SwitchMemtable:AfterCreateWAL", &io_s);
if (s.ok()) {
s = io_s;
}
@@ -3194,16 +2588,11 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
"[%s] New memtable created with log file: #%" PRIu64
". Immutable memtables: %d.\n",
cfd->GetName().c_str(), new_log_number, num_imm_unflushed);
// There should be no concurrent write as the thread is at the front of
// writer queue
cfd->mem()->ConstructFragmentedRangeTombstones();
}
// MarkImmutable() blocks concurrent AddLogicallyRedundantRangeTombstone()
// from reader threads. Once blocked, safe to construct the fragmented range
// tombstone list. Both calls happen outside the DB mutex to avoid increasing
// write stall. MarkImmutable() is idempotent, so Add() calling it again
// inside the mutex is harmless.
cfd->mem()->MarkImmutable();
cfd->mem()->ConstructFragmentedRangeTombstones();
mutex_.Lock();
if (recycle_log_number != 0) {
// Since renaming the file is done outside DB mutex, we need to ensure
@@ -3330,15 +2719,9 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
}
}
if (cfd->blob_partition_manager() != nullptr) {
cfd->blob_partition_manager()->RotateCurrentGeneration();
}
cfd->mem()->SetNextLogNumber(cur_wal_number_);
assert(new_mem != nullptr);
cfd->imm()->Add(cfd->mem(), &context->memtables_to_free_);
TEST_SYNC_POINT(
"DBImpl::SwitchMemtable:AfterConstructFragmentedRangeTombstones");
if (new_imm) {
// Need to assign memtable id here before SetMemtable() below assigns id to
// the new live memtable
+129 -478
View File
@@ -11,11 +11,7 @@
#include <limits>
#include <string>
#include <vector>
#include "db/blob/blob_fetcher.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_index.h"
#include "db/dbformat.h"
#include "db/merge_context.h"
#include "db/merge_helper.h"
@@ -26,7 +22,6 @@
#include "logging/logging.h"
#include "memory/arena.h"
#include "monitoring/perf_context_imp.h"
#include "port/likely.h"
#include "rocksdb/env.h"
#include "rocksdb/io_dispatcher.h"
#include "rocksdb/iterator.h"
@@ -42,26 +37,6 @@
namespace ROCKSDB_NAMESPACE {
namespace {
bool HasFullTimestampVisibility(const ReadOptions& read_options) {
if (read_options.iter_start_ts != nullptr) {
return false;
}
if (read_options.timestamp == nullptr) {
return true;
}
const Slice ts = *read_options.timestamp;
for (size_t i = 0; i < ts.size(); ++i) {
if (static_cast<unsigned char>(ts[i]) != 0xff) {
return false;
}
}
return true;
}
} // namespace
DBIter::DBIter(Env* _env, const ReadOptions& read_options,
const ImmutableOptions& ioptions,
const MutableCFOptions& mutable_cf_options,
@@ -76,14 +51,11 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
user_comparator_(cmp),
merge_operator_(ioptions.merge_operator.get()),
iter_(iter),
blob_reader_(
version, read_options.read_tier, read_options.verify_checksums,
read_options.fill_cache, read_options.io_activity,
cfh ? cfh->cfd()->blob_file_cache() : nullptr,
cfh != nullptr && cfh->cfd()->blob_partition_manager() != nullptr),
blob_reader_(version, read_options.read_tier,
read_options.verify_checksums, read_options.fill_cache,
read_options.io_activity),
read_callback_(read_callback),
sequence_(s),
value_columns_state_(version, read_options, cfh),
statistics_(ioptions.stats),
max_skip_(mutable_cf_options.max_sequential_skip_in_iterations),
max_skippable_internal_keys_(read_options.max_skippable_internal_keys),
@@ -100,7 +72,6 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
avg_op_scan_flush_trigger_(0),
iter_step_since_seek_(1),
mem_hidden_op_scanned_since_seek_(0),
contiguous_tombstone_count_(0),
direction_(kForward),
valid_(false),
current_entry_is_merged_(false),
@@ -111,18 +82,6 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
expect_total_order_inner_iter_(prefix_extractor_ == nullptr ||
read_options.total_order_seek ||
read_options.auto_prefix_mode),
// Read-path range conversion assumes the scan can observe all interior
// live keys. table_filter can hide whole SSTs, and timestamp filtering
// can hide newer UDT versions unless the read is at max timestamp with no
// lower timestamp bound. Legacy prefix iterators without
// prefix_same_as_start do not guarantee complete scans, so conversion
// must stay disabled for the iterator lifetime.
min_tombstones_for_range_conversion_(
active_mem != nullptr && !read_options.table_filter &&
(expect_total_order_inner_iter_ || prefix_same_as_start_) &&
HasFullTimestampVisibility(read_options)
? mutable_cf_options.min_tombstones_for_range_conversion
: 0),
expose_blob_index_(expose_blob_index),
allow_unprepared_value_(read_options.allow_unprepared_value),
is_blob_(false),
@@ -178,7 +137,7 @@ Status DBIter::GetProperty(std::string prop_name, std::string* prop) {
} else if (prop_name == "rocksdb.iterator.is-value-pinned") {
if (valid_) {
*prop = (pin_thru_lifetime_ && iter_.Valid() &&
iter_.value().data() == value_columns_state_.value().data())
iter_.value().data() == value_.data())
? "1"
: "0";
} else {
@@ -224,7 +183,6 @@ void DBIter::Next() {
bool ok = true;
if (direction_ == kReverse) {
is_key_seqnum_zero_ = false;
ResetContiguousTombstoneTracking();
if (!ReverseToForward()) {
ok = false;
}
@@ -243,7 +201,13 @@ void DBIter::Next() {
if (ok && iter_.Valid()) {
ClearSavedValue();
FindNextUserEntry(true /* skipping the current user key */);
if (prefix_same_as_start_) {
assert(prefix_extractor_ != nullptr);
const Slice prefix = prefix_.GetUserKey();
FindNextUserEntry(true /* skipping the current user key */, &prefix);
} else {
FindNextUserEntry(true /* skipping the current user key */, nullptr);
}
} else {
is_key_seqnum_zero_ = false;
valid_ = false;
@@ -254,12 +218,11 @@ void DBIter::Next() {
}
}
Status DBIter::BlobReader::RetrieveAndSetBlobValue(
const Slice& user_key, const Slice& blob_index,
bool allow_write_path_fallback) {
Status DBIter::BlobReader::RetrieveAndSetBlobValue(const Slice& user_key,
const Slice& blob_index) {
assert(blob_value_.empty());
if (!version_ && (!allow_write_path_fallback || !blob_file_cache_)) {
if (!version_) {
return Status::Corruption("Encountered unexpected blob index.");
}
@@ -274,42 +237,19 @@ Status DBIter::BlobReader::RetrieveAndSetBlobValue(
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
constexpr uint64_t* bytes_read = nullptr;
if (!allow_write_path_fallback) {
assert(version_ != nullptr);
return version_->GetBlob(read_options, user_key, blob_index,
prefetch_buffer, &blob_value_, bytes_read);
}
const Status s = version_->GetBlob(read_options, user_key, blob_index,
prefetch_buffer, &blob_value_, bytes_read);
BlobIndex blob_idx;
Status s = blob_idx.DecodeFrom(blob_index);
if (!s.ok()) {
return s;
}
return BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
read_options, user_key, blob_idx, version_, blob_file_cache_,
prefetch_buffer, &blob_value_, bytes_read);
}
BlobFetcher DBIter::BlobReader::CreateBlobFetcher() const {
ReadOptions read_options;
read_options.read_tier = read_tier_;
read_options.verify_checksums = verify_checksums_;
read_options.fill_cache = fill_cache_;
read_options.io_activity = io_activity_;
return BlobFetcher(version_, read_options, blob_file_cache_,
allow_write_path_fallback_);
return Status::OK();
}
bool DBIter::SetValueAndColumnsFromBlobImpl(const Slice& user_key,
const Slice& blob_index) {
// Keep the non-BDW iterator path on the pre-existing Version::GetBlob()
// fast path. Only enable the direct-write fallback when this CF actually
// has a write-path partition manager.
const bool allow_write_path_fallback =
cfh_ != nullptr && cfh_->cfd()->blob_partition_manager() != nullptr;
const Status s = blob_reader_.RetrieveAndSetBlobValue(
user_key, blob_index, allow_write_path_fallback);
const Status s = blob_reader_.RetrieveAndSetBlobValue(user_key, blob_index);
if (!s.ok()) {
status_ = s;
valid_ = false;
@@ -333,8 +273,8 @@ bool DBIter::SetValueAndColumnsFromBlob(const Slice& user_key,
}
if (allow_unprepared_value_) {
assert(value_columns_state_.value().empty());
assert(value_columns_state_.wide_columns().empty());
assert(value_.empty());
assert(wide_columns_.empty());
assert(lazy_blob_index_.empty());
lazy_blob_index_ = blob_index;
@@ -346,102 +286,22 @@ bool DBIter::SetValueAndColumnsFromBlob(const Slice& user_key,
}
bool DBIter::SetValueAndColumnsFromEntity(Slice slice) {
auto& state = value_columns_state_;
state.AssertReadyForEntity();
assert(value_.empty());
assert(wide_columns_.empty());
// Fast path: if no blob columns, use the simpler Deserialize
bool has_blob_columns = false;
{
const Status s_hbc =
WideColumnSerialization::HasBlobColumns(slice, has_blob_columns);
if (!s_hbc.ok()) {
status_ = s_hbc;
valid_ = false;
return false;
}
}
if (LIKELY(!has_blob_columns)) {
WideColumns& wide_columns = state.wide_columns();
const Status s = WideColumnSerialization::Deserialize(slice, wide_columns);
const Status s = WideColumnSerialization::Deserialize(slice, wide_columns_);
if (!s.ok()) {
status_ = s;
valid_ = false;
state.ClearWideColumns();
return false;
}
state.MaybeSetValueFromMaterializedDefaultColumn();
return true;
}
// Entity has blob columns.
// First, copy the serialized data to the saved entity buffer so that column
// name/value Slices remain valid after the internal iterator moves.
// Guard: if slice already aliases that saved buffer (e.g., when called from
// SetValueAndColumnsFromMergeResult), skip the redundant copy to
// avoid self-aliased std::string::assign (undefined behavior).
state.SaveEntitySliceIfNeeded(slice);
{
Slice input_copy = state.PrepareForLazyEntityDeserialize();
const Status s = WideColumnSerialization::DeserializeV2(
input_copy, state.lazy_entity_columns(), state.lazy_blob_columns());
if (!s.ok()) {
status_ = s;
valid_ = false;
state.ClearLazyEntity();
return false;
}
}
// Iterator positions must expose fully prepared values and columns once
// Valid() becomes true, so resolve and materialize all blob columns here.
state.BindLazyEntity(saved_key_.GetUserKey());
if (!MaterializeLazyEntityColumns()) {
state.ClearLazyEntity();
if (!s.ok()) {
status_ = s;
valid_ = false;
wide_columns_.clear();
return false;
}
state.MaybeSetValueFromMaterializedDefaultColumn();
return true;
}
bool DBIter::MaterializeLazyEntityColumns() const {
const auto& state = value_columns_state_;
if (state.lazy_entity_columns().empty() || !state.wide_columns().empty()) {
return true;
if (WideColumnsHelper::HasDefaultColumn(wide_columns_)) {
value_ = WideColumnsHelper::GetDefaultColumn(wide_columns_);
}
std::lock_guard<std::mutex> lock(state.lazy_entity_columns_mutex());
if (state.lazy_entity_columns().empty() || !state.wide_columns().empty()) {
return true;
}
DBIter* const mutable_this = const_cast<DBIter*>(this);
auto& mutable_state = mutable_this->value_columns_state_;
WideColumns materialized_columns;
materialized_columns.reserve(state.lazy_entity_columns().size());
for (const auto& col : state.lazy_entity_columns()) {
materialized_columns.emplace_back(col.name(), col.value());
}
for (const auto& blob_col : state.lazy_blob_columns()) {
Slice resolved_value;
const Status s = mutable_state.entity_blob_resolver().ResolveColumn(
blob_col.first, &resolved_value);
if (!s.ok()) {
mutable_this->status_ = s;
mutable_this->valid_ = false;
mutable_state.wide_columns().clear();
return false;
}
materialized_columns[blob_col.first].value() = resolved_value;
}
mutable_state.wide_columns() = std::move(materialized_columns);
return true;
}
@@ -454,7 +314,7 @@ bool DBIter::SetValueAndColumnsFromMergeResult(const Status& merge_status,
}
if (result_type == kTypeWideColumnEntity) {
if (!SetValueAndColumnsFromEntity(value_columns_state_.saved_value())) {
if (!SetValueAndColumnsFromEntity(saved_value_)) {
assert(!valid_);
return false;
}
@@ -464,9 +324,8 @@ bool DBIter::SetValueAndColumnsFromMergeResult(const Status& merge_status,
}
assert(result_type == kTypeValue);
SetValueAndColumnsFromPlain(pinned_value_.data()
? pinned_value_
: value_columns_state_.saved_value());
SetValueAndColumnsFromPlain(pinned_value_.data() ? pinned_value_
: saved_value_);
valid_ = true;
return true;
}
@@ -493,7 +352,7 @@ bool DBIter::PrepareValue() {
// POST: saved_key_ should have the next user key if valid_,
// if the current entry is a result of merge
// current_entry_is_merged_ => true
// the saved merge buffer => the merged value
// saved_value_ => the merged value
//
// NOTE: In between, saved_key_ can point to a user key that has
// a delete marker or a sequence number higher than sequence_
@@ -502,13 +361,14 @@ bool DBIter::PrepareValue() {
// The prefix parameter, if not null, indicates that we need to iterate
// within the prefix, and the iterator needs to be made invalid, if no
// more entry for the prefix can be found.
bool DBIter::FindNextUserEntry(bool skipping_saved_key) {
bool DBIter::FindNextUserEntry(bool skipping_saved_key, const Slice* prefix) {
PERF_TIMER_GUARD(find_next_user_entry_time);
return FindNextUserEntryInternal(skipping_saved_key);
return FindNextUserEntryInternal(skipping_saved_key, prefix);
}
// Actual implementation of DBIter::FindNextUserEntry()
bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key) {
bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key,
const Slice* prefix) {
// Loop until we hit an acceptable entry to yield
assert(iter_.Valid());
assert(status_.ok());
@@ -561,18 +421,12 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key) {
break;
}
assert(!prefix_.has_value() || prefix_extractor_ != nullptr);
if (!PrefixCheck(user_key_without_ts)) {
// Insert any pending tombstone run using the last tracked delete
// (saved_key_) as the end key. We cannot use the current key's
// prefix as the boundary because bloom filters may have hidden
// entire prefixes between the seek prefix and the current key.
// The tombstone covers n-1 of n deletes; the last remains as a
// point delete.
FlushPendingTombstoneRun(saved_key_.GetUserKey());
if (prefix_same_as_start_) {
break;
}
assert(prefix == nullptr || prefix_extractor_ != nullptr);
if (prefix != nullptr &&
prefix_extractor_->Transform(user_key_without_ts).compare(*prefix) !=
0) {
assert(prefix_same_as_start_);
break;
}
if (TooManyInternalKeysSkipped()) {
@@ -620,15 +474,6 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key) {
skipping_saved_key = true;
PERF_COUNTER_ADD(internal_delete_skipped_count, 1);
MarkMemtableForFlushForPerOpTrigger(mem_hidden_op_scanned);
// Track contiguous tombstones for range conversion.
// Skip if outside seek prefix — the top-of-loop check
// flushed any pending run, but we must also avoid starting
// a new run outside the prefix.
if (min_tombstones_for_range_conversion_ > 0 &&
PrefixCheck(user_key_without_ts)) {
TrackContiguousTombstone(ikey_.user_key,
/*always_update_first_key=*/false);
}
}
break;
case kTypeValue:
@@ -638,7 +483,6 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key) {
if (!PrepareValueInternal()) {
return false;
}
FlushPendingTombstoneRun(ikey_.user_key);
if (timestamp_lb_) {
saved_key_.SetInternalKey(ikey_);
} else {
@@ -672,7 +516,6 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key) {
if (!PrepareValueInternal()) {
return false;
}
FlushPendingTombstoneRun(ikey_.user_key);
saved_key_.SetUserKey(
ikey_.user_key,
!pin_thru_lifetime_ || !iter_.iter()->IsKeyPinned() /* copy */);
@@ -774,28 +617,6 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key) {
}
} while (iter_.Valid());
// If we accumulated tombstones, insert the range tombstone. Use
// iterate_upper_bound_ if within the seek prefix, otherwise fall back to
// saved_key_ (the last tracked delete, covering n-1 deletes).
if (contiguous_tombstone_count_ > 0 && iter_.status().ok()) {
if (iterate_upper_bound_ != nullptr && PrefixCheck(*iterate_upper_bound_)) {
if (timestamp_size_ == 0) {
MaybeInsertRangeTombstone(*iterate_upper_bound_);
} else {
// iterate_upper_bound_ is a plain user key without a timestamp
// suffix. Pad with min timestamp so it sorts after all entries with
// this user key, preserving the exclusive bound semantics.
std::string end_key_with_ts;
AppendKeyWithMinTimestamp(&end_key_with_ts, *iterate_upper_bound_,
timestamp_size_);
MaybeInsertRangeTombstone(end_key_with_ts);
}
} else if (prefix_.has_value()) {
MaybeInsertRangeTombstone(saved_key_.GetUserKey());
}
}
ResetContiguousTombstoneTracking();
valid_ = false;
return iter_.status().ok();
}
@@ -805,7 +626,7 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key) {
// PRE: iter_.key() points to the first merge type entry
// saved_key_ stores the user key
// iter_.PrepareValue() has been called
// POST: the saved merge buffer has the merged value for the user key
// POST: saved_value_ has the merged value for the user key
// iter_ points to the next entry (or invalid)
bool DBIter::MergeValuesNewToOld() {
if (!merge_operator_) {
@@ -855,7 +676,7 @@ bool DBIter::MergeValuesNewToOld() {
value = ParsePackedValueForValue(value);
}
// hit a put or put equivalent, merge the put value with operands and
// store the final result in the saved merge buffer. We are done!
// store the final result in saved_value_. We are done!
if (!MergeWithPlainBaseValue(value, ikey.user_key)) {
return false;
}
@@ -935,20 +756,19 @@ void DBIter::Prev() {
ResetInternalKeysSkippedCounter();
bool ok = true;
if (direction_ == kForward) {
ResetContiguousTombstoneTracking();
if (!ReverseToBackward()) {
ok = false;
}
// Transitioning to reverse: current key is the end bound
if (ok && min_tombstones_for_range_conversion_ > 0) {
range_tomb_end_key_.SetUserKey(saved_key_.GetUserKey(),
!saved_key_.IsKeyPinned());
}
}
if (ok) {
ClearSavedValue();
PrevInternal();
Slice prefix;
if (prefix_same_as_start_) {
assert(prefix_extractor_ != nullptr);
prefix = prefix_.GetUserKey();
}
PrevInternal(prefix_same_as_start_ ? &prefix : nullptr);
}
if (statistics_ != nullptr) {
@@ -1040,34 +860,22 @@ bool DBIter::ReverseToBackward() {
return FindUserKeyBeforeSavedKey();
}
void DBIter::PrevInternal() {
// Capture saved_key_ (previous live key) into range_tomb_end_key_ before
// saved_key_ is overwritten below.
if (min_tombstones_for_range_conversion_ > 0) {
range_tomb_end_key_.Swap(saved_key_);
}
void DBIter::PrevInternal(const Slice* prefix) {
while (iter_.Valid()) {
saved_key_.SetUserKey(
ExtractUserKey(iter_.key()),
!iter_.iter()->IsKeyPinned() || !pin_thru_lifetime_ /* copy */);
assert(!prefix_.has_value() || prefix_extractor_ != nullptr);
Slice saved_key_without_ts =
StripTimestampFromUserKey(saved_key_.GetUserKey(), timestamp_size_);
// When prefix filtering is active, insert any pending tombstone run
// before we leave the seek prefix.
if (!PrefixCheck(saved_key_without_ts)) {
// Insert any pending tombstone run before leaving the seek prefix.
// Only insert if end_key (previous live key) is within the seek prefix.
if (range_tomb_end_key_.Size() > 0) {
FlushPendingTombstoneRun(range_tomb_end_key_.GetUserKey(),
/*check_prefix_match=*/true);
}
if (prefix_same_as_start_) {
valid_ = false;
return;
}
assert(prefix == nullptr || prefix_extractor_ != nullptr);
if (prefix != nullptr &&
prefix_extractor_
->Transform(StripTimestampFromUserKey(saved_key_.GetUserKey(),
timestamp_size_))
.compare(*prefix) != 0) {
assert(prefix_same_as_start_);
// Current key does not have the same prefix as start
valid_ = false;
return;
}
assert(iterate_lower_bound_ == nullptr || iter_.MayBeOutOfLowerBound() ||
@@ -1079,39 +887,14 @@ void DBIter::PrevInternal() {
saved_key_.GetUserKey(), /*a_has_ts=*/true, *iterate_lower_bound_,
/*b_has_ts=*/false) < 0) {
// We've iterated earlier than the user-specified lower bound.
if (range_tomb_end_key_.Size() > 0) {
FlushPendingTombstoneRun(range_tomb_end_key_.GetUserKey(),
/*check_prefix_match=*/true);
}
valid_ = false;
return;
}
bool found_visible = false;
if (!FindValueForCurrentKey(found_visible)) { // assigns valid_
if (!FindValueForCurrentKey()) { // assigns valid_
return;
}
// Track contiguous tombstones for reverse range tombstone conversion.
// Only track when FindValueForCurrentKey found a visible entry
// (found_visible == true). When no visible entry exists (all seqno >
// snapshot), the key doesn't exist at this snapshot and must not be
// treated as a tombstone. Additionally, ikey_ is only updated when a
// visible entry is found, so reading ikey_.sequence without this guard
// would use a stale value.
if (min_tombstones_for_range_conversion_ > 0 &&
range_tomb_end_key_.Size() > 0 && timestamp_lb_ == nullptr) {
if (!valid_ && found_visible && PrefixCheck(saved_key_without_ts)) {
// Key was deleted and is within the seek prefix — track it.
TrackContiguousTombstone(saved_key_.GetUserKey(),
/*always_update_first_key=*/true);
} else if (valid_) {
// Live key breaks the run.
FlushPendingTombstoneRun(range_tomb_end_key_.GetUserKey(),
/*check_prefix_match=*/true);
}
}
// Whether or not we found a value for current key, we need iter_ to end up
// on a smaller key.
if (!FindUserKeyBeforeSavedKey()) {
@@ -1128,11 +911,6 @@ void DBIter::PrevInternal() {
}
}
if (range_tomb_end_key_.Size() > 0) {
FlushPendingTombstoneRun(range_tomb_end_key_.GetUserKey(),
/*check_prefix_match=*/true);
}
// We haven't found any key - iterator is not valid
valid_ = false;
}
@@ -1142,19 +920,13 @@ void DBIter::PrevInternal() {
// value for it, or executes a merge, or determines that the value was deleted.
// Sets valid_ to true if the value is found and is ready to be presented to
// the user through value().
// Sets valid_ to false if the value was deleted or no visible entry exists.
// Sets ikey_ to the last visible entry's internal key. When found_visible
// is false, ikey_ is not updated and may contain stale data.
// Sets found_visible to true if at least one entry passed the IsVisible()
// check (seqno <= snapshot). When false, no entry was visible — the key
// does not exist at this snapshot and should not be treated as a tombstone.
// Sets valid_ to false if the value was deleted, and we should try another key.
// Returns false if an error occurred, and !status().ok() and !valid_.
//
// PRE: iter_ is positioned on the last entry with user key equal to saved_key_.
// POST: iter_ is positioned on one of the entries equal to saved_key_, or on
// the entry just before them, or on the entry just after them.
bool DBIter::FindValueForCurrentKey(bool& found_visible) {
found_visible = false;
bool DBIter::FindValueForCurrentKey() {
assert(iter_.Valid());
merge_context_.Clear();
current_entry_is_merged_ = false;
@@ -1201,10 +973,6 @@ bool DBIter::FindValueForCurrentKey(bool& found_visible) {
break;
}
// Entry survived the visibility check — at least one visible version
// exists for this user key.
found_visible = true;
if (!ts.empty()) {
saved_timestamp_.assign(ts.data(), ts.size());
}
@@ -1225,6 +993,10 @@ bool DBIter::FindValueForCurrentKey(bool& found_visible) {
}
if (timestamp_lb_ != nullptr) {
// Only needed when timestamp_lb_ is not null
[[maybe_unused]] const bool ret = ParseKey(&ikey_);
// Since the preceding ParseKey(&ikey) succeeds, so must this.
assert(ret);
saved_key_.SetInternalKey(ikey);
} else if (user_comparator_.Compare(ikey.user_key,
saved_key_.GetUserKey()) < 0) {
@@ -1233,8 +1005,6 @@ bool DBIter::FindValueForCurrentKey(bool& found_visible) {
!pin_thru_lifetime_ || !iter_.iter()->IsKeyPinned() /* copy */);
}
// Ensure ikey_ is only set to VISIBLE keys.
ikey_ = ikey;
valid_entry_seen = true;
last_key_entry_type = ikey.type;
switch (last_key_entry_type) {
@@ -1438,11 +1208,6 @@ bool DBIter::FindValueForCurrentKeyUsingSeek() {
iter_.Next();
}
// Keep ikey_ in sync with the entry found by the seek.
ikey_ = ikey;
TEST_SYNC_POINT_CALLBACK(
"DBIter::FindValueForCurrentKeyUsingSeek:ikey_updated", &ikey_);
if (ikey.type == kTypeDeletion || ikey.type == kTypeSingleDeletion ||
kTypeDeletionWithTimestamp == ikey.type) {
if (timestamp_lb_ == nullptr) {
@@ -1592,7 +1357,7 @@ bool DBIter::MergeWithNoBaseValue(const Slice& user_key) {
merge_operator_, user_key, MergeHelper::kNoBaseValue,
merge_context_.GetOperands(), logger_, statistics_, clock_,
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
&value_columns_state_.saved_value(), &pinned_value_, &result_type);
&saved_value_, &pinned_value_, &result_type);
return SetValueAndColumnsFromMergeResult(s, result_type);
}
@@ -1605,7 +1370,7 @@ bool DBIter::MergeWithPlainBaseValue(const Slice& value,
merge_operator_, user_key, MergeHelper::kPlainBaseValue, value,
merge_context_.GetOperands(), logger_, statistics_, clock_,
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
&value_columns_state_.saved_value(), &pinned_value_, &result_type);
&saved_value_, &pinned_value_, &result_type);
return SetValueAndColumnsFromMergeResult(s, result_type);
}
@@ -1620,10 +1385,7 @@ bool DBIter::MergeWithBlobBaseValue(const Slice& blob_index,
return false;
}
const bool allow_write_path_fallback =
cfh_ != nullptr && cfh_->cfd()->blob_partition_manager() != nullptr;
const Status s = blob_reader_.RetrieveAndSetBlobValue(
user_key, blob_index, allow_write_path_fallback);
const Status s = blob_reader_.RetrieveAndSetBlobValue(user_key, blob_index);
if (!s.ok()) {
status_ = s;
valid_ = false;
@@ -1643,28 +1405,14 @@ bool DBIter::MergeWithBlobBaseValue(const Slice& blob_index,
bool DBIter::MergeWithWideColumnBaseValue(const Slice& entity,
const Slice& user_key) {
// Resolve V2 entity blob columns if present, since TimedFullMerge only
// supports V1 format.
BlobFetcher blob_fetcher = blob_reader_.CreateBlobFetcher();
std::string resolved_entity;
Slice effective_entity;
Status s_resolve = WideColumnSerialization::ResolveEntityForMerge(
entity, user_key, &blob_fetcher, nullptr /* prefetch_buffers */,
resolved_entity, effective_entity);
if (!s_resolve.ok()) {
status_ = std::move(s_resolve);
valid_ = false;
return false;
}
// `op_failure_scope` (an output parameter) is not provided (set to nullptr)
// since a failure must be propagated regardless of its value.
ValueType result_type;
Status s = MergeHelper::TimedFullMerge(
merge_operator_, user_key, MergeHelper::kWideBaseValue, effective_entity,
const Status s = MergeHelper::TimedFullMerge(
merge_operator_, user_key, MergeHelper::kWideBaseValue, entity,
merge_context_.GetOperands(), logger_, statistics_, clock_,
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
&value_columns_state_.saved_value(), &pinned_value_, &result_type);
&saved_value_, &pinned_value_, &result_type);
return SetValueAndColumnsFromMergeResult(s, result_type);
}
@@ -1738,103 +1486,6 @@ bool DBIter::FindUserKeyBeforeSavedKey() {
return true;
}
void DBIter::TrackContiguousTombstone(const Slice& user_key,
bool always_update_first_key) {
if (always_update_first_key || contiguous_tombstone_count_ == 0) {
range_tomb_first_key_.SetUserKey(user_key, true /* copy */);
}
contiguous_tombstone_count_++;
}
void DBIter::FlushPendingTombstoneRun(const Slice& end_key,
bool check_prefix_match) {
if (contiguous_tombstone_count_ == 0) {
return;
}
if (check_prefix_match) {
Slice end_key_without_ts =
StripTimestampFromUserKey(end_key, timestamp_size_);
if (PrefixCheck(end_key_without_ts)) {
MaybeInsertRangeTombstone(end_key);
}
} else {
MaybeInsertRangeTombstone(end_key);
}
ResetContiguousTombstoneTracking();
}
void DBIter::MaybeInsertRangeTombstone(const Slice& end_key) {
if (contiguous_tombstone_count_ < min_tombstones_for_range_conversion_) {
return;
}
if (active_mem_ == nullptr) {
return;
}
assert(PrefixCheck(range_tomb_first_key_.GetUserKey()));
assert(PrefixCheck(end_key));
assert(user_comparator_.Compare(range_tomb_first_key_.GetUserKey(),
end_key) <= 0);
auto earliest_seq = active_mem_->GetEarliestSequenceNumber();
// Skip if the iterator's snapshot predates the memtable. Otherwise entries
// added with seqno between sequence_ and earliest_seq will be unintentionally
// covered.
if (sequence_ < earliest_seq) {
RecordTick(statistics_, READ_PATH_RANGE_TOMBSTONES_DISCARDED);
return;
}
// Insert at the read sequence so the converted tombstone is visible only
// to readers that could already observe the deletion run.
SequenceNumber insert_seq = sequence_;
// Skip if the insertion seq could shadow prepared-but-uncommitted writes.
if (read_callback_ != nullptr &&
insert_seq >= read_callback_->min_uncommitted()) {
RecordTick(statistics_, READ_PATH_RANGE_TOMBSTONES_DISCARDED);
return;
}
// Check if the memtable already has a range tombstone covering [start, end).
{
ReadOptions ro;
// Assumption is that this should be relatively cheap as other read requests
// should be building the cached core fragmented list.
std::unique_ptr<FragmentedRangeTombstoneIterator> range_iter(
active_mem_->NewRangeTombstoneIterator(ro, sequence_,
false /* immutable_memtable */));
if (range_iter) {
range_iter->Seek(range_tomb_first_key_.GetUserKey());
if (range_iter->Valid() &&
user_comparator_.Compare(range_iter->start_key(),
range_tomb_first_key_.GetUserKey()) <= 0 &&
user_comparator_.Compare(range_iter->end_key(), end_key) >= 0) {
RecordTick(statistics_, READ_PATH_RANGE_TOMBSTONES_DISCARDED);
return;
}
}
}
assert(cfh_ != nullptr);
if (active_mem_->AddLogicallyRedundantRangeTombstone(
insert_seq, range_tomb_first_key_.GetUserKey(), end_key,
cfh_->cfd()->GetIngestSstLock())) {
RecordTick(statistics_, READ_PATH_RANGE_TOMBSTONES_INSERTED);
ROCKS_LOG_DEBUG(logger_,
"Inserted range tombstone [%s, %s) @ seq %" PRIu64
" (count=%" PRIu32 ", snapshot=%" PRIu64 ")",
range_tomb_first_key_.GetUserKey().ToString(true).c_str(),
end_key.ToString(true).c_str(), insert_seq,
contiguous_tombstone_count_, sequence_);
} else {
RecordTick(statistics_, READ_PATH_RANGE_TOMBSTONES_DISCARDED);
}
}
bool DBIter::TooManyInternalKeysSkipped(bool increment) {
if ((max_skippable_internal_keys_ > 0) &&
(num_internal_keys_skipped_ > max_skippable_internal_keys_)) {
@@ -2052,8 +1703,10 @@ void DBIter::Seek(const Slice& target) {
}
status_ = Status::OK();
ResetSeekState();
ReleaseTempPinnedData();
ResetBlobData();
ResetValueAndColumns();
ResetInternalKeysSkippedCounter();
MarkMemtableForFlushForAvgTrigger();
// Seek the inner iterator based on the target key.
@@ -2074,13 +1727,20 @@ void DBIter::Seek(const Slice& target) {
// Now the inner iterator is placed to the target position. From there,
// we need to find out the next key that is visible to the user.
ClearSavedValue();
if (ShouldSetPrefix(target)) {
prefix_.emplace();
prefix_->SetUserKey(prefix_extractor_->Transform(target));
}
FindNextUserEntry(false /* not skipping saved_key */);
if (!valid_) {
prefix_.reset();
if (prefix_same_as_start_) {
// The case where the iterator needs to be invalidated if it has exhausted
// keys within the same prefix of the seek key.
assert(prefix_extractor_ != nullptr);
Slice target_prefix = prefix_extractor_->Transform(target);
FindNextUserEntry(false /* not skipping saved_key */,
&target_prefix /* prefix */);
if (valid_) {
// Remember the prefix of the seek key for the future Next() call to
// check.
prefix_.SetUserKey(target_prefix);
}
} else {
FindNextUserEntry(false /* not skipping saved_key */, nullptr);
}
if (!valid_) {
return;
@@ -2120,8 +1780,10 @@ void DBIter::SeekForPrev(const Slice& target) {
}
status_ = Status::OK();
ResetSeekState();
ReleaseTempPinnedData();
ResetBlobData();
ResetValueAndColumns();
ResetInternalKeysSkippedCounter();
MarkMemtableForFlushForAvgTrigger();
// Seek the inner iterator based on the target key.
@@ -2141,18 +1803,19 @@ void DBIter::SeekForPrev(const Slice& target) {
// we need to find out the first key that is visible to the user in the
// backward direction.
ClearSavedValue();
if (ShouldSetPrefix(target)) {
prefix_.emplace();
prefix_->SetUserKey(prefix_extractor_->Transform(target));
}
PrevInternal();
if (!valid_) {
prefix_.reset();
}
// Set end key for first Prev() call's tombstone tracking
if (valid_ && min_tombstones_for_range_conversion_ > 0) {
range_tomb_end_key_.SetUserKey(saved_key_.GetUserKey(),
!saved_key_.IsKeyPinned());
if (prefix_same_as_start_) {
// The case where the iterator needs to be invalidated if it has exhausted
// keys within the same prefix of the seek key.
assert(prefix_extractor_ != nullptr);
Slice target_prefix = prefix_extractor_->Transform(target);
PrevInternal(&target_prefix);
if (valid_) {
// Remember the prefix of the seek key for the future Prev() call to
// check.
prefix_.SetUserKey(target_prefix);
}
} else {
PrevInternal(nullptr);
}
// Report stats and perf context.
@@ -2179,8 +1842,10 @@ void DBIter::SeekToFirst() {
// if iterator is empty, this status_ could be unchecked.
status_.PermitUncheckedError();
direction_ = kForward;
ResetSeekState();
ReleaseTempPinnedData();
ResetBlobData();
ResetValueAndColumns();
ResetInternalKeysSkippedCounter();
MarkMemtableForFlushForAvgTrigger();
ClearSavedValue();
is_key_seqnum_zero_ = false;
@@ -2195,8 +1860,8 @@ void DBIter::SeekToFirst() {
saved_key_.SetUserKey(
ExtractUserKey(iter_.key()),
!iter_.iter()->IsKeyPinned() || !pin_thru_lifetime_ /* copy */);
assert(!prefix_.has_value());
FindNextUserEntry(false /* not skipping saved_key */);
FindNextUserEntry(false /* not skipping saved_key */,
nullptr /* no prefix check */);
if (statistics_ != nullptr) {
if (valid_) {
RecordTick(statistics_, NUMBER_DB_SEEK_FOUND);
@@ -2207,13 +1872,10 @@ void DBIter::SeekToFirst() {
} else {
valid_ = false;
}
if (valid_) {
Slice user_key_without_ts =
StripTimestampFromUserKey(saved_key_.GetUserKey(), timestamp_size_);
if (ShouldSetPrefix(user_key_without_ts)) {
prefix_.emplace();
prefix_->SetUserKey(prefix_extractor_->Transform(user_key_without_ts));
}
if (valid_ && prefix_same_as_start_) {
assert(prefix_extractor_ != nullptr);
prefix_.SetUserKey(prefix_extractor_->Transform(
StripTimestampFromUserKey(saved_key_.GetUserKey(), timestamp_size_)));
}
}
@@ -2244,27 +1906,19 @@ void DBIter::SeekToLast() {
// if iterator is empty, this status_ could be unchecked.
status_.PermitUncheckedError();
direction_ = kReverse;
ResetSeekState();
ReleaseTempPinnedData();
ResetBlobData();
ResetValueAndColumns();
ResetInternalKeysSkippedCounter();
MarkMemtableForFlushForAvgTrigger();
ClearSavedValue();
is_key_seqnum_zero_ = false;
// Clear stale saved_key_ so PrevInternal()'s Swap does not pollute
// range_tomb_end_key_ with a key from a previous seek operation.
saved_key_.Clear();
{
PERF_TIMER_GUARD(seek_internal_seek_time);
iter_.SeekToLast();
}
assert(!prefix_.has_value());
PrevInternal();
// Set end key for first Prev() call's tombstone tracking
if (valid_ && min_tombstones_for_range_conversion_ > 0) {
range_tomb_end_key_.SetUserKey(saved_key_.GetUserKey(),
!saved_key_.IsKeyPinned());
}
PrevInternal(nullptr);
if (statistics_ != nullptr) {
RecordTick(statistics_, NUMBER_DB_SEEK);
if (valid_) {
@@ -2273,13 +1927,10 @@ void DBIter::SeekToLast() {
PERF_COUNTER_ADD(iter_read_bytes, key().size() + value().size());
}
}
if (valid_) {
Slice user_key_without_ts =
StripTimestampFromUserKey(saved_key_.GetUserKey(), timestamp_size_);
if (ShouldSetPrefix(user_key_without_ts)) {
prefix_.emplace();
prefix_->SetUserKey(prefix_extractor_->Transform(user_key_without_ts));
}
if (valid_ && prefix_same_as_start_) {
assert(prefix_extractor_ != nullptr);
prefix_.SetUserKey(prefix_extractor_->Transform(
StripTimestampFromUserKey(saved_key_.GetUserKey(), timestamp_size_)));
}
}
} // namespace ROCKSDB_NAMESPACE
+42 -243
View File
@@ -9,17 +9,9 @@
#pragma once
#include <cstdint>
#include <memory>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
#include "db/blob/blob_fetcher.h"
#include "db/blob/blob_index.h"
#include "db/db_impl/db_impl.h"
#include "db/wide/read_path_blob_resolver.h"
#include "db/wide/wide_columns_helper.h"
#include "memory/arena.h"
#include "options/cf_options.h"
#include "rocksdb/db.h"
@@ -29,7 +21,6 @@
#include "util/autovector.h"
namespace ROCKSDB_NAMESPACE {
class BlobFileCache;
class Version;
// This file declares the factory functions of DBIter, in its original form
@@ -195,14 +186,13 @@ class DBIter final : public Iterator {
Slice value() const override {
assert(valid_);
return value_columns_state_.value();
return value_;
}
const WideColumns& columns() const override {
assert(valid_);
assert(!value_columns_state_.HasLazyEntityColumns() ||
value_columns_state_.HasMaterializedColumns());
return value_columns_state_.wide_columns();
return wide_columns_;
}
Status status() const override {
@@ -266,23 +256,17 @@ class DBIter final : public Iterator {
public:
BlobReader(const Version* version, ReadTier read_tier,
bool verify_checksums, bool fill_cache,
Env::IOActivity io_activity, BlobFileCache* blob_file_cache,
bool allow_write_path_fallback)
Env::IOActivity io_activity)
: version_(version),
read_tier_(read_tier),
verify_checksums_(verify_checksums),
fill_cache_(fill_cache),
io_activity_(io_activity),
blob_file_cache_(blob_file_cache),
allow_write_path_fallback_(allow_write_path_fallback) {}
io_activity_(io_activity) {}
const Slice& GetBlobValue() const { return blob_value_; }
Status RetrieveAndSetBlobValue(const Slice& user_key,
const Slice& blob_index,
bool allow_write_path_fallback);
const Slice& blob_index);
void ResetBlobValue() { blob_value_.Reset(); }
// Create a BlobFetcher with the same read options as this BlobReader.
BlobFetcher CreateBlobFetcher() const;
private:
PinnableSlice blob_value_;
@@ -291,147 +275,6 @@ class DBIter final : public Iterator {
bool verify_checksums_;
bool fill_cache_;
Env::IOActivity io_activity_;
// Cache used by the write-path fallback for in-flight direct-write blob
// files that are not yet reachable through Version.
BlobFileCache* blob_file_cache_;
bool allow_write_path_fallback_;
};
// Groups the current iterator result together with the backing storage and
// lazy entity-resolution metadata it depends on. Resetting this object drops
// all aliases into the saved entity buffer and resolver cache at once.
class ValueColumnsState {
public:
ValueColumnsState(const Version* version, const ReadOptions& read_options,
ColumnFamilyHandleImpl* cfh)
: entity_blob_resolver_(
version, read_options.read_tier, read_options.verify_checksums,
read_options.fill_cache, read_options.io_activity,
cfh ? cfh->cfd()->blob_file_cache() : nullptr,
cfh != nullptr &&
cfh->cfd()->blob_partition_manager() != nullptr) {}
Slice& value() { return value_; }
const Slice& value() const { return value_; }
WideColumns& wide_columns() { return wide_columns_; }
const WideColumns& wide_columns() const { return wide_columns_; }
bool HasMaterializedColumns() const { return !wide_columns_.empty(); }
bool HasLazyEntityColumns() const { return !lazy_entity_columns_.empty(); }
std::string& saved_value() { return saved_value_; }
const std::string& saved_value() const { return saved_value_; }
std::vector<WideColumn>& lazy_entity_columns() {
return lazy_entity_columns_;
}
const std::vector<WideColumn>& lazy_entity_columns() const {
return lazy_entity_columns_;
}
std::vector<std::pair<size_t, BlobIndex>>& lazy_blob_columns() {
return lazy_blob_columns_;
}
const std::vector<std::pair<size_t, BlobIndex>>& lazy_blob_columns() const {
return lazy_blob_columns_;
}
ReadPathBlobResolver& entity_blob_resolver() {
return entity_blob_resolver_;
}
const ReadPathBlobResolver& entity_blob_resolver() const {
return entity_blob_resolver_;
}
std::mutex& lazy_entity_columns_mutex() const {
return lazy_entity_columns_mutex_;
}
// DBIter calls this after ResetValueAndColumns() before repopulating the
// current entry from a serialized wide-column entity.
void AssertReadyForEntity() const {
assert(value_.empty());
assert(wide_columns_.empty());
assert(lazy_entity_columns_.empty());
assert(lazy_blob_columns_.empty());
}
inline void ClearSavedValue() {
if (saved_value_.capacity() > 1048576) {
std::string empty;
swap(empty, saved_value_);
} else {
saved_value_.clear();
}
}
// Preserve the serialized entity bytes when DBIter needs stable backing
// storage for lazy column slices across iterator movement.
void SaveEntitySliceIfNeeded(const Slice& slice) {
if (slice.data() != saved_value_.data() ||
slice.size() != saved_value_.size()) {
saved_value_.assign(slice.data(), slice.size());
}
}
// Clears the previous lazy entity metadata and returns the saved entity
// buffer as input for DeserializeV2().
Slice PrepareForLazyEntityDeserialize() {
ClearLazyEntity();
return Slice(saved_value_);
}
// Publishes the deserialized lazy entity metadata to the blob resolver.
void BindLazyEntity(const Slice& user_key) {
entity_blob_resolver_.Reset(user_key, &lazy_entity_columns_,
&lazy_blob_columns_);
}
// Drops the lazy entity metadata and any resolver aliases into it.
void ClearLazyEntity() {
lazy_entity_columns_.clear();
lazy_blob_columns_.clear();
entity_blob_resolver_.Reset(Slice(), nullptr, nullptr);
}
// Clears materialized wide columns on error before DBIter invalidates
// itself.
void ClearWideColumns() { wide_columns_.clear(); }
// Fast path for inline entities whose default column is already
// materialized in wide_columns_.
void MaybeSetValueFromMaterializedDefaultColumn() {
if (WideColumnsHelper::HasDefaultColumn(wide_columns_)) {
value_ = WideColumnsHelper::GetDefaultColumn(wide_columns_);
}
}
void SetFromPlain(const Slice& slice) {
assert(value_.empty());
assert(wide_columns_.empty());
assert(lazy_entity_columns_.empty());
assert(lazy_blob_columns_.empty());
value_ = slice;
wide_columns_.emplace_back(kDefaultWideColumnName, slice);
}
void Reset() {
value_.clear();
wide_columns_.clear();
ClearLazyEntity();
}
private:
std::string saved_value_;
// Value of the default column.
Slice value_;
// All columns (i.e. name-value pairs).
WideColumns wide_columns_;
// Lazy resolution state for V2 entities with blob columns.
ReadPathBlobResolver entity_blob_resolver_;
std::vector<WideColumn> lazy_entity_columns_;
std::vector<std::pair<size_t, BlobIndex>> lazy_blob_columns_;
mutable std::mutex lazy_entity_columns_mutex_;
};
// For all methods in this block:
@@ -448,22 +291,22 @@ class DBIter final : public Iterator {
// It might get adjusted if the seek key is larger than iterator upper bound.
// target does not have timestamp.
void SetSavedKeyToSeekForPrevTarget(const Slice& target);
bool FindValueForCurrentKey(bool& found_visible);
bool FindValueForCurrentKey();
bool FindValueForCurrentKeyUsingSeek();
bool FindUserKeyBeforeSavedKey();
// If `skipping_saved_key` is true, the function will keep iterating until it
// finds a user key that is larger than `saved_key_`.
// When prefix_ is set, the iterator stops when all keys for the prefix are
// exhausted and the iterator is set to invalid.
bool FindNextUserEntry(bool skipping_saved_key);
// If `prefix` is not null, the iterator needs to stop when all keys for the
// prefix are exhausted and the iterator is set to invalid.
bool FindNextUserEntry(bool skipping_saved_key, const Slice* prefix);
// Internal implementation of FindNextUserEntry().
bool FindNextUserEntryInternal(bool skipping_saved_key);
bool FindNextUserEntryInternal(bool skipping_saved_key, const Slice* prefix);
bool ParseKey(ParsedInternalKey* key);
bool MergeValuesNewToOld();
// When prefix_ is set, we need to set the iterator to invalid if no more
// If prefix is not null, we need to set the iterator to invalid if no more
// entry can be found within the prefix.
void PrevInternal();
void PrevInternal(const Slice* prefix);
bool TooManyInternalKeysSkipped(bool increment = true);
bool IsVisible(SequenceNumber sequence, const Slice& ts,
bool* more_recent = nullptr);
@@ -483,7 +326,14 @@ class DBIter final : public Iterator {
}
}
inline void ClearSavedValue() { value_columns_state_.ClearSavedValue(); }
inline void ClearSavedValue() {
if (saved_value_.capacity() > 1048576) {
std::string empty;
swap(empty, saved_value_);
} else {
saved_value_.clear();
}
}
inline void ResetInternalKeysSkippedCounter() {
local_stats_.skip_count_ += num_internal_keys_skipped_;
@@ -508,7 +358,11 @@ class DBIter final : public Iterator {
}
void SetValueAndColumnsFromPlain(const Slice& slice) {
value_columns_state_.SetFromPlain(slice);
assert(value_.empty());
assert(wide_columns_.empty());
value_ = slice;
wide_columns_.emplace_back(kDefaultWideColumnName, slice);
}
bool SetValueAndColumnsFromBlobImpl(const Slice& user_key,
@@ -517,12 +371,14 @@ class DBIter final : public Iterator {
const Slice& blob_index);
bool SetValueAndColumnsFromEntity(Slice slice);
bool MaterializeLazyEntityColumns() const;
bool SetValueAndColumnsFromMergeResult(const Status& merge_status,
ValueType result_type);
void ResetValueAndColumns() { value_columns_state_.Reset(); }
void ResetValueAndColumns() {
value_.clear();
wide_columns_.clear();
}
void ResetBlobData() {
blob_reader_.ResetBlobValue();
@@ -554,59 +410,6 @@ class DBIter final : public Iterator {
return true;
}
// Record a deletion into the current contiguous tombstone run.
// In forward iteration, first_key is set only for the first tombstone
// (always_update_first_key=false). In reverse, keys arrive in decreasing
// order so first_key is updated every time (always_update_first_key=true).
void TrackContiguousTombstone(const Slice& user_key,
bool always_update_first_key);
// If a contiguous tombstone run is pending, insert a range tombstone
// (if threshold is met) and reset tracking state. When
// check_prefix_match is true, the insertion is skipped (but tracking is
// still reset) if end_key is outside the seek prefix.
void FlushPendingTombstoneRun(const Slice& end_key,
bool check_prefix_match = false);
// If enough contiguous tombstones have been tracked, insert a range
// tombstone [first_key, end_key) into the mutable memtable.
// end_key is the exclusive upper bound — typically the next live key.
void MaybeInsertRangeTombstone(const Slice& end_key);
void ResetContiguousTombstoneTracking() {
contiguous_tombstone_count_ = 0;
range_tomb_first_key_.Clear();
range_tomb_end_key_.Clear();
}
// Returns true if there is no prefix constraint (prefix_ not set) or
// if `key` is in the prefix extractor's domain and its prefix matches.
// Out-of-domain keys return false when a prefix is set.
bool PrefixCheck(const Slice& key) const {
return !prefix_.has_value() || (prefix_extractor_->InDomain(key) &&
prefix_extractor_->Transform(key).compare(
prefix_->GetUserKey()) == 0);
}
// Returns true if a prefix should be extracted from the seek target and
// used for prefix boundary tracking. True when prefix_same_as_start is
// set, or when range tombstone conversion is enabled during a legacy
// prefix seek. If target is out of domain then false is returned.
bool ShouldSetPrefix(const Slice& target) const {
return (prefix_same_as_start_ ||
(min_tombstones_for_range_conversion_ > 0 &&
!expect_total_order_inner_iter_)) &&
prefix_extractor_->InDomain(target);
}
void ResetSeekState() {
ReleaseTempPinnedData();
ResetBlobData();
ResetValueAndColumns();
ResetInternalKeysSkippedCounter();
ResetContiguousTombstoneTracking();
prefix_.reset();
}
void MarkMemtableForFlushForAvgTrigger() {
if (avg_op_scan_flush_trigger_ &&
mem_hidden_op_scanned_since_seek_ >= memtable_op_scan_flush_trigger_ &&
@@ -661,8 +464,13 @@ class DBIter final : public Iterator {
// kTypeValuePreferredSeqno entry, this is the write time specified by the
// user.
uint64_t saved_write_unix_time_;
ValueColumnsState value_columns_state_;
std::string saved_value_;
Slice pinned_value_;
// for prefix seek mode to support prev()
// Value of the default column
Slice value_;
// All columns (i.e. name-value pairs)
WideColumns wide_columns_;
Statistics* statistics_;
uint64_t max_skip_;
uint64_t max_skippable_internal_keys_;
@@ -670,16 +478,12 @@ class DBIter final : public Iterator {
const Slice* iterate_lower_bound_;
const Slice* iterate_upper_bound_;
// The prefix of the seek key. Set during Seek/SeekForPrev when either
// prefix_same_as_start_ is true or the iterator uses prefix filtering
// (!expect_total_order_inner_iter_ && InDomain(target)). Used in Next()
// and Prev() to:
// - invalidate the iterator when prefix_same_as_start_ is true and keys
// in this prefix have been exhausted.
// - bound range tombstone tracking to the seek prefix when
// min_tombstones_for_range_conversion_ > 0.
// Set via SetUserKey(), read via GetUserKey().
std::optional<IterKey> prefix_;
// The prefix of the seek key. It is only used when prefix_same_as_start_
// is true and prefix extractor is not null. In Next() or Prev(), current keys
// will be checked against this prefix, so that the iterator can be
// invalidated if the keys in this prefix has been exhausted. Set it using
// SetUserKey() and use it using GetUserKey().
IterKey prefix_;
Status status_;
Slice lazy_blob_index_;
@@ -701,7 +505,6 @@ class DBIter final : public Iterator {
uint32_t avg_op_scan_flush_trigger_;
uint32_t iter_step_since_seek_;
uint32_t mem_hidden_op_scanned_since_seek_;
uint32_t contiguous_tombstone_count_;
Direction direction_;
bool valid_;
bool current_entry_is_merged_;
@@ -716,15 +519,11 @@ class DBIter final : public Iterator {
// Expect the inner iterator to maintain a total order.
// prefix_extractor_ must be non-NULL if the value is false.
const bool expect_total_order_inner_iter_;
const uint32_t min_tombstones_for_range_conversion_;
// Whether the iterator is allowed to expose blob references. Set to true when
// the stacked BlobDB implementation is used, false otherwise.
bool expose_blob_index_;
bool allow_unprepared_value_;
bool is_blob_;
bool arena_mode_;
IterKey range_tomb_first_key_;
IterKey range_tomb_end_key_;
};
} // namespace ROCKSDB_NAMESPACE
File diff suppressed because it is too large. Load diff
Loaded 100 of 368 files, more files were not shown because too many files have changed in this diff. Show more