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
181 changed files with 1758 additions and 23519 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 -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 }}
-49
View File
@@ -1,49 +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
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
run: ccache -z
shell: bash
@@ -4,15 +4,16 @@ runs:
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"
- name: Configure ccache
shell: pwsh
run: |
ccache --set-config=base_dir=C:\a\rocksdb\rocksdb
ccache --set-config=hash_dir=false
- name: Custom steps
env:
THIRDPARTY_HOME: ${{ github.workspace }}/thirdparty
-78
View File
@@ -1,78 +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 }
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');
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.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 icon = 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');
}
};
+23 -16
View File
@@ -12,12 +12,6 @@ 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.0.0
@@ -26,7 +20,6 @@ jobs:
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');
}
-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.');
-476
View File
@@ -1,476 +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)
# - 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 {
const { data: pulls } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
sort: 'updated',
direction: 'desc',
per_page: 30
});
const match = pulls.find(p => p.head.sha === headSha);
if (match) prNumber = match.number;
}
if (!prNumber) {
core.setFailed(`Could not find PR for SHA ${headSha}`);
return;
}
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: Check for existing review
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.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.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 > /tmp/prompt.txt << 'PROMPT_EOF'
Read claude_md/code_review.md for the full review methodology, then
review the pull request described below. Follow all perspectives and
the output format defined in that document.
PROMPT_EOF
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.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: "30"
allowed_tools: "View,GlobTool,GrepTool"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Build review comment
if: steps.check_existing.outputs.skip != 'true'
uses: actions/github-script@v7
env:
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
CONCLUSION: ${{ steps.claude.outputs.conclusion }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
with:
script: |
const parse = require('./.github/scripts/parse-claude-review.js');
const body = parse({
executionFile: process.env.EXECUTION_FILE,
conclusion: process.env.CONCLUSION,
meta: {
trigger: 'auto',
headSha: process.env.HEAD_SHA,
reviewer: '',
}
});
const fs = require('fs');
fs.writeFileSync('claude-review-comment.md', body);
- name: Save PR number
if: steps.check_existing.outputs.skip != 'true'
run: echo "${{ steps.pr_info.outputs.pr_number }}" > pr_number.txt
- name: Save trigger metadata
if: steps.check_existing.outputs.skip != 'true'
run: echo "auto" > trigger_type.txt
- name: Upload review artifact
if: always() && 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.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
# ======================== 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 > /tmp/prompt.txt << 'PROMPT_EOF'
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.
---
PROMPT_EOF
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 > /tmp/prompt.txt << 'PROMPT_EOF'
Read claude_md/code_review.md for the full review methodology, then
review the pull request described below. Follow all perspectives and
the output format defined in that document.
PROMPT_EOF
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: "30"
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 }}
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 }}
with:
script: |
const parse = require('./.github/scripts/parse-claude-review.js');
const isReview = process.env.COMMAND_TYPE === 'review';
const body = parse({
executionFile: process.env.EXECUTION_FILE,
conclusion: process.env.CONCLUSION,
meta: {
trigger: 'manual',
headSha: process.env.HEAD_SHA,
reviewer: process.env.REVIEWER,
isQuery: !isReview,
}
});
const fs = require('fs');
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
+56 -176
View File
@@ -92,14 +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
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- 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')
@@ -112,9 +105,6 @@ jobs:
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,16 +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
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- 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
@@ -141,19 +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
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- 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')
@@ -168,20 +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
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- 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
@@ -190,19 +162,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: 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)"
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- 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')
@@ -215,14 +180,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: cmake-benchmark
- run: mkdir build && cd build && cmake -DWITH_GFLAGS=1 -DWITH_BENCHMARK=1 -DPORTABLE=ON -DCMAKE_CXX_FLAGS=-DNROCKSDB_THREAD_STATUS .. && make VERBOSE=1 -j20 && ctest -j20
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- 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"
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')
@@ -235,15 +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"
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
# ======================== Linux No Test Runs ======================= #
build-linux-release:
@@ -256,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
@@ -275,10 +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
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- 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')
@@ -291,18 +235,11 @@ 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
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-linux-clang-18-no_test_run:
if: needs.config.outputs.only_job == 'build-linux-clang-18-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -315,16 +252,9 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang-18
- run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 make -j32 all microbench
- run: make clean
- run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 DEBUG_LEVEL=0 make -j32 release
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- 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')
@@ -337,17 +267,30 @@ jobs:
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
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- 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')
@@ -360,16 +303,9 @@ 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
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- 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')
@@ -382,14 +318,7 @@ jobs:
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=960 --max_key=2500000' blackbox_crash_test_with_atomic_flush
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
# ======================= Linux with Sanitizers ===================== #
build-linux-clang18-asan-ubsan:
@@ -403,14 +332,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang18-asan-ubsan
- 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
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-linux-clang18-mini-tsan:
if: needs.config.outputs.only_job == 'build-linux-clang18-mini-tsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -423,14 +345,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang18-tsan
- run: COMPILE_WITH_TSAN=1 CC=clang-18 CXX=clang++-18 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j32 check
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
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')
@@ -443,14 +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
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
# ========================= MacOS build only ======================== #
build-macos:
@@ -464,18 +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
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
# ========================= MacOS with Tests ======================== #
build-macos-cmake:
@@ -490,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"
@@ -512,10 +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 }}
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
# ======================== Windows with Tests ======================= #
# NOTE: some windows jobs are in "nightly" to save resources
@@ -539,22 +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
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
# 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
@@ -564,22 +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
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
# 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
@@ -592,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"
@@ -606,10 +511,6 @@ jobs:
which javac && javac -version
- name: Test RocksDBJava
run: make V=1 J=16 -j16 jtest
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- 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')
@@ -622,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"
@@ -636,10 +534,6 @@ jobs:
which javac && javac -version
- name: Build RocksDBJava x86 and ARM Static Libraries
run: make V=1 J=16 -j16 rocksdbjavastaticosx
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- 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')
@@ -652,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"
@@ -666,10 +557,6 @@ jobs:
which javac && javac -version
- name: Build RocksDBJava Universal Binary Static Library
run: make V=1 J=16 -j16 rocksdbjavastaticosx_ub
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- 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')
@@ -706,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"
+4 -27
View File
@@ -106,7 +106,6 @@ 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/wide_column_serialization.cc",
@@ -208,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",
@@ -364,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",
@@ -5142,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"],
@@ -5634,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"],
@@ -5748,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=[])
-3
View File
@@ -250,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
+1 -9
View File
@@ -766,7 +766,6 @@ 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/wide_column_serialization.cc
@@ -843,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
@@ -1001,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
@@ -1536,7 +1531,6 @@ if(WITH_TESTS)
utilities/cassandra/cassandra_serialize_test.cc
utilities/checkpoint/checkpoint_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
@@ -1554,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 -25
View File
@@ -1,34 +1,17 @@
# Rocksdb Change Log
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
## 11.1.2 (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.1.1 (04/10/2026)
## 11.0.4 (03/11/2026)
### Public API Changes
* 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.
* Added block_decompress_count to PerfContext
* Re-remove deprecated DB::Open raw pointer variants in db.h
### Bug Fixes
* 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.
## 11.0.3 (03/03/2026)
### Public API Changes
* Relax option sanitization for kv ratio compaction
## 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
+1 -29
View File
@@ -627,22 +627,11 @@ ROCKSDBTESTS_SUBSET ?= $(TESTS)
# 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
# perf_context_test - 1GB write_buffer_size default, parallel can fill /dev/shm
# obsolete_files_test - ~1GB write_buffer_size/target_file_size_base
# backup_engine_test - 1GB write_buffer_size in FailOverwritingBackups
# prefetch_test - 1GB write_buffer_size in PrefetchWhenReseek parameterized test
# db_io_failure_test - 256MB write_buffer_size in FlushSstRangeSyncError and
# CompactionSstRangeSyncError
NON_PARALLEL_TEST = \
c_test \
env_test \
deletefile_test \
db_bloom_filter_test \
perf_context_test \
obsolete_files_test \
backup_engine_test \
prefetch_test \
db_io_failure_test \
$(PLUGIN_TESTS) \
PARALLEL_TEST = $(filter-out $(NON_PARALLEL_TEST), $(TESTS))
@@ -895,9 +884,6 @@ $(parallel_tests):
"d=\$(TEST_TMPDIR)$$TEST_SCRIPT" \
'mkdir -p $$d' \
"TEST_TMPDIR=\$$d $(DRIVER) ./$$TEST_BINARY --gtest_filter=$$TEST_NAME" \
'test_retcode=$$?' \
'[ $$test_retcode -eq 0 ] && rm -rf $$d' \
'exit $$test_retcode' \
> $$TEST_SCRIPT; \
chmod a=rx $$TEST_SCRIPT; \
done
@@ -1015,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) | \
@@ -1608,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)
@@ -1665,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)
@@ -1866,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)
-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/`
+1 -1
View File
@@ -533,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);
-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) {
-10
View File
@@ -322,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());
@@ -332,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) {
+6 -33
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;
}
@@ -4355,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) {
@@ -5802,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;
}
@@ -7548,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;
}
-15
View File
@@ -4880,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);
-22
View File
@@ -1569,28 +1569,6 @@ Status ColumnFamilyData::ValidateOptions(
"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) ==
+9 -49
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"
@@ -904,17 +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();
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,
@@ -942,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) {
@@ -973,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);
}
}
}
}
@@ -1116,9 +1092,6 @@ Status CompactionJob::Run() {
status = VerifyOutputFiles();
}
TEST_SYNC_POINT_CALLBACK("CompactionJob::Run():AfterVerifyOutputFiles",
&status);
if (status.ok()) {
SetOutputTableProperties();
}
@@ -2497,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());
@@ -2547,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;
@@ -2939,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]);
+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 -9
View File
@@ -404,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 -5
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();
+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 -1
View File
@@ -170,7 +170,7 @@ class SubcompactionState {
}
}
void Cleanup(Cache* cache, const Status& overall_status);
void Cleanup(Cache* cache);
void AggregateCompactionOutputStats(
InternalStats::CompactionStatsFull& internal_stats) const;
-313
View File
@@ -2535,319 +2535,6 @@ TEST_P(DBMultiGetTestWithParam, MultiGetBatchedValueSizeMultiLevelMerge) {
}
}
TEST_P(DBMultiGetTestWithParam, MultiGetMemtableBatchLookup) {
#ifndef USE_COROUTINES
if (std::get<1>(GetParam())) {
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
return;
}
#endif // USE_COROUTINES
// Skip for unbatched MultiGet
if (!std::get<0>(GetParam())) {
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
return;
}
Options options = CurrentOptions();
options.memtable_batch_lookup_optimization = true;
CreateAndReopenWithCF({"pikachu"}, options);
// Insert sorted keys into memtable
for (int i = 0; i < 100; i++) {
ASSERT_OK(Put(1, Key(i), "val" + std::to_string(i)));
}
// Delete some keys
ASSERT_OK(Delete(1, Key(25)));
ASSERT_OK(Delete(1, Key(75)));
// MultiGet a batch of keys - mix of existing, deleted, and missing
// Store key strings to keep Slice data alive
std::vector<std::string> key_strs = {Key(0), Key(10), Key(25), Key(50),
Key(75), Key(99), Key(200)};
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
std::vector<PinnableSlice> values(keys.size());
std::vector<Status> statuses(keys.size());
ReadOptions ro;
ro.async_io = std::get<1>(GetParam());
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
statuses.data(), true);
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0].ToString(), "val0");
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1].ToString(), "val10");
ASSERT_TRUE(statuses[2].IsNotFound()); // deleted
ASSERT_OK(statuses[3]);
ASSERT_EQ(values[3].ToString(), "val50");
ASSERT_TRUE(statuses[4].IsNotFound()); // deleted
ASSERT_OK(statuses[5]);
ASSERT_EQ(values[5].ToString(), "val99");
ASSERT_TRUE(statuses[6].IsNotFound()); // never inserted
}
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupOverwrite) {
#ifndef USE_COROUTINES
if (std::get<1>(GetParam())) {
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
return;
}
#endif // USE_COROUTINES
if (!std::get<0>(GetParam())) {
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
return;
}
Options options = CurrentOptions();
options.memtable_batch_lookup_optimization = true;
CreateAndReopenWithCF({"pikachu"}, options);
// Insert, then overwrite some keys
for (int i = 0; i < 50; i++) {
ASSERT_OK(Put(1, Key(i), "old" + std::to_string(i)));
}
for (int i = 0; i < 50; i += 5) {
ASSERT_OK(Put(1, Key(i), "new" + std::to_string(i)));
}
std::vector<std::string> key_strs;
for (int i = 0; i < 50; i += 5) {
key_strs.push_back(Key(i));
}
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
std::vector<PinnableSlice> values(keys.size());
std::vector<Status> statuses(keys.size());
ReadOptions ro;
ro.async_io = std::get<1>(GetParam());
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
statuses.data(), true);
for (size_t i = 0; i < keys.size(); i++) {
ASSERT_OK(statuses[i]);
ASSERT_EQ(values[i].ToString(), "new" + std::to_string(i * 5));
}
}
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupWithFlush) {
#ifndef USE_COROUTINES
if (std::get<1>(GetParam())) {
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
return;
}
#endif // USE_COROUTINES
if (!std::get<0>(GetParam())) {
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
return;
}
Options options = CurrentOptions();
options.memtable_batch_lookup_optimization = true;
CreateAndReopenWithCF({"pikachu"}, options);
// Put data into SST
for (int i = 0; i < 50; i++) {
ASSERT_OK(Put(1, Key(i), "sst" + std::to_string(i)));
}
ASSERT_OK(Flush(1));
// Put different data into memtable (overlapping some keys)
for (int i = 25; i < 75; i++) {
ASSERT_OK(Put(1, Key(i), "mem" + std::to_string(i)));
}
// MultiGet keys spanning both SST and memtable
std::vector<std::string> key_strs = {Key(10), Key(30), Key(60), Key(80)};
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
std::vector<PinnableSlice> values(keys.size());
std::vector<Status> statuses(keys.size());
ReadOptions ro;
ro.async_io = std::get<1>(GetParam());
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
statuses.data(), true);
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0].ToString(), "sst10");
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1].ToString(), "mem30");
ASSERT_OK(statuses[2]);
ASSERT_EQ(values[2].ToString(), "mem60");
ASSERT_TRUE(statuses[3].IsNotFound());
}
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupWithMerge) {
#ifndef USE_COROUTINES
if (std::get<1>(GetParam())) {
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
return;
}
#endif // USE_COROUTINES
if (!std::get<0>(GetParam())) {
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
return;
}
Options options = CurrentOptions();
options.memtable_batch_lookup_optimization = true;
options.merge_operator = MergeOperators::CreateStringAppendOperator();
CreateAndReopenWithCF({"pikachu"}, options);
// Put base values
ASSERT_OK(Put(1, Key(1), "a"));
ASSERT_OK(Put(1, Key(2), "x"));
// Merge on top
ASSERT_OK(Merge(1, Key(1), "b"));
ASSERT_OK(Merge(1, Key(1), "c"));
ASSERT_OK(Merge(1, Key(2), "y"));
std::vector<std::string> key_strs = {Key(1), Key(2), Key(3)};
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
std::vector<PinnableSlice> values(keys.size());
std::vector<Status> statuses(keys.size());
ReadOptions ro;
ro.async_io = std::get<1>(GetParam());
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
statuses.data(), true);
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0].ToString(), "a,b,c");
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1].ToString(), "x,y");
ASSERT_TRUE(statuses[2].IsNotFound());
}
TEST_F(DBBasicTest, MultiGetBatchLookupDisabledByDefault) {
// Verify that finger search is off by default and MultiGet still works
Options options = CurrentOptions();
ASSERT_FALSE(options.memtable_batch_lookup_optimization);
CreateAndReopenWithCF({"pikachu"}, options);
ASSERT_OK(Put(1, "k1", "v1"));
ASSERT_OK(Put(1, "k2", "v2"));
std::vector<Slice> keys = {"k1", "k2", "k3"};
std::vector<PinnableSlice> values(3);
std::vector<Status> statuses(3);
db_->MultiGet(ReadOptions(), handles_[1], 3, keys.data(), values.data(),
statuses.data(), true);
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0].ToString(), "v1");
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1].ToString(), "v2");
ASSERT_TRUE(statuses[2].IsNotFound());
}
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupWithParanoid) {
#ifndef USE_COROUTINES
if (std::get<1>(GetParam())) {
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
return;
}
#endif // USE_COROUTINES
if (!std::get<0>(GetParam())) {
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
return;
}
Options options = CurrentOptions();
options.memtable_batch_lookup_optimization = true;
options.paranoid_memory_checks = true;
CreateAndReopenWithCF({"pikachu"}, options);
// Insert sorted keys into memtable
for (int i = 0; i < 100; i++) {
ASSERT_OK(Put(1, Key(i), "val" + std::to_string(i)));
}
ASSERT_OK(Delete(1, Key(25)));
// MultiGet with both batch optimization and paranoid checks enabled
std::vector<std::string> key_strs = {Key(0), Key(10), Key(25),
Key(50), Key(99), Key(200)};
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
std::vector<PinnableSlice> values(keys.size());
std::vector<Status> statuses(keys.size());
ReadOptions ro;
ro.async_io = std::get<1>(GetParam());
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
statuses.data(), true);
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0].ToString(), "val0");
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1].ToString(), "val10");
ASSERT_TRUE(statuses[2].IsNotFound()); // deleted
ASSERT_OK(statuses[3]);
ASSERT_EQ(values[3].ToString(), "val50");
ASSERT_OK(statuses[4]);
ASSERT_EQ(values[4].ToString(), "val99");
ASSERT_TRUE(statuses[5].IsNotFound()); // never inserted
}
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupSnapshot) {
#ifndef USE_COROUTINES
if (std::get<1>(GetParam())) {
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
return;
}
#endif // USE_COROUTINES
if (!std::get<0>(GetParam())) {
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
return;
}
Options options = CurrentOptions();
options.memtable_batch_lookup_optimization = true;
CreateAndReopenWithCF({"pikachu"}, options);
ASSERT_OK(Put(1, Key(1), "v1_old"));
ASSERT_OK(Put(1, Key(2), "v2_old"));
const Snapshot* snap = db_->GetSnapshot();
// Write new values after snapshot
ASSERT_OK(Put(1, Key(1), "v1_new"));
ASSERT_OK(Put(1, Key(2), "v2_new"));
ASSERT_OK(Put(1, Key(3), "v3_new"));
// MultiGet with snapshot should see old values
std::vector<std::string> key_strs = {Key(1), Key(2), Key(3)};
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
std::vector<PinnableSlice> values(keys.size());
std::vector<Status> statuses(keys.size());
ReadOptions ro;
ro.snapshot = snap;
ro.async_io = std::get<1>(GetParam());
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
statuses.data(), true);
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0].ToString(), "v1_old");
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1].ToString(), "v2_old");
ASSERT_TRUE(statuses[2].IsNotFound()); // didn't exist at snapshot
db_->ReleaseSnapshot(snap);
// MultiGet without snapshot should see new values
for (auto& v : values) {
v.Reset();
}
db_->MultiGet(ReadOptions(), handles_[1], keys.size(), keys.data(),
values.data(), statuses.data(), true);
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0].ToString(), "v1_new");
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1].ToString(), "v2_new");
ASSERT_OK(statuses[2]);
ASSERT_EQ(values[2].ToString(), "v3_new");
}
INSTANTIATE_TEST_CASE_P(DBMultiGetTestWithParam, DBMultiGetTestWithParam,
testing::Combine(testing::Bool(), testing::Bool()));
-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 -305
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"
@@ -799,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);
@@ -824,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
@@ -11498,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
@@ -11530,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
@@ -11681,283 +11663,6 @@ TEST_F(DBCompactionTest, PeriodicTask) {
ASSERT_EQ(listener->num_periodic_compactions, 1);
Close();
}
// 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;
}
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);
}
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
-143
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,55 +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();
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+31 -54
View File
@@ -91,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;
}
@@ -165,6 +152,7 @@ void CompactedDBImpl::MultiGet(const ReadOptions& _read_options,
}
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
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);
@@ -174,40 +162,38 @@ 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);
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;
reader_list.push_back(nullptr);
} else {
if (get_context.State() == GetContext::kFound) {
statuses[i] = Status::OK();
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 {
statuses[i] = Status::NotFound();
if (get_context.State() == GetContext::kFound) {
statuses[idx] = Status::OK();
}
}
}
++idx;
}
}
@@ -244,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();
}
@@ -257,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");
@@ -277,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
View File
@@ -143,6 +143,5 @@ class CompactedDBImpl : public DBImpl {
Version* version_;
const Comparator* user_comparator_;
LevelFilesBrief files_;
int files_level_{0};
};
} // namespace ROCKSDB_NAMESPACE
+14 -108
View File
@@ -169,7 +169,6 @@ DBImpl::DBImpl(const DBOptions& options, const std::string& dbname,
const bool seq_per_batch, const bool batch_per_txn,
bool read_only)
: dbname_(dbname),
read_only_(read_only),
own_info_log_(options.info_log == nullptr),
initial_db_options_(SanitizeOptions(dbname, options, read_only,
&init_logger_creation_s_)),
@@ -473,7 +472,7 @@ Status DBImpl::ResumeImpl(DBRecoverContext context) {
void DBImpl::WaitForBackgroundWork() {
// Wait for background work to finish
while (bg_bottom_compaction_scheduled_ || bg_compaction_scheduled_ ||
bg_flush_scheduled_ || bg_pressure_callback_in_progress_) {
bg_flush_scheduled_) {
bg_cv_.Wait();
}
}
@@ -562,18 +561,11 @@ Status DBImpl::CloseHelper() {
// Wait for background work to finish
while (bg_bottom_compaction_scheduled_ || bg_compaction_scheduled_ ||
bg_flush_scheduled_ || bg_purge_scheduled_ ||
bg_pressure_callback_in_progress_ ||
bg_async_file_open_state_ == AsyncFileOpenState::kScheduled ||
pending_purge_obsolete_files_ ||
error_handler_.IsRecoveryInProgress()) {
TEST_SYNC_POINT("DBImpl::~DBImpl:WaitJob");
bg_cv_.Wait();
}
// Ensure subclasses don't forget to schedule async file opening
assert(!immutable_db_options_.open_files_async || !opened_successfully_ ||
bg_async_file_open_state_ != AsyncFileOpenState::kNotScheduled);
TEST_SYNC_POINT_CALLBACK("DBImpl::CloseHelper:PendingPurgeFinished",
&files_grabbed_for_purge_);
EraseThreadStatusDbInfo();
@@ -615,7 +607,7 @@ Status DBImpl::CloseHelper() {
// manifest file), it is not able to identify live files correctly. As a
// result, all "live" files can get deleted by accident. However, corrupted
// manifest is recoverable by RepairDB().
if (opened_successfully_ && !read_only_) {
if (opened_successfully_) {
JobContext job_context(next_job_id_.fetch_add(1));
FindObsoleteFiles(&job_context, true);
@@ -792,76 +784,7 @@ void DBImpl::PrintStatistics() {
}
}
// Computes the minimum time-based compaction interval for a CF based on
// various options.
// Returns 0 if all time-based compaction options are disabled.
static uint64_t GetMinTimeBasedCompactionInterval(
const ColumnFamilyOptions& cf_opts) {
uint64_t min_interval = UINT64_MAX;
if (cf_opts.periodic_compaction_seconds > 0) {
min_interval = std::min(min_interval, cf_opts.periodic_compaction_seconds);
}
if (cf_opts.ttl > 0) {
min_interval = std::min(min_interval, cf_opts.ttl);
}
const auto& fifo_thresholds =
cf_opts.compaction_options_fifo.file_temperature_age_thresholds;
if (!fifo_thresholds.empty() && fifo_thresholds[0].age > 0) {
// Thresholds are in increasing order by age, so first is smallest
min_interval = std::min(min_interval, fifo_thresholds[0].age);
}
if (cf_opts.bottommost_file_compaction_delay > 0) {
// NOTE: 0 does not exactly mean "disabled" in this case but it does mean
// there's no time component to the relevant compaction picking.
min_interval = std::min(min_interval,
uint64_t{cf_opts.bottommost_file_compaction_delay});
}
// Note: Assume sentinel values like UINT64_MAX - 1 (used by ttl and
// periodic_compaction_seconds) are like disabling, if they reach here
// unsanitized.
return min_interval > UINT64_MAX / 2 ? 0 : min_interval;
}
uint64_t DBImpl::ComputeTriggerCompactionPeriod() {
// Start with a maximum period of every 12 hours.
uint64_t period_sec = 12 * 60 * 60;
// Consider DB-level options that have the DB waking up periodically anyway.
// Waking up to check for compactions at the same interval should be no
// problem, as it should be less overhead than these.
if (mutable_db_options_.stats_dump_period_sec > 0) {
period_sec = std::min(period_sec,
(uint64_t)mutable_db_options_.stats_dump_period_sec);
}
if (mutable_db_options_.stats_persist_period_sec > 0) {
period_sec = std::min(
period_sec, (uint64_t)mutable_db_options_.stats_persist_period_sec);
}
// Consider per-CF settings that can trigger compaction based on time.
uint64_t compaction_trigger_sec = UINT64_MAX;
for (auto cfd : *versions_->GetColumnFamilySet()) {
if (cfd->IsDropped()) {
continue;
}
uint64_t cf_min =
GetMinTimeBasedCompactionInterval(cfd->GetLatestCFOptions());
if (cf_min > 0) {
compaction_trigger_sec = std::min(compaction_trigger_sec, cf_min);
}
}
// We might not align with those timings perfectly, so we tolerate only some
// proportional delay, up to 1/kTriggerDivisor ~= 20%.
constexpr uint64_t kTriggerDivisor = 5;
period_sec = std::min(period_sec, compaction_trigger_sec / kTriggerDivisor);
// But must be > 0
period_sec = std::max(period_sec, uint64_t{1});
return period_sec;
}
Status DBImpl::StartPeriodicTaskScheduler() {
Status s;
#ifndef NDEBUG
// It only used by test to disable scheduler
bool disable_scheduler = false;
@@ -869,7 +792,7 @@ Status DBImpl::StartPeriodicTaskScheduler() {
"DBImpl::StartPeriodicTaskScheduler:DisableScheduler",
&disable_scheduler);
if (disable_scheduler) {
return s;
return Status::OK();
}
{
@@ -880,7 +803,7 @@ Status DBImpl::StartPeriodicTaskScheduler() {
#endif // !NDEBUG
if (mutable_db_options_.stats_dump_period_sec > 0) {
s = periodic_task_scheduler_.Register(
Status s = periodic_task_scheduler_.Register(
PeriodicTaskType::kDumpStats,
periodic_task_functions_.at(PeriodicTaskType::kDumpStats),
mutable_db_options_.stats_dump_period_sec,
@@ -890,7 +813,7 @@ Status DBImpl::StartPeriodicTaskScheduler() {
}
}
if (mutable_db_options_.stats_persist_period_sec > 0) {
s = periodic_task_scheduler_.Register(
Status s = periodic_task_scheduler_.Register(
PeriodicTaskType::kPersistStats,
periodic_task_functions_.at(PeriodicTaskType::kPersistStats),
mutable_db_options_.stats_persist_period_sec,
@@ -900,18 +823,17 @@ Status DBImpl::StartPeriodicTaskScheduler() {
}
}
s = periodic_task_scheduler_.Register(
Status s = periodic_task_scheduler_.Register(
PeriodicTaskType::kFlushInfoLog,
periodic_task_functions_.at(PeriodicTaskType::kFlushInfoLog),
/*run_immediately=*/true);
if (!s.ok()) {
return s;
}
s = periodic_task_scheduler_.Register(
PeriodicTaskType::kTriggerCompaction,
periodic_task_functions_.at(PeriodicTaskType::kTriggerCompaction),
ComputeTriggerCompactionPeriod(), /*run_immediately=*/false);
if (s.ok()) {
s = periodic_task_scheduler_.Register(
PeriodicTaskType::kTriggerCompaction,
periodic_task_functions_.at(PeriodicTaskType::kTriggerCompaction),
/*run_immediately=*/false);
}
return s;
}
@@ -1549,13 +1471,6 @@ Status DBImpl::SetDBOptions(
table_cache_.get()->SetCapacity(new_options.max_open_files == -1
? TableCache::kInfiniteCapacity
: new_options.max_open_files - 10);
// Potential table cache capacity change requires updating if table
// handles should get pinned.
for (auto cfd : *versions_->GetColumnFamilySet()) {
if (!cfd->IsDropped()) {
cfd->table_cache()->UpdateShouldPinTableHandles();
}
}
wal_other_option_changed = mutable_db_options_.wal_bytes_per_sync !=
new_options.wal_bytes_per_sync;
wal_size_option_changed = mutable_db_options_.max_total_wal_size !=
@@ -7001,17 +6916,8 @@ void DBImpl::TriggerPeriodicCompaction() {
if (cfd->IsDropped()) {
continue;
}
if (cfd->queued_for_compaction()) {
continue;
}
// Check if this CF has any time-based compaction trigger configured.
// This includes periodic_compaction_seconds, ttl, or FIFO temperature
// thresholds. Note: periodic_compaction_seconds may be 0 even when
// ttl or temperature thresholds are set, due to option sanitization.
if (GetMinTimeBasedCompactionInterval(cfd->GetLatestCFOptions()) > 0) {
TEST_SYNC_POINT_CALLBACK(
"DBImpl::TriggerPeriodicCompaction:BeforeComputeCompactionScore",
cfd);
if (cfd->GetLatestCFOptions().periodic_compaction_seconds &&
!cfd->queued_for_compaction()) {
cfd->current()->storage_info()->ComputeCompactionScore(
cfd->ioptions(), cfd->GetLatestMutableCFOptions(),
cfd->GetFullHistoryTsLow());
+5 -45
View File
@@ -1248,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;
@@ -1367,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
@@ -1720,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
@@ -1741,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;
@@ -2167,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,
@@ -2433,9 +2416,6 @@ class DBImpl : public DB {
void MaybeScheduleFlushOrCompaction();
BackgroundJobPressure CaptureBackgroundJobPressure() const;
void NotifyOnBackgroundJobPressureChanged();
struct FlushRequest {
FlushReason flush_reason;
// A map from column family to flush to largest memtable id to persist for
@@ -2526,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();
@@ -3078,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;
@@ -3090,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
@@ -3158,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;
@@ -3225,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_;
+1 -98
View File
@@ -3021,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;
}
@@ -3145,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();
@@ -3667,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
@@ -3694,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(
@@ -3779,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
-5
View File
@@ -282,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();
+1 -1
View File
@@ -446,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
-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();
+3 -158
View File
@@ -12,7 +12,6 @@
#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"
@@ -1493,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
@@ -1572,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 --
@@ -1586,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;
}
@@ -2687,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();
}
@@ -2739,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();
+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;
}
+6 -19
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;
@@ -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_;
-54
View File
@@ -1827,60 +1827,6 @@ TEST_F(DBSecondaryTestWithTimestamp, Iterators) {
Close();
}
TEST_F(DBSecondaryTest, GetLiveFilesOnSecondary) {
Options options;
options.env = env_;
options.level0_file_num_compaction_trigger = 4;
Reopen(options);
// Write some data and flush to create SST files on the primary.
for (int i = 0; i < 3; ++i) {
ASSERT_OK(Put("key" + std::to_string(i), "value" + std::to_string(i)));
ASSERT_OK(Flush());
}
// Open secondary and verify GetLiveFiles works.
Options options1;
options1.env = env_;
options1.max_open_files = -1;
OpenSecondary(options1);
std::vector<std::string> live_files;
uint64_t manifest_size = 0;
ASSERT_OK(db_secondary_->GetLiveFiles(live_files, &manifest_size));
ASSERT_GT(live_files.size(), 0);
ASSERT_GT(manifest_size, 0);
// Should contain SST files, CURRENT, MANIFEST, and OPTIONS.
bool has_sst = false;
bool has_current = false;
bool has_manifest = false;
for (const auto& f : live_files) {
if (f.find(".sst") != std::string::npos) {
has_sst = true;
} else if (f.find("CURRENT") != std::string::npos) {
has_current = true;
} else if (f.find("MANIFEST") != std::string::npos) {
has_manifest = true;
}
}
ASSERT_TRUE(has_sst);
ASSERT_TRUE(has_current);
ASSERT_TRUE(has_manifest);
// Write more data on primary, catch up, and verify the file list updates.
ASSERT_OK(Put("key3", "value3"));
ASSERT_OK(Flush());
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
std::vector<std::string> live_files_after;
uint64_t manifest_size_after = 0;
ASSERT_OK(
db_secondary_->GetLiveFiles(live_files_after, &manifest_size_after));
ASSERT_GT(live_files_after.size(), live_files.size());
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+1 -1
View File
@@ -1535,7 +1535,7 @@ TEST_F(DBSSTTest, OpenDBWithInfiniteMaxOpenFiles) {
for (const auto& level : files) {
for (const auto& file : level) {
ASSERT_TRUE(file.fd.pinned_reader.Get() != nullptr);
ASSERT_TRUE(file.table_reader_handle != nullptr);
}
}
-593
View File
@@ -37,7 +37,6 @@
#include "db/write_batch_internal.h"
#include "env/mock_env.h"
#include "file/filename.h"
#include "monitoring/file_read_sample.h"
#include "monitoring/thread_status_util.h"
#include "port/port.h"
#include "port/stack_trace.h"
@@ -7763,598 +7762,6 @@ TEST_F(DBTest, ShuttingDownNotBlockStalledWrites) {
thd.join();
}
TEST_F(DBTest, FileReadSampledStats) {
SyncPoint::GetInstance()->SetCallBack(
"should_sample_file_read:override",
[](void* arg) { *static_cast<bool*>(arg) = true; });
SyncPoint::GetInstance()->EnableProcessing();
Options options = CurrentOptions();
options.disable_auto_compactions = true;
DestroyAndReopen(options);
// Write a key and flush to L0, then move to L1 so LevelIterator is used.
ASSERT_OK(Put("key1", "val1"));
ASSERT_OK(Flush());
MoveFilesToLevel(1);
auto files = GetLevelFileMetadatas(1);
ASSERT_EQ(files.size(), 1);
auto* meta = files[0];
// Reset counters (may have been incremented during compaction).
meta->stats.num_reads_sampled.store(0, std::memory_order_relaxed);
// A Get that finds the key should increment num_reads_sampled by
// kFileReadSampleRate (one sampled file access).
ASSERT_EQ("val1", Get("key1"));
ASSERT_EQ(meta->stats.num_reads_sampled.load(std::memory_order_relaxed),
kFileReadSampleRate);
meta->stats.num_reads_sampled.store(0, std::memory_order_relaxed);
// An iterator scan should also increment num_reads_sampled by
// kFileReadSampleRate (one file opened by LevelIterator).
{
std::unique_ptr<Iterator> iter(db_->NewIterator(ReadOptions()));
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
}
ASSERT_OK(iter->status());
}
ASSERT_EQ(meta->stats.num_reads_sampled.load(std::memory_order_relaxed),
kFileReadSampleRate);
// -- Test L0 Get path --
ASSERT_OK(Put("key2", "val2"));
ASSERT_OK(Flush());
auto l0_files = GetLevelFileMetadatas(0);
ASSERT_EQ(l0_files.size(), 1);
auto* l0_meta = l0_files[0];
l0_meta->stats.num_reads_sampled.store(0, std::memory_order_relaxed);
// A Get on an L0 file should increment num_reads_sampled.
ASSERT_EQ("val2", Get("key2"));
ASSERT_EQ(l0_meta->stats.num_reads_sampled.load(std::memory_order_relaxed),
kFileReadSampleRate);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(DBTest, FileCollapsibleEntryReadSampledStats) {
SyncPoint::GetInstance()->SetCallBack(
"should_sample_file_read:override",
[](void* arg) { *static_cast<bool*>(arg) = true; });
SyncPoint::GetInstance()->EnableProcessing();
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.merge_operator = MergeOperators::CreateStringAppendOperator();
DestroyAndReopen(options);
// -- Setup: create files with collapsible entries --
// File in L1 with a value, a delete tombstone, and a merge operand.
ASSERT_OK(Put("a", "val_a"));
ASSERT_OK(Put("b", "val_b"));
ASSERT_OK(Delete("b"));
ASSERT_OK(db_->Merge(WriteOptions(), "c", "merge_c"));
ASSERT_OK(Put("d", "val_d"));
ASSERT_OK(Flush());
MoveFilesToLevel(1);
auto files = GetLevelFileMetadatas(1);
ASSERT_EQ(files.size(), 1);
auto* meta = files[0];
// -- Test Get path --
// Get for a found key should NOT increment collapsible counter.
meta->stats.num_collapsible_entry_reads_sampled.store(
0, std::memory_order_relaxed);
ASSERT_EQ("val_a", Get("a"));
ASSERT_EQ(meta->stats.num_collapsible_entry_reads_sampled.load(
std::memory_order_relaxed),
0);
// Get for a missing key within the file's key range (kNotFound) should
// increment collapsible counter by kFileReadSampleRate.
ASSERT_EQ("NOT_FOUND", Get("aa"));
ASSERT_EQ(meta->stats.num_collapsible_entry_reads_sampled.load(
std::memory_order_relaxed),
kFileReadSampleRate);
// -- Test Iterator path --
// Iterating should count the delete and merge as collapsible entries.
// The file has one delete ("b") and one merge ("c"), so the counter
// should increment by 2 * kFileReadSampleRate.
meta->stats.num_collapsible_entry_reads_sampled.store(
0, std::memory_order_relaxed);
{
std::unique_ptr<Iterator> iter(db_->NewIterator(ReadOptions()));
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
}
ASSERT_OK(iter->status());
}
ASSERT_EQ(meta->stats.num_collapsible_entry_reads_sampled.load(
std::memory_order_relaxed),
2 * kFileReadSampleRate);
// -- Test L0 Get path --
DestroyAndReopen(options);
ASSERT_OK(Put("a", "val_a"));
ASSERT_OK(Delete("b"));
ASSERT_OK(Put("d", "val_d"));
ASSERT_OK(Flush());
auto l0_files = GetLevelFileMetadatas(0);
ASSERT_EQ(l0_files.size(), 1);
auto* l0_meta = l0_files[0];
// Get for a found key in L0 should NOT increment collapsible counter.
l0_meta->stats.num_collapsible_entry_reads_sampled.store(
0, std::memory_order_relaxed);
ASSERT_EQ("val_a", Get("a"));
ASSERT_EQ(l0_meta->stats.num_collapsible_entry_reads_sampled.load(
std::memory_order_relaxed),
0);
// Get for a missing key in L0 (kNotFound) should increment collapsible
// counter.
ASSERT_EQ("NOT_FOUND", Get("aa"));
ASSERT_EQ(l0_meta->stats.num_collapsible_entry_reads_sampled.load(
std::memory_order_relaxed),
kFileReadSampleRate);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
enum class ReadType : uint8_t { kGet, kMultiGet, kIterator, kNumReadTypes };
class OpenFilesAsyncTest
: public DBTest,
public testing::WithParamInterface<std::tuple<uint32_t, int, bool>> {
public:
OpenFilesAsyncTest() {
std::tie(num_flushes_, max_open_files_, read_only_) = GetParam();
}
std::string BeforeFindTableSyncPoint(ReadType rt) {
switch (rt) {
case ReadType::kGet:
return "TableCache::Get::BeforeFindTable";
case ReadType::kMultiGet:
return "TableCache::MultiGet::BeforeFindTable";
case ReadType::kIterator:
return "TableCache::NewIterator::BeforeFindTable";
default:
assert(false);
return "";
}
}
std::string KeyFor(uint32_t f) {
char buf[32];
snprintf(buf, sizeof(buf), "key%04u", f);
return buf;
}
std::string ValueFor(uint32_t f) {
char buf[32];
snprintf(buf, sizeof(buf), "value%04u", f);
return buf;
}
void SetupData(Options& options, const std::vector<std::string>& cfs = {
kDefaultColumnFamilyName}) {
options.create_if_missing = true;
options.max_open_files = max_open_files_;
// disable auto compactions/flushes
options.disable_auto_compactions = true;
options.info_log_level = FATAL_LEVEL; // reduce LOG size
DestroyAndReopen(options);
for (const auto& cf : cfs) {
if (cf != kDefaultColumnFamilyName) {
CreateColumnFamilies({cf}, options);
}
}
ASSERT_OK(TryReopenWithColumnFamilies(cfs, options));
for (uint32_t f = 0; f < num_flushes_; f++) {
for (int cf = 0; cf < static_cast<int>(cfs.size()); cf++) {
ASSERT_OK(Put(cf, KeyFor(f), ValueFor(f)));
ASSERT_OK(Flush(cf));
}
}
Close();
}
void VerifyData(ReadType rt) {
switch (rt) {
case ReadType::kGet:
for (uint32_t f = 0; f < num_flushes_; f++) {
ASSERT_EQ(Get(KeyFor(f)), ValueFor(f));
}
break;
case ReadType::kMultiGet: {
std::vector<std::string> keys;
std::vector<Slice> key_slices;
keys.reserve(num_flushes_);
key_slices.reserve(num_flushes_);
for (uint32_t f = 0; f < num_flushes_; f++) {
keys.push_back(KeyFor(f));
}
for (auto& k : keys) {
key_slices.emplace_back(k);
}
std::vector<std::string> values(num_flushes_);
std::vector<Status> statuses =
db_->MultiGet(ReadOptions(), key_slices, &values);
for (uint32_t f = 0; f < num_flushes_; f++) {
ASSERT_OK(statuses[f]);
ASSERT_EQ(values[f], ValueFor(f));
}
break;
}
case ReadType::kIterator: {
std::unique_ptr<Iterator> iter(db_->NewIterator(ReadOptions()));
iter->SeekToFirst();
for (uint32_t f = 0; f < num_flushes_; f++) {
ASSERT_TRUE(iter->Valid());
ASSERT_EQ(iter->key().ToString(), KeyFor(f));
ASSERT_EQ(iter->value().ToString(), ValueFor(f));
iter->Next();
}
ASSERT_OK(iter->status());
break;
}
default:
assert(false);
}
}
void OpenTestDB(Options& options) {
options.open_files_async = true;
options.skip_stats_update_on_db_open = true;
options.statistics = CreateDBStatistics();
options.info_log_level = FATAL_LEVEL; // reduce LOG size
std::vector<std::string> cfs;
ASSERT_OK(DB::ListColumnFamilies(options, dbname_, &cfs));
if (read_only_) {
ASSERT_OK(TryReopenReadOnlyWithColumnFamilies(cfs, options));
} else {
ASSERT_OK(TryReopenWithColumnFamilies(cfs, options));
}
}
void VerifyReadError(ReadType rt) {
switch (rt) {
case ReadType::kGet: {
PinnableSlice value;
Status read_s = db_->Get(ReadOptions(), db_->DefaultColumnFamily(),
KeyFor(0), &value);
ASSERT_TRUE(read_s.IsCorruption()) << read_s.ToString();
break;
}
case ReadType::kMultiGet: {
std::vector<std::string> keys = {KeyFor(0)};
std::vector<Slice> key_slices = {keys[0]};
std::vector<std::string> values(1);
std::vector<Status> statuses =
db_->MultiGet(ReadOptions(), key_slices, &values);
ASSERT_TRUE(statuses[0].IsCorruption()) << statuses[0].ToString();
break;
}
case ReadType::kIterator: {
std::unique_ptr<Iterator> iter(db_->NewIterator(ReadOptions()));
iter->SeekToFirst();
ASSERT_TRUE(iter->status().IsCorruption()) << iter->status().ToString();
break;
}
default:
assert(false);
}
}
uint32_t num_flushes_{};
int max_open_files_{};
bool read_only_{};
};
INSTANTIATE_TEST_CASE_P(OpenFilesAsync, OpenFilesAsyncTest,
::testing::Combine(::testing::Values(4),
::testing::Values(-1, 10),
::testing::Bool()));
TEST_F(DBTest, ReadOnlyCloseDoesNotDeleteWriterFiles) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.disable_auto_compactions = true;
DestroyAndReopen(options);
ASSERT_OK(Put("before", "value"));
ASSERT_OK(Flush());
std::unique_ptr<DB> read_only_db;
ASSERT_OK(DB::OpenForReadOnly(options, dbname_, &read_only_db));
ASSERT_OK(Put("after", "value"));
ASSERT_OK(Flush());
std::vector<LiveFileMetaData> live_files;
db_->GetLiveFilesMetaData(&live_files);
ASSERT_GE(live_files.size(), 2);
std::string newest_sst_path;
uint64_t newest_file_number = 0;
for (const auto& live_file : live_files) {
if (live_file.file_number > newest_file_number) {
newest_file_number = live_file.file_number;
newest_sst_path = live_file.directory + "/" + live_file.relative_filename;
}
}
ASSERT_FALSE(newest_sst_path.empty());
ASSERT_OK(env_->FileExists(newest_sst_path));
read_only_db.reset();
ASSERT_OK(env_->FileExists(newest_sst_path));
}
// Test mix of races with async file open, reads, compactions
TEST_P(OpenFilesAsyncTest, ConcurrentFileAccess) {
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(SetupData(options));
for (uint8_t i = 0; i < static_cast<uint8_t>(ReadType::kNumReadTypes); i++) {
auto rt = static_cast<ReadType>(i);
OpenTestDB(options);
port::Thread reader_thread([&]() { VerifyData(rt); });
port::Thread compaction_thread([&]() {
if (!read_only_) {
db_->CompactRange(CompactRangeOptions(), nullptr, nullptr)
.PermitUncheckedError();
}
});
reader_thread.join();
compaction_thread.join();
if (max_open_files_ == -1) {
EXPECT_EQ(TestGetTickerCount(options, NO_FILE_OPENS), num_flushes_);
} else {
EXPECT_GE(TestGetTickerCount(options, NO_FILE_OPENS), num_flushes_);
}
Close();
}
}
TEST_P(OpenFilesAsyncTest, AfterRead) {
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(SetupData(options));
for (uint8_t i = 0; i < static_cast<uint8_t>(ReadType::kNumReadTypes); i++) {
auto rt = static_cast<ReadType>(i);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
{"OpenFilesAsyncTest::AfterRead",
"VersionBuilder::Rep::LoadTableHandlers::BeforeFindTable"},
{"DBImpl::BGWorkAsyncFileOpen:Done",
"OpenFilesAsyncTest::AfterRead::BeforeClose"},
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
BeforeFindTableSyncPoint(rt), [&](void* arg) {
FileDescriptor* fd = static_cast<FileDescriptor*>(arg);
if (max_open_files_ == -1) {
ASSERT_EQ(fd->pinned_reader.Get(), nullptr);
}
});
std::atomic<uint32_t> file_opens = 0;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionBuilder::Rep::LoadTableHandlers::BeforeFindTable",
[&](void* arg) {
++file_opens;
FileMetaData* file_meta = static_cast<FileMetaData*>(arg);
if (max_open_files_ == -1) {
ASSERT_NE(file_meta->fd.pinned_reader.Get(), nullptr);
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
OpenTestDB(options);
VerifyData(rt);
TEST_SYNC_POINT("OpenFilesAsyncTest::AfterRead");
TEST_SYNC_POINT("OpenFilesAsyncTest::AfterRead::BeforeClose");
if (max_open_files_ == -1) {
EXPECT_EQ(file_opens, num_flushes_);
EXPECT_EQ(TestGetTickerCount(options, NO_FILE_OPENS), num_flushes_);
} else {
EXPECT_GE(TestGetTickerCount(options, NO_FILE_OPENS), num_flushes_);
}
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
Close();
}
}
TEST_P(OpenFilesAsyncTest, BeforeRead) {
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(SetupData(options));
for (uint8_t i = 0; i < static_cast<uint8_t>(ReadType::kNumReadTypes); i++) {
auto rt = static_cast<ReadType>(i);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::BGWorkAsyncFileOpen:Done", "OpenFilesAsyncTest::BeforeRead"},
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
BeforeFindTableSyncPoint(rt), [&](void* arg) {
FileDescriptor* fd = static_cast<FileDescriptor*>(arg);
if (options.max_open_files == -1) {
ASSERT_NE(fd->pinned_reader.Get(), nullptr);
}
});
std::atomic<uint32_t> file_opens = 0;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionBuilder::Rep::LoadTableHandlers::BeforeFindTable",
[&](void* arg) {
++file_opens;
FileMetaData* file_meta = static_cast<FileMetaData*>(arg);
ASSERT_EQ(file_meta->fd.pinned_reader.Get(), nullptr);
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
OpenTestDB(options);
TEST_SYNC_POINT("OpenFilesAsyncTest::BeforeRead");
VerifyData(rt);
if (max_open_files_ == -1) {
EXPECT_EQ(file_opens, num_flushes_);
EXPECT_EQ(TestGetTickerCount(options, NO_FILE_OPENS), num_flushes_);
} else {
EXPECT_GE(TestGetTickerCount(options, NO_FILE_OPENS), num_flushes_);
}
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
Close();
}
}
TEST_P(OpenFilesAsyncTest, Shutdown) {
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(SetupData(options));
std::atomic<uint32_t> file_opens = 0;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionBuilder::Rep::LoadTableHandlers::BeforeFindTable",
[&](void* /*arg*/) { ++file_opens; });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"DBImpl::~DBImpl:WaitJob", "DBImpl::BGWorkAsyncFileOpen::Start"},
{"DBImpl::BGWorkAsyncFileOpen:Done", "OpenFilesAsyncTest::Shutdown"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
OpenTestDB(options);
Close();
TEST_SYNC_POINT("OpenFilesAsyncTest::Shutdown");
ASSERT_EQ(file_opens, 0);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_P(OpenFilesAsyncTest, Error) {
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(SetupData(options));
// Get SST file paths and corrupt all of them
std::vector<std::string> sst_files;
{
Reopen(options);
std::vector<LiveFileMetaData> metadata;
db_->GetLiveFilesMetaData(&metadata);
ASSERT_GT(metadata.size(), 0);
for (const auto& m : metadata) {
sst_files.push_back(m.directory + "/" + m.name);
}
Close();
}
for (const auto& f : sst_files) {
ASSERT_OK(test::CorruptFile(env_, f, 0, 100));
}
for (uint8_t i = 0; i < static_cast<uint8_t>(ReadType::kNumReadTypes); i++) {
auto rt = static_cast<ReadType>(i);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::BGWorkAsyncFileOpen:Done", "OpenFilesAsyncTest::Error"},
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
OpenTestDB(options);
TEST_SYNC_POINT("OpenFilesAsyncTest::Error");
ASSERT_TRUE(dbfull()->TEST_GetBGError().IsCorruption());
VerifyReadError(rt);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
Close();
}
}
TEST_P(OpenFilesAsyncTest, DropColumnFamily) {
// Can't drop CFs in read-only mode
if (read_only_) {
return;
}
for (uint8_t i = 0; i < static_cast<uint8_t>(ReadType::kNumReadTypes); i++) {
auto rt = static_cast<ReadType>(i);
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(
SetupData(options, {kDefaultColumnFamilyName, "cf1"}));
// Delay the async opener until after we drop the CF
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
{"OpenFilesAsyncTest::DropColumnFamily::AfterDrop",
"DBImpl::BGWorkAsyncFileOpen::Start"},
{"DBImpl::BGWorkAsyncFileOpen:Done",
"OpenFilesAsyncTest::DropColumnFamily::BeforeClose"},
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
OpenTestDB(options);
// Drop the non-default CF before async opener runs
ASSERT_OK(db_->DropColumnFamily(handles_[1]));
ASSERT_OK(db_->DestroyColumnFamilyHandle(handles_[1]));
handles_[1] = nullptr;
TEST_SYNC_POINT("OpenFilesAsyncTest::DropColumnFamily::AfterDrop");
// Default CF data should still be readable
VerifyData(rt);
TEST_SYNC_POINT("OpenFilesAsyncTest::DropColumnFamily::BeforeClose");
if (max_open_files_ == -1) {
EXPECT_EQ(TestGetTickerCount(options, NO_FILE_OPENS), num_flushes_);
} else {
EXPECT_GE(TestGetTickerCount(options, NO_FILE_OPENS), num_flushes_);
}
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
Close();
}
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
-38
View File
@@ -94,44 +94,6 @@ TEST_F(DBTest2, OpenForReadOnlyWithColumnFamilies) {
ASSERT_NOK(env_->FileExists(dbname));
}
// Regression test: wal_in_db_path_ was not initialized in the read-only DB
// open path, causing UBSan "invalid-bool-load" when CloseHelper calls
// PurgeObsoleteFiles -> DeleteObsoleteFileImpl which reads wal_in_db_path_.
TEST_F(DBTest2, ReadOnlyDBWalInDbPathInitialized) {
// Create a normal DB with some data and WAL files
Options options = CurrentOptions();
options.create_if_missing = true;
DestroyAndReopen(options);
ASSERT_OK(Put("key1", "value1"));
ASSERT_OK(Flush());
ASSERT_OK(Put("key2", "value2"));
Close();
// Reopen as read-only — wal_in_db_path_ must be properly initialized.
// Before the fix, closing this DB would read an uninitialized bool in
// DeleteObsoleteFileImpl, which UBSan catches as undefined behavior.
std::unique_ptr<DB> db_ptr;
ASSERT_OK(DB::OpenForReadOnly(options, dbname_, &db_ptr));
std::string value;
ASSERT_OK(db_ptr->Get(ReadOptions(), "key1", &value));
ASSERT_EQ("value1", value);
// Close the read-only DB — this triggers PurgeObsoleteFiles which reads
// wal_in_db_path_. Under UBSan, an uninitialized bool here would fail.
db_ptr.reset();
// Also test the column-families variant
std::vector<ColumnFamilyDescriptor> column_families;
column_families.emplace_back(kDefaultColumnFamilyName,
ColumnFamilyOptions(options));
std::vector<ColumnFamilyHandle*> handles;
ASSERT_OK(DB::OpenForReadOnly(DBOptions(options), dbname_, column_families,
&handles, &db_ptr));
for (auto* h : handles) {
delete h;
}
db_ptr.reset();
}
class PartitionedIndexTestListener : public EventListener {
public:
void OnFlushCompleted(DB* /*db*/, const FlushJobInfo& info) override {
-22
View File
@@ -675,28 +675,6 @@ Status DBTestBase::TryReopenWithColumnFamilies(
return TryReopenWithColumnFamilies(cfs, v_opts);
}
Status DBTestBase::TryReopenReadOnlyWithColumnFamilies(
const std::vector<std::string>& cfs, const std::vector<Options>& options) {
Close();
EXPECT_EQ(cfs.size(), options.size());
std::vector<ColumnFamilyDescriptor> column_families;
for (size_t i = 0; i < cfs.size(); ++i) {
column_families.emplace_back(cfs[i], options[i]);
}
DBOptions db_opts = DBOptions(options[0]);
last_options_ = options[0];
MaybeInstallTimeElapseOnlySleep(db_opts);
return DB::OpenForReadOnly(db_opts, dbname_, column_families, &handles_,
&db_);
}
Status DBTestBase::TryReopenReadOnlyWithColumnFamilies(
const std::vector<std::string>& cfs, const Options& options) {
Close();
std::vector<Options> v_opts(cfs.size(), options);
return TryReopenReadOnlyWithColumnFamilies(cfs, v_opts);
}
void DBTestBase::Reopen(const Options& options) {
ASSERT_OK(TryReopen(options));
}
-6
View File
@@ -1177,12 +1177,6 @@ class DBTestBase : public testing::Test {
Status TryReopenWithColumnFamilies(const std::vector<std::string>& cfs,
const Options& options);
Status TryReopenReadOnlyWithColumnFamilies(
const std::vector<std::string>& cfs, const std::vector<Options>& options);
Status TryReopenReadOnlyWithColumnFamilies(
const std::vector<std::string>& cfs, const Options& options);
void Reopen(const Options& options);
void Close();
+2 -2
View File
@@ -600,9 +600,9 @@ TEST_F(DBWALTest, RecoverWithTableHandle) {
for (const auto& level : files) {
for (const auto& file : level) {
if (options.max_open_files == kSmallMaxOpenFiles) {
ASSERT_TRUE(file.fd.pinned_reader.Get() == nullptr);
ASSERT_TRUE(file.table_reader_handle == nullptr);
} else {
ASSERT_TRUE(file.fd.pinned_reader.Get() != nullptr);
ASSERT_TRUE(file.table_reader_handle != nullptr);
}
}
}
-264
View File
@@ -913,270 +913,6 @@ TEST_F(DBWriteBufferManagerTest, RuntimeChangeableAllowStall) {
sleeping_task->WakeUp();
}
// Test that enforce_write_buffer_manager_during_recovery option controls
// whether WriteBufferManager limits are respected during WAL recovery.
// When enabled, flushes are triggered to keep memory bounded.
// When disabled (default), memory can grow beyond the configured limit.
TEST_F(DBWriteBufferManagerTest,
WriteBufferManagerLimitDuringWALRecoverySingleDB) {
const size_t kWbmLimit = 1 * 1024 * 1024; // 1 MB
const size_t kWbmLimitForWrites = 100 * 1024 * 1024; // 100 MB (no flush)
Options options = CurrentOptions();
options.arena_block_size = 4096;
options.write_buffer_size = 10 * 1024 * 1024; // 10MB per CF, never hit
options.max_write_buffer_number = 10; // Allow many memtables
options.disable_auto_compactions = true;
// Use avoid_flush_during_recovery = true to prevent any flushes triggered
// during the recovery
options.avoid_flush_during_recovery = true;
const int kNumKeys = 50;
const int kValueSize = 50 * 1024; // 50 KB each, total ~2.5 MB > 1MB limit
// ========== Part 1: Test with enforcement DISABLED (default behavior) =====
// WBM limits are not enforced during recovery
options.enforce_write_buffer_manager_during_recovery = false;
// Use large WBM limit during writes to avoid triggering flushes
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimitForWrites, nullptr, true);
DestroyAndReopen(options);
for (int i = 0; i < kNumKeys; i++) {
ASSERT_OK(Put(Key(i), DummyString(kValueSize)));
}
// Check to make sure there's no L0 file
ASSERT_EQ(0, TotalTableFiles());
Close();
// Use smaller WBM limit for recovery
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimit, nullptr, true);
// Recovery without enforcement - memory should exceed the limit
Reopen(options);
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
size_t memory_without_enforcement =
options.write_buffer_manager->mutable_memtable_memory_usage();
ASSERT_GT(memory_without_enforcement, kWbmLimit)
<< "Without enforcement, memory (" << memory_without_enforcement
<< ") should exceed the WBM limit (" << kWbmLimit << ")";
// Still no L0 file since avoid_flush_during_recovery is true
ASSERT_EQ(0, TotalTableFiles());
// ========== Part 2: Test with enforcement ENABLED ==========================
options.enforce_write_buffer_manager_during_recovery = true;
// Use large WBM limit during writes to avoid triggering flushes
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimitForWrites, nullptr, true);
DestroyAndReopen(options);
for (int i = 0; i < kNumKeys; i++) {
ASSERT_OK(Put(Key(i), DummyString(kValueSize)));
}
// Check to make sure there's no L0 file
ASSERT_EQ(0, TotalTableFiles());
Close();
// Use smaller WBM limit for recovery
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimit, nullptr, true);
// Recovery with enforcement - memory should be bounded
Reopen(options);
// Wait for flush to finish
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
// WBM's ShouldFlush() compares active memtable mem usage against
// mutable_limit_ which is 7/8 of buffer_size.
size_t expected_num_l0_files =
memory_without_enforcement / (kWbmLimit * 7 / 8) + 1;
ASSERT_EQ(expected_num_l0_files, TotalTableFiles());
size_t memory_with_enforcement =
options.write_buffer_manager->mutable_memtable_memory_usage();
ASSERT_LT(memory_with_enforcement, kWbmLimit)
<< "With enforcement, memory (" << memory_with_enforcement
<< ") should be less than the limit " << kWbmLimit << ")";
}
TEST_F(DBWriteBufferManagerTest,
WriteBufferManagerLimitDuringWALRecoveryMultipleDBs) {
// Two DBs with 4MB WBM limit.
// First DB writes 2.5MB and closes, no flush (mem usage goes back to 0)
// Second DB writes 2.5MB then first DB reopens.
const size_t kWbmLimitForTwoDbs = 4 * 1024 * 1024;
Options options = CurrentOptions();
options.arena_block_size = 2048;
options.write_buffer_size = 10 * 1024 * 1024; // 10MB per CF, never hit
options.max_write_buffer_number = 10; // Allow many memtables
options.disable_auto_compactions = true;
// Use avoid_flush_during_recovery = true to prevent any flushes triggered
// during the recovery
options.avoid_flush_during_recovery = true;
const int kNumKeys = 50;
const int kValueSize = 50 * 1024;
// ========== Part 1: Test with enforcement DISABLED (default behavior) =====
// WBM limits are not enforced during recovery
options.enforce_write_buffer_manager_during_recovery = false;
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimitForTwoDbs, nullptr, true);
DestroyAndReopen(options);
// Use of 2.5MB shouldn't trigger flush
for (int i = 0; i < kNumKeys; i++) {
ASSERT_OK(Put(Key(i), DummyString(kValueSize)));
}
ASSERT_EQ(0, TotalTableFiles());
size_t mem_usage_first_db_only =
options.write_buffer_manager->mutable_memtable_memory_usage();
ASSERT_LT(mem_usage_first_db_only, kWbmLimitForTwoDbs)
<< "Memory (" << mem_usage_first_db_only
<< ") should be less than the limit " << kWbmLimitForTwoDbs << ")";
Close();
ASSERT_EQ(0, options.write_buffer_manager->mutable_memtable_memory_usage());
// Open a second DB sharing the same WBM, write data to consume memory
std::string second_dbname = test::PerThreadDBPath("db_shared_wbm_recovery");
std::unique_ptr<DB> second_db;
ASSERT_OK(DestroyDB(second_dbname, options));
ASSERT_OK(DB::Open(options, second_dbname, &second_db));
WriteOptions wo;
for (int i = 0; i < kNumKeys; i++) {
ASSERT_OK(second_db->Put(wo, Key(i), DummyString(kValueSize)));
}
// First DB reopens without enforcement
Reopen(options);
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
// No flush
ASSERT_EQ(0, TotalTableFiles());
size_t memory_usage_for_both =
options.write_buffer_manager->mutable_memtable_memory_usage();
ASSERT_GT(memory_usage_for_both, kWbmLimitForTwoDbs)
<< "Without enforcement + shared WBM, memory (" << memory_usage_for_both
<< ") should be greater than the limit (" << kWbmLimitForTwoDbs << ")";
// Close the first DB
Close();
// ========== Part 2: Test with enforcement ENABLED =====
// WBM limits enforced during recovery
options.enforce_write_buffer_manager_during_recovery = true;
// Reopen the first DB with enforcement option enabled.
Reopen(options);
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
// With enforcement enabled, there were flushes
ASSERT_GT(TotalTableFiles(), 0);
memory_usage_for_both =
options.write_buffer_manager->mutable_memtable_memory_usage();
ASSERT_LT(memory_usage_for_both, kWbmLimitForTwoDbs)
<< "With enforcement + shared WBM, memory (" << memory_usage_for_both
<< ") should be less than the limit (" << kWbmLimitForTwoDbs << ")";
Close();
// Clean up second DB
ASSERT_OK(second_db->Close());
ASSERT_OK(DestroyDB(second_dbname, options));
second_db.reset();
}
// Regression test: a WriteBatch that exceeds both per-CF memtable limit and
// WBM global limit during WAL recovery should not double-schedule a CF on
// flush_scheduler_ (which crashes debug builds via assert).
TEST_F(DBWriteBufferManagerTest, DoubleSchedulingBugDuringWALRecovery) {
Options options = CurrentOptions();
options.arena_block_size = 4096;
// Small per-CF limit so memtable triggers CheckMemtableFull during recovery
options.write_buffer_size = 64 * 1024; // 64KB
options.max_write_buffer_number = 10;
options.disable_auto_compactions = true;
options.avoid_flush_during_recovery = true;
options.enforce_write_buffer_manager_during_recovery = true;
// WBM limit also small so the WBM loop in InsertLogRecordToMemtable fires
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(128 * 1024, nullptr, true);
DestroyAndReopen(options);
// Write enough data to exceed both limits during recovery replay
for (int i = 0; i < 50; i++) {
ASSERT_OK(Put(Key(i), DummyString(4096))); // ~200KB total > both limits
}
Close();
// Reopen triggers WAL recovery. Without the fix, this crashes in debug
// builds with assert(checking_set_.count(cfd) == 0) in ScheduleWork().
ASSERT_OK(TryReopen(options));
}
// Read-only WAL recovery with enforce_write_buffer_manager_during_recovery
// should not crash due to duplicate ScheduleWork() calls on the
// flush_scheduler_. The flush scheduler was not drained in read-only mode,
// causing the same CFD to be scheduled twice on successive WAL records,
// triggering assert(checking_set_.count(cfd) == 0).
TEST_F(DBWriteBufferManagerTest, ReadOnlyRecoveryWithEnforceWBMDoesNotAssert) {
Options options = CurrentOptions();
options.arena_block_size = 4096;
options.write_buffer_size = 10 * 1024 * 1024; // 10MB, never hit
options.max_write_buffer_number = 10;
options.disable_auto_compactions = true;
options.avoid_flush_during_recovery = true;
options.enforce_write_buffer_manager_during_recovery = true;
const size_t kWbmLimitForWrites = 100 * 1024 * 1024; // 100MB (no flush)
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimitForWrites, nullptr, true);
DestroyAndReopen(options);
// Write enough data so that WAL recovery will trigger
// WriteBufferManager::ShouldFlush() multiple times with a small WBM limit.
for (int i = 0; i < 50; i++) {
ASSERT_OK(Put(Key(i), DummyString(50 * 1024))); // ~2.5MB total
}
ASSERT_EQ(0, TotalTableFiles());
Close();
// Reopen read-only with a small WBM limit that will trigger ShouldFlush()
// during WAL recovery. Without the fix, this crashes in debug builds with
// assert(checking_set_.count(cfd) == 0) in FlushScheduler::ScheduleWork().
const size_t kWbmLimit = 1 * 1024 * 1024; // 1MB
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimit, nullptr, true);
ASSERT_OK(ReadOnlyReopen(options));
// Verify data is readable
for (int i = 0; i < 50; i++) {
ASSERT_EQ(DummyString(50 * 1024), Get(Key(i)));
}
}
INSTANTIATE_TEST_CASE_P(DBWriteBufferManagerTest, DBWriteBufferManagerTest,
testing::Bool());
-24
View File
@@ -201,25 +201,6 @@ std::map<std::tuple<BackgroundErrorReason, Status::Code, bool>,
{std::make_tuple(BackgroundErrorReason::kManifestWriteNoWAL,
Status::Code::kIOError, false),
Status::Severity::kFatalError},
// Errors during BG async file open
{std::make_tuple(BackgroundErrorReason::kAsyncFileOpen,
Status::Code::kCorruption, true),
Status::Severity::kUnrecoverableError},
{std::make_tuple(BackgroundErrorReason::kAsyncFileOpen,
Status::Code::kCorruption, false),
Status::Severity::kNoError},
{std::make_tuple(BackgroundErrorReason::kAsyncFileOpen,
Status::Code::kIncomplete, true),
Status::Severity::kFatalError},
{std::make_tuple(BackgroundErrorReason::kAsyncFileOpen,
Status::Code::kIncomplete, false),
Status::Severity::kNoError},
{std::make_tuple(BackgroundErrorReason::kAsyncFileOpen,
Status::Code::kNotSupported, true),
Status::Severity::kFatalError},
{std::make_tuple(BackgroundErrorReason::kAsyncFileOpen,
Status::Code::kNotSupported, false),
Status::Severity::kNoError},
};
std::map<std::tuple<BackgroundErrorReason, bool>, Status::Severity>
@@ -244,11 +225,6 @@ std::map<std::tuple<BackgroundErrorReason, bool>, Status::Severity>
Status::Severity::kFatalError},
{std::make_tuple(BackgroundErrorReason::kMemTable, false),
Status::Severity::kFatalError},
// Errors during BG async file open
{std::make_tuple(BackgroundErrorReason::kAsyncFileOpen, true),
Status::Severity::kSoftError},
{std::make_tuple(BackgroundErrorReason::kAsyncFileOpen, false),
Status::Severity::kNoError},
};
void ErrorHandler::CancelErrorRecoveryForShutDown() {
+2 -9
View File
@@ -616,17 +616,10 @@ Status FlushJob::MemPurge() {
!(new_mem->ShouldFlushNow())) {
// Construct fragmented memtable range tombstones without mutex
new_mem->ConstructFragmentedRangeTombstones();
TEST_SYNC_POINT("FlushJob::MemPurge:BeforeReacquireMutex");
TEST_SYNC_POINT("FlushJob::MemPurge:AfterWaitForTest");
db_mutex_->Lock();
// Take the newest id, so that memtables in MemtableList don't have
// out-of-order memtable ids. While the db mutex was released during
// MemPurge, new memtables may have been switched to the immutable
// list with higher IDs, so we must use the maximum of the original
// flush batch ID and the current latest immutable memtable ID.
uint64_t new_mem_id = std::max(
mems_.back()->GetID(),
cfd_->imm()->GetLatestMemTableID(false /*for_atomic_flush*/));
// out-of-order memtable ids.
uint64_t new_mem_id = mems_.back()->GetID();
new_mem->SetID(new_mem_id);
// Take the latest memtable's next log number.
-123
View File
@@ -1643,129 +1643,6 @@ TEST_F(EventListenerTest, BlobDBFileTest) {
blob_event_listener->CheckCounters();
}
class BackgroundJobPressureTestListener : public EventListener {
public:
void OnBackgroundJobPressureChanged(
DB* /*db*/, const BackgroundJobPressure& snapshot) override {
std::lock_guard<std::mutex> lock(mu_);
snapshots_.push_back(snapshot);
}
std::vector<BackgroundJobPressure> GetSnapshots() {
std::lock_guard<std::mutex> lock(mu_);
return snapshots_;
}
void Reset() {
std::lock_guard<std::mutex> lock(mu_);
snapshots_.clear();
}
private:
std::mutex mu_;
std::vector<BackgroundJobPressure> snapshots_;
};
TEST_F(EventListenerTest, BackgroundJobPressure) {
auto listener = std::make_shared<BackgroundJobPressureTestListener>();
Options options = CurrentOptions();
options.create_if_missing = true;
options.max_background_jobs = 8;
options.disable_auto_compactions = false;
options.level0_slowdown_writes_trigger = 4;
options.level0_stop_writes_trigger = 100;
options.level0_file_num_compaction_trigger = 4;
options.listeners.push_back(listener);
DestroyAndReopen(options);
// Structural invariant checked on every snapshot.
auto CheckInvariants = [](const std::vector<BackgroundJobPressure>& snaps) {
ASSERT_GT(snaps.size(), 0u);
for (const auto& s : snaps) {
ASSERT_EQ(s.compaction_scheduled,
s.compaction_low_scheduled + s.compaction_bottom_scheduled);
ASSERT_EQ(s.compaction_running,
s.compaction_low_running + s.compaction_bottom_running);
}
};
// Block compaction so L0 files accumulate.
env_->SetBackgroundThreads(1, Env::Priority::LOW);
test::SleepingBackgroundTask sleeping_task;
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask, &sleeping_task,
Env::Priority::LOW);
sleeping_task.WaitUntilSleeping();
// Phase 1: No pressure — 3 SST files (below slowdown trigger=4).
for (int i = 0; i < 3; i++) {
ASSERT_OK(Put("k" + std::to_string(i), std::string(100, 'x')));
ASSERT_OK(Flush());
}
auto snapshots = listener->GetSnapshots();
CheckInvariants(snapshots);
for (const auto& s : snapshots) {
ASSERT_EQ(s.compaction_running, 0);
ASSERT_EQ(s.compaction_scheduled, 0);
ASSERT_FALSE(s.compaction_speedup_active);
ASSERT_LT(s.write_stall_proximity_pct, 100);
}
// Note: we don't assert flush_running/flush_scheduled == 0 here because
// MaybeScheduleFlushOrCompaction() runs before the pressure callback and
// may schedule new flush work, making flush counts non-deterministic.
// Phase 2: Build pressure — flush past slowdown trigger (4 L0 SST files).
// Compaction is blocked, so L0 SST files pile up.
listener->Reset();
{
FlushOptions fo;
fo.allow_write_stall = true;
for (int i = 3; i < 10; i++) {
ASSERT_OK(Put("k" + std::to_string(i), std::string(100, 'x')));
ASSERT_OK(db_->Flush(fo));
}
}
snapshots = listener->GetSnapshots();
CheckInvariants(snapshots);
for (const auto& s : snapshots) {
ASSERT_EQ(s.compaction_running, 0);
}
// Scan history: pressure indicators appear as L0 SST files accumulate
bool found_speedup = false;
bool found_compaction_scheduled = false;
bool found_high_proximity = false;
for (const auto& s : snapshots) {
if (s.compaction_speedup_active) {
found_speedup = true;
}
if (s.compaction_scheduled > 0) {
found_compaction_scheduled = true;
}
if (s.write_stall_proximity_pct >= 100) {
found_high_proximity = true;
}
}
ASSERT_TRUE(found_speedup);
ASSERT_TRUE(found_compaction_scheduled);
ASSERT_TRUE(found_high_proximity);
// Phase 3: Relieve pressure — unblock compaction, wait for completion.
listener->Reset();
sleeping_task.WakeUp();
sleeping_task.WaitUntilDone();
ASSERT_OK(dbfull()->TEST_WaitForCompact());
snapshots = listener->GetSnapshots();
CheckInvariants(snapshots);
// Latest: all compactions finished, healthy
const auto& latest3 = snapshots.back();
ASSERT_EQ(latest3.compaction_running, 0);
ASSERT_EQ(latest3.compaction_scheduled, 0);
ASSERT_FALSE(latest3.compaction_speedup_active);
ASSERT_LT(latest3.write_stall_proximity_pct, 100);
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+53 -199
View File
@@ -72,9 +72,7 @@ ImmutableMemTableOptions::ImmutableMemTableOptions(
allow_data_in_errors(ioptions.allow_data_in_errors),
paranoid_memory_checks(mutable_cf_options.paranoid_memory_checks),
memtable_veirfy_per_key_checksum_on_seek(
mutable_cf_options.memtable_veirfy_per_key_checksum_on_seek),
memtable_batch_lookup_optimization(
ioptions.memtable_batch_lookup_optimization) {}
mutable_cf_options.memtable_veirfy_per_key_checksum_on_seek) {}
MemTable::MemTable(const InternalKeyComparator& cmp,
const ImmutableOptions& ioptions,
@@ -1577,179 +1575,65 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
}
}
}
// Use batch lookup optimization when enabled
bool use_batch_optimization = moptions_.memtable_batch_lookup_optimization;
bool validate = moptions_.paranoid_memory_checks ||
moptions_.memtable_veirfy_per_key_checksum_on_seek;
if (use_batch_optimization) {
// Phase 1: Handle range tombstones and set up Savers for batched lookup
std::array<Saver, MultiGetContext::MAX_BATCH_SIZE> savers{};
std::array<const char*, MultiGetContext::MAX_BATCH_SIZE> memtable_keys{};
std::array<void*, MultiGetContext::MAX_BATCH_SIZE> callback_args{};
std::array<bool, MultiGetContext::MAX_BATCH_SIZE> found_final_values{};
std::array<bool, MultiGetContext::MAX_BATCH_SIZE> merge_in_progresses{};
size_t num_keys = 0;
for (auto iter = temp_range.begin(); iter != temp_range.end(); ++iter) {
if (!no_range_del) {
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
NewRangeTombstoneIteratorInternal(
read_options, GetInternalKeySeqno(iter->lkey->internal_key()),
immutable_memtable));
SequenceNumber covering_seq =
range_del_iter->MaxCoveringTombstoneSeqnum(iter->lkey->user_key());
if (covering_seq > iter->max_covering_tombstone_seq) {
iter->max_covering_tombstone_seq = covering_seq;
if (iter->timestamp) {
iter->timestamp->assign(range_del_iter->timestamp().data(),
range_del_iter->timestamp().size());
}
}
}
found_final_values[num_keys] = false;
merge_in_progresses[num_keys] = iter->s->IsMergeInProgress();
Saver& saver = savers[num_keys];
saver.status = iter->s;
saver.found_final_value = &found_final_values[num_keys];
saver.merge_in_progress = &merge_in_progresses[num_keys];
saver.key = iter->lkey;
saver.value = iter->value ? iter->value->GetSelf() : nullptr;
saver.columns = iter->columns;
saver.timestamp = iter->timestamp;
saver.seq = kMaxSequenceNumber;
saver.mem = this;
saver.merge_context = &(iter->merge_context);
saver.max_covering_tombstone_seq = iter->max_covering_tombstone_seq;
saver.merge_operator = moptions_.merge_operator;
saver.logger = moptions_.info_log;
saver.inplace_update_support = moptions_.inplace_update_support;
saver.statistics = moptions_.statistics;
saver.clock = clock_;
saver.callback_ = callback;
saver.is_blob_index = &iter->is_blob_index;
saver.do_merge = true;
saver.allow_data_in_errors = moptions_.allow_data_in_errors;
saver.protection_bytes_per_key = moptions_.protection_bytes_per_key;
memtable_keys[num_keys] = iter->lkey->memtable_key().data();
callback_args[num_keys] = &savers[num_keys];
num_keys++;
}
// Phase 2: Batched lookup
if (num_keys > 0) {
Status check_s =
table_->MultiGet(num_keys, memtable_keys.data(), callback_args.data(),
SaveValue, moptions_.allow_data_in_errors,
validate ? moptions_.paranoid_memory_checks : false,
validate ? key_validation_callback_ : nullptr);
if (check_s.IsCorruption()) {
// Mark all remaining keys as corruption
for (auto iter = temp_range.begin(); iter != temp_range.end(); ++iter) {
*(iter->s) = check_s;
range->MarkKeyDone(iter);
}
return;
}
}
// Phase 3: Process results
size_t result_idx = 0;
for (auto iter = temp_range.begin(); iter != temp_range.end();
++iter, ++result_idx) {
bool found_final_value = found_final_values[result_idx];
bool merge_in_progress = merge_in_progresses[result_idx];
if (!found_final_value && merge_in_progress) {
if (iter->s->ok()) {
*(iter->s) = Status::MergeInProgress();
} else {
assert(iter->s->IsMergeInProgress());
}
}
if (found_final_value ||
(!iter->s->ok() && !iter->s->IsMergeInProgress())) {
assert(found_final_value);
if (iter->value) {
iter->value->PinSelf();
range->AddValueSize(iter->value->size());
} else {
assert(iter->columns);
range->AddValueSize(iter->columns->serialized_size());
}
range->MarkKeyDone(iter);
RecordTick(moptions_.statistics, MEMTABLE_HIT);
if (range->GetValueSize() > read_options.value_size_soft_limit) {
for (auto range_iter = range->begin(); range_iter != range->end();
++range_iter) {
range->MarkKeyDone(range_iter);
*(range_iter->s) = Status::Aborted();
}
break;
for (auto iter = temp_range.begin(); iter != temp_range.end(); ++iter) {
bool found_final_value{false};
bool merge_in_progress = iter->s->IsMergeInProgress();
if (!no_range_del) {
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
NewRangeTombstoneIteratorInternal(
read_options, GetInternalKeySeqno(iter->lkey->internal_key()),
immutable_memtable));
SequenceNumber covering_seq =
range_del_iter->MaxCoveringTombstoneSeqnum(iter->lkey->user_key());
if (covering_seq > iter->max_covering_tombstone_seq) {
iter->max_covering_tombstone_seq = covering_seq;
if (iter->timestamp) {
// Will be overwritten in SaveValue() if there is a point key with
// a higher seqno.
iter->timestamp->assign(range_del_iter->timestamp().data(),
range_del_iter->timestamp().size());
}
}
}
} else {
// Per-key lookup path
for (auto iter = temp_range.begin(); iter != temp_range.end(); ++iter) {
bool found_final_value{false};
bool merge_in_progress = iter->s->IsMergeInProgress();
if (!no_range_del) {
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
NewRangeTombstoneIteratorInternal(
read_options, GetInternalKeySeqno(iter->lkey->internal_key()),
immutable_memtable));
SequenceNumber covering_seq =
range_del_iter->MaxCoveringTombstoneSeqnum(iter->lkey->user_key());
if (covering_seq > iter->max_covering_tombstone_seq) {
iter->max_covering_tombstone_seq = covering_seq;
if (iter->timestamp) {
iter->timestamp->assign(range_del_iter->timestamp().data(),
range_del_iter->timestamp().size());
}
}
}
SequenceNumber dummy_seq;
GetFromTable(
*(iter->lkey), iter->max_covering_tombstone_seq, true, callback,
&iter->is_blob_index, iter->value ? iter->value->GetSelf() : nullptr,
iter->columns, iter->timestamp, iter->s, &(iter->merge_context),
&dummy_seq, &found_final_value, &merge_in_progress);
SequenceNumber dummy_seq;
GetFromTable(*(iter->lkey), iter->max_covering_tombstone_seq, true,
callback, &iter->is_blob_index,
iter->value ? iter->value->GetSelf() : nullptr, iter->columns,
iter->timestamp, iter->s, &(iter->merge_context), &dummy_seq,
&found_final_value, &merge_in_progress);
if (!found_final_value && merge_in_progress) {
if (iter->s->ok()) {
*(iter->s) = Status::MergeInProgress();
} else {
assert(iter->s->IsMergeInProgress());
}
if (!found_final_value && merge_in_progress) {
if (iter->s->ok()) {
*(iter->s) = Status::MergeInProgress();
} else {
assert(iter->s->IsMergeInProgress());
}
}
if (found_final_value ||
(!iter->s->ok() && !iter->s->IsMergeInProgress())) {
// `found_final_value` should be set if an error/corruption occurs.
// The check on iter->s is just there in case GetFromTable() did not
// set `found_final_value` properly.
assert(found_final_value);
if (iter->value) {
iter->value->PinSelf();
range->AddValueSize(iter->value->size());
} else {
assert(iter->columns);
range->AddValueSize(iter->columns->serialized_size());
}
if (found_final_value ||
(!iter->s->ok() && !iter->s->IsMergeInProgress())) {
assert(found_final_value);
if (iter->value) {
iter->value->PinSelf();
range->AddValueSize(iter->value->size());
} else {
assert(iter->columns);
range->AddValueSize(iter->columns->serialized_size());
}
range->MarkKeyDone(iter);
RecordTick(moptions_.statistics, MEMTABLE_HIT);
if (range->GetValueSize() > read_options.value_size_soft_limit) {
for (auto range_iter = range->begin(); range_iter != range->end();
++range_iter) {
range->MarkKeyDone(range_iter);
*(range_iter->s) = Status::Aborted();
}
break;
range->MarkKeyDone(iter);
RecordTick(moptions_.statistics, MEMTABLE_HIT);
if (range->GetValueSize() > read_options.value_size_soft_limit) {
// Set all remaining keys in range to Abort
for (auto range_iter = range->begin(); range_iter != range->end();
++range_iter) {
range->MarkKeyDone(range_iter);
*(range_iter->s) = Status::Aborted();
}
break;
}
}
}
@@ -1957,36 +1841,6 @@ void MemTableRep::Get(const LookupKey& k, void* callback_args,
}
}
Status MemTableRep::MultiGet(
size_t num_keys, const char* const* keys, void** callback_args,
bool (*callback_func)(void* arg, const char* entry),
bool allow_data_in_errors, bool detect_key_out_of_order,
const std::function<Status(const char*, bool)>& key_validation_callback) {
bool validate = detect_key_out_of_order || key_validation_callback != nullptr;
std::unique_ptr<Iterator> iter(GetIterator());
for (size_t i = 0; i < num_keys; ++i) {
Slice dummy;
if (validate) {
Status s = iter->SeekAndValidate(dummy, keys[i], allow_data_in_errors,
detect_key_out_of_order,
key_validation_callback);
for (; iter->Valid() && s.ok() &&
callback_func(callback_args[i], iter->key());
s = iter->NextAndValidate(allow_data_in_errors)) {
}
if (!s.ok()) {
return s;
}
} else {
iter->Seek(dummy, keys[i]);
for (; iter->Valid() && callback_func(callback_args[i], iter->key());
iter->Next()) {
}
}
}
return Status::OK();
}
void MemTable::RefLogContainingPrepSection(uint64_t log) {
assert(log > 0);
auto cur = min_prep_log_referenced_.load();
-6
View File
@@ -65,7 +65,6 @@ struct ImmutableMemTableOptions {
bool allow_data_in_errors;
bool paranoid_memory_checks;
bool memtable_veirfy_per_key_checksum_on_seek;
bool memtable_batch_lookup_optimization;
};
// Batched counters to updated when inserting keys in one write batch.
@@ -600,11 +599,6 @@ class MemTable final : public ReadOnlyMemTable {
std::memory_order_relaxed);
}
// Returns true if a flush has already been scheduled for this memtable
bool HasFlushScheduled() const {
return flush_state_.load(std::memory_order_relaxed) == FLUSH_SCHEDULED;
}
InternalIterator* NewIterator(
const ReadOptions& read_options,
UnownedPtr<const SeqnoToTimeMapping> seqno_to_time_mapping, Arena* arena,
+1 -10
View File
@@ -7,7 +7,6 @@
#include "db/periodic_task_scheduler.h"
#include "rocksdb/system_clock.h"
#include "test_util/sync_point.h"
namespace ROCKSDB_NAMESPACE {
@@ -27,7 +26,7 @@ static const std::map<PeriodicTaskType, uint64_t> kDefaultPeriodSeconds = {
{PeriodicTaskType::kPersistStats, kInvalidPeriodSec},
{PeriodicTaskType::kFlushInfoLog, 10},
{PeriodicTaskType::kRecordSeqnoTime, kInvalidPeriodSec},
{PeriodicTaskType::kTriggerCompaction, kInvalidPeriodSec},
{PeriodicTaskType::kTriggerCompaction, 12 * 60 * 60} // 12 hours
};
static const std::map<PeriodicTaskType, std::string> kPeriodicTaskTypeNames = {
@@ -86,14 +85,6 @@ Status PeriodicTaskScheduler::Register(PeriodicTaskType task_type,
if (!result.second) {
return Status::Aborted("Failed to add periodic task");
}
#ifndef NDEBUG
{
std::pair<PeriodicTaskType, uint64_t> task_info{task_type,
repeat_period_seconds};
TEST_SYNC_POINT_CALLBACK("PeriodicTaskScheduler::Register:TaskRegistered",
&task_info);
}
#endif // NDEBUG
return Status::OK();
}
+9 -286
View File
@@ -6,9 +6,6 @@
#include "db/periodic_task_scheduler.h"
#include <set>
#include "db/column_family.h"
#include "db/db_test_util.h"
#include "env/composite_env_wrapper.h"
#include "test_util/mock_time_env.h"
@@ -127,289 +124,15 @@ TEST_F(PeriodicTaskSchedulerTest, Basic) {
ASSERT_EQ(3, pst_st_counter);
ASSERT_EQ(5, flush_info_log_counter);
// With the new ComputeTriggerCompactionPeriod() logic, since
// stats_dump_period_sec = stats_persist_period_sec = kPeriodSec, the trigger
// compaction period is also kPeriodSec. Unlike other periodic tasks,
// kTriggerCompaction uses run_immediately=false, so it doesn't fire on
// initial registration. It fires after each period elapses.
// By now we've had: initial 9s + 10s + 10s + 10s + 10s = 49s elapsed.
// kTriggerCompaction fires at t=10, 20, 30, 40 = 4 times.
ASSERT_EQ(4, trigger_compaction_counter);
// Sleep one more period and verify it increments
dbfull()->TEST_WaitForPeriodicTaskRun(
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(kPeriodSec)); });
ASSERT_EQ(5, trigger_compaction_counter);
Close();
}
TEST_F(PeriodicTaskSchedulerTest, TriggerCompactionPeriodComputation) {
// Test that ComputeTriggerCompactionPeriod() returns the expected values
// for different configurations.
Close();
// Helper to capture the trigger compaction period on Reopen
uint64_t trigger_compaction_period = 0;
SyncPoint::GetInstance()->SetCallBack(
"PeriodicTaskScheduler::Register:TaskRegistered", [&](void* arg) {
auto* task_info =
static_cast<std::pair<PeriodicTaskType, uint64_t>*>(arg);
if (task_info->first == PeriodicTaskType::kTriggerCompaction) {
trigger_compaction_period = task_info->second;
}
});
SyncPoint::GetInstance()->EnableProcessing();
// Helper to test the trigger compaction period with optional second CF
auto test_period = [&](Options& options, uint64_t expected_period,
const ColumnFamilyOptions* cf2_options = nullptr) {
options.create_if_missing = true;
options.env = mock_env_.get();
trigger_compaction_period = 0;
if (cf2_options == nullptr) {
Reopen(options);
} else {
// Open with two column families
// First destroy DB to start fresh
ASSERT_OK(DestroyDB(dbname_, options));
// Open and create cf2
ASSERT_OK(TryReopen(options));
CreateColumnFamilies({"cf2"}, options);
Close();
// Now reopen with both CFs
std::vector<ColumnFamilyDescriptor> cf_descs;
cf_descs.emplace_back(kDefaultColumnFamilyName, options);
cf_descs.emplace_back("cf2", *cf2_options);
std::vector<ColumnFamilyHandle*> handles;
ASSERT_OK(DB::Open(options, dbname_, cf_descs, &handles, &db_));
for (auto* h : handles) {
delete h;
}
}
ASSERT_EQ(expected_period, trigger_compaction_period);
Close();
// Destroy DB to clean up for next test case
ASSERT_OK(DestroyDB(dbname_, options));
};
// Case 1: 12-hour cap when stats_dump_period is longer
{
Options options;
options.stats_dump_period_sec = 24 * 60 * 60; // 24 hours
options.stats_persist_period_sec = 0;
test_period(options, 12 * 60 * 60); // Expect 12 hours (cap)
}
// Case 2: stats_dump_period_sec sets the period (when smaller than 12 hours)
{
Options options;
options.stats_dump_period_sec = 3600; // 1 hour
options.stats_persist_period_sec = 0;
test_period(options, 3600);
}
// Case 3: stats_persist_period_sec sets the period
{
Options options;
options.stats_dump_period_sec = 0;
options.stats_persist_period_sec = 1800; // 30 minutes
test_period(options, 1800);
}
// Case 4: periodic_compaction_seconds / kTriggerDivisor
{
Options options;
options.stats_dump_period_sec = 24 * 60 * 60;
options.stats_persist_period_sec = 0;
options.periodic_compaction_seconds = 500; // 500 / 5 = 100
test_period(options, 100);
}
// Case 5: ttl / kTriggerDivisor
{
Options options;
options.stats_dump_period_sec = 24 * 60 * 60;
options.stats_persist_period_sec = 0;
options.ttl = 600; // 600 / 5 = 120
test_period(options, 120);
}
// Case 6: bottommost_file_compaction_delay / kTriggerDivisor
{
Options options;
options.stats_dump_period_sec = 24 * 60 * 60;
options.stats_persist_period_sec = 0;
options.bottommost_file_compaction_delay = 250; // 250 / 5 = 50
test_period(options, 50);
}
// Case 7: file_temperature_age_thresholds / kTriggerDivisor (FIFO)
{
Options options;
options.stats_dump_period_sec = 24 * 60 * 60;
options.stats_persist_period_sec = 0;
options.compaction_style = kCompactionStyleFIFO;
options.num_levels = 1; // Required for file_temperature_age_thresholds
options.compaction_options_fifo.file_temperature_age_thresholds = {
{Temperature::kWarm, 1000}}; // 1000 / 5 = 200
// FIFO requires max_table_files_size to be set
options.compaction_options_fifo.max_table_files_size = 1024 * 1024 * 1024;
test_period(options, 200);
}
// Case 8: Minimum of multiple CF options is used
{
Options options;
options.stats_dump_period_sec = 24 * 60 * 60;
options.stats_persist_period_sec = 0;
options.periodic_compaction_seconds = 1000; // 1000 / 5 = 200
ColumnFamilyOptions cf2_opts;
cf2_opts.ttl = 500; // 500 / 5 = 100 (smaller, should be used)
test_period(options, 100, &cf2_opts);
}
// Case 9: Second CF has larger value, first CF's value is used
{
Options options;
options.stats_dump_period_sec = 24 * 60 * 60;
options.stats_persist_period_sec = 0;
options.ttl = 300; // 300 / 5 = 60
ColumnFamilyOptions cf2_opts;
cf2_opts.periodic_compaction_seconds = 1000; // 1000 / 5 = 200 (larger)
test_period(options, 60, &cf2_opts);
}
// Case 10: Minimum is 1 second (never 0)
{
Options options;
options.stats_dump_period_sec = 0;
options.stats_persist_period_sec = 0;
options.ttl = 3; // 3 / 5 = 0, but clamped to 1
test_period(options, 1);
}
}
// Test that TriggerPeriodicCompaction() properly considers CFs for compaction
// based on all time-based compaction options (not just
// periodic_compaction_seconds). This is a regression test for the bug where
// only periodic_compaction_seconds was checked, causing CFs with ttl,
// bottommost_file_compaction_delay, or file_temperature_age_thresholds to not
// be considered.
//
// NOTE: This test uses a separate test class without mock time because the
// PeriodicTaskSchedulerTest fixture's mock clock setup does not integrate
// properly with opening the DB via DB::Open() with multiple column families
// having different options.
class TriggerCompactionTest : public DBTestBase {
public:
TriggerCompactionTest()
: DBTestBase("trigger_compaction_test", /*env_do_fsync=*/true) {}
};
TEST_F(TriggerCompactionTest, QueuesAllTimeBasedOptions) {
Close();
// Track which CFs get their compaction score computed during
// TriggerPeriodicCompaction
std::set<std::string> cfs_with_score_computed;
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::TriggerPeriodicCompaction:BeforeComputeCompactionScore",
[&](void* arg) {
auto* cfd = static_cast<ColumnFamilyData*>(arg);
cfs_with_score_computed.insert(cfd->GetName());
});
SyncPoint::GetInstance()->EnableProcessing();
// Use small period so the test completes quickly with real time
constexpr unsigned int kPeriodSec = 1;
// Default CF: no time-based compaction options
Options options;
options.create_if_missing = true;
options.stats_dump_period_sec = kPeriodSec;
options.stats_persist_period_sec = 0;
options.periodic_compaction_seconds = 0;
options.ttl = 0;
options.bottommost_file_compaction_delay = 0;
// Create the DB with default CF first
DestroyAndReopen(options);
// Create the additional CFs (using same options, will set specific options
// on reopen)
CreateColumnFamilies(
{"cf_periodic", "cf_ttl", "cf_bottommost", "cf_fifo", "cf_none"},
options);
Close();
// Now reopen with specific options for each CF
std::vector<std::string> cf_names = {
kDefaultColumnFamilyName, "cf_periodic", "cf_ttl",
"cf_bottommost", "cf_fifo", "cf_none"};
std::vector<Options> cf_options;
// default: no time-based options
cf_options.push_back(options);
// cf_periodic: periodic_compaction_seconds
Options opt_periodic = options;
opt_periodic.periodic_compaction_seconds = 100;
cf_options.push_back(opt_periodic);
// cf_ttl: ttl
Options opt_ttl = options;
opt_ttl.ttl = 100;
cf_options.push_back(opt_ttl);
// cf_bottommost: bottommost_file_compaction_delay
Options opt_bottommost = options;
opt_bottommost.bottommost_file_compaction_delay = 100;
cf_options.push_back(opt_bottommost);
// cf_fifo: file_temperature_age_thresholds (requires FIFO compaction)
Options opt_fifo = options;
opt_fifo.compaction_style = kCompactionStyleFIFO;
opt_fifo.num_levels = 1;
opt_fifo.compaction_options_fifo.max_table_files_size = 1024 * 1024 * 1024;
opt_fifo.compaction_options_fifo.file_temperature_age_thresholds = {
{Temperature::kWarm, 100}};
cf_options.push_back(opt_fifo);
// cf_none: explicitly no time-based options
Options opt_none = options;
opt_none.periodic_compaction_seconds = 0;
opt_none.ttl = 0;
opt_none.bottommost_file_compaction_delay = 0;
cf_options.push_back(opt_none);
ReopenWithColumnFamilies(cf_names, cf_options);
// Wait for TriggerPeriodicCompaction to run using TEST_WaitForPeriodicTaskRun
// with a real sleep callback to advance real time
dbfull()->TEST_WaitForPeriodicTaskRun(
[&] { Env::Default()->SleepForMicroseconds(kPeriodSec * 1000000); });
// Verify CFs with time-based options had their compaction score computed
EXPECT_GT(cfs_with_score_computed.count("cf_periodic"), 0u)
<< "Expected cf_periodic to have compaction score computed";
EXPECT_GT(cfs_with_score_computed.count("cf_ttl"), 0u)
<< "Expected cf_ttl to have compaction score computed";
EXPECT_GT(cfs_with_score_computed.count("cf_bottommost"), 0u)
<< "Expected cf_bottommost to have compaction score computed";
EXPECT_GT(cfs_with_score_computed.count("cf_fifo"), 0u)
<< "Expected cf_fifo to have compaction score computed";
// CFs without time-based options should NOT have score computed
EXPECT_EQ(cfs_with_score_computed.count("default"), 0u)
<< "Expected default CF to NOT have compaction score computed";
EXPECT_EQ(cfs_with_score_computed.count("cf_none"), 0u)
<< "Expected cf_none to NOT have compaction score computed";
ASSERT_EQ(0, trigger_compaction_counter);
dbfull()->TEST_WaitForPeriodicTaskRun([&] {
mock_clock_->MockSleepForSeconds(static_cast<int>(12 * 60 * 60));
});
ASSERT_EQ(1, trigger_compaction_counter);
dbfull()->TEST_WaitForPeriodicTaskRun([&] {
mock_clock_->MockSleepForSeconds(static_cast<int>(12 * 60 * 60));
});
ASSERT_EQ(2, trigger_compaction_counter);
Close();
}
+133 -139
View File
@@ -16,7 +16,6 @@
#include "file/file_util.h"
#include "file/filename.h"
#include "file/random_access_file_reader.h"
#include "monitoring/file_read_sample.h"
#include "monitoring/perf_context_imp.h"
#include "rocksdb/advanced_options.h"
#include "rocksdb/statistics.h"
@@ -73,8 +72,6 @@ TableCache::TableCache(const ImmutableOptions& ioptions,
file_options_(*file_options),
cache_(cache),
immortal_tables_(false),
should_pin_table_handles_(cache_.get()->GetCapacity() >=
kInfiniteCapacity),
block_cache_tracer_(block_cache_tracer),
loader_mutex_(kLoadConcurency),
io_tracer_(io_tracer),
@@ -172,29 +169,15 @@ Cache::Handle* TableCache::Lookup(Cache* cache, uint64_t file_number) {
return cache->Lookup(key);
}
// TODO: consider making handle RAII.
Status TableCache::FindTable(
const ReadOptions& ro, const FileOptions& file_options,
const InternalKeyComparator& internal_comparator,
const FileMetaData& file_meta, TypedHandle** handle,
const MutableCFOptions& mutable_cf_options, TableReader** out_table_reader,
const bool no_io, HistogramImpl* file_read_hist, bool skip_filters,
int level, bool prefetch_index_and_filter_in_cache,
size_t max_file_size_for_l0_meta_pin, Temperature file_temperature,
bool pin_table_handle) {
assert(out_table_reader != nullptr && *out_table_reader == nullptr);
assert(handle != nullptr && *handle == nullptr);
const MutableCFOptions& mutable_cf_options, const bool no_io,
HistogramImpl* file_read_hist, bool skip_filters, int level,
bool prefetch_index_and_filter_in_cache,
size_t max_file_size_for_l0_meta_pin, Temperature file_temperature) {
PERF_TIMER_GUARD_WITH_CLOCK(find_table_nanos, ioptions_.clock);
// Fast path: if table reader is already pinned, return it directly without a
// cache lookup.
auto pinned_reader = file_meta.fd.pinned_reader.Get();
if (pinned_reader != nullptr) {
*handle = nullptr;
*out_table_reader = pinned_reader;
return Status::OK();
}
uint64_t number = file_meta.fd.GetNumber();
// NOTE: sharing same Cache with BlobFileCache
Slice key = GetSliceForFileNumber(&number);
@@ -202,71 +185,39 @@ Status TableCache::FindTable(
TEST_SYNC_POINT_CALLBACK("TableCache::FindTable:0",
const_cast<bool*>(&no_io));
Status s = Status::OK();
if (*handle == nullptr) {
if (no_io) {
s = Status::Incomplete("Table not found in table_cache, no_io is set");
return s;
return Status::Incomplete("Table not found in table_cache, no_io is set");
}
MutexLock load_lock(&loader_mutex_.Get(key));
// Check if another thread has already pinned the table reader
pinned_reader = file_meta.fd.pinned_reader.Get();
if (pinned_reader != nullptr) {
*handle = nullptr;
*out_table_reader = pinned_reader;
return s;
}
// We check the cache again under loading mutex
*handle = cache_.Lookup(key);
if (*handle == nullptr) {
std::unique_ptr<TableReader> table_reader;
s = GetTableReader(ro, file_options, internal_comparator, file_meta,
false /* sequential mode */, file_read_hist,
&table_reader, mutable_cf_options, skip_filters, level,
prefetch_index_and_filter_in_cache,
max_file_size_for_l0_meta_pin, file_temperature);
if (!s.ok()) {
assert(table_reader == nullptr);
RecordTick(ioptions_.stats, NO_FILE_ERRORS);
// We do not cache error results so that if the error is transient,
// or somebody repairs the file, we recover automatically.
IGNORE_STATUS_IF_ERROR(s);
} else {
s = cache_.Insert(key, table_reader.get(), 1, handle);
if (s.ok()) {
// Release ownership of table reader.
(void)table_reader.release();
}
}
if (*handle != nullptr) {
return Status::OK();
}
if (s.ok()) {
*out_table_reader = cache_.Value(*handle);
if (pin_table_handle) {
file_meta.fd.pinned_reader.Pin(*handle, *out_table_reader);
*handle = nullptr;
std::unique_ptr<TableReader> table_reader;
Status s = GetTableReader(ro, file_options, internal_comparator, file_meta,
false /* sequential mode */, file_read_hist,
&table_reader, mutable_cf_options, skip_filters,
level, prefetch_index_and_filter_in_cache,
max_file_size_for_l0_meta_pin, file_temperature);
if (!s.ok()) {
assert(table_reader == nullptr);
RecordTick(ioptions_.stats, NO_FILE_ERRORS);
// We do not cache error results so that if the error is transient,
// or somebody repairs the file, we recover automatically.
IGNORE_STATUS_IF_ERROR(s);
} else {
s = cache_.Insert(key, table_reader.get(), 1, handle);
if (s.ok()) {
// Release ownership of table reader.
table_reader.release();
}
}
} else {
*out_table_reader = cache_.Value(*handle);
if (pin_table_handle) {
// handle is in cache but not pinned. This should happen fairly rarely,
// and once the reader is pinned, we will no longer need to go through
// these mutexes again.
MutexLock load_lock(&loader_mutex_.Get(key));
if (file_meta.fd.pinned_reader.Get() != nullptr) {
// Another thread has pinned the handle; release our lookup ref.
cache_.Release(*handle);
} else {
file_meta.fd.pinned_reader.Pin(*handle, *out_table_reader);
}
*handle = nullptr;
}
return s;
}
return s;
return Status::OK();
}
InternalIterator* TableCache::NewIterator(
@@ -279,8 +230,7 @@ InternalIterator* TableCache::NewIterator(
const InternalKey* smallest_compaction_key,
const InternalKey* largest_compaction_key, bool allow_unprepared_value,
const SequenceNumber* read_seqno,
std::unique_ptr<TruncatedRangeDelIterator>* range_del_iter,
bool maybe_pin_table_handle) {
std::unique_ptr<TruncatedRangeDelIterator>* range_del_iter) {
PERF_TIMER_GUARD(new_table_iterator_nanos);
Status s;
@@ -290,15 +240,19 @@ InternalIterator* TableCache::NewIterator(
*table_reader_ptr = nullptr;
}
bool for_compaction = caller == TableReaderCaller::kCompaction;
TEST_SYNC_POINT_CALLBACK("TableCache::NewIterator::BeforeFindTable",
const_cast<FileDescriptor*>(&file_meta.fd));
s = FindTable(options, file_options, icomparator, file_meta, &handle,
mutable_cf_options, &table_reader,
options.read_tier == kBlockCacheTier /* no_io */,
file_read_hist, skip_filters, level,
true /* prefetch_index_and_filter_in_cache */,
max_file_size_for_l0_meta_pin, file_meta.temperature,
maybe_pin_table_handle && should_pin_table_handles_);
auto& fd = file_meta.fd;
table_reader = fd.table_reader;
if (table_reader == nullptr) {
s = FindTable(options, file_options, icomparator, file_meta, &handle,
mutable_cf_options,
options.read_tier == kBlockCacheTier /* no_io */,
file_read_hist, skip_filters, level,
true /* prefetch_index_and_filter_in_cache */,
max_file_size_for_l0_meta_pin, file_meta.temperature);
if (s.ok()) {
table_reader = cache_.Value(handle);
}
}
InternalIterator* result = nullptr;
if (s.ok()) {
if (options.table_filter &&
@@ -339,7 +293,7 @@ InternalIterator* TableCache::NewIterator(
}
}
if (range_del_agg != nullptr) {
if (range_del_agg->AddFile(file_meta.fd.GetNumber())) {
if (range_del_agg->AddFile(fd.GetNumber())) {
std::unique_ptr<FragmentedRangeTombstoneIterator> new_range_del_iter(
static_cast<FragmentedRangeTombstoneIterator*>(
table_reader->NewRangeTombstoneIterator(options)));
@@ -378,11 +332,17 @@ Status TableCache::GetRangeTombstoneIterator(
const FileMetaData& file_meta, const MutableCFOptions& mutable_cf_options,
std::unique_ptr<FragmentedRangeTombstoneIterator>* out_iter) {
assert(out_iter);
const FileDescriptor& fd = file_meta.fd;
Status s;
TableReader* t = nullptr;
TableReader* t = fd.table_reader;
TypedHandle* handle = nullptr;
s = FindTable(options, file_options_, internal_comparator, file_meta, &handle,
mutable_cf_options, &t);
if (t == nullptr) {
s = FindTable(options, file_options_, internal_comparator, file_meta,
&handle, mutable_cf_options);
if (s.ok()) {
t = cache_.Value(handle);
}
}
if (s.ok()) {
// Note: NewRangeTombstoneIterator could return nullptr
out_iter->reset(t->NewRangeTombstoneIterator(options));
@@ -496,18 +456,20 @@ Status TableCache::Get(const ReadOptions& options,
row_cache_entry = &row_cache_entry_buffer;
}
}
TEST_SYNC_POINT_CALLBACK("TableCache::Get::BeforeFindTable",
const_cast<FileDescriptor*>(&fd));
TableReader* t = nullptr;
TableReader* t = fd.table_reader;
TypedHandle* handle = nullptr;
if (s.ok() && !done) {
s = FindTable(options, file_options_, internal_comparator, file_meta,
&handle, mutable_cf_options, &t,
options.read_tier == kBlockCacheTier /* no_io */,
file_read_hist, skip_filters, level,
true /* prefetch_index_and_filter_in_cache */,
max_file_size_for_l0_meta_pin, file_meta.temperature,
should_pin_table_handles_);
if (t == nullptr) {
s = FindTable(options, file_options_, internal_comparator, file_meta,
&handle, mutable_cf_options,
options.read_tier == kBlockCacheTier /* no_io */,
file_read_hist, skip_filters, level,
true /* prefetch_index_and_filter_in_cache */,
max_file_size_for_l0_meta_pin, file_meta.temperature);
if (s.ok()) {
t = cache_.Value(handle);
}
}
SequenceNumber* max_covering_tombstone_seq =
get_context->max_covering_tombstone_seq();
if (s.ok() && max_covering_tombstone_seq != nullptr &&
@@ -584,8 +546,8 @@ Status TableCache::MultiGetFilter(
const InternalKeyComparator& internal_comparator,
const FileMetaData& file_meta, const MutableCFOptions& mutable_cf_options,
HistogramImpl* file_read_hist, int level,
MultiGetContext::Range* mget_range, TypedHandle** handle) {
assert(*handle == nullptr);
MultiGetContext::Range* mget_range, TypedHandle** table_handle) {
auto& fd = file_meta.fd;
IterKey row_cache_key;
std::string row_cache_entry_buffer;
@@ -597,17 +559,23 @@ Status TableCache::MultiGetFilter(
return Status::NotSupported();
}
Status s;
TableReader* t = nullptr;
TableReader* t = fd.table_reader;
TypedHandle* handle = nullptr;
MultiGetContext::Range tombstone_range(*mget_range, mget_range->begin(),
mget_range->end());
s = FindTable(options, file_options_, internal_comparator, file_meta, handle,
mutable_cf_options, &t,
options.read_tier == kBlockCacheTier /* no_io */,
file_read_hist,
/*skip_filters=*/false, level,
true /* prefetch_index_and_filter_in_cache */,
/*max_file_size_for_l0_meta_pin=*/0, file_meta.temperature,
should_pin_table_handles_);
if (t == nullptr) {
s = FindTable(options, file_options_, internal_comparator, file_meta,
&handle, mutable_cf_options,
options.read_tier == kBlockCacheTier /* no_io */,
file_read_hist,
/*skip_filters=*/false, level,
true /* prefetch_index_and_filter_in_cache */,
/*max_file_size_for_l0_meta_pin=*/0, file_meta.temperature);
if (s.ok()) {
t = cache_.Value(handle);
}
*table_handle = handle;
}
if (s.ok()) {
s = t->MultiGetFilter(options, mutable_cf_options.prefix_extractor.get(),
mget_range);
@@ -618,9 +586,9 @@ Status TableCache::MultiGetFilter(
// is, it may be called with fewer keys in the rangedue to filtering.
UpdateRangeTombstoneSeqnums(options, t, tombstone_range);
}
if (mget_range->empty() && *handle) {
cache_.Release(*handle);
*handle = nullptr;
if (mget_range->empty() && handle) {
cache_.Release(handle);
*table_handle = nullptr;
}
return s;
@@ -632,18 +600,24 @@ Status TableCache::GetTableProperties(
const FileMetaData& file_meta,
std::shared_ptr<const TableProperties>* properties,
const MutableCFOptions& mutable_cf_options, bool no_io) {
auto table_reader = file_meta.fd.table_reader;
// table already been pre-loaded?
if (table_reader) {
*properties = table_reader->GetTableProperties();
return Status::OK();
}
TypedHandle* table_handle = nullptr;
TableReader* table = nullptr;
Status s =
FindTable(read_options, file_options, internal_comparator, file_meta,
&table_handle, mutable_cf_options, &table, no_io);
Status s = FindTable(read_options, file_options, internal_comparator,
file_meta, &table_handle, mutable_cf_options, no_io);
if (!s.ok()) {
return s;
}
assert(table_handle);
auto table = cache_.Value(table_handle);
*properties = table->GetTableProperties();
if (table_handle) {
cache_.Release(table_handle);
}
cache_.Release(table_handle);
return s;
}
@@ -653,10 +627,15 @@ Status TableCache::ApproximateKeyAnchors(
std::vector<TableReader::Anchor>& anchors) {
Status s;
TableReader* t = nullptr;
TableReader* t = file_meta.fd.table_reader;
TypedHandle* handle = nullptr;
s = FindTable(ro, file_options_, internal_comparator, file_meta, &handle,
mutable_cf_options, &t);
if (t == nullptr) {
s = FindTable(ro, file_options_, internal_comparator, file_meta, &handle,
mutable_cf_options);
if (s.ok()) {
t = cache_.Value(handle);
}
}
if (s.ok() && t != nullptr) {
s = t->ApproximateKeyAnchors(ro, anchors);
}
@@ -670,18 +649,23 @@ size_t TableCache::GetMemoryUsageByTableReader(
const FileOptions& file_options, const ReadOptions& read_options,
const InternalKeyComparator& internal_comparator,
const FileMetaData& file_meta, const MutableCFOptions& mutable_cf_options) {
auto table_reader = file_meta.fd.table_reader;
// table already been pre-loaded?
if (table_reader) {
return table_reader->ApproximateMemoryUsage();
}
TypedHandle* table_handle = nullptr;
TableReader* table = nullptr;
Status s =
FindTable(read_options, file_options, internal_comparator, file_meta,
&table_handle, mutable_cf_options, &table, true /* no_io */);
&table_handle, mutable_cf_options, true /* no_io */);
if (!s.ok()) {
return 0;
}
assert(table_handle);
auto table = cache_.Value(table_handle);
auto ret = table->ApproximateMemoryUsage();
if (table_handle) {
cache_.Release(table_handle);
}
cache_.Release(table_handle);
return ret;
}
@@ -695,13 +679,18 @@ uint64_t TableCache::ApproximateOffsetOf(
const InternalKeyComparator& internal_comparator,
const MutableCFOptions& mutable_cf_options) {
uint64_t result = 0;
TableReader* table_reader = nullptr;
TableReader* table_reader = file_meta.fd.table_reader;
TypedHandle* table_handle = nullptr;
Status s =
FindTable(read_options, file_options_, internal_comparator, file_meta,
&table_handle, mutable_cf_options, &table_reader);
if (table_reader == nullptr) {
Status s =
FindTable(read_options, file_options_, internal_comparator, file_meta,
&table_handle, mutable_cf_options, false /* no_io */);
if (s.ok()) {
table_reader = cache_.Value(table_handle);
}
}
if (s.ok() && table_reader != nullptr) {
if (table_reader != nullptr) {
result = table_reader->ApproximateOffsetOf(read_options, key, caller);
}
if (table_handle != nullptr) {
@@ -717,13 +706,18 @@ uint64_t TableCache::ApproximateSize(
const InternalKeyComparator& internal_comparator,
const MutableCFOptions& mutable_cf_options) {
uint64_t result = 0;
TableReader* table_reader = nullptr;
TableReader* table_reader = file_meta.fd.table_reader;
TypedHandle* table_handle = nullptr;
Status s =
FindTable(read_options, file_options_, internal_comparator, file_meta,
&table_handle, mutable_cf_options, &table_reader);
if (table_reader == nullptr) {
Status s =
FindTable(read_options, file_options_, internal_comparator, file_meta,
&table_handle, mutable_cf_options, false /* no_io */);
if (s.ok()) {
table_reader = cache_.Value(table_handle);
}
}
if (s.ok() && table_reader != nullptr) {
if (table_reader != nullptr) {
result = table_reader->ApproximateSize(read_options, start, end, caller);
}
if (table_handle != nullptr) {
+6 -30
View File
@@ -99,8 +99,7 @@ class TableCache {
const InternalKey* smallest_compaction_key,
const InternalKey* largest_compaction_key, bool allow_unprepared_value,
const SequenceNumber* range_del_read_seqno = nullptr,
std::unique_ptr<TruncatedRangeDelIterator>* range_del_iter = nullptr,
bool maybe_pin_table_handle = false);
std::unique_ptr<TruncatedRangeDelIterator>* range_del_iter = nullptr);
// If a seek to internal key "k" in specified file finds an entry,
// call get_context->SaveValue() repeatedly until
@@ -173,31 +172,19 @@ class TableCache {
// Return handle to an existing cache entry if there is one
static Cache::Handle* Lookup(Cache* cache, uint64_t file_number);
// Look up the TableReader for the given file in the cache, or open the file
// and create a new TableReader if not cached. On success, sets *table_reader
// to point to the TableReader (owned by the cache) and *handle to the cache
// handle (caller must release via cache_.Release() unless pin_table_handle is
// true). If the table reader is already pinned on file_meta, returns it
// directly without a cache lookup.
//
// @param no_io If true, returns Status::Incomplete() when the table is not
// already in cache rather than reading from disk.
// @param skip_filters Disables loading/accessing the filter block.
// @param level The LSM level of this table, -1 if not specified.
// @param pin_table_handle If true, pins the table reader on file_meta so
// future lookups bypass the cache. *handle is set to nullptr
// on return in this case.
// Find table reader
// @param skip_filters Disables loading/accessing the filter block
// @param level == -1 means not specified
Status FindTable(const ReadOptions& ro, const FileOptions& toptions,
const InternalKeyComparator& internal_comparator,
const FileMetaData& file_meta, TypedHandle**,
const MutableCFOptions& mutable_cf_options,
TableReader** table_reader, const bool no_io = false,
const bool no_io = false,
HistogramImpl* file_read_hist = nullptr,
bool skip_filters = false, int level = -1,
bool prefetch_index_and_filter_in_cache = true,
size_t max_file_size_for_l0_meta_pin = 0,
Temperature file_temperature = Temperature::kUnknown,
bool pin_table_handle = false);
Temperature file_temperature = Temperature::kUnknown);
// Get the table properties of a given table.
// @no_io: indicates if we should load table to the cache if it is not present
@@ -244,8 +231,6 @@ class TableCache {
CacheInterface& get_cache() { return cache_; }
const FileOptions& file_options() const { return file_options_; }
// Capacity of the backing Cache that indicates infinite TableCache capacity.
// For example when max_open_files is -1 we set the backing Cache to this.
static const int kInfiniteCapacity = 0x400000;
@@ -258,14 +243,6 @@ class TableCache {
}
}
// Re-evaluates should_pin_table_handles_ from the current cache capacity.
// Must be called after the underlying cache capacity changes (e.g. via
// SetDBOptions changing max_open_files).
void UpdateShouldPinTableHandles() {
should_pin_table_handles_ =
cache_.get()->GetCapacity() >= kInfiniteCapacity;
}
private:
// Build a table reader
Status GetTableReader(const ReadOptions& ro, const FileOptions& file_options,
@@ -306,7 +283,6 @@ class TableCache {
CacheInterface cache_;
std::string row_cache_id_;
bool immortal_tables_;
bool should_pin_table_handles_;
BlockCacheTracer* const block_cache_tracer_;
Striped<CacheAlignedWrapper<port::Mutex>> loader_mutex_;
std::shared_ptr<IOTracer> io_tracer_;
+7 -7
View File
@@ -21,9 +21,7 @@ DEFINE_SYNC_AND_ASYNC(Status, TableCache::MultiGet)
bool skip_filters, bool skip_range_deletions, int level, TypedHandle* handle) {
auto& fd = file_meta.fd;
Status s;
TEST_SYNC_POINT_CALLBACK("TableCache::MultiGet::BeforeFindTable",
const_cast<FileDescriptor*>(&fd));
TableReader* t = fd.pinned_reader.Get();
TableReader* t = fd.table_reader;
MultiGetRange table_range(*mget_range, mget_range->begin(),
mget_range->end());
if (handle != nullptr && t == nullptr) {
@@ -72,14 +70,16 @@ DEFINE_SYNC_AND_ASYNC(Status, TableCache::MultiGet)
if (t == nullptr) {
assert(handle == nullptr);
s = FindTable(options, file_options_, internal_comparator, file_meta,
&handle, mutable_cf_options, &t,
&handle, mutable_cf_options,
options.read_tier == kBlockCacheTier /* no_io */,
file_read_hist, skip_filters, level,
true /* prefetch_index_and_filter_in_cache */,
0 /*max_file_size_for_l0_meta_pin*/, file_meta.temperature,
should_pin_table_handles_);
0 /*max_file_size_for_l0_meta_pin*/, file_meta.temperature);
TEST_SYNC_POINT_CALLBACK("TableCache::MultiGet:FindTable", &s);
assert(!s.ok() || t);
if (s.ok()) {
t = cache_.Value(handle);
assert(t);
}
}
if (s.ok() && !options.ignore_range_deletions && !skip_range_deletions) {
UpdateRangeTombstoneSeqnums(options, t, table_range);
+50 -11
View File
@@ -32,10 +32,8 @@
#include "db/version_edit.h"
#include "db/version_edit_handler.h"
#include "db/version_set.h"
#include "db/version_util.h"
#include "port/port.h"
#include "table/table_reader.h"
#include "test_util/sync_point.h"
#include "util/string_util.h"
namespace ROCKSDB_NAMESPACE {
@@ -432,11 +430,12 @@ class VersionBuilder::Rep {
void UnrefFile(FileMetaData* f) {
f->refs--;
if (f->refs <= 0) {
if (f->fd.pinned_reader.Get() != nullptr) {
if (f->table_reader_handle) {
assert(table_cache_ != nullptr);
// NOTE: have to release in raw cache interface to avoid using a
// TypedHandle for PinnedTableReader's internal handle
f->fd.pinned_reader.Release(table_cache_->get_cache().get());
// TypedHandle for FileMetaData::table_reader_handle
table_cache_->get_cache().get()->Release(f->table_reader_handle);
f->table_reader_handle = nullptr;
}
if (file_metadata_cache_res_mgr_) {
@@ -1681,6 +1680,7 @@ class VersionBuilder::Rep {
// <file metadata, level>
std::vector<std::pair<FileMetaData*, int>> files_meta;
std::vector<Status> statuses;
for (int level = 0; level < num_levels_; level++) {
for (auto& file_meta_pair : levels_[level].added_files) {
auto* file_meta = file_meta_pair.second;
@@ -1690,8 +1690,9 @@ class VersionBuilder::Rep {
continue;
}
// If the file has been opened before, just skip it.
if (file_meta->fd.pinned_reader.Get() == nullptr) {
if (!file_meta->table_reader_handle) {
files_meta.emplace_back(file_meta, level);
statuses.emplace_back(Status::OK());
}
if (files_meta.size() >= max_load) {
break;
@@ -1702,11 +1703,49 @@ class VersionBuilder::Rep {
}
}
return LoadTableHandlersHelper(
files_meta, table_cache_, file_options_,
*base_vstorage_->InternalComparator(), internal_stats, max_threads,
prefetch_index_and_filter_in_cache, mutable_cf_options,
max_file_size_for_l0_meta_pin, read_options);
std::atomic<size_t> next_file_meta_idx(0);
std::function<void()> load_handlers_func([&]() {
while (true) {
size_t file_idx = next_file_meta_idx.fetch_add(1);
if (file_idx >= files_meta.size()) {
break;
}
auto* file_meta = files_meta[file_idx].first;
int level = files_meta[file_idx].second;
TableCache::TypedHandle* handle = nullptr;
statuses[file_idx] = table_cache_->FindTable(
read_options, file_options_,
*(base_vstorage_->InternalComparator()), *file_meta, &handle,
mutable_cf_options, false /*no_io */,
internal_stats->GetFileReadHist(level), false, level,
prefetch_index_and_filter_in_cache, max_file_size_for_l0_meta_pin,
file_meta->temperature);
if (handle != nullptr) {
file_meta->table_reader_handle = handle;
// Load table_reader
file_meta->fd.table_reader = table_cache_->get_cache().Value(handle);
}
}
});
std::vector<port::Thread> threads;
for (int i = 1; i < max_threads; i++) {
threads.emplace_back(load_handlers_func);
}
load_handlers_func();
for (auto& t : threads) {
t.join();
}
Status ret;
for (const auto& s : statuses) {
if (!s.ok()) {
if (ret.ok()) {
ret = s;
}
}
}
return ret;
}
};
+1 -2
View File
@@ -53,8 +53,7 @@ class VersionBuilder {
// Save the current Version to the provided `vstorage`.
Status SaveTo(VersionStorageInfo* vstorage) const;
// Load table handlers for newly added files in the builder. This does not
// load any files in the base storage.
// Load all the table handlers for the current Version in the builder.
Status LoadTableHandlers(InternalStats* internal_stats, int max_threads,
bool prefetch_index_and_filter_in_cache,
bool is_initial_load,
-34
View File
@@ -22,40 +22,6 @@ namespace ROCKSDB_NAMESPACE {
namespace {} // anonymous namespace
// After copying, we rely on the caller to ensure no double releases. Fragile,
// but keeps copying cheap.
PinnedTableReader& PinnedTableReader::operator=(
const PinnedTableReader& other) {
TableReader* r = other.reader_.load(std::memory_order_acquire);
// Only read handle_ when reader_ is non-null. Pin() writes handle_ before
// reader_ (with release), so a non-null reader_ guarantees handle_ is stable.
// If reader_ is null, Pin() may be in progress — avoid reading handle_.
handle_ = (r != nullptr) ? other.handle_ : nullptr;
reader_.store(r, std::memory_order_release);
return *this;
}
Cache::Handle* PinnedTableReader::GetCacheHandle() const {
(void)reader_.load(std::memory_order_acquire);
return handle_;
}
void PinnedTableReader::Pin(Cache::Handle* handle, TableReader* reader) {
assert(handle != nullptr);
assert(reader != nullptr);
handle_ = handle;
reader_.store(reader, std::memory_order_release);
}
void PinnedTableReader::Release(Cache* cache) {
(void)reader_.load(std::memory_order_acquire);
if (handle_ != nullptr) {
cache->Release(handle_);
handle_ = nullptr;
reader_.store(nullptr, std::memory_order_relaxed);
}
}
uint64_t PackFileNumberAndPathId(uint64_t number, uint64_t path_id) {
assert(number <= kFileNumberMask);
return number | (path_id * (kFileNumberMask + 1));
+18 -64
View File
@@ -134,57 +134,14 @@ constexpr uint64_t kReservedEpochNumberForFileIngestedBehind = 1;
uint64_t PackFileNumberAndPathId(uint64_t number, uint64_t path_id);
// PinnedTableReader is used to safely access a table reader in a multi-threaded
// context. It holds both a pointer to the table reader and a cache handle.
class PinnedTableReader {
public:
PinnedTableReader() : reader_(nullptr), handle_(nullptr) {}
~PinnedTableReader() = default;
PinnedTableReader(const PinnedTableReader& other)
: reader_(nullptr), handle_(nullptr) {
*this = other;
}
PinnedTableReader& operator=(const PinnedTableReader& other);
PinnedTableReader(PinnedTableReader&&) = delete;
PinnedTableReader& operator=(PinnedTableReader&&) = delete;
// Returns the pinned TableReader, or nullptr if not pinned.
TableReader* Get() const { return reader_.load(std::memory_order_acquire); }
// Returns the cache handle that keeps TableReader alive, or nullptr if not
// pinned.
Cache::Handle* GetCacheHandle() const;
// Pin a table reader with its cache handle.
void Pin(Cache::Handle* handle, TableReader* reader);
// Release the pinned handle via the given cache and reset state.
void Release(Cache* cache);
// Test-only: set a reader without a cache handle.
void TEST_SetReader(TableReader* reader) {
reader_.store(reader, std::memory_order_release);
}
private:
// Internally, we need to ensure reads and writes to reader_ and handle_ are
// properly ordered. handle_ must be written to before reader_ is written to
// with release semantics. handle_ must be read after reader_ is read with
// acquire semantics.
std::atomic<TableReader*> reader_;
Cache::Handle* handle_;
};
// A copyable structure contains information needed to read data from an SST
// file. It can contain a pointer to a table reader opened for the file, or
// file number and size, which can be used to create a new table reader for it.
// The behavior is undefined when a copied of the structure is used when the
// file is not in any live version any more.
struct FileDescriptor {
// Fast access to table reader without cache lookup. Marked mutable because
// reads can pin the table reader, but can also be done safely in a
// multi-threaded context.
mutable PinnedTableReader pinned_reader;
// Table reader in table_reader_handle
TableReader* table_reader;
uint64_t packed_number_and_path_id;
uint64_t file_size; // File size in bytes
SequenceNumber smallest_seqno; // The smallest seqno in this file
@@ -197,7 +154,7 @@ struct FileDescriptor {
FileDescriptor(uint64_t number, uint32_t path_id, uint64_t _file_size,
SequenceNumber _smallest_seqno, SequenceNumber _largest_seqno)
: pinned_reader(),
: table_reader(nullptr),
packed_number_and_path_id(PackFileNumberAndPathId(number, path_id)),
file_size(_file_size),
smallest_seqno(_smallest_seqno),
@@ -206,7 +163,7 @@ struct FileDescriptor {
FileDescriptor(const FileDescriptor& fd) { *this = fd; }
FileDescriptor& operator=(const FileDescriptor& fd) {
pinned_reader = fd.pinned_reader;
table_reader = fd.table_reader;
packed_number_and_path_id = fd.packed_number_and_path_id;
file_size = fd.file_size;
smallest_seqno = fd.smallest_seqno;
@@ -225,20 +182,15 @@ struct FileDescriptor {
};
struct FileSampledStats {
FileSampledStats()
: num_reads_sampled(0), num_collapsible_entry_reads_sampled(0) {}
FileSampledStats() : num_reads_sampled(0) {}
FileSampledStats(const FileSampledStats& other) { *this = other; }
FileSampledStats& operator=(const FileSampledStats& other) {
num_reads_sampled = other.num_reads_sampled.load();
num_collapsible_entry_reads_sampled =
other.num_collapsible_entry_reads_sampled.load();
return *this;
}
// number of user reads to this file.
mutable std::atomic<uint64_t> num_reads_sampled;
// number of reads of type kNotFound, kMerge, kTypeSingleDeletion
mutable std::atomic<uint64_t> num_collapsible_entry_reads_sampled;
};
struct FileMetaData {
@@ -246,6 +198,9 @@ struct FileMetaData {
InternalKey smallest; // Smallest internal key served by table
InternalKey largest; // Largest internal key served by table
// Needs to be disposed when refs becomes 0.
Cache::Handle* table_reader_handle = nullptr;
FileSampledStats stats;
// Stats for compensating deletion entries during compaction
@@ -392,10 +347,9 @@ struct FileMetaData {
uint64_t TryGetOldestAncesterTime() {
if (oldest_ancester_time != kUnknownOldestAncesterTime) {
return oldest_ancester_time;
}
TableReader* reader = fd.pinned_reader.Get();
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
return reader->GetTableProperties()->creation_time;
} else if (fd.table_reader != nullptr &&
fd.table_reader->GetTableProperties() != nullptr) {
return fd.table_reader->GetTableProperties()->creation_time;
}
return kUnknownOldestAncesterTime;
}
@@ -403,10 +357,9 @@ struct FileMetaData {
uint64_t TryGetFileCreationTime() {
if (file_creation_time != kUnknownFileCreationTime) {
return file_creation_time;
}
TableReader* reader = fd.pinned_reader.Get();
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
return reader->GetTableProperties()->file_creation_time;
} else if (fd.table_reader != nullptr &&
fd.table_reader->GetTableProperties() != nullptr) {
return fd.table_reader->GetTableProperties()->file_creation_time;
}
return kUnknownFileCreationTime;
}
@@ -414,9 +367,10 @@ struct FileMetaData {
// Tries to get the newest key time from the current file
// Falls back on oldest ancestor time of previous (newer) file
uint64_t TryGetNewestKeyTime(FileMetaData* prev_file = nullptr) {
TableReader* reader = fd.pinned_reader.Get();
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
uint64_t newest_key_time = reader->GetTableProperties()->newest_key_time;
if (fd.table_reader != nullptr &&
fd.table_reader->GetTableProperties() != nullptr) {
uint64_t newest_key_time =
fd.table_reader->GetTableProperties()->newest_key_time;
if (newest_key_time != kUnknownNewestKeyTime) {
return newest_key_time;
}
+7 -7
View File
@@ -146,13 +146,13 @@ class VersionEditHandler : public VersionEditHandlerBase {
const std::shared_ptr<IOTracer>& io_tracer,
const ReadOptions& read_options, bool allow_incomplete_valid_version,
EpochNumberRequirement epoch_number_requirement =
EpochNumberRequirement::kMustPresent,
bool skip_load_table_files = false)
: VersionEditHandler(
read_only, column_families, version_set,
track_found_and_missing_files, no_error_if_files_missing, io_tracer,
read_options, skip_load_table_files, allow_incomplete_valid_version,
epoch_number_requirement) {}
EpochNumberRequirement::kMustPresent)
: VersionEditHandler(read_only, column_families, version_set,
track_found_and_missing_files,
no_error_if_files_missing, io_tracer, read_options,
/*skip_load_table_files=*/false,
allow_incomplete_valid_version,
epoch_number_requirement) {}
~VersionEditHandler() override {}
+31 -152
View File
@@ -170,7 +170,7 @@ class FilePicker {
if (!search_ended_) {
// Prefetch Level 0 table data to avoid cache miss if possible.
for (unsigned int i = 0; i < (*level_files_brief_)[0].num_files; ++i) {
auto* r = (*level_files_brief_)[0].files[i].fd.pinned_reader.Get();
auto* r = (*level_files_brief_)[0].files[i].fd.table_reader;
if (r) {
r->Prepare(ikey);
}
@@ -395,7 +395,7 @@ class FilePickerMultiGet {
// prefetching. This may not be necessary anymore once we implement
// batching in those table readers
for (unsigned int i = 0; i < (*level_files_brief_)[0].num_files; ++i) {
auto* r = (*level_files_brief_)[0].files[i].fd.pinned_reader.Get();
auto* r = (*level_files_brief_)[0].files[i].fd.table_reader;
if (r) {
for (auto iter = range_.begin(); iter != range_.end(); ++iter) {
r->Prepare(iter->ikey);
@@ -974,8 +974,9 @@ class LevelIterator final : public InternalIterator {
TableCache* table_cache, const ReadOptions& read_options,
const FileOptions& file_options, const InternalKeyComparator& icomparator,
const LevelFilesBrief* flevel, const MutableCFOptions& mutable_cf_options,
HistogramImpl* file_read_hist, TableReaderCaller caller,
bool skip_filters, int level, RangeDelAggregator* range_del_agg,
bool should_sample, HistogramImpl* file_read_hist,
TableReaderCaller caller, bool skip_filters, int level,
RangeDelAggregator* range_del_agg,
const std::vector<AtomicCompactionUnitBoundary>* compaction_boundaries =
nullptr,
bool allow_unprepared_value = false,
@@ -1001,6 +1002,7 @@ class LevelIterator final : public InternalIterator {
? read_options.snapshot->GetSequenceNumber()
: kMaxSequenceNumber),
level_(level),
should_sample_(should_sample),
skip_filters_(skip_filters),
allow_unprepared_value_(allow_unprepared_value),
is_next_read_sequential_(false),
@@ -1265,26 +1267,6 @@ class LevelIterator final : public InternalIterator {
*read_options_.iterate_upper_bound, /*b_has_ts=*/false) >= 0;
}
template <bool IsSeek>
void SampleRead() {
bool sampled =
IsSeek ? should_sample_file_read() : should_sample_file_read_next();
if (!sampled) {
return;
}
if (file_index_ >= flevel_->num_files || !file_iter_.Valid()) {
return;
}
const FileMetaData* meta = flevel_->files[file_index_].file_metadata;
sample_file_read_inc(meta);
ValueType type = ExtractValueType(file_iter_.key());
if (type == kTypeDeletion || type == kTypeSingleDeletion ||
type == kTypeDeletionWithTimestamp || type == kTypeMerge) {
sample_collapsible_entry_file_read_inc(meta);
}
}
void ClearRangeTombstoneIter() {
if (range_tombstone_iter_) {
range_tombstone_iter_->reset();
@@ -1297,6 +1279,9 @@ class LevelIterator final : public InternalIterator {
InternalIterator* NewFileIterator() {
assert(file_index_ < flevel_->num_files);
auto file_meta = flevel_->files[file_index_];
if (should_sample_) {
sample_file_read_inc(file_meta.file_metadata);
}
const InternalKey* smallest_compaction_key = nullptr;
const InternalKey* largest_compaction_key = nullptr;
@@ -1313,8 +1298,7 @@ class LevelIterator final : public InternalIterator {
/*arena=*/nullptr, skip_filters_, level_,
/*max_file_size_for_l0_meta_pin=*/0, smallest_compaction_key,
largest_compaction_key, allow_unprepared_value_, &read_seq_,
range_tombstone_iter_,
/*maybe_pin_table_handle=*/true);
range_tombstone_iter_);
}
// Check if current file being fully within iterate_lower_bound.
@@ -1377,6 +1361,7 @@ class LevelIterator final : public InternalIterator {
SequenceNumber read_seq_;
int level_;
bool should_sample_;
bool skip_filters_;
bool allow_unprepared_value_;
bool may_be_out_of_lower_bound_ = true;
@@ -1513,7 +1498,6 @@ void LevelIterator::Seek(const Slice& target) {
}
SkipEmptyFileForward();
CheckMayBeOutOfLowerBound();
SampleRead<true>();
}
void LevelIterator::SeekForPrev(const Slice& target) {
@@ -1549,7 +1533,6 @@ void LevelIterator::SeekForPrev(const Slice& target) {
SkipEmptyFileBackward();
}
CheckMayBeOutOfLowerBound();
SampleRead<true>();
}
void LevelIterator::SeekToFirst() {
@@ -1566,7 +1549,6 @@ void LevelIterator::SeekToFirst() {
}
SkipEmptyFileForward();
CheckMayBeOutOfLowerBound();
SampleRead<true>();
}
void LevelIterator::SeekToLast() {
@@ -1581,7 +1563,6 @@ void LevelIterator::SeekToLast() {
}
SkipEmptyFileBackward();
CheckMayBeOutOfLowerBound();
SampleRead<true>();
}
void LevelIterator::Next() {
@@ -1596,7 +1577,6 @@ void LevelIterator::Next() {
}
}
SkipEmptyFileForward();
SampleRead<false>();
}
bool LevelIterator::NextAndGetResult(IterateResult* result) {
@@ -1630,7 +1610,6 @@ bool LevelIterator::NextAndGetResult(IterateResult* result) {
}
}
}
SampleRead<false>();
return is_valid;
}
@@ -1645,7 +1624,6 @@ void LevelIterator::Prev() {
}
}
SkipEmptyFileBackward();
SampleRead<false>();
}
bool LevelIterator::SkipEmptyFileForward() {
@@ -2243,7 +2221,7 @@ void Version::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
uint64_t oldest_time = std::numeric_limits<uint64_t>::max();
for (int level = 0; level < storage_info_.num_non_empty_levels_; level++) {
for (FileMetaData* meta : storage_info_.LevelFiles(level)) {
assert(meta->fd.pinned_reader.Get() != nullptr);
assert(meta->fd.table_reader != nullptr);
uint64_t file_creation_time = meta->TryGetFileCreationTime();
if (file_creation_time == kUnknownFileCreationTime) {
*creation_time = 0;
@@ -2266,7 +2244,8 @@ InternalIterator* Version::TEST_GetLevelIterator(
auto level_iter = new (mem) LevelIterator(
cfd_->table_cache(), read_options, file_options_,
cfd_->internal_comparator(), &storage_info_.LevelFilesBrief(level),
mutable_cf_options_, cfd_->internal_stats()->GetFileReadHist(level),
mutable_cf_options_, should_sample_file_read(),
cfd_->internal_stats()->GetFileReadHist(level),
TableReaderCaller::kUserIterator, IsFilterSkipped(level), level,
nullptr /* range_del_agg */, nullptr /* compaction_boundaries */,
allow_unprepared_value, &tombstone_iter_ptr, db_statistics_, clock_);
@@ -2362,6 +2341,8 @@ void Version::AddIteratorsForLevel(const ReadOptions& read_options,
return;
}
bool should_sample = should_sample_file_read();
auto* arena = merge_iter_builder->GetArena();
if (level == 0) {
// Merge all level zero files together since they may overlap
@@ -2376,8 +2357,7 @@ void Version::AddIteratorsForLevel(const ReadOptions& read_options,
/*skip_filters=*/false, /*level=*/0, max_file_size_for_l0_meta_pin_,
/*smallest_compaction_key=*/nullptr,
/*largest_compaction_key=*/nullptr, allow_unprepared_value,
/*range_del_read_seqno=*/nullptr, &tombstone_iter,
/*maybe_pin_table_handle=*/true);
/*range_del_read_seqno=*/nullptr, &tombstone_iter);
if (read_options.ignore_range_deletions) {
merge_iter_builder->AddIterator(table_iter);
} else {
@@ -2385,10 +2365,11 @@ void Version::AddIteratorsForLevel(const ReadOptions& read_options,
table_iter, std::move(tombstone_iter));
}
}
if (should_sample_file_read()) {
if (should_sample) {
// Count ones for every L0 files. This is done per iterator creation
// rather than Seek(), while files in other levels are sampled on
// seek/next/prev.
// rather than Seek(), while files in other levels are recored per seek.
// If users execute one range query per iterator, there may be some
// discrepancy here.
for (FileMetaData* meta : storage_info_.LevelFiles(0)) {
sample_file_read_inc(meta);
}
@@ -2402,7 +2383,8 @@ void Version::AddIteratorsForLevel(const ReadOptions& read_options,
auto level_iter = new (mem) LevelIterator(
cfd_->table_cache(), read_options, soptions,
cfd_->internal_comparator(), &storage_info_.LevelFilesBrief(level),
mutable_cf_options_, cfd_->internal_stats()->GetFileReadHist(level),
mutable_cf_options_, should_sample_file_read(),
cfd_->internal_stats()->GetFileReadHist(level),
TableReaderCaller::kUserIterator, IsFilterSkipped(level), level,
/*range_del_agg=*/nullptr,
/*compaction_boundaries=*/nullptr, allow_unprepared_value,
@@ -2460,7 +2442,8 @@ Status Version::OverlapWithLevelIterator(const ReadOptions& read_options,
ScopedArenaPtr<InternalIterator> iter(new (mem) LevelIterator(
cfd_->table_cache(), read_options, file_options,
cfd_->internal_comparator(), &storage_info_.LevelFilesBrief(level),
mutable_cf_options_, cfd_->internal_stats()->GetFileReadHist(level),
mutable_cf_options_, should_sample_file_read(),
cfd_->internal_stats()->GetFileReadHist(level),
TableReaderCaller::kUserIterator, IsFilterSkipped(level), level,
&range_del_agg, nullptr, false, nullptr, db_statistics_, clock_));
status = OverlapWithIterator(ucmp, smallest_user_key, largest_user_key,
@@ -2801,15 +2784,9 @@ void Version::Get(const ReadOptions& read_options, const LookupKey& k,
switch (get_context.State()) {
case GetContext::kNotFound:
// Keep searching in other files
if (get_context.sample()) {
sample_collapsible_entry_file_read_inc(f->file_metadata);
}
break;
case GetContext::kMerge:
// TODO: update per-level perfcontext user_key_return_count for kMerge
if (get_context.sample()) {
sample_collapsible_entry_file_read_inc(f->file_metadata);
}
break;
case GetContext::kFound:
if (fp.GetHitFileLevel() == 0) {
@@ -2858,9 +2835,6 @@ void Version::Get(const ReadOptions& read_options, const LookupKey& k,
case GetContext::kDeleted:
// Use empty error message for speed
*status = Status::NotFound();
if (get_context.sample()) {
sample_collapsible_entry_file_read_inc(f->file_metadata);
}
return;
case GetContext::kCorrupt:
*status = Status::Corruption("corrupted key for ", user_key);
@@ -5607,96 +5581,6 @@ Status VersionSet::Close(FSDirectory* db_dir, InstrumentedMutex* mu) {
s = LogAndApply(cfd, ReadOptions(), WriteOptions(), &edit, mu, db_dir);
}
// Content validation: read back the manifest and verify CRC + decode.
// Loop up to 2 checks with 1 rewrite attempt in between, so we also verify
// the rewritten manifest is healthy.
if (s.ok() && verify_manifest_content_on_close_) {
TEST_SYNC_POINT("VersionSet::Close:BeforeContentValidation");
constexpr int kMaxContentChecks = 2;
for (int content_check = 0; s.ok() && content_check < kMaxContentChecks;
++content_check) {
// Re-read the manifest file name in case it was rotated by a rewrite
std::string content_manifest_name =
DescriptorFileName(dbname_, manifest_file_number_);
std::unique_ptr<FSSequentialFile> manifest_file;
IOStatus content_io_s = fs_->NewSequentialFile(
content_manifest_name, fs_->OptimizeForManifestRead(file_options_),
&manifest_file, nullptr);
if (!content_io_s.ok()) {
// Surface I/O errors to the caller — users who call DB::Close() and
// check the status should know about filesystem problems.
s = content_io_s;
ROCKS_LOG_ERROR(db_options_->info_log,
"MANIFEST content verification on Close: "
"could not open %s for reading: %s\n",
content_manifest_name.c_str(),
content_io_s.ToString().c_str());
break;
}
std::unique_ptr<SequentialFileReader> manifest_file_reader(
new SequentialFileReader(std::move(manifest_file),
content_manifest_name,
db_options_->log_readahead_size, io_tracer_,
db_options_->listeners));
LogReporter reporter;
Status log_read_status;
reporter.status = &log_read_status;
log::Reader reader(nullptr, std::move(manifest_file_reader), &reporter,
/*checksum=*/true, /*log_num=*/0);
Slice record;
std::string scratch;
bool content_corrupt = false;
while (reader.ReadRecord(&record, &scratch,
WALRecoveryMode::kAbsoluteConsistency)) {
VersionEdit edit;
Status decode_s = edit.DecodeFrom(record);
if (!decode_s.ok()) {
content_corrupt = true;
break;
}
}
if (!content_corrupt && !log_read_status.ok()) {
content_corrupt = true;
}
if (!content_corrupt) {
// Manifest is healthy, no need to check again
break;
}
IOStatus corrupt_io_s =
IOStatus::Corruption("MANIFEST content validation failed");
IOErrorInfo io_error_info(corrupt_io_s, FileOperationType::kVerify,
content_manifest_name, /*length=*/0,
/*offset=*/0);
for (auto& listener : db_options_->listeners) {
listener->OnIOError(io_error_info);
}
corrupt_io_s.PermitUncheckedError();
io_error_info.io_status.PermitUncheckedError();
if (content_check == 0) {
// First check failed — rewrite and verify again
ROCKS_LOG_ERROR(db_options_->info_log,
"MANIFEST content verification on Close failed, "
"filename %s, rewriting manifest\n",
content_manifest_name.c_str());
ColumnFamilyData* cfd = GetColumnFamilySet()->GetDefault();
VersionEdit recovery_edit;
assert(cfd);
s = LogAndApply(cfd, ReadOptions(), WriteOptions(), &recovery_edit, mu,
db_dir);
} else {
// Rewritten manifest is also corrupt — likely a recurring filesystem
// issue. Surface it so DB::Close() callers can detect the problem.
ROCKS_LOG_ERROR(db_options_->info_log,
"MANIFEST content verification on Close failed again "
"after rewrite, filename %s\n",
content_manifest_name.c_str());
s = Status::Corruption(
"MANIFEST content verification failed after rewrite: " +
content_manifest_name);
}
}
}
closed_ = true;
return s;
}
@@ -5713,10 +5597,9 @@ VersionSet::~VersionSet() {
// Using uncache_aggressiveness=0 overrides any previous marking to
// attempt to uncache the file's blocks (which after cleaning up
// column families could cause use-after-free)
TableCache::ReleaseObsolete(
table_cache_, file.metadata->fd.GetNumber(),
file.metadata->fd.pinned_reader.GetCacheHandle(),
/*uncache_aggressiveness=*/0);
TableCache::ReleaseObsolete(table_cache_, file.metadata->fd.GetNumber(),
file.metadata->table_reader_handle,
/*uncache_aggressiveness=*/0);
file.DeleteMetadata();
}
obsolete_files_.clear();
@@ -5780,8 +5663,6 @@ void VersionSet::UpdatedMutableDbOptions(
max_manifest_space_amp_pct_ = static_cast<unsigned>(
std::max(updated_options.max_manifest_space_amp_pct, 0));
manifest_preallocation_size_ = updated_options.manifest_preallocation_size;
verify_manifest_content_on_close_ =
updated_options.verify_manifest_content_on_close;
TuneMaxManifestFileSize();
}
@@ -6656,8 +6537,7 @@ Status VersionSet::Recover(
read_only, column_families, const_cast<VersionSet*>(this),
/*track_found_and_missing_files=*/false, no_error_if_files_missing,
io_tracer_, read_options, /*allow_incomplete_valid_version=*/false,
EpochNumberRequirement::kMightMissing,
/*skip_load_table_files=*/db_options_->open_files_async);
EpochNumberRequirement::kMightMissing);
handler.Iterate(reader, &log_read_status);
s = handler.status();
if (s.ok()) {
@@ -7710,6 +7590,7 @@ InternalIterator* VersionSet::MakeInputIterator(
list[num++] = new LevelIterator(
cfd->table_cache(), read_options, file_options_compactions,
cfd->internal_comparator(), flevel, c->mutable_cf_options(),
/*should_sample=*/false,
/*no per level latency histogram=*/nullptr,
TableReaderCaller::kCompaction, /*skip_filters=*/false,
/*level=*/static_cast<int>(c->level(which)), range_del_agg,
@@ -7975,12 +7856,10 @@ Status VersionSet::VerifyFileMetadata(const ReadOptions& read_options,
InternalStats* internal_stats = cfd->internal_stats();
TableCache::TypedHandle* handle = nullptr;
TableReader* table_reader = nullptr;
FileMetaData meta_copy = meta;
status = table_cache->FindTable(
read_options, file_opts, *icmp, meta_copy, &handle, cf_opts,
&table_reader, /*no_io=*/false, internal_stats->GetFileReadHist(level),
false, level,
/*no_io=*/false, internal_stats->GetFileReadHist(level), false, level,
/*prefetch_index_and_filter_in_cache*/ false, max_sz_for_l0_meta_pin,
meta_copy.temperature);
if (handle) {
-2
View File
@@ -1779,8 +1779,6 @@ class VersionSet {
unsigned max_manifest_space_amp_pct_;
// Saved copy from (Mutable)DBOptions
size_t manifest_preallocation_size_;
// Saved copy from (Mutable)DBOptions
bool verify_manifest_content_on_close_;
// Obsolete files, or during DB shutdown any files not referenced by what's
// left of the in-memory LSM state.
-5
View File
@@ -58,11 +58,6 @@ DEFINE_SYNC_AND_ASYNC(Status, Version::MultiGetFromSST)
if (get_context.sample()) {
sample_file_read_inc(f->file_metadata);
if (get_context.State() == GetContext::kNotFound ||
get_context.State() == GetContext::kMerge ||
get_context.State() == GetContext::kDeleted) {
sample_collapsible_entry_file_read_inc(f->file_metadata);
}
}
batch_size++;
num_index_read += get_context.get_context_stats_.num_index_read;
+12 -276
View File
@@ -1228,12 +1228,11 @@ class VersionSetTestBase {
SetIdentityFile(WriteOptions(), env_, dbname_, Temperature::kUnknown));
VersionEdit new_db;
if (imm_db_options_.write_dbid_to_manifest) {
DBOptions tmp_db_options;
tmp_db_options.env = env_;
std::unique_ptr<DBImpl> impl(new DBImpl(tmp_db_options, dbname_));
std::string db_id;
ASSERT_OK(ReadFileToString(env_, IdentityFileName(dbname_), &db_id));
// Strip trailing newline if present (old GenerateUniqueId format)
if (!db_id.empty() && db_id.back() == '\n') {
db_id.pop_back();
}
ASSERT_OK(impl->GetDbIdentityFromIdentityFile(IOOptions(), &db_id));
new_db.SetDBId(db_id);
}
new_db.SetLogNumber(0);
@@ -2408,267 +2407,6 @@ TEST_F(VersionSetTest, ManifestTruncateAfterClose) {
ReopenDB();
}
TEST_F(VersionSetTest, ManifestContentValidationOnCloseClean) {
// Enable content validation, perform normal operations, close.
// Verify no manifest rotation (file number unchanged).
NewDB();
mutable_db_options_.verify_manifest_content_on_close = true;
mutex_.Lock();
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
mutex_.Unlock();
VersionEdit edit;
ASSERT_OK(LogAndApplyToDefaultCF(edit));
std::string manifest_path_before;
GetManifestPath(&manifest_path_before);
bool content_validation_ran = false;
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->SetCallBack(
"VersionSet::Close:BeforeContentValidation",
[&](void*) { content_validation_ran = true; });
SyncPoint::GetInstance()->EnableProcessing();
CloseDB();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// Verify content validation actually ran
ASSERT_TRUE(content_validation_ran);
// Manifest path should be unchanged (no rotation)
std::string manifest_path_after;
uint64_t manifest_file_number = 0;
ASSERT_OK(GetCurrentManifestPath(dbname_, fs_.get(), /*is_retry=*/false,
&manifest_path_after,
&manifest_file_number));
ASSERT_EQ(manifest_path_before, manifest_path_after);
ReopenDB();
}
TEST_F(VersionSetTest, ManifestContentValidationOnCloseCorruptRecord) {
// Enable content validation, corrupt the manifest after closing the writer,
// verify manifest rotation occurs and DB reopens cleanly.
NewDB();
mutable_db_options_.verify_manifest_content_on_close = true;
mutex_.Lock();
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
mutex_.Unlock();
VersionEdit edit;
ASSERT_OK(LogAndApplyToDefaultCF(edit));
std::string manifest_path_before;
GetManifestPath(&manifest_path_before);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->SetCallBack(
"VersionSet::Close:BeforeContentValidation", [&](void*) {
// Corrupt bytes in the middle of the manifest
std::string manifest_path;
GetManifestPath(&manifest_path);
std::string content;
Status s = ReadFileToString(env_, manifest_path, &content);
EXPECT_OK(s);
if (!s.ok()) {
return;
}
ASSERT_GT(content.size(), 20u);
// Corrupt several bytes in the middle to break CRC
for (size_t i = content.size() / 2; i < content.size() / 2 + 8; i++) {
content[i] ^= 0xFF;
}
s = WriteStringToFile(env_, content, manifest_path);
EXPECT_OK(s);
});
SyncPoint::GetInstance()->EnableProcessing();
CloseDB();
SyncPoint::GetInstance()->DisableProcessing();
// Manifest should have been rotated (new file number)
std::string manifest_path_after;
uint64_t manifest_file_number = 0;
ASSERT_OK(GetCurrentManifestPath(dbname_, fs_.get(), /*is_retry=*/false,
&manifest_path_after,
&manifest_file_number));
ASSERT_NE(manifest_path_before, manifest_path_after);
// DB should reopen cleanly with the fresh manifest
ReopenDB();
}
TEST_F(VersionSetTest, ManifestContentValidationOnCloseDisabled) {
// Default (option disabled), corrupt manifest after writer close,
// verify no rotation occurred — corrupt manifest persists.
NewDB();
// verify_manifest_content_on_close defaults to false
VersionEdit edit;
ASSERT_OK(LogAndApplyToDefaultCF(edit));
std::string manifest_path_before;
GetManifestPath(&manifest_path_before);
bool content_validation_ran = false;
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->SetCallBack(
"VersionSet::Close:BeforeContentValidation",
[&](void*) { content_validation_ran = true; });
SyncPoint::GetInstance()->EnableProcessing();
CloseDB();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_FALSE(content_validation_ran);
// Manifest path should be unchanged (no rotation since validation is off)
std::string manifest_path_after;
uint64_t manifest_file_number = 0;
ASSERT_OK(GetCurrentManifestPath(dbname_, fs_.get(), /*is_retry=*/false,
&manifest_path_after,
&manifest_file_number));
ASSERT_EQ(manifest_path_before, manifest_path_after);
}
TEST_F(VersionSetTest, ManifestContentValidationOnCloseSizeCheckFails) {
// Truncate manifest so size check fails first.
// Verify recovery happens via size-check path. Content validation still
// runs afterward on the freshly rewritten manifest.
NewDB();
mutable_db_options_.verify_manifest_content_on_close = true;
mutex_.Lock();
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
mutex_.Unlock();
VersionEdit edit;
ASSERT_OK(LogAndApplyToDefaultCF(edit));
std::string manifest_path_before;
GetManifestPath(&manifest_path_before);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->SetCallBack(
"VersionSet::Close:AfterClose", [&](void*) {
std::string manifest_path;
GetManifestPath(&manifest_path);
std::unique_ptr<WritableFile> manifest_file;
ASSERT_OK(env_->ReopenWritableFile(manifest_path, &manifest_file,
EnvOptions()));
ASSERT_OK(manifest_file->Truncate(0));
ASSERT_OK(manifest_file->Close());
});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_NO_FATAL_FAILURE(CloseDB());
SyncPoint::GetInstance()->DisableProcessing();
// Size check should have triggered rotation
std::string manifest_path_after;
uint64_t manifest_file_number = 0;
ASSERT_OK(GetCurrentManifestPath(dbname_, fs_.get(), /*is_retry=*/false,
&manifest_path_after,
&manifest_file_number));
ASSERT_NE(manifest_path_before, manifest_path_after);
// DB should reopen cleanly
ReopenDB();
}
TEST_F(VersionSetTest, ManifestContentValidationOnCloseCorruptAfterRewrite) {
// Corrupt the manifest before content validation AND after the rewrite.
// The loop should detect corruption twice: once triggering a rewrite, and
// once reporting that the rewritten manifest is also corrupt.
NewDB();
mutable_db_options_.verify_manifest_content_on_close = true;
mutex_.Lock();
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
mutex_.Unlock();
VersionEdit edit;
ASSERT_OK(LogAndApplyToDefaultCF(edit));
int io_error_count = 0;
class IOErrorCountListener : public EventListener {
public:
int* count;
explicit IOErrorCountListener(int* c) : count(c) {}
void OnIOError(const IOErrorInfo& /*info*/) override { ++(*count); }
};
imm_db_options_.listeners.push_back(
std::make_shared<IOErrorCountListener>(&io_error_count));
auto corrupt_current_manifest = [&]() {
std::string manifest_path;
GetManifestPath(&manifest_path);
std::string content;
ASSERT_OK(ReadFileToString(env_, manifest_path, &content));
ASSERT_GT(content.size(), 20u);
for (size_t i = content.size() / 2; i < content.size() / 2 + 8; i++) {
content[i] ^= 0xFF;
}
ASSERT_OK(WriteStringToFile(env_, content, manifest_path));
};
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// Corrupt before the first content check
SyncPoint::GetInstance()->SetCallBack(
"VersionSet::Close:BeforeContentValidation",
[&](void*) { corrupt_current_manifest(); });
// Corrupt again after the rewrite completes
SyncPoint::GetInstance()->SetCallBack(
"VersionSet::LogAndApply:WriteManifestDone",
[&](void*) { corrupt_current_manifest(); });
SyncPoint::GetInstance()->EnableProcessing();
mutex_.Lock();
Status close_s = versions_->Close(nullptr, &mutex_);
versions_.reset();
mutex_.Unlock();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// Close should report the persistent corruption
ASSERT_TRUE(close_s.IsCorruption()) << close_s.ToString();
// OnIOError should have fired twice (once per corrupt detection)
ASSERT_EQ(io_error_count, 2);
}
TEST_F(VersionSetTest, ManifestContentValidationOnCloseOpenFails) {
// Delete the manifest before content validation so it can't be opened.
// Close() should surface the I/O error to the caller.
NewDB();
mutable_db_options_.verify_manifest_content_on_close = true;
mutex_.Lock();
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
mutex_.Unlock();
VersionEdit edit;
ASSERT_OK(LogAndApplyToDefaultCF(edit));
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->SetCallBack(
"VersionSet::Close:BeforeContentValidation", [&](void*) {
std::string manifest_path;
GetManifestPath(&manifest_path);
ASSERT_OK(env_->DeleteFile(manifest_path));
});
SyncPoint::GetInstance()->EnableProcessing();
mutex_.Lock();
Status close_s = versions_->Close(nullptr, &mutex_);
versions_.reset();
mutex_.Unlock();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_TRUE(close_s.IsIOError()) << close_s.ToString();
}
TEST_F(VersionStorageInfoTest, AddRangeDeletionCompensatedFileSize) {
// Tests that compensated range deletion size is added to compensated file
// size.
@@ -3774,12 +3512,11 @@ class VersionSetTestEmptyDb
if (imm_db_options_.write_dbid_to_manifest) {
ASSERT_OK(SetIdentityFile(WriteOptions(), env_, dbname_,
Temperature::kUnknown));
DBOptions tmp_db_options;
tmp_db_options.env = env_;
std::unique_ptr<DBImpl> impl(new DBImpl(tmp_db_options, dbname_));
std::string db_id;
ASSERT_OK(ReadFileToString(env_, IdentityFileName(dbname_), &db_id));
// Strip trailing newline if present (old GenerateUniqueId format)
if (!db_id.empty() && db_id.back() == '\n') {
db_id.pop_back();
}
ASSERT_OK(impl->GetDbIdentityFromIdentityFile(IOOptions(), &db_id));
new_db.SetDBId(db_id);
}
const std::string manifest_path = DescriptorFileName(dbname_, 1);
@@ -4101,12 +3838,11 @@ class VersionSetTestMissingFiles : public VersionSetTestBase,
log_writer->reset(new log::Writer(std::move(file_writer), 0, false));
VersionEdit new_db;
if (imm_db_options_.write_dbid_to_manifest) {
DBOptions tmp_db_options;
tmp_db_options.env = env_;
std::unique_ptr<DBImpl> impl(new DBImpl(tmp_db_options, dbname_));
std::string db_id;
ASSERT_OK(ReadFileToString(env_, IdentityFileName(dbname_), &db_id));
// Strip trailing newline if present (old GenerateUniqueId format)
if (!db_id.empty() && db_id.back() == '\n') {
db_id.pop_back();
}
ASSERT_OK(impl->GetDbIdentityFromIdentityFile(IOOptions(), &db_id));
new_db.SetDBId(db_id);
}
{
-96
View File
@@ -1,96 +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/version_util.h"
#include <atomic>
#include <functional>
#include <thread>
#include <utility>
#include <vector>
#include "db/internal_stats.h"
#include "db/table_cache.h"
#include "port/port.h"
#include "test_util/sync_point.h"
namespace ROCKSDB_NAMESPACE {
Status LoadTableHandlersHelper(
const std::vector<std::pair<FileMetaData*, int>>& files_meta,
TableCache* table_cache, const FileOptions& file_options,
const InternalKeyComparator& internal_comparator,
InternalStats* internal_stats, int max_threads,
bool prefetch_index_and_filter_in_cache,
const MutableCFOptions& mutable_cf_options,
size_t max_file_size_for_l0_meta_pin, const ReadOptions& read_options,
std::atomic<bool>* stop) {
assert(table_cache != nullptr);
std::atomic<size_t> next_file_meta_idx(0);
std::atomic<bool> has_error(false);
Status ret;
std::function<void()> load_handlers_func([&]() {
while (true) {
size_t file_idx = next_file_meta_idx.fetch_add(1);
if (has_error.load(std::memory_order_relaxed)) {
break;
}
if (file_idx >= files_meta.size()) {
break;
}
if (stop != nullptr && stop->load()) {
break;
}
auto* cache = table_cache->get_cache().get();
if (cache->GetCapacity() < TableCache::kInfiniteCapacity &&
cache->GetUsage() >= cache->GetCapacity()) {
break;
}
auto* file_meta = files_meta[file_idx].first;
int level = files_meta[file_idx].second;
TEST_SYNC_POINT_CALLBACK(
"VersionBuilder::Rep::LoadTableHandlers::BeforeFindTable", file_meta);
TableCache::TypedHandle* handle = nullptr;
TableReader* table_reader = nullptr;
auto status = table_cache->FindTable(
read_options, file_options, internal_comparator, *file_meta, &handle,
mutable_cf_options, &table_reader, false /* no_io */,
internal_stats->GetFileReadHist(level), false /* skip_filters */,
level, prefetch_index_and_filter_in_cache,
max_file_size_for_l0_meta_pin, file_meta->temperature,
true /* pin_table_handle */);
TEST_SYNC_POINT_CALLBACK(
"VersionBuilder::Rep::LoadTableHandlers::AfterFindTable", &status);
if (!status.ok()) {
bool expected = false;
if (has_error.compare_exchange_strong(expected, true)) {
ret = status;
}
}
}
});
std::vector<port::Thread> threads;
for (int i = 1; i < max_threads; i++) {
threads.emplace_back(load_handlers_func);
}
load_handlers_func();
for (auto& t : threads) {
t.join();
}
return ret;
}
} // namespace ROCKSDB_NAMESPACE
-24
View File
@@ -5,34 +5,10 @@
#pragma once
#include <atomic>
#include <utility>
#include <vector>
#include "db/version_set.h"
#include "rocksdb/file_system.h"
#include "rocksdb/options.h"
#include "rocksdb/status.h"
namespace ROCKSDB_NAMESPACE {
class TableCache;
struct FileMetaData;
class InternalKeyComparator;
class InternalStats;
// Load table handlers (i.e., open SST files and populate the table cache) for
// the given set of files. Used during DB open to eagerly warm the table cache.
Status LoadTableHandlersHelper(
const std::vector<std::pair<FileMetaData*, int>>& files_meta,
TableCache* table_cache, const FileOptions& file_options,
const InternalKeyComparator& internal_comparator,
InternalStats* internal_stats, int max_threads,
bool prefetch_index_and_filter_in_cache,
const MutableCFOptions& mutable_cf_options,
size_t max_file_size_for_l0_meta_pin, const ReadOptions& read_options,
std::atomic<bool>* stop = nullptr);
// Instead of opening a `DB` to perform certain manifest updates, this
// uses the underlying `VersionSet` API to read and modify the MANIFEST. This
// allows us to use the user's real options, while not having to worry about
-9
View File
@@ -113,7 +113,6 @@ DECLARE_bool(use_write_buffer_manager);
DECLARE_double(memtable_prefix_bloom_size_ratio);
DECLARE_bool(memtable_whole_key_filtering);
DECLARE_int32(open_files);
DECLARE_bool(open_files_async);
DECLARE_uint64(compressed_secondary_cache_size);
DECLARE_int32(compressed_secondary_cache_numshardbits);
DECLARE_int32(secondary_cache_update_interval);
@@ -180,9 +179,6 @@ DECLARE_bool(use_sqfc_for_range_queries);
DECLARE_int32(index_type);
DECLARE_int32(data_block_index_type);
DECLARE_int32(index_block_search_type);
DECLARE_double(uniform_cv_threshold);
DECLARE_bool(use_trie_index);
DECLARE_bool(test_backward_scan);
DECLARE_string(db);
DECLARE_string(secondaries_base);
DECLARE_bool(test_secondary);
@@ -260,7 +256,6 @@ DECLARE_uint64(ops_per_thread);
DECLARE_uint64(log2_keys_per_lock);
DECLARE_uint64(max_manifest_file_size);
DECLARE_int32(max_manifest_space_amp_pct);
DECLARE_bool(verify_manifest_content_on_close);
DECLARE_bool(in_place_update);
DECLARE_string(memtablerep);
DECLARE_int32(prefix_size);
@@ -274,7 +269,6 @@ DECLARE_bool(avoid_unnecessary_blocking_io);
DECLARE_bool(write_dbid_to_manifest);
DECLARE_bool(write_identity_file);
DECLARE_bool(avoid_flush_during_recovery);
DECLARE_bool(enforce_write_buffer_manager_during_recovery);
DECLARE_uint64(max_write_batch_group_size_bytes);
DECLARE_bool(level_compaction_dynamic_level_bytes);
DECLARE_int32(verify_checksum_one_in);
@@ -288,10 +282,8 @@ DECLARE_bool(verification_only);
DECLARE_string(last_level_temperature);
DECLARE_string(default_write_temperature);
DECLARE_string(default_temperature);
DECLARE_uint32(verify_output_flags);
DECLARE_bool(paranoid_memory_checks);
DECLARE_bool(memtable_veirfy_per_key_checksum_on_seek);
DECLARE_bool(memtable_batch_lookup_optimization);
// Options for transaction dbs.
// Use TransactionDB (a.k.a. Pessimistic Transaction DB)
@@ -449,7 +441,6 @@ DECLARE_uint32(ingest_wbwi_one_in);
DECLARE_bool(universal_reduce_file_locking);
DECLARE_bool(use_multiscan);
DECLARE_bool(multiscan_use_async_io);
DECLARE_uint64(multiscan_max_prefetch_memory_bytes);
// Compaction deletion trigger declarations for stress testing
DECLARE_bool(enable_compaction_on_deletion_trigger);
-39
View File
@@ -203,9 +203,6 @@ DEFINE_int32(open_files, ROCKSDB_NAMESPACE::Options().max_open_files,
"Maximum number of files to keep open at the same time "
"(use default if == 0)");
DEFINE_bool(open_files_async, ROCKSDB_NAMESPACE::Options().open_files_async,
"Options.open_files_async");
DEFINE_uint64(compressed_secondary_cache_size, 0,
"Number of bytes to use as a cache of compressed data."
" 0 means use default settings.");
@@ -622,20 +619,6 @@ DEFINE_int32(index_block_search_type,
"Search algorithm for index blocks (see `enum BlockSearchType` in "
"table.h)");
DEFINE_double(uniform_cv_threshold,
ROCKSDB_NAMESPACE::BlockBasedTableOptions().uniform_cv_threshold,
"CV threshold for marking index blocks as uniform. Set to -1 to "
"disable. (see `uniform_cv_threshold` in table.h)");
DEFINE_bool(use_trie_index, false,
"Use trie-based user defined index (UDI) for SST files. "
"Compatible with all operation types (Put, Delete, Merge, etc.). "
"Backward scan is disabled when this is enabled.");
DEFINE_bool(test_backward_scan, true,
"Test backward iteration (Prev, SeekForPrev) in stress tests. "
"Automatically set to false when use_trie_index is enabled.");
DEFINE_string(db, "", "Use the db with the following name.");
DEFINE_string(secondaries_base, "",
@@ -1020,9 +1003,6 @@ DEFINE_uint64(max_manifest_file_size, 16384,
DEFINE_int32(max_manifest_space_amp_pct, 500,
"Max manifest space amp percentage for auto-tuning");
DEFINE_bool(verify_manifest_content_on_close, false,
"If true, verify MANIFEST content (CRC + decode) on DB close");
DEFINE_bool(in_place_update, false, "On true, does inplace update in memtable");
DEFINE_string(memtablerep, "skip_list", "");
@@ -1080,11 +1060,6 @@ DEFINE_bool(avoid_flush_during_recovery,
ROCKSDB_NAMESPACE::Options().avoid_flush_during_recovery,
"Avoid flush during recovery");
DEFINE_bool(
enforce_write_buffer_manager_during_recovery,
ROCKSDB_NAMESPACE::Options().enforce_write_buffer_manager_during_recovery,
"Enforce write buffer manager memory limit during WAL recovery");
DEFINE_uint64(max_write_batch_group_size_bytes,
ROCKSDB_NAMESPACE::Options().max_write_batch_group_size_bytes,
"Max write batch group size");
@@ -1538,10 +1513,6 @@ DEFINE_uint32(uncache_aggressiveness,
"obsolete. 0 = disabled, 1 = minimum, 100 = moderate, 10000 = "
"normal max");
DEFINE_uint32(verify_output_flags, 0,
"Sets CF option verify_output_flags as a uint32_t bitmask. "
"See VerifyOutputFlags enum for bit definitions.");
DEFINE_bool(paranoid_memory_checks,
ROCKSDB_NAMESPACE::Options().paranoid_memory_checks,
"Sets CF option paranoid_memory_checks.");
@@ -1551,10 +1522,6 @@ DEFINE_bool(
ROCKSDB_NAMESPACE::Options().memtable_veirfy_per_key_checksum_on_seek,
"Sets CF option memtable_veirfy_per_key_checksum_on_seek.");
DEFINE_bool(memtable_batch_lookup_optimization,
ROCKSDB_NAMESPACE::Options().memtable_batch_lookup_optimization,
"Sets CF option memtable_batch_lookup_optimization.");
DEFINE_uint32(commit_bypass_memtable_one_in, 0,
"If greater than zero, transaction option will set "
"commit_bypass_memtable to per every N transactions on average.");
@@ -1609,10 +1576,4 @@ DEFINE_bool(use_multiscan, false,
DEFINE_bool(multiscan_use_async_io, false,
"If set, enable async_io for MultiScan operations.");
DEFINE_uint64(multiscan_max_prefetch_memory_bytes, 0,
"If non-zero, sets the max_prefetch_memory_bytes on the "
"IODispatcher used for MultiScan. This limits the total memory "
"used for prefetching data blocks across all concurrent "
"MultiScan ReadSets.");
#endif // GFLAGS
-9
View File
@@ -226,13 +226,6 @@ class DbStressListener : public EventListener {
RandomSleep();
}
void OnBackgroundJobPressureChanged(
DB* /*db*/, const BackgroundJobPressure& pressure) override {
RandomSleep();
std::lock_guard<std::mutex> lk(bg_pressure_mu_);
last_bg_pressure_ = pressure;
}
void OnFileReadFinish(const FileOperationInfo& info) override {
// Even empty callback is valuable because sometimes some locks are
// released in order to make the callback.
@@ -373,8 +366,6 @@ class DbStressListener : public EventListener {
std::atomic<int> num_pending_file_creations_;
UniqueIdVerifier unique_ids_;
SharedState* shared_;
mutable std::mutex bg_pressure_mu_;
BackgroundJobPressure last_bg_pressure_;
};
} // namespace ROCKSDB_NAMESPACE
#endif // GFLAGS
+17 -86
View File
@@ -29,7 +29,6 @@
#include "options/options_parser.h"
#include "rocksdb/convenience.h"
#include "rocksdb/filter_policy.h"
#include "rocksdb/io_dispatcher.h"
#include "rocksdb/secondary_cache.h"
#include "rocksdb/sst_file_manager.h"
#include "rocksdb/table_properties.h"
@@ -42,7 +41,6 @@
#include "utilities/backup/backup_engine_impl.h"
#include "utilities/fault_injection_fs.h"
#include "utilities/fault_injection_secondary_cache.h"
#include "utilities/trie_index/trie_index_factory.h"
namespace ROCKSDB_NAMESPACE {
@@ -931,22 +929,6 @@ Status StressTest::CommitTxn(Transaction& txn, ThreadState* thread) {
return s;
}
bool StressTest::IsExpectedTxnError(const Status& s) {
if ((s.IsDeadlock() || s.IsTimedOut()) &&
(FLAGS_use_multiget || FLAGS_use_multi_get_entity)) {
return true;
}
// Optimistic transaction may return TryAgain when memtable history is
// insufficient for conflict detection (controlled by
// max_write_buffer_size_to_maintain). ExecuteTransaction retries up to 10
// times, and if all retries fail, it returns TryAgain. This is an expected
// condition and should not crash the stress test.
if (s.IsTryAgain() && FLAGS_use_optimistic_txn) {
return true;
}
return false;
}
Status StressTest::ExecuteTransaction(WriteOptions& write_opts,
ThreadState* thread,
std::function<Status(Transaction&)>&& ops,
@@ -1003,9 +985,6 @@ void StressTest::OperateDb(ThreadState* thread) {
read_opts.allow_unprepared_value = FLAGS_allow_unprepared_value;
read_opts.auto_refresh_iterator_with_snapshot =
FLAGS_auto_refresh_iterator_with_snapshot;
if (FLAGS_use_trie_index && udi_factory_) {
read_opts.table_index_factory = udi_factory_.get();
}
WriteOptions write_opts;
if (FLAGS_rate_limit_auto_wal_flush) {
@@ -1055,9 +1034,6 @@ void StressTest::OperateDb(ThreadState* thread) {
}
// Commenting this out as we don't want to reset stats on each open.
// thread->stats.Start();
if (FLAGS_use_trie_index && udi_factory_) {
read_opts.table_index_factory = udi_factory_.get();
}
}
#ifndef NDEBUG
@@ -1716,14 +1692,6 @@ Status StressTest::TestMultiScan(ThreadState* thread,
FLAGS_multiscan_use_async_io &&
CheckFSFeatureSupport(options_.env->GetFileSystem().get(),
FSSupportedOps::kAsyncIO);
std::shared_ptr<IODispatcher> io_dispatcher;
if (FLAGS_multiscan_max_prefetch_memory_bytes > 0) {
IODispatcherOptions io_opts;
io_opts.max_prefetch_memory_bytes =
FLAGS_multiscan_max_prefetch_memory_bytes;
io_dispatcher.reset(NewIODispatcher(io_opts));
scan_opts.io_dispatcher = io_dispatcher;
}
start_key_strs.reserve(num_scans);
end_key_strs.reserve(num_scans);
@@ -2008,19 +1976,7 @@ Status StressTest::TestIterateImpl(ThreadState* thread,
Slice key(key_str);
// UserDefinedIndexIterator supports Seek(target), Next(), and
// SeekToFirst(). However, SeekToLast, SeekForPrev, and Prev are not
// supported. Check if UDI is being used either via ReadOptions or
// CF-level configuration.
const bool using_udi =
(ro.table_index_factory != nullptr) || (udi_factory_ != nullptr);
// SeekToFirst is supported by UDI, so only total_order is required.
const bool support_seek_to_first =
expect_total_order && (FLAGS_test_backward_scan || using_udi);
// SeekToLast requires backward scan support which UDI does not provide.
const bool support_seek_to_last =
expect_total_order && FLAGS_test_backward_scan && !using_udi;
const bool support_seek_for_prev = FLAGS_test_backward_scan && !using_udi;
const bool support_seek_first_or_last = expect_total_order;
// Write-prepared and Write-unprepared and multi-cf-iterator do not support
// Refresh() yet.
@@ -2035,17 +1991,17 @@ Status StressTest::TestIterateImpl(ThreadState* thread,
}
LastIterateOp last_op;
if (support_seek_to_first && thread->rand.OneIn(100)) {
if (support_seek_first_or_last && thread->rand.OneIn(100)) {
iter->SeekToFirst();
cmp_iter->SeekToFirst();
last_op = kLastOpSeekToFirst;
op_logs += "STF ";
} else if (support_seek_to_last && thread->rand.OneIn(100)) {
} else if (support_seek_first_or_last && thread->rand.OneIn(100)) {
iter->SeekToLast();
cmp_iter->SeekToLast();
last_op = kLastOpSeekToLast;
op_logs += "STL ";
} else if (support_seek_for_prev && thread->rand.OneIn(8)) {
} else if (thread->rand.OneIn(8)) {
iter->SeekForPrev(key);
cmp_iter->SeekForPrev(key);
last_op = kLastOpSeekForPrev;
@@ -2077,8 +2033,7 @@ Status StressTest::TestIterateImpl(ThreadState* thread,
key, op_logs, verify_func, &diverged);
const bool no_reverse =
(FLAGS_memtablerep == "prefix_hash" && !expect_total_order) ||
!FLAGS_test_backward_scan;
(FLAGS_memtablerep == "prefix_hash" && !expect_total_order);
for (uint64_t i = 0; i < FLAGS_num_iterations && iter->Valid(); ++i) {
if (no_reverse || thread->rand.OneIn(2)) {
iter->Next();
@@ -3675,11 +3630,8 @@ void StressTest::Open(SharedState* shared, bool reopen) {
assert(db_ == nullptr);
assert(txn_db_ == nullptr);
assert(optimistic_txn_db_ == nullptr);
if (FLAGS_use_trie_index) {
udi_factory_ = std::make_shared<trie_index::TrieIndexFactory>();
}
if (!InitializeOptionsFromFile(options_)) {
InitializeOptionsFromFlags(cache_, filter_policy_, udi_factory_, options_);
InitializeOptionsFromFlags(cache_, filter_policy_, options_);
}
InitializeOptionsGeneral(cache_, filter_policy_, sqfc_factory_, options_);
DbStressCustomCompressionManager::Register();
@@ -3717,6 +3669,13 @@ void StressTest::Open(SharedState* shared, bool reopen) {
// Remote Compaction
if (FLAGS_remote_compaction_worker_threads > 0) {
// TODO(jaykorean) Remove this after fix - remote worker shouldn't recover
// from WAL
if (!FLAGS_disable_wal) {
fprintf(stderr,
"WAL is not compatible with Remote Compaction in Stress Test\n");
exit(1);
}
if ((options_.enable_blob_files ||
options_.enable_blob_garbage_collection ||
FLAGS_allow_setting_blob_options_dynamically)) {
@@ -4312,7 +4271,6 @@ bool InitializeOptionsFromFile(Options& options) {
void InitializeOptionsFromFlags(
const std::shared_ptr<Cache>& cache,
const std::shared_ptr<const FilterPolicy>& filter_policy,
const std::shared_ptr<UserDefinedIndexFactory>& udi_factory,
Options& options) {
BlockBasedTableOptions block_based_options;
block_based_options.decouple_partitioned_filters =
@@ -4373,7 +4331,6 @@ void InitializeOptionsFromFlags(
block_based_options.index_block_search_type =
static_cast<BlockBasedTableOptions::BlockSearchType>(
FLAGS_index_block_search_type);
block_based_options.uniform_cv_threshold = FLAGS_uniform_cv_threshold;
block_based_options.prepopulate_block_cache =
static_cast<BlockBasedTableOptions::PrepopulateBlockCache>(
FLAGS_prepopulate_block_cache);
@@ -4396,9 +4353,6 @@ void InitializeOptionsFromFlags(
fLU64::FLAGS_super_block_alignment_size;
block_based_options.super_block_alignment_space_overhead_ratio =
fLU64::FLAGS_super_block_alignment_space_overhead_ratio;
if (udi_factory) {
block_based_options.user_defined_index_factory = udi_factory;
}
options.table_factory.reset(NewBlockBasedTableFactory(block_based_options));
options.db_write_buffer_size = FLAGS_db_write_buffer_size;
options.write_buffer_size = FLAGS_write_buffer_size;
@@ -4445,13 +4399,6 @@ void InitializeOptionsFromFlags(
}
}
options.max_open_files = FLAGS_open_files;
options.open_files_async = FLAGS_open_files_async;
if (FLAGS_open_files_async && !FLAGS_skip_stats_update_on_db_open) {
FLAGS_skip_stats_update_on_db_open = true;
fprintf(stderr,
"open_files_async requires skip_stats_update_on_db_open, "
"enabling it automatically\n");
}
if (FLAGS_statistics) {
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
}
@@ -4497,8 +4444,6 @@ void InitializeOptionsFromFlags(
}
options.max_manifest_file_size = FLAGS_max_manifest_file_size;
options.max_manifest_space_amp_pct = FLAGS_max_manifest_space_amp_pct;
options.verify_manifest_content_on_close =
FLAGS_verify_manifest_content_on_close;
options.max_subcompactions = static_cast<uint32_t>(FLAGS_subcompactions);
options.allow_concurrent_memtable_write =
FLAGS_allow_concurrent_memtable_write;
@@ -4527,8 +4472,6 @@ void InitializeOptionsFromFlags(
options.write_dbid_to_manifest = FLAGS_write_dbid_to_manifest;
options.write_identity_file = FLAGS_write_identity_file;
options.avoid_flush_during_recovery = FLAGS_avoid_flush_during_recovery;
options.enforce_write_buffer_manager_during_recovery =
FLAGS_enforce_write_buffer_manager_during_recovery;
options.max_write_batch_group_size_bytes =
FLAGS_max_write_batch_group_size_bytes;
options.level_compaction_dynamic_level_bytes =
@@ -4540,13 +4483,9 @@ void InitializeOptionsFromFlags(
options.memtable_protection_bytes_per_key =
FLAGS_memtable_protection_bytes_per_key;
options.block_protection_bytes_per_key = FLAGS_block_protection_bytes_per_key;
options.verify_output_flags =
static_cast<VerifyOutputFlags>(FLAGS_verify_output_flags);
options.paranoid_memory_checks = FLAGS_paranoid_memory_checks;
options.memtable_veirfy_per_key_checksum_on_seek =
FLAGS_memtable_veirfy_per_key_checksum_on_seek;
options.memtable_batch_lookup_optimization =
FLAGS_memtable_batch_lookup_optimization;
// Integrated BlobDB
options.enable_blob_files = FLAGS_enable_blob_files;
@@ -4775,18 +4714,10 @@ void InitializeOptionsGeneral(
"`preserve_unverified_changes` to keep all files\n");
options.avoid_flush_during_recovery = true;
}
if (options.enforce_write_buffer_manager_during_recovery) {
fprintf(
stderr,
"WARNING: flipping `enforce_write_buffer_manager_during_recovery` "
"to false for `preserve_unverified_changes` to keep all files\n");
options.enforce_write_buffer_manager_during_recovery = false;
}
// Together with `avoid_flush_during_recovery == true` and
// `enforce_write_buffer_manager_during_recovery == false`, this will
// prevent live files from becoming obsolete and deleted between
// `DB::Open()` and `DisableFileDeletions()` due to flush or compaction.
// We do not need to warn the user since we will reenable compaction soon.
// Together with `avoid_flush_during_recovery == true`, this will prevent
// live files from becoming obsolete and deleted between `DB::Open()` and
// `DisableFileDeletions()` due to flush or compaction. We do not need to
// warn the user since we will reenable compaction soon.
options.disable_auto_compactions = true;
}
+1 -11
View File
@@ -14,7 +14,6 @@
#include "db_stress_tool/db_stress_common.h"
#include "db_stress_tool/db_stress_shared_state.h"
#include "rocksdb/experimental.h"
#include "rocksdb/user_defined_index.h"
#include "utilities/fault_injection_fs.h"
namespace ROCKSDB_NAMESPACE {
@@ -34,12 +33,6 @@ class StressTest {
!status_to_io_status(Status(error_s)).GetDataLoss();
}
// Returns true if the status is an expected transactional error, including
// lock conflicts (deadlock or timeout) from MaybeAddKeyToTxnForRYW writing
// to the same key space without the stress-test-level mutex, and TryAgain
// from optimistic transactions when conflict detection retries are exhausted.
static bool IsExpectedTxnError(const Status& s);
StressTest();
virtual ~StressTest() {}
@@ -429,7 +422,6 @@ class StressTest {
std::vector<std::string> options_index_;
std::atomic<bool> db_preload_finished_;
std::shared_ptr<SstQueryFilterConfigsManager::Factory> sqfc_factory_;
std::shared_ptr<UserDefinedIndexFactory> udi_factory_;
std::unique_ptr<DB> secondary_db_;
std::vector<ColumnFamilyHandle*> secondary_cfhs_;
@@ -445,9 +437,7 @@ bool InitializeOptionsFromFile(Options& options);
// input arguments.
void InitializeOptionsFromFlags(
const std::shared_ptr<Cache>& cache,
const std::shared_ptr<const FilterPolicy>& filter_policy,
const std::shared_ptr<UserDefinedIndexFactory>& udi_factory,
Options& options);
const std::shared_ptr<const FilterPolicy>& filter_policy, Options& options);
// Initialize `options` on which `InitializeOptionsFromFile()` and
// `InitializeOptionsFromFlags()` have both been called already.
-45
View File
@@ -24,7 +24,6 @@
#include "db_stress_tool/db_stress_common.h"
#include "db_stress_tool/db_stress_driver.h"
#include "db_stress_tool/db_stress_shared_state.h"
#include "port/stack_trace.h"
#include "rocksdb/convenience.h"
#include "utilities/fault_injection_fs.h"
@@ -93,15 +92,6 @@ int db_stress_tool(int argc, char** argv) {
fault_env_guard =
std::make_shared<CompositeEnvWrapper>(raw_env, fault_fs_guard);
raw_env = fault_env_guard.get();
// Register a crash callback so that recently injected errors are
// printed to stderr when the process crashes (SIGABRT, SIGSEGV, etc.).
// This helps diagnose stress test failures caused by fault injection.
port::RegisterCrashCallback([]() {
if (fault_fs_guard) {
fault_fs_guard->PrintRecentInjectedErrors();
}
});
}
auto db_stress_fs =
@@ -229,22 +219,6 @@ int db_stress_tool(int argc, char** argv) {
FLAGS_atomic_flush = true;
}
// Trie UDI supports Seek, Next, and SeekToFirst, but not SeekToLast,
// SeekForPrev, or Prev. Disable backward scan testing.
if (FLAGS_use_trie_index) {
FLAGS_test_backward_scan = false;
}
// Trie UDI uses zero-copy pointers into block data, which is
// incompatible with mmap_read.
if (FLAGS_use_trie_index && FLAGS_mmap_read) {
fprintf(stderr,
"Error: use_trie_index is incompatible with mmap_read. "
"The trie index uses zero-copy pointers into block data "
"which is unsafe with mmap'd reads.\n");
exit(1);
}
if (FLAGS_read_only) {
if (FLAGS_writepercent != 0 || FLAGS_delpercent != 0 ||
FLAGS_delrangepercent != 0) {
@@ -266,25 +240,6 @@ int db_stress_tool(int argc, char** argv) {
FLAGS_db = default_db_path;
}
// Now that FLAGS_db is resolved, set the fault injection log file path
// so that PrintAll() writes to a file instead of stderr (signal-safe).
// Store the log in TEST_TMPDIR (outside the DB directory) so it survives
// DB reopen (which cleans untracked files) and gets included in the
// sandcastle db.tar.gz artifact for post-failure analysis.
if (fault_fs_guard) {
std::string log_dir;
const char* test_tmpdir = getenv("TEST_TMPDIR");
if (test_tmpdir && test_tmpdir[0] != '\0') {
log_dir = test_tmpdir;
} else {
log_dir = "/tmp";
}
std::string log_path = log_dir + "/fault_injection_" +
std::to_string(getpid()) + "_" +
std::to_string(time(nullptr)) + ".log";
fault_fs_guard->SetInjectedErrorLogPath(log_path);
}
if ((FLAGS_test_secondary || FLAGS_continuous_verification_interval > 0) &&
FLAGS_secondaries_base.empty()) {
std::string default_secondaries_path;
+85 -93
View File
@@ -543,10 +543,7 @@ class NonBatchedOpsStressTest : public StressTest {
}
std::unique_ptr<Iterator> iter(
secondary_db_->NewIterator(read_opts, handle));
// Skip SeekToFirst, SeekToLast, SeekForPrev, and Prev when backward
// scan is disabled.
uint32_t rnd =
(!FLAGS_test_backward_scan) ? 2 : (thread->rand.Next()) % 4;
uint32_t rnd = (thread->rand.Next()) % 4;
if (0 == rnd) {
// SeekToFirst() + Next()*5
read_opts.total_order_seek = true;
@@ -1908,7 +1905,13 @@ class NonBatchedOpsStressTest : public StressTest {
} while (!s.ok() && IsErrorInjectedAndRetryable(s) &&
initial_wal_write_may_succeed);
if (IsExpectedTxnError(s)) {
if ((s.IsDeadlock() || s.IsTimedOut()) &&
(FLAGS_use_multiget || FLAGS_use_multi_get_entity)) {
// Deadlock or timeout is ok, when multi get is tested. Because multi get
// tests execute MaybeAddKeyToTxnForRYW function which writes to the
// same key space but does not acquire stress test level mutex. So it is
// possible RocksDB returns deadlock or timeout. Return OK() for these
// cases
pending_expected_value.Rollback();
return Status::OK();
}
@@ -2013,11 +2016,6 @@ class NonBatchedOpsStressTest : public StressTest {
} while (!s.ok() && IsErrorInjectedAndRetryable(s) &&
initial_wal_write_may_succeed);
if (IsExpectedTxnError(s)) {
pending_expected_value.Rollback();
return Status::OK();
}
if (!s.ok()) {
pending_expected_value.Rollback();
if (IsErrorInjectedAndRetryable(s)) {
@@ -2085,11 +2083,6 @@ class NonBatchedOpsStressTest : public StressTest {
} while (!s.ok() && IsErrorInjectedAndRetryable(s) &&
initial_wal_write_may_succeed);
if (IsExpectedTxnError(s)) {
pending_expected_value.Rollback();
return Status::OK();
}
if (!s.ok()) {
pending_expected_value.Rollback();
if (IsErrorInjectedAndRetryable(s)) {
@@ -2663,78 +2656,76 @@ class NonBatchedOpsStressTest : public StressTest {
op_logs += "N";
}
// backward scan — skip when backward iteration is not supported
if (FLAGS_test_backward_scan) {
key_str = Key(ub - 1);
iter->SeekForPrev(key_str);
// backward scan
key_str = Key(ub - 1);
iter->SeekForPrev(key_str);
op_logs += " SFP " + Slice(key_str).ToString(true) + " ";
op_logs += " SFP " + Slice(key_str).ToString(true) + " ";
last_key = ub;
while (true) {
assert(lb < last_key);
last_key = ub;
while (true) {
assert(lb < last_key);
if (iter->Valid() && ro.allow_unprepared_value) {
op_logs += "*";
if (iter->Valid() && ro.allow_unprepared_value) {
op_logs += "*";
if (!iter->PrepareValue()) {
assert(!iter->Valid());
assert(!iter->status().ok());
}
if (!iter->PrepareValue()) {
assert(!iter->Valid());
assert(!iter->status().ok());
}
if (!iter->Valid()) {
if (!iter->status().ok()) {
if (IsErrorInjectedAndRetryable(iter->status())) {
return iter->status();
} else {
thread->shared->SetVerificationFailure();
fprintf(stderr, "TestIterate against expected state error: %s\n",
iter->status().ToString().c_str());
fprintf(stderr, "Column family: %s, op_logs: %s\n",
cfh->GetName().c_str(), op_logs.c_str());
thread->stats.AddErrors(1);
return iter->status();
}
}
if (!check_no_key_in_range(lb, last_key)) {
return Status::OK();
}
break;
}
if (!check_columns()) {
return Status::OK();
}
// the range (current key, last key) was skipped
GetIntVal(iter->key().ToString(), &curr);
if (last_key <= static_cast<int64_t>(curr)) {
thread->shared->SetVerificationFailure();
fprintf(stderr,
"TestIterateAgainstExpected failed: found unexpectedly large "
"key\n");
fprintf(stderr, "Column family: %s, op_logs: %s\n",
cfh->GetName().c_str(), op_logs.c_str());
fprintf(stderr, "Last op found key: %s, expected at most: %s\n",
Slice(Key(curr)).ToString(true).c_str(),
Slice(Key(last_key - 1)).ToString(true).c_str());
thread->stats.AddErrors(1);
return Status::OK();
}
if (!check_no_key_in_range(static_cast<int64_t>(curr + 1), last_key)) {
return Status::OK();
}
last_key = static_cast<int64_t>(curr);
if (last_key <= lb) {
break;
}
iter->Prev();
op_logs += "P";
}
if (!iter->Valid()) {
if (!iter->status().ok()) {
if (IsErrorInjectedAndRetryable(iter->status())) {
return iter->status();
} else {
thread->shared->SetVerificationFailure();
fprintf(stderr, "TestIterate against expected state error: %s\n",
iter->status().ToString().c_str());
fprintf(stderr, "Column family: %s, op_logs: %s\n",
cfh->GetName().c_str(), op_logs.c_str());
thread->stats.AddErrors(1);
return iter->status();
}
}
if (!check_no_key_in_range(lb, last_key)) {
return Status::OK();
}
break;
}
if (!check_columns()) {
return Status::OK();
}
// the range (current key, last key) was skipped
GetIntVal(iter->key().ToString(), &curr);
if (last_key <= static_cast<int64_t>(curr)) {
thread->shared->SetVerificationFailure();
fprintf(stderr,
"TestIterateAgainstExpected failed: found unexpectedly large "
"key\n");
fprintf(stderr, "Column family: %s, op_logs: %s\n",
cfh->GetName().c_str(), op_logs.c_str());
fprintf(stderr, "Last op found key: %s, expected at most: %s\n",
Slice(Key(curr)).ToString(true).c_str(),
Slice(Key(last_key - 1)).ToString(true).c_str());
thread->stats.AddErrors(1);
return Status::OK();
}
if (!check_no_key_in_range(static_cast<int64_t>(curr + 1), last_key)) {
return Status::OK();
}
last_key = static_cast<int64_t>(curr);
if (last_key <= lb) {
break;
}
iter->Prev();
op_logs += "P";
}
// Write-prepared/write-unprepared transactions and multi-CF iterator do not
@@ -2776,8 +2767,7 @@ class NonBatchedOpsStressTest : public StressTest {
key_str = Key(mid);
const Slice key(key_str);
// Skip SeekForPrev and Prev when backward scan is not supported.
if (!FLAGS_test_backward_scan || thread->rand.OneIn(2)) {
if (thread->rand.OneIn(2)) {
iter->Seek(key);
op_logs += " S " + key.ToString(true) + " ";
if (!iter->Valid() && iter->status().ok()) {
@@ -2846,14 +2836,8 @@ class NonBatchedOpsStressTest : public StressTest {
iter->Next();
op_logs += "N";
} else if (static_cast<int64_t>(curr) >= ub) {
// Use Next when backward scan is not supported.
if (!FLAGS_test_backward_scan) {
iter->Next();
op_logs += "N";
} else {
iter->Prev();
op_logs += "P";
}
iter->Prev();
op_logs += "P";
} else {
const uint32_t value_base_from_db = GetValueBase(iter->value());
std::size_t index = static_cast<std::size_t>(curr - lb);
@@ -2880,7 +2864,7 @@ class NonBatchedOpsStressTest : public StressTest {
break;
}
if (!FLAGS_test_backward_scan || thread->rand.OneIn(2)) {
if (thread->rand.OneIn(2)) {
iter->Next();
op_logs += "N";
if (!iter->Valid()) {
@@ -3225,7 +3209,15 @@ class NonBatchedOpsStressTest : public StressTest {
assert(false);
}
if (IsExpectedTxnError(s)) {
// It is possible that multiple thread concurrently try to write to the
// same key, which could cause lock timeout or deadlock in the
// transactiondb layer, before transaction is rolled back.
// E.g.
// Timestamp 1: Transaction A: lock key M for write
// Timestamp 2: Transaction B: lock key N for write
// Timestamp 3: Transaction B: try to lock key M for write -> wait
// Timestamp 4: Transaction A: try to lock key N for write -> deadlock
if (s.IsTimedOut() || s.IsDeadlock()) {
return;
}
+9
View File
@@ -1,6 +1,15 @@
<!DOCTYPE html>
<html>
{% include head.html %}
<div class="socialBanner">
<div>
Support Ukraine 🇺🇦
<a href="https://opensource.facebook.com/support-ukraine">
Help Provide Humanitarian Aid to Ukraine
</a>
.
</div>
</div>
<body>
{% include nav.html alwayson=true %}
<div class="navPusher">
+2 -5
View File
@@ -1940,10 +1940,7 @@ IOStatus PosixDirectory::FsyncWithDirOptions(
assert(fd_ >= 0); // Check use after close
IOStatus s = IOStatus::OK();
#ifndef OS_AIX
bool test_is_btrfs = is_btrfs_;
TEST_SYNC_POINT_CALLBACK("PosixDirectory::FsyncWithDirOptions:ForceBtrfs",
&test_is_btrfs);
if (test_is_btrfs) {
if (is_btrfs_) {
// skip dir fsync for new file creation, which is not needed for btrfs
if (dir_fsync_options.reason == DirFsyncOptions::kNewFileSynced) {
return s;
@@ -1962,7 +1959,7 @@ IOStatus PosixDirectory::FsyncWithDirOptions(
} else if (fsync(fd) < 0) {
s = IOError("While fsync renaming file", new_name, errno);
}
if (fd >= 0 && close(fd) < 0) {
if (close(fd) < 0) {
s = IOError("While closing file after fsync", new_name, errno);
}
return s;
-43
View File
@@ -3,13 +3,11 @@
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "test_util/sync_point.h"
#include "test_util/testharness.h"
#include "util/random.h"
#ifdef ROCKSDB_LIB_IO_POSIX
#include "env/io_posix.h"
#include "rocksdb/file_system.h"
namespace ROCKSDB_NAMESPACE {
@@ -176,47 +174,6 @@ TEST_F(PosixWritableFileTest, SeekAfterExtend) {
ASSERT_OK(fs->DeleteFile(path, IOOptions(), nullptr));
}
#ifdef OS_LINUX
class PosixDirectoryTest : public testing::Test {};
TEST_F(PosixDirectoryTest, BtrfsFsyncFailedOpenDoesNotCloseInvalidFd) {
// When FsyncWithDirOptions is called with kFileRenamed on a btrfs filesystem
// and open() fails, close(-1) should not be called. Without the fix,
// close(-1) is called which is POSIX undefined behavior and overwrites the
// meaningful open error with a misleading "While closing file after fsync".
std::shared_ptr<FileSystem> fs = FileSystem::Default();
std::string dir_path =
test::PerThreadDBPath("PosixDirectoryTest_BtrfsFsyncFailedOpen");
ASSERT_OK(fs->CreateDirIfMissing(dir_path, IOOptions(), nullptr));
std::unique_ptr<FSDirectory> dir;
ASSERT_OK(fs->NewDirectory(dir_path, IOOptions(), &dir, nullptr));
// Force the btrfs code path via sync point
SyncPoint::GetInstance()->SetCallBack(
"PosixDirectory::FsyncWithDirOptions:ForceBtrfs",
[](void* arg) { *static_cast<bool*>(arg) = true; });
SyncPoint::GetInstance()->EnableProcessing();
// Call FsyncWithDirOptions with a non-existent file for rename sync.
// open() will fail since the file doesn't exist.
DirFsyncOptions opts(std::string(dir_path + "/nonexistent_file"));
IOStatus s = dir->FsyncWithDirOptions(IOOptions(), nullptr, opts);
// Should get an error about open failing, NOT about closing
ASSERT_TRUE(s.IsIOError());
// The error message should mention "open", not "closing"
ASSERT_TRUE(s.ToString().find("open") != std::string::npos);
ASSERT_TRUE(s.ToString().find("closing") == std::string::npos);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_OK(dir->Close(IOOptions(), nullptr));
ASSERT_OK(fs->DeleteDir(dir_path, IOOptions(), nullptr));
}
#endif
} // namespace ROCKSDB_NAMESPACE
#endif
+1 -1
View File
@@ -351,7 +351,7 @@ void DeleteScheduler::BackgroundEmptyTrash() {
auto iter = pending_files_in_buckets_.find(bucket.value());
assert(iter != pending_files_in_buckets_.end());
if (iter != pending_files_in_buckets_.end()) {
pending_files_in_bucket = --iter->second;
pending_files_in_bucket = iter->second--;
}
}
}
-108
View File
@@ -7,7 +7,6 @@
#include <atomic>
#include <cinttypes>
#include <future>
#include <thread>
#include <vector>
@@ -857,113 +856,6 @@ TEST_F(DeleteSchedulerTest,
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
}
TEST_F(DeleteSchedulerTest, BucketSignalOnSingleFileCompletion) {
// Test that WaitForEmptyTrashBucket wakes up correctly when a single file
// in a bucket is deleted. This requires the pre-decrement fix: with
// post-decrement, the pending count is checked before it reaches zero,
// so the signal is never fired for the bucket, causing a hang.
//
// Strategy:
// 1. Block the background thread inside DeleteTrashFile for file 0 (mu_ is
// NOT held at this point, since it's unlocked before calling
// DeleteTrashFile).
// 2. Start WaitForEmptyTrashBucket(bucket0) on another thread. Since mu_ is
// free, it acquires mu_, sees bucket0's pending count == 1, enters
// cv_.Wait() (which releases mu_).
// 3. Unblock file 0's deletion. The background thread completes
// DeleteTrashFile, re-acquires mu_, decrements the bucket counter. With
// the post-decrement bug, pending_files_in_bucket gets 1 (old value),
// so cv_.SignalAll() is NOT called. The wait thread stays stuck.
// 4. Also block file 1 inside DeleteTrashFile to keep pending_files_ > 0,
// preventing the global pending_files_ == 0 check from triggering a
// signal.
// 5. Assert the wait thread times out (bug) or completes (fix).
rate_bytes_per_sec_ = 1024 * 1024; // 1 MB / s
NewDeleteScheduler();
std::atomic<int> delete_calls{0};
std::atomic<bool> unblock_file0{false};
std::atomic<bool> unblock_file1{false};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DeleteScheduler::DeleteTrashFile:DeleteFile", [&](void* /*arg*/) {
int n = delete_calls.fetch_add(1) + 1;
if (n == 1) {
// Block file 0's deletion until we've set up the wait thread
while (!unblock_file0.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
} else if (n == 2) {
// Block file 1's deletion to keep pending_files_ > 0
while (!unblock_file1.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Bucket 0: single file
std::optional<int32_t> bucket0 = delete_scheduler_->NewTrashBucket();
ASSERT_TRUE(bucket0.has_value());
std::string file0 =
NewDummyFile("bucket_signal_file0.data", 1024, 0, /*track=*/false);
ASSERT_OK(delete_scheduler_->DeleteUnaccountedFile(file0, "",
/*force_bg=*/false,
bucket0));
// Bucket 1: single file (keeps pending_files_ > 0 when bucket 0 empties)
std::optional<int32_t> bucket1 = delete_scheduler_->NewTrashBucket();
ASSERT_TRUE(bucket1.has_value());
std::string file1 =
NewDummyFile("bucket_signal_file1.data", 1024, 0, /*track=*/false);
ASSERT_OK(delete_scheduler_->DeleteUnaccountedFile(file1, "",
/*force_bg=*/false,
bucket1));
// Wait for the background thread to reach the DeleteFile syncpoint for
// file 0. At this point mu_ is NOT held by the background thread.
while (delete_calls.load() < 1) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
// Start waiting for bucket 0 on another thread. Since the background thread
// doesn't hold mu_, this thread acquires mu_, sees iter->second == 1,
// and enters cv_.Wait() (releasing mu_).
std::atomic<bool> wait_done{false};
auto wait_future = std::async(std::launch::async, [&]() {
delete_scheduler_->WaitForEmptyTrashBucket(bucket0.value());
wait_done.store(true);
});
// Give the wait thread time to enter cv_.Wait(). We need it to be blocked
// in cv_.Wait() before unblocking the background thread.
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// Unblock file 0's deletion. The background thread will:
// - Complete DeleteTrashFile, re-acquire mu_
// - Decrement pending_files_ (2 -> 1) and bucket0 counter (1 -> 0)
// - With bug (post-decrement): pending_files_in_bucket = 1, no signal fired
// - With fix (pre-decrement): pending_files_in_bucket = 0, signal fired
// Then it moves to file 1 and blocks at the second DeleteFile syncpoint.
unblock_file0.store(true);
// Check if WaitForEmptyTrashBucket returns within 5 seconds.
// With the bug: no signal fired, wait thread stays stuck -> timeout
// With the fix: signal fired, wait thread wakes up -> completes quickly
auto status = wait_future.wait_for(std::chrono::seconds(5));
ASSERT_EQ(status, std::future_status::ready)
<< "WaitForEmptyTrashBucket(bucket0) timed out - bucket signal not fired";
// Unblock file 1 and wait for bucket 1
unblock_file1.store(true);
delete_scheduler_->WaitForEmptyTrashBucket(bucket1.value());
ASSERT_EQ(0, delete_scheduler_->GetTotalTrashSize());
ASSERT_GE(delete_calls.load(), 2);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+2 -10
View File
@@ -3221,11 +3221,7 @@ class FilePrefetchBufferTest : public testing::Test {
stats_ = CreateDBStatistics();
}
void TearDown() override {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
EXPECT_OK(DestroyDir(env_, test_dir_));
}
void TearDown() override { EXPECT_OK(DestroyDir(env_, test_dir_)); }
void Write(const std::string& fname, const std::string& content) {
std::unique_ptr<FSWritableFile> f;
@@ -3751,11 +3747,7 @@ class FSBufferPrefetchTest
stats_ = CreateDBStatistics();
}
void TearDown() override {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
EXPECT_OK(DestroyDir(env_, test_dir_));
}
void TearDown() override { EXPECT_OK(DestroyDir(env_, test_dir_)); }
void Write(const std::string& fname, const std::string& content) {
std::unique_ptr<FSWritableFile> f;
+1 -1
View File
@@ -98,7 +98,7 @@ endif # FMT_SOURCE_PATH
PLATFORM_LDFLAGS += -lglog
endif
FOLLY_COMMIT_HASH = 3b0eed0a128c484a2b077546c3e08c824a311357
FOLLY_COMMIT_HASH = 1e8ce1e5d35acff7b78fedbca3e7311b39f43529
# For public CI runs, checkout folly in a way that can build with RocksDB.
# This is mostly intended as a test-only simulation of Meta-internal folly
+2 -13
View File
@@ -213,7 +213,8 @@ enum class VerifyOutputFlags : uint32_t {
// by comparing the one inserted into a
// file, and what is read back.
kVerifyFileChecksum = 1 << 2, // Verify file-level checksum
// TODO - Implement
// kVerifyFileChecksum = 1 << 2, // Verify file-level checksum
// Second set of bits: when to enable verification
kEnableForLocalCompaction = 1 << 10, // Enable for local compaction
@@ -1305,18 +1306,6 @@ struct AdvancedColumnFamilyOptions {
// Immutable.
bool cf_allow_ingest_behind = false;
// If true, use batch lookup optimization for memtable MultiGet. For skip
// list memtables, after looking up each key, 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.
//
// This optimization exploits the fact that MultiGet keys are sorted.
// Non-skip-list memtable implementations fall back to per-key lookups.
//
// Default: false
// Immutable.
bool memtable_batch_lookup_optimization = false;
// Create ColumnFamilyOptions with default values for all fields
AdvancedColumnFamilyOptions();
// Create ColumnFamilyOptions from Options
+1 -10
View File
@@ -1479,10 +1479,6 @@ extern ROCKSDB_LIBRARY_API void rocksdb_options_set_paranoid_checks(
rocksdb_options_t*, unsigned char);
extern ROCKSDB_LIBRARY_API unsigned char rocksdb_options_get_paranoid_checks(
rocksdb_options_t*);
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_open_files_async(
rocksdb_options_t*, unsigned char);
extern ROCKSDB_LIBRARY_API unsigned char rocksdb_options_get_open_files_async(
rocksdb_options_t*);
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_db_paths(
rocksdb_options_t*, const rocksdb_dbpath_t** path_values, size_t num_paths);
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_cf_paths(
@@ -2181,12 +2177,7 @@ enum {
rocksdb_internal_range_del_reseek_count,
rocksdb_block_read_cpu_time,
rocksdb_internal_merge_point_lookup_count,
rocksdb_data_block_read_byte,
rocksdb_index_block_read_byte,
rocksdb_filter_block_read_byte,
rocksdb_compression_dict_block_read_byte,
rocksdb_metadata_block_read_byte,
rocksdb_total_metric_count = 85
rocksdb_total_metric_count = 80
};
extern ROCKSDB_LIBRARY_API void rocksdb_set_perf_level(int);
+2 -2
View File
@@ -115,14 +115,14 @@ class ExternalTableReader {
// Point lookup the given key and return its value
virtual Status Get(const ReadOptions& read_options, const Slice& key,
const SliceTransform* prefix_extractor,
PinnableSlice* value) = 0;
std::string* value) = 0;
// Point lookup the given vector of keys and return the values, as well
// as status of each individual lookup in statuses.
virtual void MultiGet(const ReadOptions& read_options,
const std::vector<Slice>& keys,
const SliceTransform* prefix_extractor,
std::vector<PinnableSlice>* values,
std::vector<std::string>* values,
std::vector<Status>* statuses) = 0;
// Allocate and return the contents of the properties block. If the builder
-40
View File
@@ -201,7 +201,6 @@ enum class BackgroundErrorReason {
kManifestWrite,
kFlushNoWAL,
kManifestWriteNoWAL,
kAsyncFileOpen,
};
struct WriteStallInfo {
@@ -554,34 +553,6 @@ struct IOErrorInfo {
uint64_t offset;
};
// EXPERIMENTAL — under active development, fields may change.
// Point-in-time snapshot of background job pressure for one DB: how busy
// compaction and flush are, and how close the DB is to write-stalling.
struct BackgroundJobPressure {
// Compaction scheduling (LOW + BOTTOM priority combined)
int compaction_scheduled = 0;
int compaction_running = 0;
// Per-priority compaction breakdown
int compaction_low_scheduled = 0;
int compaction_low_running = 0;
int compaction_bottom_scheduled = 0;
int compaction_bottom_running = 0;
// Flush scheduling
int flush_scheduled = 0;
int flush_running = 0;
// How close the DB is to a write stall, as a percentage (0 = healthy,
// 100 = at stall threshold). Can exceed 100 when already stalling.
// Max across all column families based on write-stall triggers.
int write_stall_proximity_pct = 0;
// Whether RocksDB has activated compaction speedup due to write pressure
bool compaction_speedup_active = false;
bool operator==(const BackgroundJobPressure&) const = default;
};
// EventListener class contains a set of callback functions that will
// be called when specific RocksDB event happens such as flush. It can
// be used as a building block for developing custom features such as
@@ -898,17 +869,6 @@ class EventListener : public Customizable {
// happens. ShouldBeNotifiedOnFileIO should be set to true to get a callback.
virtual void OnIOError(const IOErrorInfo& /*info*/) {}
// EXPERIMENTAL
// Called after a flush or compaction background job completes, providing a
// snapshot of current background job scheduling pressure and write-stall
// proximity. Fires on the background thread that completed the job, without
// holding db_mutex_. This callback fires on every completion, even if
// pressure values have not changed from the previous call.
// Implementations should not run for an extended period of time before
// returning, as this blocks RocksDB background work.
virtual void OnBackgroundJobPressureChanged(
DB* /*db*/, const BackgroundJobPressure& /*pressure*/) {}
~EventListener() override {}
};
-20
View File
@@ -211,26 +211,6 @@ class MemTableRep {
return Status::NotSupported("GetAndValidate() not implemented.");
}
// Batch lookup of multiple sorted keys. For each key, finds the first
// matching entry and calls callback_func with callback_args[i] and the
// entry. Continues calling for subsequent entries until callback_func
// returns false.
//
// Keys must be memtable-encoded and in non-decreasing order. Implementations
// may exploit the sorted key order for more efficient lookups.
//
// When detect_key_out_of_order is true, validates key ordering during
// traversal and returns Corruption if out-of-order keys are found.
// When key_validation_callback is non-null, calls it on each visited node.
//
// Default implementation calls Get() per key via an iterator.
virtual Status MultiGet(
size_t num_keys, const char* const* keys, void** callback_args,
bool (*callback_func)(void* arg, const char* entry),
bool allow_data_in_errors = false, bool detect_key_out_of_order = false,
const std::function<Status(const char*, bool)>& key_validation_callback =
nullptr);
virtual uint64_t ApproximateNumEntries(const Slice& /*start_ikey*/,
const Slice& /*end_key*/) {
return 0;
+3 -51
View File
@@ -783,25 +783,6 @@ struct DBOptions {
// Default: 16
int max_file_opening_threads = 16;
// If true, SST files are opened and validated asynchronously in the
// background after DB::Open returns. This reduces DB open time for
// databases with many SST files and high latency file systems. Mostly useful
// when max_open_files = -1, as max_open_files != -1 usually has fast open
// times. See also `max_file_opening_threads` and
// `skip_stats_update_on_db_open` to improve file open latency.
//
// Note: This option is currently not compatible with FIFO compaction and
// requires skip_stats_update_on_db_open=true.
//
// Errors will no longer show up in DB::Open, but instead can show up as
// either background errors and/or operations that access the file (e.g.
// reads, compactions).
//
// When false (default), SST files are opened and validated during DB::Open.
//
// Default: false
bool open_files_async = false;
// Once write-ahead logs exceed this size, we will start forcing the flush of
// column families whose memtables are backed by the oldest live WAL file
// (i.e. the ones that are causing all the space amplification). If set to 0
@@ -994,13 +975,6 @@ struct DBOptions {
// manifest write (e.g. completed DB compaction or flush).
uint64_t max_manifest_file_size = 1024 * 1024 * 1024;
// If true, on DB close, read back the entire MANIFEST file and validate
// CRC checksums and logical record content. If corruption is detected,
// a fresh MANIFEST is written from in-memory state before closing.
//
// This option is mutable with SetDBOptions().
bool verify_manifest_content_on_close = false;
// This option mostly replaces max_manifest_file_size to control an auto-tuned
// balance of manifest write amplification and space amplification. A new
// manifest file is created with the "compacted" contents of the old one when
@@ -1418,30 +1392,9 @@ struct DBOptions {
// WAL logs will be kept, so that if crash happened before flush, we still
// have logs to recover from.
//
// Note: when `enforce_write_buffer_manager_during_recovery` is also enabled,
// flushes may still occur during recovery to respect the
// WriteBufferManager's global memory limit, even if this option is true.
// Once any such WBM-triggered flush happens, all remaining memtables will
// also be flushed at the end of recovery (similar to the behavior when this
// option is false).
//
// DEFAULT: false
bool avoid_flush_during_recovery = false;
// If true and a WriteBufferManager is configured, RocksDB will check
// WriteBufferManager::ShouldFlush() during WAL recovery and schedule
// flushes when needed. This prevents OOM when multiple RocksDB instances
// share a WriteBufferManager and one instance is recovering from WAL.
//
// When triggered, all column families with non-empty memtables are scheduled
// for flush, which may produce smaller L0 files in some column families.
// This also overrides `avoid_flush_during_recovery`: once a WBM-triggered
// flush occurs mid-recovery, all remaining non-empty memtables will be
// flushed at the end of recovery as well.
//
// DEFAULT: true
bool enforce_write_buffer_manager_during_recovery = true;
// By default RocksDB will flush all memtables on DB close if there are
// unpersisted data (i.e. with WAL disabled) The flush can be skip to speedup
// DB close. Unpersisted data WILL BE LOST.
@@ -2282,10 +2235,9 @@ struct ReadOptions {
// block based table index. The table_factory used for the column family
// must support building/reading this index.
//
// Forward scans (SeekToFirst, Seek, Next) and point lookups (Get) are
// supported. Reverse operations (SeekToLast, SeekForPrev, Prev) are not
// yet supported and will return NotSupported when this is set. Leave this
// null to use the native index for reverse operations.
// Currently, only forward scans are supported. For forward scans, only Seek()
// is supported. SeekToFirst() is not supported. If the caller wishes to scan
// from start to end, the native index must be used.
const UserDefinedIndexFactory* table_index_factory = nullptr;
// *** END options only relevant to iterators or scans ***
+1 -10
View File
@@ -106,8 +106,7 @@ struct PerfContextBase {
uint64_t compressed_sec_cache_compressed_bytes;
uint64_t block_checksum_time; // total nanos spent on block checksum
uint64_t block_decompress_time; // total nanos spent on block decompression
uint64_t block_decompress_count; // total number of block decompressions
uint64_t block_decompress_time; // total nanos spent on block decompression
uint64_t get_read_bytes; // bytes for vals returned by Get
uint64_t multiget_read_bytes; // bytes for vals returned by MultiGet
@@ -291,14 +290,6 @@ struct PerfContextBase {
uint64_t file_ingestion_nanos;
// Time IngestExternalFile blocked live writes.
uint64_t file_ingestion_blocking_live_writes_nanos;
// Bytes read from storage for each block category. These add up to
// block_read_byte.
uint64_t data_block_read_byte;
uint64_t index_block_read_byte;
uint64_t filter_block_read_byte;
uint64_t compression_dict_block_read_byte;
uint64_t metadata_block_read_byte;
};
struct PerfContext : public PerfContextBase {
-13
View File
@@ -35,19 +35,6 @@ class SstFileReader {
const std::vector<Slice>& keys,
std::vector<std::string>* values);
// MultiGet variant that returns PinnableSlice values, enabling zero-copy
// when the underlying TableReader supports pinning.
std::vector<Status> MultiGet(const ReadOptions& options,
const std::vector<Slice>& keys,
std::vector<PinnableSlice>* values);
// Point lookup a single key from the SST.
Status Get(const ReadOptions& options, const Slice& key, std::string* value);
// Point lookup variant that returns a PinnableSlice.
Status Get(const ReadOptions& options, const Slice& key,
PinnableSlice* value);
// Returns a new iterator over the table contents as a raw table iterator,
// a.k.a a `TableIterator`that iterates all point data entries in the table
// including logically invisible entries like delete entries.
-5
View File
@@ -730,11 +730,6 @@ enum Histograms : uint32_t {
// Distribution of blocks prefetched per MultiScan Prepare()
MULTISCAN_BLOCKS_PER_PREPARE,
// Coefficient of variation of key gaps in blocks, scaled by 10000
// (e.g., CV of 0.4532 is recorded as 4532). Currently only used by index
// blocks for uniform key distribution tracking.
BLOCK_KEY_DISTRIBUTION_CV,
HISTOGRAM_ENUM_MAX
};
+3 -49
View File
@@ -264,9 +264,6 @@ struct BlockBasedTableOptions {
IndexType index_type = kBinarySearch;
// The search algorithm used when seeking to entries in the index block.
//
// Note: This option is only used at read time and is compatible with any type
// of block.
enum BlockSearchType : char {
// Standard binary search
kBinary = 0x00,
@@ -277,16 +274,6 @@ struct BlockBasedTableOptions {
// succesor can skew the end key and make interpolation search significantly
// less performant.
kInterpolation = 0x01,
// See `uniform_cv_threshold`. On the write path if `uniform_cv_threshold`
// >= 0, then it is possible for a block to be marked as "is_uniform=true"
// in the block footer via bit flag. On files from older versions or
// produced via `uniform_cv_threshold` < 0, blocks are always marked as
// "is_uniform=false".
//
// When kAuto is used, the search algorithm will use interpolation search if
// "is_uniform" flag is set in the block footer, otherwise it will use
// binary search.
kAuto = 0x02,
};
BlockSearchType index_block_search_type = kBinary;
@@ -633,22 +620,6 @@ struct BlockBasedTableOptions {
// Default: false
bool separate_key_value_in_data_block = false;
// Coefficient of variation (CV) threshold used to determine if keys in an
// index block are uniformly distributed. Lower CV means more "uniform", and
// the more likely interpolation search will outperform binary search.
//
// On the write path, if the CV of key gaps in an index
// block is less than this threshold, the "is_uniform" hint is set in that
// block's footer. To disable (i.e. always have "is_uniform=false"), set value
// to -1.
//
// On the read path, if `BlockSearchType::kAuto` is set, then it will use the
// is_uniform hint to select an appropriate search algorithm for the block.
//
// NOTE: Currently only supports index blocks. May update to include data
// blocks in the future.
double uniform_cv_threshold = -1;
// Store index blocks on disk in compressed format. Changing this option to
// false will avoid the overhead of decompression if index blocks are evicted
// and read back
@@ -739,34 +710,17 @@ struct BlockBasedTableOptions {
// If enabled, prepopulate warm/hot blocks (data, uncompressed dict, index and
// filter blocks) which are already in memory into block cache at the time of
// flush or compaction.
//
// On a flush, the data block that is in memory (in memtables) gets flushed to
// the device. If using Direct IO, additional IO is incurred to read this data
// back into memory again, which is avoided by enabling this option. This
// flush. On a flush, the block that is in memory (in memtables) get flushed
// to the device. If using Direct IO, additional IO is incurred to read this
// data back into memory again, which is avoided by enabling this option. This
// further helps if the workload exhibits high temporal locality, where most
// of the reads go to recently written data. This also helps in case of
// Distributed FileSystem.
//
// On a compaction, output SST files are written to disk but not placed in the
// block cache by default. With tiered or remote storage (e.g., HDFS, S3),
// reading recently compacted data back incurs high latency.
// Enabling compaction warming avoids these cold reads. However, unlike flush
// output, it is hard to distinguish hot from cold blocks in compaction
// output, so warming all of it risks polluting the cache. To mitigate this,
// compaction-warmed blocks are inserted at BOTTOM priority (vs LOW for flush)
// so they are evicted first under cache pressure. Even so,
// kFlushAndCompaction is recommended only when most or all of the database is
// expected to reside in cache. For workloads where only a fraction of the
// data is hot, kFlushOnly is the safer choice.
enum class PrepopulateBlockCache : char {
// Disable prepopulate block cache.
kDisable,
// Prepopulate blocks during flush only.
kFlushOnly,
// Prepopulate blocks during flush and compaction. Flush-warmed blocks are
// inserted at LOW priority, compaction-warmed blocks at BOTTOM priority.
kFlushAndCompaction,
};
PrepopulateBlockCache prepopulate_block_cache =
+46 -106
View File
@@ -16,34 +16,30 @@
#include "rocksdb/options.h"
#include "rocksdb/slice.h"
#include "rocksdb/status.h"
#include "rocksdb/types.h"
namespace ROCKSDB_NAMESPACE {
// Prefix for user-defined index block names
inline constexpr const char* kUserDefinedIndexPrefix =
inline const std::string kUserDefinedIndexPrefix =
"rocksdb.user_defined_index.";
// This is a public API for user-defined index builders.
// It allows users to define their own index format and build custom
// indexes during table building. Currently, only a monolithic index
// block is supported (no partitioned index).
//
// This is currently supported only for a restricted set of use cases. The
// CF must be ingest only, and only files containing Puts generated by
// SstFileWriter are supported.
// The interface for building user-defined index.
class UserDefinedIndexBuilder {
public:
// Indicates the type of key-value entry being added via OnKeyAdded().
// UDI builders that only use AddIndexEntry() (e.g., trie-based indexes)
// can safely ignore this.
enum ValueType : uint8_t {
kValue = 0, // Put: the value is the full user value.
kDelete = 1, // Deletion (Delete, SingleDelete, or DeleteWithTimestamp):
// the value is typically empty.
kMerge = 2, // Merge operand: the value is a partial update.
kOther = 3, // Other types (e.g., blob reference, wide-column entity).
// The value format is type-specific and may not be the
// actual user data.
kTypeMax, // Sentinel — must be last. Value may change across releases.
// Right now, we only support Puts. In the future, we may support merges,
// deletions etc.
enum ValueType {
kValue,
kTypeMax,
};
// File offset and size of the data block
@@ -52,68 +48,41 @@ class UserDefinedIndexBuilder {
uint64_t size;
};
// Optional context for AddIndexEntry providing sequence numbers at block
// boundaries. Passed as a struct for forward-compatible extensibility
// (new fields can be added without breaking existing implementations).
struct IndexEntryContext {
// Sequence number of last_key_in_current_block.
SequenceNumber last_key_seq = 0;
// Sequence number of first_key_in_next_block (valid only when
// first_key_in_next_block != nullptr).
SequenceNumber first_key_seq = 0;
};
virtual ~UserDefinedIndexBuilder() = default;
// Add a new index entry for a data block boundary.
// Add a new index entry to index block. The key for the new index entry
// should be >= last_key_in_current_block and < first_key_in_next_block.
// The previous index entry key and the new index entry key cover
// all the keys in the data block associated with the new index entry.
//
// The keys are user keys (without the 8-byte internal key trailer).
//
// The UDI is free to compute a separator between the two user keys and
// store it along with the block handle. The separator must satisfy:
// last_key_in_current_block <= separator < first_key_in_next_block
// in user-key order (ignoring sequence numbers).
// The last_key_in_current_block and first_key_in_next_block will be user
// keys, i.e the user key string, and optionally the user timestamp if one
// is configured, without a sequence number suffix.
//
// Called before the OnKeyAdded() call for first_key_in_next_block.
// @last_key_in_current_block: The last user key in the current data block
// @first_key_in_next_block: First user key in the next data block, or
// nullptr if this is the last block
// @block_handle: offset/size of the data block
// @separator_scratch: scratch buffer for a computed separator
// @context: sequence number context for block boundaries. The sequence
// numbers are needed when the same user key spans a data block boundary
// (e.g., when snapshots keep multiple versions of a key). Without
// sequence numbers, the UDI cannot produce a separator that distinguishes
// the two blocks. This mirrors the internal index's behavior of switching
// to full internal-key separators (see
// ShortenedIndexBuilder::must_use_separator_with_seq_).
// Implementations that don't need sequence numbers can ignore the context.
// @return: the separator stored in the index
// @last_key_in_current_block: The last key in the current data block
// @first_key_in_next_block: it will be nullptr if the entry being added is
// the last one in the table
// @block_handle: offset/size of the data block referenced by this index
// entry. This should be stored along with the index entry
// key
// @separator_scratch: a scratch buffer to back a computed separator between
// those, as needed. May be modified on each call.
// @return: the key or separator stored in the index, which could be
// last_key_in_current_block or a computed separator backed by
// separator_scratch.
virtual Slice AddIndexEntry(const Slice& last_key_in_current_block,
const Slice* first_key_in_next_block,
const BlockHandle& block_handle,
std::string* separator_scratch,
const IndexEntryContext& context) = 0;
std::string* separator_scratch) = 0;
// Called for every key-value pair added to the SST file. UDI builders may
// override this to collect per-key information (e.g., for secondary
// indexes). Builders that only use separator keys from AddIndexEntry()
// (e.g., trie-based indexes) can leave this as a no-op.
// This method will be called whenever a key is added. The subclasses may
// override OnKeyAdded() if they need to collect additional information.
// The type argument indicates whether the value is a full value or partial.
// At the moment, only full values are supported.
//
// @key: The user key (without sequence number or type suffix).
// @type: The entry type — kValue (Put), kDelete, kMerge, or kOther.
// For kDelete entries, the value may be empty. For kOther, the
// value format is type-specific and may not be actual user data.
// @value: The associated value (may be empty for deletions).
//
// NOTE: In SST files produced by flush or compaction, there may be multiple
// entries for the same user key with different sequence numbers (e.g., when
// snapshots are active). UDI builders that use OnKeyAdded() should be
// prepared for this.
//
// Thread safety: For a given builder instance, OnKeyAdded() and
// AddIndexEntry() are always called from a single thread. Builders do
// not need internal synchronization.
// The key will be a user key. RocksDB guarantees that there will only be
// one entry for each key in the file/index.
virtual void OnKeyAdded(const Slice& /*key*/, ValueType /*type*/,
const Slice& /*value*/) {}
@@ -135,36 +104,13 @@ class UserDefinedIndexIterator {
// this as an opportunity to do any prefetching and buffering of results.
virtual void Prepare(const ScanOptions scan_opts[], size_t num_opts) = 0;
// Optional context for SeekAndGetResult providing the target sequence
// number. Passed as a struct for forward-compatible extensibility.
struct SeekContext {
// Sequence number of the target key. Used by UDI implementations that
// encode sequence numbers (when the same user key spans multiple data
// blocks) to locate the correct block.
SequenceNumber target_seq = 0;
};
// Position the index iterator at the very first index entry. The result
// must be populated the same way as SeekAndGetResult.
//
// The default implementation calls SeekAndGetResult with an empty key,
// which works for BytewiseComparator (empty string is the smallest key).
// Implementations should override this if they can reach the first entry
// more efficiently or if they use a comparator where empty is not smallest.
virtual Status SeekToFirstAndGetResult(IterateResult* result) {
return SeekAndGetResult(Slice(), result, SeekContext{});
}
// Given the target key, position the index iterator at the index entry
// for the data block that may contain the target.
//
// The target is a user key.
//
// The result must be updated with the index key and bound_check_result.
// bound_check_result should be kOutOfBound if no block satisfies the
// target, kInbound if the data block is definitely within bounds, or
// kUnknown if partially within bounds.
//
// with the smallest key >= target. The result must be updated with the
// index key, and the bound_check_result. The bound_check_result should
// be set to kOutOfBound if no block satisfies the target key and
// termination criteria, kInbound if the data block is definitely fully
// within bounds, or kUnknown if the data block could be partially
// within bounds.
// The UDI implementation needs to be careful about returning kOutOfBound.
// If a limit key is specified in ScanOptions, an implementation that
// does not store the first key in the block for the corresponding index
@@ -173,14 +119,8 @@ class UserDefinedIndexIterator {
// is out of bounds w.r.t the limit. Other termination criteria (specified
// in property_bag) may cause the scan to terminate earlier, in which case
// kOutOfBound can be returned earlier.
//
// @context: sequence number context for the seek. The sequence number is
// needed when the same user key spans multiple data blocks with different
// sequence numbers. Without it, the UDI cannot distinguish which block to
// return for a given (user_key, seqno) target. Implementations that don't
// need sequence numbers can ignore the context.
virtual Status SeekAndGetResult(const Slice& target, IterateResult* result,
const SeekContext& context) = 0;
virtual Status SeekAndGetResult(const Slice& target,
IterateResult* result) = 0;
// Advance to the next index entry. The result must be populated similar
// to SeekAndGetResult.
@@ -212,7 +152,7 @@ struct UserDefinedIndexOption {
// Factory for creating user-defined index builders.
class UserDefinedIndexFactory : public Customizable {
public:
~UserDefinedIndexFactory() override = default;
virtual ~UserDefinedIndexFactory() = default;
static const char* Type() { return "UserDefinedIndexFactory"; }
@@ -234,14 +174,14 @@ class UserDefinedIndexFactory : public Customizable {
std::unique_ptr<UserDefinedIndexBuilder>& builder) const {
builder.reset(NewBuilder());
return Status::OK();
}
};
virtual Status NewReader(
const UserDefinedIndexOption& /*option*/, Slice& index_block,
std::unique_ptr<UserDefinedIndexReader>& reader) const {
reader = NewReader(index_block);
return Status::OK();
}
};
};
} // namespace ROCKSDB_NAMESPACE
Loaded 100 of 181 files, more files were not shown because too many files have changed in this diff. Show more