mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-07 11:32:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b44608914 | ||
|
|
83a5b52eda | ||
|
|
6cdeb9d9d0 | ||
|
|
ea72ee89da | ||
|
|
37c3a6de09 | ||
|
|
ede2cbd67b | ||
|
|
b21eaa91c2 | ||
|
|
405e1a4ac6 | ||
|
|
a201a3c61a | ||
|
|
77a0e347bb | ||
|
|
89e384ca6f | ||
|
|
3d99378791 | ||
|
|
f455ab7bd6 | ||
|
|
63c86160cd | ||
|
|
89322fdd9e | ||
|
|
b23fc77aca | ||
|
|
b6f498b2c9 | ||
|
|
bcaf2794dc | ||
|
|
1cb7594b87 | ||
|
|
166b4a4487 | ||
|
|
03bded03b4 | ||
|
|
ec22903914 | ||
|
|
0c6f741422 | ||
|
|
c66c14258f | ||
|
|
e43171d6ca | ||
|
|
1e1097d8d3 | ||
|
|
e44a99f560 | ||
|
|
5494bc1d76 | ||
|
|
91f227d9be | ||
|
|
cb8bc56d14 | ||
|
|
1ed5052486 | ||
|
|
db9449a154 | ||
|
|
26aff32db3 | ||
|
|
2afb387917 | ||
|
|
a066318a31 | ||
|
|
42eff8b632 | ||
|
|
cb43abb1f1 | ||
|
|
e27114b135 | ||
|
|
3ad23b2d94 | ||
|
|
8a86620653 | ||
|
|
071b5eff7a | ||
|
|
3aa706c2bf | ||
|
|
2dc6d6f765 | ||
|
|
ef5b145140 | ||
|
|
3b5cb114e3 | ||
|
|
16c81a27e9 | ||
|
|
3b66de3fac | ||
|
|
9a9cc1e2ef | ||
|
|
ea2f33c81a | ||
|
|
15f2f2bf6f | ||
|
|
9d3d3c4b61 | ||
|
|
cc19bd7555 | ||
|
|
c2580e2b7a | ||
|
|
f25fb41da6 | ||
|
|
b48d6f2c12 | ||
|
|
aa9160e8d7 | ||
|
|
da2c3c0ee6 | ||
|
|
8707abfc11 | ||
|
|
ce0fff9f41 | ||
|
|
372995470a | ||
|
|
1a8471b17e | ||
|
|
c311362ab6 | ||
|
|
5cb88bae30 | ||
|
|
300fb8cace | ||
|
|
5db2eb0f3c | ||
|
|
1ba60ee9ee | ||
|
|
e95f3759e9 | ||
|
|
de3616dc3b | ||
|
|
4213f9e14a | ||
|
|
49a4165e3a | ||
|
|
bd5b299b92 | ||
|
|
e7c3391640 |
No files matched your search
+1
-3
@@ -17,9 +17,7 @@ Checks: "-*,\
|
||||
cert-err58-cpp,\
|
||||
cert-msc30-c,\
|
||||
cert-msc50-cpp,\
|
||||
clang-analyzer-core.NullDereference,\
|
||||
clang-analyzer-core.StackAddressEscape,\
|
||||
clang-analyzer-deadcode.DeadStores,\
|
||||
clang-analyzer-*,\
|
||||
clang-diagnostic-*,\
|
||||
-clang-diagnostic-missing-designated-field-initializers,\
|
||||
concurrency-mt-unsafe,\
|
||||
|
||||
@@ -30,4 +30,4 @@ runs:
|
||||
# - The docker image, which may not always be specified/known
|
||||
# - Hash of folly.mk, which includes the folly repository commit hash
|
||||
# NOTE: this is still only intended for DEBUG folly builds
|
||||
key: folly-build-${{ runner.os }}-${{ runner.arch }}-${{ github.job_container.image }}-${{ steps.extract-folly-hash.outputs.hash }}
|
||||
key: folly-build-${{ runner.os }}-${{ runner.arch }}-${{ github.job_container.image }}-${{ steps.extract-folly-hash.outputs.hash }}-${{ env.PORTABLE == '1' && 'portable' || 'native' }}
|
||||
@@ -0,0 +1,49 @@
|
||||
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,16 +4,15 @@ 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
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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');
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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');
|
||||
}
|
||||
};
|
||||
@@ -12,6 +12,12 @@ 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
|
||||
@@ -20,6 +26,7 @@ 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
|
||||
@@ -32,27 +39,13 @@ jobs:
|
||||
}
|
||||
const body = fs.readFileSync('clang-tidy-comment.md', 'utf8');
|
||||
const prNumber = parseInt(fs.readFileSync('pr_number.txt', 'utf8').trim());
|
||||
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 post = require('./.github/scripts/post-pr-comment.js');
|
||||
|
||||
await post({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
prNumber,
|
||||
body,
|
||||
marker: '<!-- clang-tidy-bot -->',
|
||||
});
|
||||
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');
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
# 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.');
|
||||
@@ -0,0 +1,476 @@
|
||||
# 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
|
||||
+176
-56
@@ -92,7 +92,14 @@ 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')
|
||||
@@ -105,6 +112,9 @@ 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: |-
|
||||
@@ -113,12 +123,16 @@ 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: 16-core-ubuntu
|
||||
labels: 32-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
@@ -127,12 +141,19 @@ jobs:
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: make-with-folly
|
||||
- uses: "./.github/actions/cache-folly"
|
||||
id: cache-folly
|
||||
- uses: "./.github/actions/build-folly"
|
||||
with:
|
||||
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
|
||||
- run: USE_FOLLY=1 LIB_MODE=static V=1 make -j32 check
|
||||
- run: USE_FOLLY=1 LIB_MODE=static V=1 make -j64 check
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-make-with-folly-lite-no-test:
|
||||
if: needs.config.outputs.only_job == 'build-linux-make-with-folly-lite-no-test' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
@@ -147,13 +168,20 @@ 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: 16-core-ubuntu
|
||||
labels: 32-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
@@ -162,12 +190,19 @@ jobs:
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: cmake-folly-coroutines
|
||||
- uses: "./.github/actions/cache-folly"
|
||||
id: cache-folly
|
||||
- uses: "./.github/actions/build-folly"
|
||||
with:
|
||||
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
|
||||
- run: "(mkdir build && cd build && cmake -DUSE_COROUTINES=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 .. && make VERBOSE=1 -j20 && ctest -j20)"
|
||||
- run: "(mkdir build && cd build && cmake -DUSE_COROUTINES=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 -DPORTABLE=ON .. && make VERBOSE=1 -j64 && ctest -j64)"
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- 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')
|
||||
@@ -180,7 +215,14 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: mkdir build && cd build && cmake -DWITH_GFLAGS=1 -DWITH_BENCHMARK=1 -DCMAKE_CXX_FLAGS=-DNROCKSDB_THREAD_STATUS .. && make VERBOSE=1 -j20 && ctest -j20
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: cmake-benchmark
|
||||
- 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
|
||||
- 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')
|
||||
@@ -193,8 +235,15 @@ 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:
|
||||
@@ -207,6 +256,9 @@ jobs:
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: release
|
||||
- run: make V=1 -j32 LIB_MODE=shared release
|
||||
- run: ls librocksdb.so
|
||||
- run: "./trace_analyzer --version" # A tool dependent on gflags that can run in release build
|
||||
@@ -223,6 +275,10 @@ 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')
|
||||
@@ -235,11 +291,18 @@ 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')
|
||||
@@ -252,9 +315,16 @@ 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')
|
||||
@@ -267,30 +337,17 @@ 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')
|
||||
@@ -303,9 +360,16 @@ 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')
|
||||
@@ -318,7 +382,14 @@ 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:
|
||||
@@ -332,7 +403,14 @@ 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')
|
||||
@@ -345,7 +423,14 @@ 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')
|
||||
@@ -358,7 +443,14 @@ 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:
|
||||
@@ -372,11 +464,18 @@ 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:
|
||||
@@ -391,6 +490,9 @@ jobs:
|
||||
- uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: 16.4.0
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: macos-cmake
|
||||
- uses: "./.github/actions/increase-max-open-files-on-macos"
|
||||
- uses: "./.github/actions/install-gflags-on-macos"
|
||||
- uses: "./.github/actions/pre-steps-macos"
|
||||
@@ -410,6 +512,10 @@ 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
|
||||
@@ -433,29 +539,22 @@ jobs:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
# The docker image is intentionally based on an OS that has an older GLIBC version.
|
||||
# That GLIBC is incompatibile with GitHub's actions/checkout. Thus we implement a manual checkout step.
|
||||
# NOTE: replaced evolvedbinary/rocksjava:centos7_x64-be with ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
# until a more appropriate docker image with C++20 support is made.
|
||||
- name: Checkout
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
chown `whoami` . || true
|
||||
git clone --no-checkout https://oath2:$GH_TOKEN@github.com/${{ github.repository }}.git .
|
||||
git -c protocol.version=2 fetch --update-head-ok --no-tags --prune --no-recurse-submodules --depth=1 origin +${{ github.sha }}:${{ github.ref }}
|
||||
git checkout --progress --force ${{ github.ref }}
|
||||
git log -1 --format='%H'
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: java
|
||||
- name: Set Java Environment
|
||||
run: |-
|
||||
echo "JAVA_HOME=${JAVA_HOME}"
|
||||
which java && java -version
|
||||
which javac && javac -version
|
||||
- name: Test RocksDBJava
|
||||
# NOTE: replaced scl enable devtoolset-7 'make V=1 J=8 -j8 jtest'
|
||||
run: make V=1 J=8 -j8 jtest
|
||||
# post-steps skipped because of compatibility issues with docker image
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
build-linux-java-static:
|
||||
if: needs.config.outputs.only_job == 'build-linux-java-static' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
@@ -465,29 +564,22 @@ jobs:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
# The docker image is intentionally based on an OS that has an older GLIBC version.
|
||||
# That GLIBC is incompatibile with GitHub's actions/checkout. Thus we implement a manual checkout step.
|
||||
# NOTE: replaced evolvedbinary/rocksjava:centos7_x64-be with ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
# until a more appropriate docker image with C++20 support is made.
|
||||
- name: Checkout
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
chown `whoami` . || true
|
||||
git clone --no-checkout https://oath2:$GH_TOKEN@github.com/${{ github.repository }}.git .
|
||||
git -c protocol.version=2 fetch --update-head-ok --no-tags --prune --no-recurse-submodules --depth=1 origin +${{ github.sha }}:${{ github.ref }}
|
||||
git checkout --progress --force ${{ github.ref }}
|
||||
git log -1 --format='%H'
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: java-static
|
||||
- name: Set Java Environment
|
||||
run: |-
|
||||
echo "JAVA_HOME=${JAVA_HOME}"
|
||||
which java && java -version
|
||||
which javac && javac -version
|
||||
- name: Build RocksDBJava Static Library
|
||||
# NOTE: replaced scl enable devtoolset-7 'make V=1 J=8 -j8 rocksdbjavastatic'
|
||||
run: make V=1 J=8 -j8 rocksdbjavastatic
|
||||
# post-steps skipped because of compatibility issues with docker image
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
build-macos-java:
|
||||
if: needs.config.outputs.only_job == 'build-macos-java' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
@@ -500,6 +592,9 @@ jobs:
|
||||
- uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: 16.4.0
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: macos-java
|
||||
- uses: "./.github/actions/increase-max-open-files-on-macos"
|
||||
- uses: "./.github/actions/install-gflags-on-macos"
|
||||
- uses: "./.github/actions/install-jdk8-on-macos"
|
||||
@@ -511,6 +606,10 @@ 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')
|
||||
@@ -523,6 +622,9 @@ jobs:
|
||||
- uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: 16.4.0
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: macos-java-static
|
||||
- uses: "./.github/actions/increase-max-open-files-on-macos"
|
||||
- uses: "./.github/actions/install-gflags-on-macos"
|
||||
- uses: "./.github/actions/install-jdk8-on-macos"
|
||||
@@ -534,6 +636,10 @@ 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')
|
||||
@@ -546,6 +652,9 @@ jobs:
|
||||
- uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: 16.4.0
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: macos-java-static-universal
|
||||
- uses: "./.github/actions/increase-max-open-files-on-macos"
|
||||
- uses: "./.github/actions/install-gflags-on-macos"
|
||||
- uses: "./.github/actions/install-jdk8-on-macos"
|
||||
@@ -557,6 +666,10 @@ 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')
|
||||
@@ -593,6 +706,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: sudo apt-get update && sudo apt-get install -y build-essential
|
||||
- run: sudo apt-get update && sudo apt-get install -y build-essential ccache
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: arm
|
||||
- run: ROCKSDBTESTS_PLATFORM_DEPENDENT=only make V=1 J=4 -j4 all_but_some_tests check_some
|
||||
- name: Print ccache stats
|
||||
run: ccache -s
|
||||
if: always()
|
||||
shell: bash
|
||||
- uses: "./.github/actions/post-steps"
|
||||
@@ -106,6 +106,7 @@ 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",
|
||||
@@ -207,6 +208,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"table/block_based/hash_index_reader.cc",
|
||||
"table/block_based/index_builder.cc",
|
||||
"table/block_based/index_reader_common.cc",
|
||||
"table/block_based/multi_scan_index_iterator.cc",
|
||||
"table/block_based/parsed_full_filter_block.cc",
|
||||
"table/block_based/partitioned_filter_block.cc",
|
||||
"table/block_based/partitioned_index_iterator.cc",
|
||||
@@ -362,6 +364,9 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"utilities/transactions/write_prepared_txn_db.cc",
|
||||
"utilities/transactions/write_unprepared_txn.cc",
|
||||
"utilities/transactions/write_unprepared_txn_db.cc",
|
||||
"utilities/trie_index/bitvector.cc",
|
||||
"utilities/trie_index/louds_trie.cc",
|
||||
"utilities/trie_index/trie_index_factory.cc",
|
||||
"utilities/ttl/db_ttl_impl.cc",
|
||||
"utilities/types_util.cc",
|
||||
"utilities/wal_filter.cc",
|
||||
@@ -5137,6 +5142,12 @@ cpp_unittest_wrapper(name="faiss_ivf_index_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="fault_injection_fs_test",
|
||||
srcs=["utilities/fault_injection_fs_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="fault_injection_test",
|
||||
srcs=["db/fault_injection_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -5623,6 +5634,18 @@ cpp_unittest_wrapper(name="transaction_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="trie_index_db_test",
|
||||
srcs=["utilities/trie_index/trie_index_db_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="trie_index_test",
|
||||
srcs=["utilities/trie_index/trie_index_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="ttl_test",
|
||||
srcs=["utilities/ttl/ttl_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -5725,14 +5748,14 @@ cpp_unittest_wrapper(name="write_controller_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="write_prepared_transaction_test",
|
||||
srcs=["utilities/transactions/write_prepared_transaction_test.cc"],
|
||||
cpp_unittest_wrapper(name="write_prepared_transaction_seqno_test",
|
||||
srcs=["utilities/transactions/write_prepared_transaction_seqno_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="write_prepared_transaction_test_seqno",
|
||||
srcs=["utilities/transactions/write_prepared_transaction_test_seqno.cc"],
|
||||
cpp_unittest_wrapper(name="write_prepared_transaction_test",
|
||||
srcs=["utilities/transactions/write_prepared_transaction_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
@@ -250,6 +250,9 @@ The following patterns emerged as frequent sources of review feedback:
|
||||
### Adding new option
|
||||
Refer to claude_md/add_option.md
|
||||
|
||||
### Removing deprecated option
|
||||
Refer to claude_md/remove_option.md
|
||||
|
||||
### Metrics
|
||||
* When adding a new feature, evaluate whether there is opportunity to add
|
||||
metrics. Try to avoid causing performance regression on hot path when adding
|
||||
|
||||
+9
-1
@@ -766,6 +766,7 @@ 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
|
||||
@@ -842,6 +843,7 @@ set(SOURCES
|
||||
table/block_based/block_based_table_builder.cc
|
||||
table/block_based/block_based_table_factory.cc
|
||||
table/block_based/block_based_table_iterator.cc
|
||||
table/block_based/multi_scan_index_iterator.cc
|
||||
table/block_based/block_based_table_reader.cc
|
||||
table/block_based/block_builder.cc
|
||||
table/block_based/block_cache.cc
|
||||
@@ -999,6 +1001,9 @@ set(SOURCES
|
||||
utilities/transactions/write_prepared_txn_db.cc
|
||||
utilities/transactions/write_unprepared_txn.cc
|
||||
utilities/transactions/write_unprepared_txn_db.cc
|
||||
utilities/trie_index/bitvector.cc
|
||||
utilities/trie_index/louds_trie.cc
|
||||
utilities/trie_index/trie_index_factory.cc
|
||||
utilities/types_util.cc
|
||||
utilities/ttl/db_ttl_impl.cc
|
||||
utilities/wal_filter.cc
|
||||
@@ -1531,6 +1536,7 @@ 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
|
||||
@@ -1548,10 +1554,12 @@ if(WITH_TESTS)
|
||||
utilities/transactions/lock/point/point_lock_manager_stress_test.cc
|
||||
utilities/transactions/write_committed_transaction_ts_test.cc
|
||||
utilities/transactions/write_prepared_transaction_test.cc
|
||||
utilities/transactions/write_prepared_transaction_test_seqno.cc
|
||||
utilities/transactions/write_prepared_transaction_seqno_test.cc
|
||||
utilities/transactions/write_unprepared_transaction_test.cc
|
||||
utilities/transactions/lock/range/range_locking_test.cc
|
||||
utilities/transactions/timestamped_snapshot_test.cc
|
||||
utilities/trie_index/trie_index_db_test.cc
|
||||
utilities/trie_index/trie_index_test.cc
|
||||
utilities/ttl/ttl_test.cc
|
||||
utilities/types_util_test.cc
|
||||
utilities/util_merge_operators_test.cc
|
||||
|
||||
+25
-8
@@ -1,17 +1,34 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 11.0.4 (03/11/2026)
|
||||
### Public API Changes
|
||||
* Re-remove deprecated DB::Open raw pointer variants in db.h
|
||||
## 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.0.3 (03/03/2026)
|
||||
## 11.1.1 (04/10/2026)
|
||||
### Public API Changes
|
||||
* Relax option sanitization for kv ratio compaction
|
||||
* 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
|
||||
|
||||
## 11.0.2 (02/26/2026)
|
||||
### Public API Changes
|
||||
* Revert removing 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.1.0 (03/23/2026)
|
||||
### New Features
|
||||
* Add a new option `open_files_async`. The existing behavior is on DB open, we open all sst files and do basic validations. For very large DBs on remote filesystems with many ssts, this may take very long. This option performs these validations instead in the background. Open errors found by this async background task are surfaced as a new background error kAsyncFileOpen.
|
||||
* Added `BlockBasedTableOptions::kAuto` index block search type that automatically selects between binary and interpolation search on a per-index-block basis. During SST construction, each index block's key distribution uniformity is analyzed using the coefficient of variation of key gaps, and index blocks with uniform keys use interpolation search while others fall back to binary search. The uniformity threshold is configurable via `BlockBasedTableOptions::uniform_cv_threshold` (default: 0.2).
|
||||
* Introduced enforce_write_buffer_manager_during_recovery option to allow WriteBufferManager to be enforced during WAL recovery. (Default: true)
|
||||
* Add `memtable_batch_lookup_optimization` option to use batch lookup optimization for memtable MultiGet. For skip list memtables, after each key lookup, the search path is cached and reused for the next key, reducing per-key cost from O(log N) to O(log d) where d is the distance between consecutive keys. Benchmarks show ~7% improvement in memtable-resident MultiGet throughput.
|
||||
* Added `BlockBasedTableOptions::PrepopulateBlockCache::kFlushAndCompaction` to prepopulate the block cache during both flush and compaction. Compaction-warmed blocks are inserted at `BOTTOM` priority (vs `LOW` for flush) so they are evicted first under cache pressure. Recommended only for use cases where most or all of the database is expected to reside in cache (e.g., tiered or remote storage where the working set fits in cache).
|
||||
* Added new mutable DB option `verify_manifest_content_on_close` (default: false). When enabled, on DB close the MANIFEST file is read back and all records are validated (CRC checksums and logical content). If corruption is detected, a fresh MANIFEST is written from in-memory state.
|
||||
|
||||
### Behavior Changes
|
||||
* num_reads_sampled now factors in re-seeks and next/prev() on file iterators for files in L1+. next/prev() is discounted by 64x compared to seek due to being a much cheaper call.
|
||||
* Remote compaction workers now skip WAL recovery when opening the secondary DB instance, since only the LSM state from MANIFEST is needed for compaction. This reduces I/O and speeds up remote compaction startup.
|
||||
|
||||
### Bug Fixes
|
||||
* Fix a bug in round-robin compaction that missed selecting input files that are needed to guarantee data correctness and cause crashing in debug builds or silent data corruption in release builds for Get().
|
||||
|
||||
## 11.0.0 (02/23/2026)
|
||||
### New Features
|
||||
|
||||
@@ -627,11 +627,22 @@ 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))
|
||||
@@ -884,6 +895,9 @@ $(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
|
||||
@@ -1001,6 +1015,11 @@ check-progress:
|
||||
# If J != 1 and GNU parallel is installed, run the tests in parallel,
|
||||
# via the check_0 rule above. Otherwise, run them sequentially.
|
||||
check: all
|
||||
$(AM_V_at)echo "Cleaning up stale test directories older than 3 hours..."; \
|
||||
test_tmpdir_parent=$$(dirname $(TEST_TMPDIR)); \
|
||||
find $$test_tmpdir_parent -maxdepth 1 -name 'rocksdb.*' -type d \
|
||||
-mmin +180 -exec rm -rf {} + 2>/dev/null; \
|
||||
true
|
||||
$(MAKE) gen_parallel_tests
|
||||
$(AM_V_GEN)if test "$(J)" != 1 \
|
||||
&& (build_tools/gnu_parallel --gnu --help 2>/dev/null) | \
|
||||
@@ -1589,6 +1608,12 @@ object_registry_test: $(OBJ_DIR)/utilities/object_registry_test.o $(TEST_LIBRARY
|
||||
ttl_test: $(OBJ_DIR)/utilities/ttl/ttl_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
trie_index_db_test: $(OBJ_DIR)/utilities/trie_index/trie_index_db_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
trie_index_test: $(OBJ_DIR)/utilities/trie_index/trie_index_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
types_util_test: $(OBJ_DIR)/utilities/types_util_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -1640,6 +1665,9 @@ io_posix_test: $(OBJ_DIR)/env/io_posix_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
fault_injection_test: $(OBJ_DIR)/db/fault_injection_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
fault_injection_fs_test: $(OBJ_DIR)/utilities/fault_injection_fs_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
rate_limiter_test: $(OBJ_DIR)/util/rate_limiter_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -1838,7 +1866,7 @@ write_committed_transaction_ts_test: $(OBJ_DIR)/utilities/transactions/write_com
|
||||
write_prepared_transaction_test: $(OBJ_DIR)/utilities/transactions/write_prepared_transaction_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
write_prepared_transaction_test_seqno: $(OBJ_DIR)/utilities/transactions/write_prepared_transaction_test_seqno.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
write_prepared_transaction_seqno_test: $(OBJ_DIR)/utilities/transactions/write_prepared_transaction_seqno_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
write_unprepared_transaction_test: $(OBJ_DIR)/utilities/transactions/write_unprepared_transaction_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# Code Review Skill
|
||||
|
||||
This document defines the methodology for performing thorough, multi-perspective
|
||||
code reviews on RocksDB changes. It is used both by CI (GitHub Actions) and
|
||||
local review workflows.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting a review:
|
||||
- Read `CLAUDE.md` in the repository root for project-specific guidelines
|
||||
- Read any files in `claude_md/` for architecture context
|
||||
- Identify the commit/diff/PR to review and parse the changed files
|
||||
|
||||
## Review Methodology
|
||||
|
||||
Conduct the review from multiple perspectives, then synthesize findings into a
|
||||
single report. Each perspective catches different classes of bugs.
|
||||
|
||||
### Perspective 1: Codebase Context & Call-Chain Analysis
|
||||
|
||||
Before reviewing the diff itself, build deep context:
|
||||
- For each changed function/method, trace the call chain UPWARD 3-5 levels.
|
||||
Who consumes the changed behavior? What invariants do callers rely on?
|
||||
- Trace DOWNWARD into callees. What side effects do they have? What shared
|
||||
state do they mutate (counters, sequence numbers, flags, metadata)?
|
||||
- Identify the "sibling" or "reference" implementation that handles the same
|
||||
scenario the standard way. Compare behaviors.
|
||||
- For data written to shared structures (memtable, SST block, cache entry),
|
||||
find ALL readers and verify their visibility rules match the writer.
|
||||
|
||||
### Perspective 2: Correctness & Edge Cases
|
||||
|
||||
- Thread safety and concurrency (lock ordering, data races)
|
||||
- Error handling and Status propagation
|
||||
- Edge cases: empty inputs, overflow, boundary conditions
|
||||
- Data corruption scenarios
|
||||
- Behavioral contract changes: do return value semantics match callers?
|
||||
|
||||
### Perspective 3: Cross-Component & Adversarial Analysis
|
||||
|
||||
Check the change in ALL execution contexts:
|
||||
|
||||
| Context | Key difference |
|
||||
|---------|---------------|
|
||||
| WritePreparedTxnDB | read_callback_ controls visibility |
|
||||
| ReadOnly DB / SecondaryInstance | No mutable memtable |
|
||||
| CompactionService / Remote compaction | Different process |
|
||||
| User-defined timestamps | Extra key comparison dimension |
|
||||
| MemPurge | Memtable-to-memtable path |
|
||||
| BlobDB | Values in blob files, not inline |
|
||||
| Old snapshots | Seqno far behind current |
|
||||
| Concurrent writers | Lock-free vs locked paths |
|
||||
| FIFO / Universal compaction | Different invariants |
|
||||
| Prefix seek / total order seek | Different iterator behavior |
|
||||
|
||||
When the change claims a property ("logically redundant", "safe because X"),
|
||||
systematically try to break it: list preconditions, construct counterexamples.
|
||||
|
||||
### Perspective 4: Performance
|
||||
|
||||
RocksDB is a high-performance storage engine.
|
||||
- Memory allocation on hot paths
|
||||
- Unnecessary copies (prefer move semantics, Slice, string_view)
|
||||
- Cache efficiency and data locality
|
||||
- Loop optimization, branch prediction (LIKELY/UNLIKELY)
|
||||
|
||||
### Perspective 5: API, Compatibility & Testing
|
||||
|
||||
- Public API backwards compatibility
|
||||
- Serialization format correctness and versioning
|
||||
- Test coverage for edge cases, failure modes, and system-level interactions
|
||||
|
||||
## How to Explore the Codebase
|
||||
|
||||
Use available tools to explore deeply — do NOT just review the diff in isolation.
|
||||
The most critical bugs hide at component boundaries.
|
||||
|
||||
- Read the header files for changed implementations
|
||||
- Search for callers of changed functions (3-5 levels up)
|
||||
- Read existing tests to understand guaranteed behaviors
|
||||
- Check related implementations for conventions and patterns
|
||||
|
||||
## Output Format
|
||||
|
||||
### Summary
|
||||
Brief overall assessment (1-2 sentences).
|
||||
|
||||
### Issues Found
|
||||
Categorize by severity:
|
||||
- :red_circle: **Critical**: Must fix (correctness, data corruption, security)
|
||||
- :yellow_circle: **Suggestion**: Should consider (performance, edge cases)
|
||||
- :green_circle: **Nitpick**: Minor style issues
|
||||
|
||||
For each issue include:
|
||||
- File and line reference
|
||||
- Which review perspective found it
|
||||
- Description with root cause analysis
|
||||
- Suggested fix
|
||||
|
||||
### Cross-Component Analysis
|
||||
Execution context checks and assumption stress-test results.
|
||||
|
||||
### Positive Observations
|
||||
Good patterns, clever optimizations, or improvements.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Removing Deprecated Options from RocksDB
|
||||
### Keep in these files:
|
||||
- [ ] **KEEP** entry in type_info (`options/cf_options.cc` or `options/db_options.cc`) with `OptionVerificationType::kDeprecated` for loading old option files
|
||||
- [ ] **KEEP** or add test in `options/options_test.cc` `OptionsOldApiTest::GetOptionsFromMapTest` for loading old option files
|
||||
|
||||
### Documentation:
|
||||
- [ ] Add release note to `HISTORY.md` and `unreleased_history/`
|
||||
@@ -533,7 +533,7 @@ void BlobFileReader::MultiGetBlob(
|
||||
}
|
||||
|
||||
// Uncompress blob if needed
|
||||
Slice value_slice(record_slice.data() + adjustments[i], req->len);
|
||||
Slice value_slice(record_slice.data() + adjustments[j - 1], req->len);
|
||||
*req->status = UncompressBlobIfNeeded(
|
||||
value_slice, compression_type_, decompressor_.get(), allocator, clock_,
|
||||
statistics_, &blob_reqs[i].second);
|
||||
|
||||
@@ -1011,6 +1011,92 @@ TEST_P(BlobFileReaderDecodingErrorTest, DecodingError) {
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(BlobFileReaderTest, MultiGetBlobWithFailedValidation) {
|
||||
// Test that MultiGetBlob correctly handles the case where some requests
|
||||
// fail validation. The adjustments vector is only populated for requests
|
||||
// that pass validation, so the indices must track correctly.
|
||||
Options options;
|
||||
options.env = mock_env_.get();
|
||||
options.cf_paths.emplace_back(
|
||||
test::PerThreadDBPath(
|
||||
mock_env_.get(),
|
||||
"BlobFileReaderTest_MultiGetBlobWithFailedValidation"),
|
||||
0);
|
||||
options.enable_blob_files = true;
|
||||
|
||||
ImmutableOptions immutable_options(options);
|
||||
|
||||
constexpr uint32_t column_family_id = 1;
|
||||
constexpr bool has_ttl = false;
|
||||
constexpr ExpirationRange expiration_range;
|
||||
constexpr uint64_t blob_file_number = 1;
|
||||
constexpr size_t num_blobs = 3;
|
||||
const std::vector<std::string> key_strs = {"key1", "key2", "key3"};
|
||||
const std::vector<std::string> blob_strs = {"blob1", "blob2", "blob3"};
|
||||
|
||||
const std::vector<Slice> keys = {key_strs[0], key_strs[1], key_strs[2]};
|
||||
const std::vector<Slice> blobs = {blob_strs[0], blob_strs[1], blob_strs[2]};
|
||||
|
||||
std::vector<uint64_t> blob_offsets(keys.size());
|
||||
std::vector<uint64_t> blob_sizes(keys.size());
|
||||
|
||||
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
|
||||
expiration_range, blob_file_number, keys, blobs, kNoCompression,
|
||||
blob_offsets, blob_sizes);
|
||||
|
||||
constexpr HistogramImpl* blob_file_read_hist = nullptr;
|
||||
|
||||
std::unique_ptr<BlobFileReader> reader;
|
||||
|
||||
ReadOptions read_options;
|
||||
ASSERT_OK(BlobFileReader::Create(
|
||||
immutable_options, read_options, FileOptions(), column_family_id,
|
||||
blob_file_read_hist, blob_file_number, nullptr /*IOTracer*/, &reader));
|
||||
|
||||
// Enable checksum verification so adjustments are non-zero
|
||||
read_options.verify_checksums = true;
|
||||
constexpr MemoryAllocator* allocator = nullptr;
|
||||
|
||||
// Fail the first request by giving it an invalid offset (too small).
|
||||
// This causes the first request to be skipped in the adjustments vector,
|
||||
// so adjustments has only 2 entries (for blobs 1 and 2), while blob_reqs
|
||||
// has 3 entries. Without the fix, adjustments[i] would use the wrong
|
||||
// index for the remaining valid requests.
|
||||
std::array<Status, num_blobs> statuses_buf;
|
||||
std::array<BlobReadRequest, num_blobs> requests_buf;
|
||||
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
|
||||
blob_reqs;
|
||||
|
||||
// First request: invalid offset (too small, will fail validation)
|
||||
requests_buf[0] = BlobReadRequest(keys[0], /*offset=*/0, blob_sizes[0],
|
||||
kNoCompression, nullptr, &statuses_buf[0]);
|
||||
// Second and third requests: valid
|
||||
requests_buf[1] = BlobReadRequest(keys[1], blob_offsets[1], blob_sizes[1],
|
||||
kNoCompression, nullptr, &statuses_buf[1]);
|
||||
requests_buf[2] = BlobReadRequest(keys[2], blob_offsets[2], blob_sizes[2],
|
||||
kNoCompression, nullptr, &statuses_buf[2]);
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
blob_reqs.emplace_back(&requests_buf[i], std::unique_ptr<BlobContents>());
|
||||
}
|
||||
|
||||
uint64_t bytes_read = 0;
|
||||
reader->MultiGetBlob(read_options, allocator, blob_reqs, &bytes_read);
|
||||
|
||||
// First request should fail validation
|
||||
ASSERT_TRUE(statuses_buf[0].IsCorruption());
|
||||
ASSERT_EQ(blob_reqs[0].second, nullptr);
|
||||
|
||||
// Second and third requests should succeed with correct data
|
||||
ASSERT_OK(statuses_buf[1]);
|
||||
ASSERT_NE(blob_reqs[1].second, nullptr);
|
||||
ASSERT_EQ(blob_reqs[1].second->data(), blobs[1]);
|
||||
|
||||
ASSERT_OK(statuses_buf[2]);
|
||||
ASSERT_NE(blob_reqs[2].second, nullptr);
|
||||
ASSERT_EQ(blob_reqs[2].second->data(), blobs[2]);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -322,6 +322,8 @@ Status BuildTable(
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT("BuildTable:BeforeFinishBuildTable");
|
||||
TEST_SYNC_POINT_CALLBACK("BuildTable:BeforeCheckEmpty",
|
||||
static_cast<TableBuilder*>(builder));
|
||||
const bool empty = builder->IsEmpty();
|
||||
if (flush_stats) {
|
||||
assert(c_iter.HasNumInputEntryScanned());
|
||||
@@ -330,6 +332,14 @@ Status BuildTable(
|
||||
}
|
||||
if (!s.ok() || empty) {
|
||||
builder->Abandon();
|
||||
// Propagate the builder's error when the builder is empty due to an
|
||||
// internal error (e.g., write fault injection causing all Add() calls to
|
||||
// return early). Without this, `s` would remain OK and the downstream
|
||||
// key count validation in flush_job.cc would produce a misleading
|
||||
// Corruption error instead of the actual builder error.
|
||||
if (s.ok() && !builder->status().ok()) {
|
||||
s = builder->status();
|
||||
}
|
||||
} else {
|
||||
SeqnoToTimeMapping relevant_mapping;
|
||||
if (seqno_to_time_mapping) {
|
||||
|
||||
@@ -1759,9 +1759,13 @@ rocksdb_column_family_handle_t* rocksdb_create_column_family(
|
||||
rocksdb_t* db, const rocksdb_options_t* column_family_options,
|
||||
const char* column_family_name, char** errptr) {
|
||||
rocksdb_column_family_handle_t* handle = new rocksdb_column_family_handle_t;
|
||||
SaveError(errptr, db->rep->CreateColumnFamily(
|
||||
ColumnFamilyOptions(column_family_options->rep),
|
||||
std::string(column_family_name), &(handle->rep)));
|
||||
handle->rep = nullptr;
|
||||
if (SaveError(errptr, db->rep->CreateColumnFamily(
|
||||
ColumnFamilyOptions(column_family_options->rep),
|
||||
std::string(column_family_name), &(handle->rep)))) {
|
||||
delete handle;
|
||||
return nullptr;
|
||||
}
|
||||
handle->immortal = false;
|
||||
return handle;
|
||||
}
|
||||
@@ -4351,6 +4355,15 @@ unsigned char rocksdb_options_get_paranoid_checks(rocksdb_options_t* opt) {
|
||||
return opt->rep.paranoid_checks;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_open_files_async(rocksdb_options_t* opt,
|
||||
unsigned char v) {
|
||||
opt->rep.open_files_async = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_open_files_async(rocksdb_options_t* opt) {
|
||||
return opt->rep.open_files_async;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_db_paths(rocksdb_options_t* opt,
|
||||
const rocksdb_dbpath_t** dbpath_values,
|
||||
size_t num_paths) {
|
||||
@@ -5789,6 +5802,16 @@ uint64_t rocksdb_perfcontext_metric(rocksdb_perfcontext_t* context,
|
||||
return rep->internal_range_del_reseek_count;
|
||||
case rocksdb_block_read_cpu_time:
|
||||
return rep->block_read_cpu_time;
|
||||
case rocksdb_data_block_read_byte:
|
||||
return rep->data_block_read_byte;
|
||||
case rocksdb_index_block_read_byte:
|
||||
return rep->index_block_read_byte;
|
||||
case rocksdb_filter_block_read_byte:
|
||||
return rep->filter_block_read_byte;
|
||||
case rocksdb_compression_dict_block_read_byte:
|
||||
return rep->compression_dict_block_read_byte;
|
||||
case rocksdb_metadata_block_read_byte:
|
||||
return rep->metadata_block_read_byte;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -7525,9 +7548,13 @@ rocksdb_column_family_handle_t* rocksdb_transactiondb_create_column_family(
|
||||
const rocksdb_options_t* column_family_options,
|
||||
const char* column_family_name, char** errptr) {
|
||||
rocksdb_column_family_handle_t* handle = new rocksdb_column_family_handle_t;
|
||||
SaveError(errptr, txn_db->rep->CreateColumnFamily(
|
||||
ColumnFamilyOptions(column_family_options->rep),
|
||||
std::string(column_family_name), &(handle->rep)));
|
||||
handle->rep = nullptr;
|
||||
if (SaveError(errptr, txn_db->rep->CreateColumnFamily(
|
||||
ColumnFamilyOptions(column_family_options->rep),
|
||||
std::string(column_family_name), &(handle->rep)))) {
|
||||
delete handle;
|
||||
return nullptr;
|
||||
}
|
||||
handle->immortal = false;
|
||||
return handle;
|
||||
}
|
||||
|
||||
+15
@@ -4880,6 +4880,21 @@ int main(int argc, char** argv) {
|
||||
rocksdb_sst_file_manager_destroy(sst_file_manager);
|
||||
}
|
||||
|
||||
StartPhase("create_column_family_error_returns_null");
|
||||
{
|
||||
// Creating a column family with a name that already exists should fail
|
||||
// and return NULL. Without the fix, the handle is leaked and a non-NULL
|
||||
// pointer with an indeterminate rep field is returned.
|
||||
char* cf_err = NULL;
|
||||
rocksdb_column_family_handle_t* cf_handle =
|
||||
rocksdb_create_column_family(db, options, "default", &cf_err);
|
||||
// Should have an error since "default" already exists
|
||||
CheckCondition(cf_err != NULL);
|
||||
// The handle should be NULL on error (this is the bug fix)
|
||||
CheckCondition(cf_handle == NULL);
|
||||
free(cf_err);
|
||||
}
|
||||
|
||||
StartPhase("cancel_all_background_work");
|
||||
rocksdb_cancel_all_background_work(db, 1);
|
||||
|
||||
|
||||
@@ -1569,6 +1569,28 @@ 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) ==
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "db/range_del_aggregator.h"
|
||||
#include "db/version_edit.h"
|
||||
#include "db/version_set.h"
|
||||
#include "file/file_util.h"
|
||||
#include "file/filename.h"
|
||||
#include "file/read_write_util.h"
|
||||
#include "file/sst_file_manager_impl.h"
|
||||
@@ -903,17 +904,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 verify_table_read_options(Env::IOActivity::kCompaction);
|
||||
verify_table_read_options.verify_checksums = true;
|
||||
verify_table_read_options.readahead_size =
|
||||
ReadOptions verification_read_options(Env::IOActivity::kCompaction);
|
||||
verification_read_options.verify_checksums = true;
|
||||
verification_read_options.readahead_size =
|
||||
file_options_for_read_.compaction_readahead_size;
|
||||
|
||||
std::unique_ptr<TableReader> table_reader_guard;
|
||||
TableReader* table_reader_ptr = table_reader_guard.get();
|
||||
verify_table_read_options.rate_limiter_priority =
|
||||
verification_read_options.rate_limiter_priority =
|
||||
GetRateLimiterPriority();
|
||||
InternalIterator* iter = cfd->table_cache()->NewIterator(
|
||||
verify_table_read_options, file_options_, cfd->internal_comparator(),
|
||||
verification_read_options, file_options_, cfd->internal_comparator(),
|
||||
output_file.meta,
|
||||
/*range_del_agg=*/nullptr, compact_->compaction->mutable_cf_options(),
|
||||
/*table_reader_ptr=*/&table_reader_ptr,
|
||||
@@ -941,12 +942,17 @@ Status CompactionJob::VerifyOutputFiles() {
|
||||
!!(verify_output_flags & VerifyOutputFlags::kVerifyBlockChecksum);
|
||||
const bool should_verify_iteration =
|
||||
!!(verify_output_flags & VerifyOutputFlags::kVerifyIteration);
|
||||
const bool should_verify_file_checksum =
|
||||
!!(verify_output_flags &
|
||||
VerifyOutputFlags::kVerifyFileChecksum) &&
|
||||
db_options_.file_checksum_gen_factory != nullptr &&
|
||||
output_file.meta.file_checksum != kUnknownFileChecksum;
|
||||
if (should_verify_block_checksum) {
|
||||
assert(table_reader_ptr != nullptr);
|
||||
// If verifying iteration as well, verify meta blocks here only to
|
||||
// avoid redundant checks on data blocks
|
||||
s = table_reader_ptr->VerifyChecksum(
|
||||
verify_table_read_options, TableReaderCaller::kCompaction,
|
||||
verification_read_options, TableReaderCaller::kCompaction,
|
||||
/*meta_blocks_only=*/should_verify_iteration);
|
||||
}
|
||||
if (s.ok() && should_verify_iteration) {
|
||||
@@ -967,6 +973,24 @@ Status CompactionJob::VerifyOutputFiles() {
|
||||
"was computed when written");
|
||||
}
|
||||
}
|
||||
if (s.ok() && should_verify_file_checksum) {
|
||||
std::string file_checksum;
|
||||
std::string file_checksum_func_name;
|
||||
std::string fname =
|
||||
GetTableFileName(output_file.meta.fd.GetNumber());
|
||||
s = GenerateOneFileChecksum(
|
||||
fs_.get(), fname, db_options_.file_checksum_gen_factory.get(),
|
||||
output_file.meta.file_checksum_func_name, &file_checksum,
|
||||
&file_checksum_func_name,
|
||||
verification_read_options.readahead_size,
|
||||
db_options_.allow_mmap_reads, io_tracer_,
|
||||
db_options_.rate_limiter.get(), verification_read_options,
|
||||
stats_, db_options_.clock, file_options_for_read_);
|
||||
if (s.ok() && file_checksum != output_file.meta.file_checksum) {
|
||||
s = Status::Corruption(
|
||||
"File checksum mismatch for compaction output file " + fname);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1092,6 +1116,9 @@ Status CompactionJob::Run() {
|
||||
status = VerifyOutputFiles();
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("CompactionJob::Run():AfterVerifyOutputFiles",
|
||||
&status);
|
||||
|
||||
if (status.ok()) {
|
||||
SetOutputTableProperties();
|
||||
}
|
||||
@@ -2470,8 +2497,16 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
return s;
|
||||
}
|
||||
|
||||
// Enable hash computation if paranoid_file_checks is on or if
|
||||
// verify_output_flags includes kVerifyIteration, so that
|
||||
// VerifyOutputFiles() can compare the hash of the written data
|
||||
// against a re-read of the output file.
|
||||
bool enable_output_hash =
|
||||
paranoid_file_checks_ ||
|
||||
!!(sub_compact->compaction->mutable_cf_options().verify_output_flags &
|
||||
VerifyOutputFlags::kVerifyIteration);
|
||||
outputs.AddOutput(std::move(meta), cfd->internal_comparator(),
|
||||
paranoid_file_checks_);
|
||||
enable_output_hash);
|
||||
}
|
||||
|
||||
writable_file->SetIOPriority(GetRateLimiterPriority());
|
||||
@@ -2512,7 +2547,7 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
|
||||
void CompactionJob::CleanupCompaction() {
|
||||
for (SubcompactionState& sub_compact : compact_->sub_compact_states) {
|
||||
sub_compact.Cleanup(table_cache_.get());
|
||||
sub_compact.Cleanup(table_cache_.get(), compact_->status);
|
||||
}
|
||||
delete compact_;
|
||||
compact_ = nullptr;
|
||||
@@ -2904,12 +2939,17 @@ void CompactionJob::RestoreCompactionOutputs(
|
||||
|
||||
const auto& output_files = subcompaction_progress_per_level.GetOutputFiles();
|
||||
|
||||
const bool enable_output_hash =
|
||||
paranoid_file_checks_ ||
|
||||
!!(compact_->compaction->mutable_cf_options().verify_output_flags &
|
||||
VerifyOutputFlags::kVerifyIteration);
|
||||
|
||||
for (size_t i = 0; i < output_files.size(); i++) {
|
||||
FileMetaData file_copy = output_files[i];
|
||||
|
||||
outputs_to_restore->AddOutput(std::move(file_copy),
|
||||
cfd->internal_comparator(),
|
||||
paranoid_file_checks_, true /* finished */);
|
||||
enable_output_hash, true /* finished */);
|
||||
|
||||
outputs_to_restore->UpdateTableProperties(
|
||||
*output_files_table_properties[i]);
|
||||
|
||||
@@ -102,10 +102,10 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
|
||||
for (auto ritr = level_files.rbegin(); ritr != level_files.rend(); ++ritr) {
|
||||
FileMetaData* f = *ritr;
|
||||
assert(f);
|
||||
if (f->fd.table_reader && f->fd.table_reader->GetTableProperties()) {
|
||||
TableReader* reader = f->fd.pinned_reader.Get();
|
||||
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
|
||||
uint64_t newest_key_time = f->TryGetNewestKeyTime();
|
||||
uint64_t creation_time =
|
||||
f->fd.table_reader->GetTableProperties()->creation_time;
|
||||
uint64_t creation_time = reader->GetTableProperties()->creation_time;
|
||||
uint64_t est_newest_key_time = newest_key_time == kUnknownNewestKeyTime
|
||||
? creation_time
|
||||
: newest_key_time;
|
||||
@@ -166,8 +166,9 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
|
||||
assert(f);
|
||||
uint64_t newest_key_time = f->TryGetNewestKeyTime();
|
||||
uint64_t creation_time = 0;
|
||||
if (f->fd.table_reader && f->fd.table_reader->GetTableProperties()) {
|
||||
creation_time = f->fd.table_reader->GetTableProperties()->creation_time;
|
||||
TableReader* reader = f->fd.pinned_reader.Get();
|
||||
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
|
||||
creation_time = reader->GetTableProperties()->creation_time;
|
||||
}
|
||||
uint64_t est_newest_key_time = newest_key_time == kUnknownNewestKeyTime
|
||||
? creation_time
|
||||
|
||||
@@ -404,7 +404,15 @@ void LevelCompactionBuilder::SetupOtherFilesWithRoundRobinExpansion() {
|
||||
tmp_start_level_inputs = start_level_inputs_;
|
||||
// TODO (zichen): Future parallel round-robin may also need to update this
|
||||
// Constraint 1b (only expand till the end)
|
||||
for (size_t i = start_index + 1; i < level_files.size(); i++) {
|
||||
size_t next_index = start_index + 1;
|
||||
const FileMetaData* last_file = start_level_inputs_.files.back();
|
||||
for (size_t j = next_index; j < level_files.size(); j++) {
|
||||
if (level_files[j] == last_file) {
|
||||
next_index = j + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (size_t i = next_index; i < level_files.size(); i++) {
|
||||
auto* f = level_files[i];
|
||||
if (f->being_compacted) {
|
||||
// Constraint 1a
|
||||
|
||||
@@ -178,7 +178,8 @@ class CompactionPickerTestBase : public testing::Test {
|
||||
}
|
||||
TableProperties tp;
|
||||
tp.newest_key_time = newest_key_time;
|
||||
f->fd.table_reader = new mock::MockTableReader(mock::KVVector{}, tp);
|
||||
f->fd.pinned_reader.TEST_SetReader(
|
||||
new mock::MockTableReader(mock::KVVector{}, tp));
|
||||
|
||||
vstorage->AddFile(level, f);
|
||||
files_.emplace_back(f);
|
||||
@@ -272,8 +273,9 @@ class CompactionPickerTestBase : public testing::Test {
|
||||
|
||||
void ClearFiles() {
|
||||
for (auto& file : files_) {
|
||||
if (file->fd.table_reader != nullptr) {
|
||||
delete file->fd.table_reader;
|
||||
TableReader* t = file->fd.pinned_reader.Get();
|
||||
if (t != nullptr) {
|
||||
delete t;
|
||||
}
|
||||
}
|
||||
files_.clear();
|
||||
|
||||
@@ -409,6 +409,34 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, SkipWALRecoveryInOpenAndCompact) {
|
||||
// Verify that OpenAndCompact skips WAL recovery when opening the secondary
|
||||
// instance. WAL replay is unnecessary for remote compaction since it only
|
||||
// needs the LSM state from MANIFEST.
|
||||
Options options = CurrentOptions();
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
// Track whether FindAndRecoverLogFiles is called during compaction
|
||||
std::atomic_bool wal_recovery_called{false};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::FindAndRecoverLogFiles:Begin",
|
||||
[&](void* /* arg */) { wal_recovery_called.store(true); });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Generate data and trigger compaction (which uses OpenAndCompact)
|
||||
GenerateTestData();
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
// WAL recovery should NOT have been called during OpenAndCompact
|
||||
ASSERT_FALSE(wal_recovery_called.load());
|
||||
|
||||
// Data should still be correct (compaction worked without WAL recovery)
|
||||
VerifyTestData();
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, ManualCompaction) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
@@ -1113,6 +1141,8 @@ TEST_F(CompactionServiceTest, CorruptedOutputVerifyOutputFlags) {
|
||||
VerifyOutputFlags::kVerifyBlockChecksum,
|
||||
VerifyOutputFlags::kEnableForRemoteCompaction |
|
||||
VerifyOutputFlags::kVerifyIteration,
|
||||
VerifyOutputFlags::kEnableForRemoteCompaction |
|
||||
VerifyOutputFlags::kVerifyFileChecksum,
|
||||
VerifyOutputFlags::kVerifyAll}) {
|
||||
SCOPED_TRACE(
|
||||
"verify_output_flags=" +
|
||||
@@ -1124,6 +1154,7 @@ TEST_F(CompactionServiceTest, CorruptedOutputVerifyOutputFlags) {
|
||||
options.disable_auto_compactions = true;
|
||||
options.paranoid_file_checks = false;
|
||||
options.verify_output_flags = verify_output_flags;
|
||||
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
|
||||
ReopenWithCompactionService(&options);
|
||||
GenerateTestData();
|
||||
|
||||
@@ -1159,10 +1190,13 @@ TEST_F(CompactionServiceTest, CorruptedOutputVerifyOutputFlags) {
|
||||
!!(verify_output_flags & VerifyOutputFlags::kVerifyBlockChecksum);
|
||||
const bool should_verify_iteration =
|
||||
!!(verify_output_flags & VerifyOutputFlags::kVerifyIteration);
|
||||
const bool should_verify_file_checksum =
|
||||
!!(verify_output_flags & VerifyOutputFlags::kVerifyFileChecksum);
|
||||
|
||||
Status s = db_->CompactRange(CompactRangeOptions(), &start, &end);
|
||||
if (is_enabled_for_remote_compaction &&
|
||||
(should_verify_block_checksum || should_verify_iteration)) {
|
||||
(should_verify_block_checksum || should_verify_iteration ||
|
||||
should_verify_file_checksum)) {
|
||||
ASSERT_NOK(s);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
} else {
|
||||
|
||||
@@ -38,11 +38,11 @@ OutputIterator SubcompactionState::GetOutputs() const {
|
||||
compaction_outputs_.outputs_);
|
||||
}
|
||||
|
||||
void SubcompactionState::Cleanup(Cache* cache) {
|
||||
void SubcompactionState::Cleanup(Cache* cache, const Status& overall_status) {
|
||||
proximal_level_outputs_.Cleanup();
|
||||
compaction_outputs_.Cleanup();
|
||||
|
||||
if (!status.ok()) {
|
||||
if (!status.ok() || !overall_status.ok()) {
|
||||
for (const auto& out : GetOutputs()) {
|
||||
// If this file was inserted into the table cache then remove it here
|
||||
// because this compaction was not committed. This is not strictly
|
||||
|
||||
@@ -170,7 +170,7 @@ class SubcompactionState {
|
||||
}
|
||||
}
|
||||
|
||||
void Cleanup(Cache* cache);
|
||||
void Cleanup(Cache* cache, const Status& overall_status);
|
||||
|
||||
void AggregateCompactionOutputStats(
|
||||
InternalStats::CompactionStatsFull& internal_stats) const;
|
||||
|
||||
@@ -2535,6 +2535,319 @@ TEST_P(DBMultiGetTestWithParam, MultiGetBatchedValueSizeMultiLevelMerge) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetMemtableBatchLookup) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
// Skip for unbatched MultiGet
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Insert sorted keys into memtable
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), "val" + std::to_string(i)));
|
||||
}
|
||||
// Delete some keys
|
||||
ASSERT_OK(Delete(1, Key(25)));
|
||||
ASSERT_OK(Delete(1, Key(75)));
|
||||
|
||||
// MultiGet a batch of keys - mix of existing, deleted, and missing
|
||||
// Store key strings to keep Slice data alive
|
||||
std::vector<std::string> key_strs = {Key(0), Key(10), Key(25), Key(50),
|
||||
Key(75), Key(99), Key(200)};
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "val0");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "val10");
|
||||
ASSERT_TRUE(statuses[2].IsNotFound()); // deleted
|
||||
ASSERT_OK(statuses[3]);
|
||||
ASSERT_EQ(values[3].ToString(), "val50");
|
||||
ASSERT_TRUE(statuses[4].IsNotFound()); // deleted
|
||||
ASSERT_OK(statuses[5]);
|
||||
ASSERT_EQ(values[5].ToString(), "val99");
|
||||
ASSERT_TRUE(statuses[6].IsNotFound()); // never inserted
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupOverwrite) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Insert, then overwrite some keys
|
||||
for (int i = 0; i < 50; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), "old" + std::to_string(i)));
|
||||
}
|
||||
for (int i = 0; i < 50; i += 5) {
|
||||
ASSERT_OK(Put(1, Key(i), "new" + std::to_string(i)));
|
||||
}
|
||||
|
||||
std::vector<std::string> key_strs;
|
||||
for (int i = 0; i < 50; i += 5) {
|
||||
key_strs.push_back(Key(i));
|
||||
}
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
for (size_t i = 0; i < keys.size(); i++) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
ASSERT_EQ(values[i].ToString(), "new" + std::to_string(i * 5));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupWithFlush) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Put data into SST
|
||||
for (int i = 0; i < 50; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), "sst" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
// Put different data into memtable (overlapping some keys)
|
||||
for (int i = 25; i < 75; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), "mem" + std::to_string(i)));
|
||||
}
|
||||
|
||||
// MultiGet keys spanning both SST and memtable
|
||||
std::vector<std::string> key_strs = {Key(10), Key(30), Key(60), Key(80)};
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "sst10");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "mem30");
|
||||
ASSERT_OK(statuses[2]);
|
||||
ASSERT_EQ(values[2].ToString(), "mem60");
|
||||
ASSERT_TRUE(statuses[3].IsNotFound());
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupWithMerge) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Put base values
|
||||
ASSERT_OK(Put(1, Key(1), "a"));
|
||||
ASSERT_OK(Put(1, Key(2), "x"));
|
||||
// Merge on top
|
||||
ASSERT_OK(Merge(1, Key(1), "b"));
|
||||
ASSERT_OK(Merge(1, Key(1), "c"));
|
||||
ASSERT_OK(Merge(1, Key(2), "y"));
|
||||
|
||||
std::vector<std::string> key_strs = {Key(1), Key(2), Key(3)};
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "a,b,c");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "x,y");
|
||||
ASSERT_TRUE(statuses[2].IsNotFound());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetBatchLookupDisabledByDefault) {
|
||||
// Verify that finger search is off by default and MultiGet still works
|
||||
Options options = CurrentOptions();
|
||||
ASSERT_FALSE(options.memtable_batch_lookup_optimization);
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "k1", "v1"));
|
||||
ASSERT_OK(Put(1, "k2", "v2"));
|
||||
|
||||
std::vector<Slice> keys = {"k1", "k2", "k3"};
|
||||
std::vector<PinnableSlice> values(3);
|
||||
std::vector<Status> statuses(3);
|
||||
|
||||
db_->MultiGet(ReadOptions(), handles_[1], 3, keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "v1");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "v2");
|
||||
ASSERT_TRUE(statuses[2].IsNotFound());
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupWithParanoid) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
options.paranoid_memory_checks = true;
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Insert sorted keys into memtable
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), "val" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Delete(1, Key(25)));
|
||||
|
||||
// MultiGet with both batch optimization and paranoid checks enabled
|
||||
std::vector<std::string> key_strs = {Key(0), Key(10), Key(25),
|
||||
Key(50), Key(99), Key(200)};
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "val0");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "val10");
|
||||
ASSERT_TRUE(statuses[2].IsNotFound()); // deleted
|
||||
ASSERT_OK(statuses[3]);
|
||||
ASSERT_EQ(values[3].ToString(), "val50");
|
||||
ASSERT_OK(statuses[4]);
|
||||
ASSERT_EQ(values[4].ToString(), "val99");
|
||||
ASSERT_TRUE(statuses[5].IsNotFound()); // never inserted
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupSnapshot) {
|
||||
#ifndef USE_COROUTINES
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
|
||||
return;
|
||||
}
|
||||
#endif // USE_COROUTINES
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, Key(1), "v1_old"));
|
||||
ASSERT_OK(Put(1, Key(2), "v2_old"));
|
||||
|
||||
const Snapshot* snap = db_->GetSnapshot();
|
||||
|
||||
// Write new values after snapshot
|
||||
ASSERT_OK(Put(1, Key(1), "v1_new"));
|
||||
ASSERT_OK(Put(1, Key(2), "v2_new"));
|
||||
ASSERT_OK(Put(1, Key(3), "v3_new"));
|
||||
|
||||
// MultiGet with snapshot should see old values
|
||||
std::vector<std::string> key_strs = {Key(1), Key(2), Key(3)};
|
||||
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
|
||||
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
std::vector<Status> statuses(keys.size());
|
||||
|
||||
ReadOptions ro;
|
||||
ro.snapshot = snap;
|
||||
ro.async_io = std::get<1>(GetParam());
|
||||
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
|
||||
statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "v1_old");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "v2_old");
|
||||
ASSERT_TRUE(statuses[2].IsNotFound()); // didn't exist at snapshot
|
||||
|
||||
db_->ReleaseSnapshot(snap);
|
||||
|
||||
// MultiGet without snapshot should see new values
|
||||
for (auto& v : values) {
|
||||
v.Reset();
|
||||
}
|
||||
db_->MultiGet(ReadOptions(), handles_[1], keys.size(), keys.data(),
|
||||
values.data(), statuses.data(), true);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0].ToString(), "v1_new");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1].ToString(), "v2_new");
|
||||
ASSERT_OK(statuses[2]);
|
||||
ASSERT_EQ(values[2].ToString(), "v3_new");
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(DBMultiGetTestWithParam, DBMultiGetTestWithParam,
|
||||
testing::Combine(testing::Bool(), testing::Bool()));
|
||||
|
||||
|
||||
@@ -466,6 +466,90 @@ TEST_F(DBBlockCacheTest, WarmCacheWithDataBlocksDuringFlush) {
|
||||
options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD));
|
||||
}
|
||||
|
||||
// Cache wrapper that tracks the priority of each Insert call.
|
||||
namespace {
|
||||
class PriorityTrackingCache : public CacheWrapper {
|
||||
public:
|
||||
explicit PriorityTrackingCache(std::shared_ptr<Cache> target)
|
||||
: CacheWrapper(std::move(target)) {}
|
||||
|
||||
const char* Name() const override { return "PriorityTrackingCache"; }
|
||||
|
||||
Status Insert(const Slice& key, ObjectPtr value,
|
||||
const CacheItemHelper* helper, size_t charge,
|
||||
Handle** handle = nullptr, Priority priority = Priority::LOW,
|
||||
const Slice& compressed_value = Slice(),
|
||||
CompressionType type = kNoCompression) override {
|
||||
insert_priorities_.push_back(priority);
|
||||
return CacheWrapper::Insert(key, value, helper, charge, handle, priority,
|
||||
compressed_value, type);
|
||||
}
|
||||
|
||||
void ResetPriorities() { insert_priorities_.clear(); }
|
||||
|
||||
bool HasPriority(Priority p) const {
|
||||
return std::find(insert_priorities_.begin(), insert_priorities_.end(), p) !=
|
||||
insert_priorities_.end();
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<Priority> insert_priorities_;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_F(DBBlockCacheTest, WarmCacheWithDataBlocksDuringCompaction) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
auto tracking_cache =
|
||||
std::make_shared<PriorityTrackingCache>(NewLRUCache(1 << 25, 0, false));
|
||||
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.block_cache = tracking_cache;
|
||||
table_options.cache_index_and_filter_blocks = false;
|
||||
table_options.prepopulate_block_cache =
|
||||
BlockBasedTableOptions::PrepopulateBlockCache::kFlushAndCompaction;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
DestroyAndReopen(options);
|
||||
|
||||
std::string value(kValueSize, 'a');
|
||||
|
||||
// Flush warming: inserts should use LOW priority.
|
||||
tracking_cache->ResetPriorities();
|
||||
ASSERT_OK(Put("key", value));
|
||||
ASSERT_OK(Flush());
|
||||
EXPECT_TRUE(tracking_cache->HasPriority(Cache::Priority::LOW));
|
||||
EXPECT_FALSE(tracking_cache->HasPriority(Cache::Priority::BOTTOM));
|
||||
|
||||
// Write overlapping key to force a real merge compaction (not trivial move).
|
||||
ASSERT_OK(Put("key", value + "2"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
auto data_add_before =
|
||||
options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD);
|
||||
|
||||
// Compaction warming: data block inserts should use BOTTOM priority.
|
||||
// Internal cache bookkeeping (e.g., cache entry stats) may insert at HIGH,
|
||||
// so we check for BOTTOM presence and LOW absence.
|
||||
tracking_cache->ResetPriorities();
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForceOptimized;
|
||||
ASSERT_OK(db_->CompactRange(cro, /*begin=*/nullptr, /*end=*/nullptr));
|
||||
EXPECT_GT(options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD),
|
||||
data_add_before);
|
||||
EXPECT_TRUE(tracking_cache->HasPriority(Cache::Priority::BOTTOM));
|
||||
EXPECT_FALSE(tracking_cache->HasPriority(Cache::Priority::LOW));
|
||||
|
||||
// Compaction output is in cache — reads should have zero misses.
|
||||
auto data_miss_before =
|
||||
options.statistics->getTickerCount(BLOCK_CACHE_DATA_MISS);
|
||||
ASSERT_EQ(value + "2", Get("key"));
|
||||
EXPECT_EQ(data_miss_before,
|
||||
options.statistics->getTickerCount(BLOCK_CACHE_DATA_MISS));
|
||||
}
|
||||
|
||||
// This test cache data, index and filter blocks during flush.
|
||||
class DBBlockCacheTest1 : public DBTestBase,
|
||||
public ::testing::WithParamInterface<uint32_t> {
|
||||
@@ -620,6 +704,36 @@ TEST_F(DBBlockCacheTest, DynamicOptions) {
|
||||
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
|
||||
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
|
||||
|
||||
// Switch to kFlushAndCompaction
|
||||
++i;
|
||||
ASSERT_OK(dbfull()->SetOptions(
|
||||
{{"block_based_table_factory",
|
||||
"{prepopulate_block_cache=kFlushAndCompaction;}"}}));
|
||||
|
||||
ASSERT_OK(Put(std::to_string(i), value));
|
||||
ASSERT_OK(Flush());
|
||||
// Flush warming still works
|
||||
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
|
||||
|
||||
ASSERT_EQ(value, Get(std::to_string(i)));
|
||||
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
|
||||
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
|
||||
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
|
||||
|
||||
// Switch back to kDisable
|
||||
++i;
|
||||
ASSERT_OK(dbfull()->SetOptions(
|
||||
{{"block_based_table_factory", "{prepopulate_block_cache=kDisable;}"}}));
|
||||
|
||||
ASSERT_OK(Put(std::to_string(i), value));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
|
||||
|
||||
ASSERT_EQ(value, Get(std::to_string(i)));
|
||||
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
|
||||
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
|
||||
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
|
||||
|
||||
++i;
|
||||
// NOT YET SUPPORTED
|
||||
// FIXME: find a way to make this fail again (until well supported)
|
||||
|
||||
+305
-10
@@ -14,11 +14,13 @@
|
||||
#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"
|
||||
@@ -797,6 +799,13 @@ 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);
|
||||
@@ -815,6 +824,7 @@ 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
|
||||
@@ -11488,20 +11498,24 @@ 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.table_reader->GetTableProperties()->newest_key_time;
|
||||
uint64_t first_newest_key_time = file_metadatas[0]
|
||||
->fd.pinned_reader.Get()
|
||||
->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.table_reader->GetTableProperties()
|
||||
->fd.pinned_reader.Get()
|
||||
->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.table_reader->GetTableProperties()
|
||||
->fd.pinned_reader.Get()
|
||||
->GetTableProperties()
|
||||
->creation_time);
|
||||
}
|
||||
// The delta between the first and last newest_key_times is 15s
|
||||
@@ -11516,14 +11530,18 @@ 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.table_reader->GetTableProperties()->newest_key_time,
|
||||
first_newest_key_time);
|
||||
ASSERT_EQ(file_metadatas[0]
|
||||
->fd.pinned_reader.Get()
|
||||
->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.table_reader->GetTableProperties()->creation_time,
|
||||
last_newest_key_time);
|
||||
ASSERT_EQ(file_metadatas[0]
|
||||
->fd.pinned_reader.Get()
|
||||
->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
|
||||
@@ -11663,6 +11681,283 @@ 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) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#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"
|
||||
@@ -33,6 +34,17 @@ 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,
|
||||
@@ -1055,6 +1067,7 @@ TEST_F(DBFlushTest, MemPurgeBasic) {
|
||||
ASSERT_EQ(Get(RNDKEY2), p_rv2);
|
||||
ASSERT_EQ(Get(RNDKEY3), p_rv3);
|
||||
|
||||
ASSERT_OK(WaitForFlushCallbacks());
|
||||
Close();
|
||||
}
|
||||
|
||||
@@ -1169,6 +1182,7 @@ 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
|
||||
@@ -1425,6 +1439,7 @@ TEST_F(DBFlushTest, MemPurgeDeleteAndDeleteRange) {
|
||||
delete iter;
|
||||
}
|
||||
|
||||
ASSERT_OK(WaitForFlushCallbacks());
|
||||
Close();
|
||||
}
|
||||
|
||||
@@ -1879,6 +1894,85 @@ 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;
|
||||
@@ -3706,6 +3800,55 @@ 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) {
|
||||
|
||||
@@ -91,8 +91,21 @@ Status CompactedDBImpl::Get(const ReadOptions& _read_options,
|
||||
/*b_has_ts=*/false) < 0) {
|
||||
return Status::NotFound();
|
||||
}
|
||||
Status s = f.fd.table_reader->Get(read_options, lkey.internal_key(),
|
||||
&get_context, nullptr);
|
||||
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);
|
||||
}
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
return s;
|
||||
}
|
||||
@@ -152,7 +165,6 @@ 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);
|
||||
@@ -162,38 +174,40 @@ void CompactedDBImpl::MultiGet(const ReadOptions& _read_options,
|
||||
ExtractUserKeyAndStripTimestamp(f.smallest_key,
|
||||
user_comparator_->timestamp_size()),
|
||||
/*b_has_ts=*/false) < 0) {
|
||||
reader_list.push_back(nullptr);
|
||||
} else {
|
||||
f.fd.table_reader->Prepare(lkey.internal_key());
|
||||
reader_list.push_back(f.fd.table_reader);
|
||||
statuses[i] = Status::NotFound();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
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 ? ×tamps[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;
|
||||
PinnableSlice& pinnable_val = values[i];
|
||||
std::string* timestamp = timestamps ? ×tamps[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;
|
||||
} else {
|
||||
if (get_context.State() == GetContext::kFound) {
|
||||
statuses[i] = Status::OK();
|
||||
} else {
|
||||
if (get_context.State() == GetContext::kFound) {
|
||||
statuses[idx] = Status::OK();
|
||||
}
|
||||
statuses[i] = Status::NotFound();
|
||||
}
|
||||
}
|
||||
++idx;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +244,7 @@ Status CompactedDBImpl::Init(const Options& options) {
|
||||
return Status::NotSupported("Both L0 and other level contain files");
|
||||
}
|
||||
files_ = l0;
|
||||
files_level_ = 0;
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -242,6 +257,7 @@ 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");
|
||||
@@ -261,6 +277,13 @@ 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()) {
|
||||
|
||||
@@ -143,5 +143,6 @@ class CompactedDBImpl : public DBImpl {
|
||||
Version* version_;
|
||||
const Comparator* user_comparator_;
|
||||
LevelFilesBrief files_;
|
||||
int files_level_{0};
|
||||
};
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+108
-14
@@ -169,6 +169,7 @@ 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_)),
|
||||
@@ -472,7 +473,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_flush_scheduled_ || bg_pressure_callback_in_progress_) {
|
||||
bg_cv_.Wait();
|
||||
}
|
||||
}
|
||||
@@ -561,11 +562,18 @@ 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();
|
||||
@@ -607,7 +615,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_) {
|
||||
if (opened_successfully_ && !read_only_) {
|
||||
JobContext job_context(next_job_id_.fetch_add(1));
|
||||
FindObsoleteFiles(&job_context, true);
|
||||
|
||||
@@ -784,7 +792,76 @@ 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;
|
||||
@@ -792,7 +869,7 @@ Status DBImpl::StartPeriodicTaskScheduler() {
|
||||
"DBImpl::StartPeriodicTaskScheduler:DisableScheduler",
|
||||
&disable_scheduler);
|
||||
if (disable_scheduler) {
|
||||
return Status::OK();
|
||||
return s;
|
||||
}
|
||||
|
||||
{
|
||||
@@ -803,7 +880,7 @@ Status DBImpl::StartPeriodicTaskScheduler() {
|
||||
|
||||
#endif // !NDEBUG
|
||||
if (mutable_db_options_.stats_dump_period_sec > 0) {
|
||||
Status s = periodic_task_scheduler_.Register(
|
||||
s = periodic_task_scheduler_.Register(
|
||||
PeriodicTaskType::kDumpStats,
|
||||
periodic_task_functions_.at(PeriodicTaskType::kDumpStats),
|
||||
mutable_db_options_.stats_dump_period_sec,
|
||||
@@ -813,7 +890,7 @@ Status DBImpl::StartPeriodicTaskScheduler() {
|
||||
}
|
||||
}
|
||||
if (mutable_db_options_.stats_persist_period_sec > 0) {
|
||||
Status s = periodic_task_scheduler_.Register(
|
||||
s = periodic_task_scheduler_.Register(
|
||||
PeriodicTaskType::kPersistStats,
|
||||
periodic_task_functions_.at(PeriodicTaskType::kPersistStats),
|
||||
mutable_db_options_.stats_persist_period_sec,
|
||||
@@ -823,18 +900,19 @@ Status DBImpl::StartPeriodicTaskScheduler() {
|
||||
}
|
||||
}
|
||||
|
||||
Status s = periodic_task_scheduler_.Register(
|
||||
s = periodic_task_scheduler_.Register(
|
||||
PeriodicTaskType::kFlushInfoLog,
|
||||
periodic_task_functions_.at(PeriodicTaskType::kFlushInfoLog),
|
||||
/*run_immediately=*/true);
|
||||
|
||||
if (s.ok()) {
|
||||
s = periodic_task_scheduler_.Register(
|
||||
PeriodicTaskType::kTriggerCompaction,
|
||||
periodic_task_functions_.at(PeriodicTaskType::kTriggerCompaction),
|
||||
/*run_immediately=*/false);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
s = periodic_task_scheduler_.Register(
|
||||
PeriodicTaskType::kTriggerCompaction,
|
||||
periodic_task_functions_.at(PeriodicTaskType::kTriggerCompaction),
|
||||
ComputeTriggerCompactionPeriod(), /*run_immediately=*/false);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -1471,6 +1549,13 @@ 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 !=
|
||||
@@ -6916,8 +7001,17 @@ void DBImpl::TriggerPeriodicCompaction() {
|
||||
if (cfd->IsDropped()) {
|
||||
continue;
|
||||
}
|
||||
if (cfd->GetLatestCFOptions().periodic_compaction_seconds &&
|
||||
!cfd->queued_for_compaction()) {
|
||||
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);
|
||||
cfd->current()->storage_info()->ComputeCompactionScore(
|
||||
cfd->ioptions(), cfd->GetLatestMutableCFOptions(),
|
||||
cfd->GetFullHistoryTsLow());
|
||||
|
||||
+45
-5
@@ -1248,6 +1248,7 @@ 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;
|
||||
@@ -1366,6 +1367,7 @@ 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
|
||||
@@ -1718,6 +1720,17 @@ 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
|
||||
@@ -1728,8 +1741,12 @@ 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;
|
||||
@@ -2150,7 +2167,7 @@ class DBImpl : public DB {
|
||||
Status InsertLogRecordToMemtable(WriteBatch* batch_to_use,
|
||||
uint64_t wal_number,
|
||||
SequenceNumber* next_sequence,
|
||||
bool* has_valid_writes);
|
||||
bool* has_valid_writes, bool read_only);
|
||||
|
||||
Status MaybeWriteLevel0TableForRecovery(
|
||||
bool has_valid_writes, bool read_only, uint64_t wal_number, int job_id,
|
||||
@@ -2416,6 +2433,9 @@ 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
|
||||
@@ -2506,6 +2526,13 @@ 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();
|
||||
|
||||
@@ -3051,6 +3078,9 @@ 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;
|
||||
|
||||
@@ -3060,6 +3090,19 @@ 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
|
||||
@@ -3115,9 +3158,6 @@ 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;
|
||||
@@ -3185,7 +3225,7 @@ class DBImpl : public DB {
|
||||
// installed to MANIFEST first.
|
||||
InstrumentedCondVar atomic_flush_install_cv_;
|
||||
|
||||
bool wal_in_db_path_;
|
||||
bool wal_in_db_path_ = false;
|
||||
std::atomic<uint64_t> max_total_wal_size_;
|
||||
|
||||
BlobFileCompletionCallback blob_callback_;
|
||||
|
||||
@@ -3021,7 +3021,7 @@ void DBImpl::ResumeAllCompactions() {
|
||||
void DBImpl::MaybeScheduleFlushOrCompaction() {
|
||||
mutex_.AssertHeld();
|
||||
TEST_SYNC_POINT("DBImpl::MaybeScheduleFlushOrCompaction:Start");
|
||||
if (!opened_successfully_) {
|
||||
if (!opened_successfully_ || read_only_) {
|
||||
// Compaction may introduce data race to DB open
|
||||
return;
|
||||
}
|
||||
@@ -3145,6 +3145,93 @@ 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();
|
||||
@@ -3580,6 +3667,9 @@ 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
|
||||
@@ -3604,6 +3694,9 @@ 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(
|
||||
@@ -3686,11 +3779,15 @@ 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
|
||||
|
||||
@@ -282,6 +282,11 @@ 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();
|
||||
|
||||
@@ -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->table_reader_handle;
|
||||
auto* handle = file.metadata->fd.pinned_reader.GetCacheHandle();
|
||||
if (file.only_delete_metadata) {
|
||||
if (handle) {
|
||||
// Simply release handle of file that is not being deleted
|
||||
|
||||
@@ -321,6 +321,7 @@ Status DB::OpenAsFollower(
|
||||
sv_context.NewSuperVersion();
|
||||
cfd->InstallSuperVersion(&sv_context, &impl->mutex_);
|
||||
}
|
||||
impl->MarkAsyncFileOpenNotNeeded();
|
||||
}
|
||||
impl->mutex_.Unlock();
|
||||
sv_context.Clean();
|
||||
|
||||
+158
-3
@@ -12,6 +12,7 @@
|
||||
#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"
|
||||
@@ -1492,8 +1493,8 @@ Status DBImpl::ProcessLogRecord(
|
||||
}
|
||||
|
||||
assert(process_status.ok());
|
||||
process_status = InsertLogRecordToMemtable(batch_to_use, wal_number,
|
||||
next_sequence, &has_valid_writes);
|
||||
process_status = InsertLogRecordToMemtable(
|
||||
batch_to_use, wal_number, next_sequence, &has_valid_writes, read_only);
|
||||
MaybeIgnoreError(&process_status);
|
||||
// We are treating this as a failure while reading since we read valid
|
||||
// blocks that do not form coherent data
|
||||
@@ -1571,7 +1572,8 @@ void DBImpl::MaybeReviseStopReplayForCorruption(
|
||||
Status DBImpl::InsertLogRecordToMemtable(WriteBatch* batch_to_use,
|
||||
uint64_t wal_number,
|
||||
SequenceNumber* next_sequence,
|
||||
bool* has_valid_writes) {
|
||||
bool* has_valid_writes,
|
||||
bool read_only) {
|
||||
// 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 --
|
||||
@@ -1584,6 +1586,34 @@ 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;
|
||||
}
|
||||
|
||||
@@ -2657,6 +2687,10 @@ 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();
|
||||
}
|
||||
|
||||
@@ -2705,4 +2739,125 @@ 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
|
||||
@@ -357,6 +357,12 @@ 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();
|
||||
|
||||
@@ -43,7 +43,6 @@ 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_,
|
||||
@@ -61,24 +60,12 @@ Status DBImplSecondary::Recover(
|
||||
max_total_in_memory_state_ += mutable_cf_options.write_buffer_size *
|
||||
mutable_cf_options.max_write_buffer_number;
|
||||
}
|
||||
if (s.ok()) {
|
||||
default_cf_handle_ = new ColumnFamilyHandleImpl(
|
||||
versions_->GetColumnFamilySet()->GetDefault(), this, &mutex_);
|
||||
default_cf_internal_stats_ = default_cf_handle_->cfd()->internal_stats();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -88,6 +75,7 @@ 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);
|
||||
@@ -740,6 +728,17 @@ 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);
|
||||
@@ -784,7 +783,21 @@ Status DB::OpenAsSecondary(
|
||||
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 =
|
||||
@@ -802,9 +815,11 @@ Status DB::OpenAsSecondary(
|
||||
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) {
|
||||
@@ -1538,11 +1553,13 @@ Status DB::OpenAndCompact(
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Open db As Secondary
|
||||
// 5. Open db As Secondary (skip WAL recovery — remote compaction only
|
||||
// needs LSM state from MANIFEST, not memtable data from WAL replay)
|
||||
std::unique_ptr<DB> db;
|
||||
std::vector<ColumnFamilyHandle*> handles;
|
||||
s = DB::OpenAsSecondary(db_options, name, output_directory, column_families,
|
||||
&handles, &db);
|
||||
s = DBImplSecondary::OpenAsSecondaryImpl(db_options, name, output_directory,
|
||||
column_families, &handles, &db,
|
||||
/*recover_wal=*/false);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -78,8 +78,9 @@ class DBImplSecondary : public DBImpl {
|
||||
std::string secondary_path);
|
||||
~DBImplSecondary() override;
|
||||
|
||||
// Recover by replaying MANIFEST and WAL. Also initialize manifest_reader_
|
||||
// and log_readers_ to facilitate future operations.
|
||||
// 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).
|
||||
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,
|
||||
@@ -194,10 +195,11 @@ class DBImplSecondary : public DBImpl {
|
||||
return Status::NotSupported("Not supported operation in secondary mode.");
|
||||
}
|
||||
|
||||
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.");
|
||||
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 */);
|
||||
}
|
||||
|
||||
using DBImpl::Flush;
|
||||
@@ -384,6 +386,17 @@ 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_;
|
||||
|
||||
@@ -1827,6 +1827,60 @@ 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
@@ -1535,7 +1535,7 @@ TEST_F(DBSSTTest, OpenDBWithInfiniteMaxOpenFiles) {
|
||||
|
||||
for (const auto& level : files) {
|
||||
for (const auto& file : level) {
|
||||
ASSERT_TRUE(file.table_reader_handle != nullptr);
|
||||
ASSERT_TRUE(file.fd.pinned_reader.Get() != nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+593
@@ -37,6 +37,7 @@
|
||||
#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"
|
||||
@@ -7762,6 +7763,598 @@ 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) {
|
||||
|
||||
@@ -94,6 +94,44 @@ 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 {
|
||||
|
||||
@@ -675,6 +675,28 @@ 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));
|
||||
}
|
||||
|
||||
@@ -1177,6 +1177,12 @@ 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
@@ -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.table_reader_handle == nullptr);
|
||||
ASSERT_TRUE(file.fd.pinned_reader.Get() == nullptr);
|
||||
} else {
|
||||
ASSERT_TRUE(file.table_reader_handle != nullptr);
|
||||
ASSERT_TRUE(file.fd.pinned_reader.Get() != nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -913,6 +913,270 @@ 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());
|
||||
|
||||
|
||||
@@ -201,6 +201,25 @@ 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>
|
||||
@@ -225,6 +244,11 @@ 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() {
|
||||
|
||||
+9
-2
@@ -616,10 +616,17 @@ 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.
|
||||
uint64_t new_mem_id = mems_.back()->GetID();
|
||||
// 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*/));
|
||||
|
||||
new_mem->SetID(new_mem_id);
|
||||
// Take the latest memtable's next log number.
|
||||
|
||||
@@ -1643,6 +1643,129 @@ 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) {
|
||||
|
||||
+197
-51
@@ -72,7 +72,9 @@ 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) {}
|
||||
mutable_cf_options.memtable_veirfy_per_key_checksum_on_seek),
|
||||
memtable_batch_lookup_optimization(
|
||||
ioptions.memtable_batch_lookup_optimization) {}
|
||||
|
||||
MemTable::MemTable(const InternalKeyComparator& cmp,
|
||||
const ImmutableOptions& ioptions,
|
||||
@@ -1575,65 +1577,179 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
}
|
||||
}
|
||||
}
|
||||
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());
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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());
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
|
||||
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;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1841,6 +1957,36 @@ 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();
|
||||
|
||||
@@ -65,6 +65,7 @@ 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.
|
||||
@@ -599,6 +600,11 @@ 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,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "db/periodic_task_scheduler.h"
|
||||
|
||||
#include "rocksdb/system_clock.h"
|
||||
#include "test_util/sync_point.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -26,7 +27,7 @@ static const std::map<PeriodicTaskType, uint64_t> kDefaultPeriodSeconds = {
|
||||
{PeriodicTaskType::kPersistStats, kInvalidPeriodSec},
|
||||
{PeriodicTaskType::kFlushInfoLog, 10},
|
||||
{PeriodicTaskType::kRecordSeqnoTime, kInvalidPeriodSec},
|
||||
{PeriodicTaskType::kTriggerCompaction, 12 * 60 * 60} // 12 hours
|
||||
{PeriodicTaskType::kTriggerCompaction, kInvalidPeriodSec},
|
||||
};
|
||||
|
||||
static const std::map<PeriodicTaskType, std::string> kPeriodicTaskTypeNames = {
|
||||
@@ -85,6 +86,14 @@ 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
|
||||
#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"
|
||||
@@ -124,15 +127,289 @@ TEST_F(PeriodicTaskSchedulerTest, Basic) {
|
||||
ASSERT_EQ(3, pst_st_counter);
|
||||
ASSERT_EQ(5, flush_info_log_counter);
|
||||
|
||||
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);
|
||||
// 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";
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
+141
-135
@@ -16,6 +16,7 @@
|
||||
#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"
|
||||
@@ -72,6 +73,8 @@ 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),
|
||||
@@ -169,15 +172,29 @@ 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, 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) {
|
||||
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);
|
||||
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);
|
||||
@@ -185,39 +202,71 @@ 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) {
|
||||
return Status::Incomplete("Table not found in table_cache, no_io is set");
|
||||
s = Status::Incomplete("Table not found in table_cache, no_io is set");
|
||||
return s;
|
||||
}
|
||||
MutexLock load_lock(&loader_mutex_.Get(key));
|
||||
// We check the cache again under loading mutex
|
||||
*handle = cache_.Lookup(key);
|
||||
if (*handle != nullptr) {
|
||||
return Status::OK();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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();
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
return s;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
} 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 Status::OK();
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
InternalIterator* TableCache::NewIterator(
|
||||
@@ -230,7 +279,8 @@ 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) {
|
||||
std::unique_ptr<TruncatedRangeDelIterator>* range_del_iter,
|
||||
bool maybe_pin_table_handle) {
|
||||
PERF_TIMER_GUARD(new_table_iterator_nanos);
|
||||
|
||||
Status s;
|
||||
@@ -240,19 +290,15 @@ InternalIterator* TableCache::NewIterator(
|
||||
*table_reader_ptr = nullptr;
|
||||
}
|
||||
bool for_compaction = caller == TableReaderCaller::kCompaction;
|
||||
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);
|
||||
}
|
||||
}
|
||||
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_);
|
||||
InternalIterator* result = nullptr;
|
||||
if (s.ok()) {
|
||||
if (options.table_filter &&
|
||||
@@ -293,7 +339,7 @@ InternalIterator* TableCache::NewIterator(
|
||||
}
|
||||
}
|
||||
if (range_del_agg != nullptr) {
|
||||
if (range_del_agg->AddFile(fd.GetNumber())) {
|
||||
if (range_del_agg->AddFile(file_meta.fd.GetNumber())) {
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> new_range_del_iter(
|
||||
static_cast<FragmentedRangeTombstoneIterator*>(
|
||||
table_reader->NewRangeTombstoneIterator(options)));
|
||||
@@ -332,17 +378,11 @@ 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 = fd.table_reader;
|
||||
TableReader* t = nullptr;
|
||||
TypedHandle* handle = nullptr;
|
||||
if (t == nullptr) {
|
||||
s = FindTable(options, file_options_, internal_comparator, file_meta,
|
||||
&handle, mutable_cf_options);
|
||||
if (s.ok()) {
|
||||
t = cache_.Value(handle);
|
||||
}
|
||||
}
|
||||
s = FindTable(options, file_options_, internal_comparator, file_meta, &handle,
|
||||
mutable_cf_options, &t);
|
||||
if (s.ok()) {
|
||||
// Note: NewRangeTombstoneIterator could return nullptr
|
||||
out_iter->reset(t->NewRangeTombstoneIterator(options));
|
||||
@@ -456,20 +496,18 @@ Status TableCache::Get(const ReadOptions& options,
|
||||
row_cache_entry = &row_cache_entry_buffer;
|
||||
}
|
||||
}
|
||||
TableReader* t = fd.table_reader;
|
||||
TEST_SYNC_POINT_CALLBACK("TableCache::Get::BeforeFindTable",
|
||||
const_cast<FileDescriptor*>(&fd));
|
||||
TableReader* t = nullptr;
|
||||
TypedHandle* handle = nullptr;
|
||||
if (s.ok() && !done) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
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_);
|
||||
SequenceNumber* max_covering_tombstone_seq =
|
||||
get_context->max_covering_tombstone_seq();
|
||||
if (s.ok() && max_covering_tombstone_seq != nullptr &&
|
||||
@@ -546,8 +584,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** table_handle) {
|
||||
auto& fd = file_meta.fd;
|
||||
MultiGetContext::Range* mget_range, TypedHandle** handle) {
|
||||
assert(*handle == nullptr);
|
||||
IterKey row_cache_key;
|
||||
std::string row_cache_entry_buffer;
|
||||
|
||||
@@ -559,23 +597,17 @@ Status TableCache::MultiGetFilter(
|
||||
return Status::NotSupported();
|
||||
}
|
||||
Status s;
|
||||
TableReader* t = fd.table_reader;
|
||||
TypedHandle* handle = nullptr;
|
||||
TableReader* t = nullptr;
|
||||
MultiGetContext::Range tombstone_range(*mget_range, mget_range->begin(),
|
||||
mget_range->end());
|
||||
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;
|
||||
}
|
||||
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 (s.ok()) {
|
||||
s = t->MultiGetFilter(options, mutable_cf_options.prefix_extractor.get(),
|
||||
mget_range);
|
||||
@@ -586,9 +618,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);
|
||||
*table_handle = nullptr;
|
||||
if (mget_range->empty() && *handle) {
|
||||
cache_.Release(*handle);
|
||||
*handle = nullptr;
|
||||
}
|
||||
|
||||
return s;
|
||||
@@ -600,24 +632,18 @@ 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;
|
||||
Status s = FindTable(read_options, file_options, internal_comparator,
|
||||
file_meta, &table_handle, mutable_cf_options, no_io);
|
||||
TableReader* table = nullptr;
|
||||
Status s =
|
||||
FindTable(read_options, file_options, internal_comparator, file_meta,
|
||||
&table_handle, mutable_cf_options, &table, no_io);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
assert(table_handle);
|
||||
auto table = cache_.Value(table_handle);
|
||||
*properties = table->GetTableProperties();
|
||||
cache_.Release(table_handle);
|
||||
if (table_handle) {
|
||||
cache_.Release(table_handle);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -627,15 +653,10 @@ Status TableCache::ApproximateKeyAnchors(
|
||||
|
||||
std::vector<TableReader::Anchor>& anchors) {
|
||||
Status s;
|
||||
TableReader* t = file_meta.fd.table_reader;
|
||||
TableReader* t = nullptr;
|
||||
TypedHandle* handle = nullptr;
|
||||
if (t == nullptr) {
|
||||
s = FindTable(ro, file_options_, internal_comparator, file_meta, &handle,
|
||||
mutable_cf_options);
|
||||
if (s.ok()) {
|
||||
t = cache_.Value(handle);
|
||||
}
|
||||
}
|
||||
s = FindTable(ro, file_options_, internal_comparator, file_meta, &handle,
|
||||
mutable_cf_options, &t);
|
||||
if (s.ok() && t != nullptr) {
|
||||
s = t->ApproximateKeyAnchors(ro, anchors);
|
||||
}
|
||||
@@ -649,23 +670,18 @@ 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, true /* no_io */);
|
||||
&table_handle, mutable_cf_options, &table, true /* no_io */);
|
||||
if (!s.ok()) {
|
||||
return 0;
|
||||
}
|
||||
assert(table_handle);
|
||||
auto table = cache_.Value(table_handle);
|
||||
auto ret = table->ApproximateMemoryUsage();
|
||||
cache_.Release(table_handle);
|
||||
if (table_handle) {
|
||||
cache_.Release(table_handle);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -679,18 +695,13 @@ uint64_t TableCache::ApproximateOffsetOf(
|
||||
const InternalKeyComparator& internal_comparator,
|
||||
const MutableCFOptions& mutable_cf_options) {
|
||||
uint64_t result = 0;
|
||||
TableReader* table_reader = file_meta.fd.table_reader;
|
||||
TableReader* table_reader = nullptr;
|
||||
TypedHandle* table_handle = nullptr;
|
||||
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);
|
||||
}
|
||||
}
|
||||
Status s =
|
||||
FindTable(read_options, file_options_, internal_comparator, file_meta,
|
||||
&table_handle, mutable_cf_options, &table_reader);
|
||||
|
||||
if (table_reader != nullptr) {
|
||||
if (s.ok() && table_reader != nullptr) {
|
||||
result = table_reader->ApproximateOffsetOf(read_options, key, caller);
|
||||
}
|
||||
if (table_handle != nullptr) {
|
||||
@@ -706,18 +717,13 @@ uint64_t TableCache::ApproximateSize(
|
||||
const InternalKeyComparator& internal_comparator,
|
||||
const MutableCFOptions& mutable_cf_options) {
|
||||
uint64_t result = 0;
|
||||
TableReader* table_reader = file_meta.fd.table_reader;
|
||||
TableReader* table_reader = nullptr;
|
||||
TypedHandle* table_handle = nullptr;
|
||||
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);
|
||||
}
|
||||
}
|
||||
Status s =
|
||||
FindTable(read_options, file_options_, internal_comparator, file_meta,
|
||||
&table_handle, mutable_cf_options, &table_reader);
|
||||
|
||||
if (table_reader != nullptr) {
|
||||
if (s.ok() && table_reader != nullptr) {
|
||||
result = table_reader->ApproximateSize(read_options, start, end, caller);
|
||||
}
|
||||
if (table_handle != nullptr) {
|
||||
|
||||
+30
-6
@@ -99,7 +99,8 @@ 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);
|
||||
std::unique_ptr<TruncatedRangeDelIterator>* range_del_iter = nullptr,
|
||||
bool maybe_pin_table_handle = false);
|
||||
|
||||
// If a seek to internal key "k" in specified file finds an entry,
|
||||
// call get_context->SaveValue() repeatedly until
|
||||
@@ -172,19 +173,31 @@ class TableCache {
|
||||
// Return handle to an existing cache entry if there is one
|
||||
static Cache::Handle* Lookup(Cache* cache, uint64_t file_number);
|
||||
|
||||
// Find table reader
|
||||
// @param skip_filters Disables loading/accessing the filter block
|
||||
// @param level == -1 means not specified
|
||||
// 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.
|
||||
Status FindTable(const ReadOptions& ro, const FileOptions& toptions,
|
||||
const InternalKeyComparator& internal_comparator,
|
||||
const FileMetaData& file_meta, TypedHandle**,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
const bool no_io = false,
|
||||
TableReader** table_reader, 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);
|
||||
Temperature file_temperature = Temperature::kUnknown,
|
||||
bool pin_table_handle = false);
|
||||
|
||||
// Get the table properties of a given table.
|
||||
// @no_io: indicates if we should load table to the cache if it is not present
|
||||
@@ -231,6 +244,8 @@ 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;
|
||||
@@ -243,6 +258,14 @@ 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,
|
||||
@@ -283,6 +306,7 @@ 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_;
|
||||
|
||||
@@ -21,7 +21,9 @@ 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;
|
||||
TableReader* t = fd.table_reader;
|
||||
TEST_SYNC_POINT_CALLBACK("TableCache::MultiGet::BeforeFindTable",
|
||||
const_cast<FileDescriptor*>(&fd));
|
||||
TableReader* t = fd.pinned_reader.Get();
|
||||
MultiGetRange table_range(*mget_range, mget_range->begin(),
|
||||
mget_range->end());
|
||||
if (handle != nullptr && t == nullptr) {
|
||||
@@ -70,16 +72,14 @@ 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,
|
||||
&handle, mutable_cf_options, &t,
|
||||
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);
|
||||
0 /*max_file_size_for_l0_meta_pin*/, file_meta.temperature,
|
||||
should_pin_table_handles_);
|
||||
TEST_SYNC_POINT_CALLBACK("TableCache::MultiGet:FindTable", &s);
|
||||
if (s.ok()) {
|
||||
t = cache_.Value(handle);
|
||||
assert(t);
|
||||
}
|
||||
assert(!s.ok() || t);
|
||||
}
|
||||
if (s.ok() && !options.ignore_range_deletions && !skip_range_deletions) {
|
||||
UpdateRangeTombstoneSeqnums(options, t, table_range);
|
||||
|
||||
+11
-50
@@ -32,8 +32,10 @@
|
||||
#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 {
|
||||
@@ -430,12 +432,11 @@ class VersionBuilder::Rep {
|
||||
void UnrefFile(FileMetaData* f) {
|
||||
f->refs--;
|
||||
if (f->refs <= 0) {
|
||||
if (f->table_reader_handle) {
|
||||
if (f->fd.pinned_reader.Get() != nullptr) {
|
||||
assert(table_cache_ != nullptr);
|
||||
// NOTE: have to release in raw cache interface to avoid using a
|
||||
// TypedHandle for FileMetaData::table_reader_handle
|
||||
table_cache_->get_cache().get()->Release(f->table_reader_handle);
|
||||
f->table_reader_handle = nullptr;
|
||||
// TypedHandle for PinnedTableReader's internal handle
|
||||
f->fd.pinned_reader.Release(table_cache_->get_cache().get());
|
||||
}
|
||||
|
||||
if (file_metadata_cache_res_mgr_) {
|
||||
@@ -1680,7 +1681,6 @@ 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,9 +1690,8 @@ class VersionBuilder::Rep {
|
||||
continue;
|
||||
}
|
||||
// If the file has been opened before, just skip it.
|
||||
if (!file_meta->table_reader_handle) {
|
||||
if (file_meta->fd.pinned_reader.Get() == nullptr) {
|
||||
files_meta.emplace_back(file_meta, level);
|
||||
statuses.emplace_back(Status::OK());
|
||||
}
|
||||
if (files_meta.size() >= max_load) {
|
||||
break;
|
||||
@@ -1703,49 +1702,11 @@ class VersionBuilder::Rep {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -53,7 +53,8 @@ class VersionBuilder {
|
||||
// Save the current Version to the provided `vstorage`.
|
||||
Status SaveTo(VersionStorageInfo* vstorage) const;
|
||||
|
||||
// Load all the table handlers for the current Version in the builder.
|
||||
// Load table handlers for newly added files in the builder. This does not
|
||||
// load any files in the base storage.
|
||||
Status LoadTableHandlers(InternalStats* internal_stats, int max_threads,
|
||||
bool prefetch_index_and_filter_in_cache,
|
||||
bool is_initial_load,
|
||||
|
||||
@@ -22,6 +22,40 @@ 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));
|
||||
|
||||
+64
-18
@@ -134,14 +134,57 @@ 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 {
|
||||
// Table reader in table_reader_handle
|
||||
TableReader* table_reader;
|
||||
// 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;
|
||||
uint64_t packed_number_and_path_id;
|
||||
uint64_t file_size; // File size in bytes
|
||||
SequenceNumber smallest_seqno; // The smallest seqno in this file
|
||||
@@ -154,7 +197,7 @@ struct FileDescriptor {
|
||||
|
||||
FileDescriptor(uint64_t number, uint32_t path_id, uint64_t _file_size,
|
||||
SequenceNumber _smallest_seqno, SequenceNumber _largest_seqno)
|
||||
: table_reader(nullptr),
|
||||
: pinned_reader(),
|
||||
packed_number_and_path_id(PackFileNumberAndPathId(number, path_id)),
|
||||
file_size(_file_size),
|
||||
smallest_seqno(_smallest_seqno),
|
||||
@@ -163,7 +206,7 @@ struct FileDescriptor {
|
||||
FileDescriptor(const FileDescriptor& fd) { *this = fd; }
|
||||
|
||||
FileDescriptor& operator=(const FileDescriptor& fd) {
|
||||
table_reader = fd.table_reader;
|
||||
pinned_reader = fd.pinned_reader;
|
||||
packed_number_and_path_id = fd.packed_number_and_path_id;
|
||||
file_size = fd.file_size;
|
||||
smallest_seqno = fd.smallest_seqno;
|
||||
@@ -182,15 +225,20 @@ struct FileDescriptor {
|
||||
};
|
||||
|
||||
struct FileSampledStats {
|
||||
FileSampledStats() : num_reads_sampled(0) {}
|
||||
FileSampledStats()
|
||||
: num_reads_sampled(0), num_collapsible_entry_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 {
|
||||
@@ -198,9 +246,6 @@ 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
|
||||
@@ -347,9 +392,10 @@ struct FileMetaData {
|
||||
uint64_t TryGetOldestAncesterTime() {
|
||||
if (oldest_ancester_time != kUnknownOldestAncesterTime) {
|
||||
return oldest_ancester_time;
|
||||
} else if (fd.table_reader != nullptr &&
|
||||
fd.table_reader->GetTableProperties() != nullptr) {
|
||||
return fd.table_reader->GetTableProperties()->creation_time;
|
||||
}
|
||||
TableReader* reader = fd.pinned_reader.Get();
|
||||
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
|
||||
return reader->GetTableProperties()->creation_time;
|
||||
}
|
||||
return kUnknownOldestAncesterTime;
|
||||
}
|
||||
@@ -357,9 +403,10 @@ struct FileMetaData {
|
||||
uint64_t TryGetFileCreationTime() {
|
||||
if (file_creation_time != kUnknownFileCreationTime) {
|
||||
return file_creation_time;
|
||||
} else if (fd.table_reader != nullptr &&
|
||||
fd.table_reader->GetTableProperties() != nullptr) {
|
||||
return fd.table_reader->GetTableProperties()->file_creation_time;
|
||||
}
|
||||
TableReader* reader = fd.pinned_reader.Get();
|
||||
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
|
||||
return reader->GetTableProperties()->file_creation_time;
|
||||
}
|
||||
return kUnknownFileCreationTime;
|
||||
}
|
||||
@@ -367,10 +414,9 @@ 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) {
|
||||
if (fd.table_reader != nullptr &&
|
||||
fd.table_reader->GetTableProperties() != nullptr) {
|
||||
uint64_t newest_key_time =
|
||||
fd.table_reader->GetTableProperties()->newest_key_time;
|
||||
TableReader* reader = fd.pinned_reader.Get();
|
||||
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
|
||||
uint64_t newest_key_time = reader->GetTableProperties()->newest_key_time;
|
||||
if (newest_key_time != kUnknownNewestKeyTime) {
|
||||
return newest_key_time;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
: 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) {}
|
||||
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) {}
|
||||
|
||||
~VersionEditHandler() override {}
|
||||
|
||||
|
||||
+152
-31
@@ -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.table_reader;
|
||||
auto* r = (*level_files_brief_)[0].files[i].fd.pinned_reader.Get();
|
||||
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.table_reader;
|
||||
auto* r = (*level_files_brief_)[0].files[i].fd.pinned_reader.Get();
|
||||
if (r) {
|
||||
for (auto iter = range_.begin(); iter != range_.end(); ++iter) {
|
||||
r->Prepare(iter->ikey);
|
||||
@@ -974,9 +974,8 @@ 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,
|
||||
bool should_sample, HistogramImpl* file_read_hist,
|
||||
TableReaderCaller caller, bool skip_filters, int level,
|
||||
RangeDelAggregator* range_del_agg,
|
||||
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,
|
||||
@@ -1002,7 +1001,6 @@ 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),
|
||||
@@ -1267,6 +1265,26 @@ 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();
|
||||
@@ -1279,9 +1297,6 @@ 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;
|
||||
@@ -1298,7 +1313,8 @@ 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_);
|
||||
range_tombstone_iter_,
|
||||
/*maybe_pin_table_handle=*/true);
|
||||
}
|
||||
|
||||
// Check if current file being fully within iterate_lower_bound.
|
||||
@@ -1361,7 +1377,6 @@ 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;
|
||||
@@ -1498,6 +1513,7 @@ void LevelIterator::Seek(const Slice& target) {
|
||||
}
|
||||
SkipEmptyFileForward();
|
||||
CheckMayBeOutOfLowerBound();
|
||||
SampleRead<true>();
|
||||
}
|
||||
|
||||
void LevelIterator::SeekForPrev(const Slice& target) {
|
||||
@@ -1533,6 +1549,7 @@ void LevelIterator::SeekForPrev(const Slice& target) {
|
||||
SkipEmptyFileBackward();
|
||||
}
|
||||
CheckMayBeOutOfLowerBound();
|
||||
SampleRead<true>();
|
||||
}
|
||||
|
||||
void LevelIterator::SeekToFirst() {
|
||||
@@ -1549,6 +1566,7 @@ void LevelIterator::SeekToFirst() {
|
||||
}
|
||||
SkipEmptyFileForward();
|
||||
CheckMayBeOutOfLowerBound();
|
||||
SampleRead<true>();
|
||||
}
|
||||
|
||||
void LevelIterator::SeekToLast() {
|
||||
@@ -1563,6 +1581,7 @@ void LevelIterator::SeekToLast() {
|
||||
}
|
||||
SkipEmptyFileBackward();
|
||||
CheckMayBeOutOfLowerBound();
|
||||
SampleRead<true>();
|
||||
}
|
||||
|
||||
void LevelIterator::Next() {
|
||||
@@ -1577,6 +1596,7 @@ void LevelIterator::Next() {
|
||||
}
|
||||
}
|
||||
SkipEmptyFileForward();
|
||||
SampleRead<false>();
|
||||
}
|
||||
|
||||
bool LevelIterator::NextAndGetResult(IterateResult* result) {
|
||||
@@ -1610,6 +1630,7 @@ bool LevelIterator::NextAndGetResult(IterateResult* result) {
|
||||
}
|
||||
}
|
||||
}
|
||||
SampleRead<false>();
|
||||
return is_valid;
|
||||
}
|
||||
|
||||
@@ -1624,6 +1645,7 @@ void LevelIterator::Prev() {
|
||||
}
|
||||
}
|
||||
SkipEmptyFileBackward();
|
||||
SampleRead<false>();
|
||||
}
|
||||
|
||||
bool LevelIterator::SkipEmptyFileForward() {
|
||||
@@ -2221,7 +2243,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.table_reader != nullptr);
|
||||
assert(meta->fd.pinned_reader.Get() != nullptr);
|
||||
uint64_t file_creation_time = meta->TryGetFileCreationTime();
|
||||
if (file_creation_time == kUnknownFileCreationTime) {
|
||||
*creation_time = 0;
|
||||
@@ -2244,8 +2266,7 @@ 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_, should_sample_file_read(),
|
||||
cfd_->internal_stats()->GetFileReadHist(level),
|
||||
mutable_cf_options_, 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_);
|
||||
@@ -2341,8 +2362,6 @@ 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
|
||||
@@ -2357,7 +2376,8 @@ 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);
|
||||
/*range_del_read_seqno=*/nullptr, &tombstone_iter,
|
||||
/*maybe_pin_table_handle=*/true);
|
||||
if (read_options.ignore_range_deletions) {
|
||||
merge_iter_builder->AddIterator(table_iter);
|
||||
} else {
|
||||
@@ -2365,11 +2385,10 @@ void Version::AddIteratorsForLevel(const ReadOptions& read_options,
|
||||
table_iter, std::move(tombstone_iter));
|
||||
}
|
||||
}
|
||||
if (should_sample) {
|
||||
if (should_sample_file_read()) {
|
||||
// Count ones for every L0 files. This is done per iterator creation
|
||||
// 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.
|
||||
// rather than Seek(), while files in other levels are sampled on
|
||||
// seek/next/prev.
|
||||
for (FileMetaData* meta : storage_info_.LevelFiles(0)) {
|
||||
sample_file_read_inc(meta);
|
||||
}
|
||||
@@ -2383,8 +2402,7 @@ 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_, should_sample_file_read(),
|
||||
cfd_->internal_stats()->GetFileReadHist(level),
|
||||
mutable_cf_options_, cfd_->internal_stats()->GetFileReadHist(level),
|
||||
TableReaderCaller::kUserIterator, IsFilterSkipped(level), level,
|
||||
/*range_del_agg=*/nullptr,
|
||||
/*compaction_boundaries=*/nullptr, allow_unprepared_value,
|
||||
@@ -2442,8 +2460,7 @@ 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_, should_sample_file_read(),
|
||||
cfd_->internal_stats()->GetFileReadHist(level),
|
||||
mutable_cf_options_, 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,
|
||||
@@ -2784,9 +2801,15 @@ 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) {
|
||||
@@ -2835,6 +2858,9 @@ 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);
|
||||
@@ -5581,6 +5607,96 @@ 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;
|
||||
}
|
||||
@@ -5597,9 +5713,10 @@ 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->table_reader_handle,
|
||||
/*uncache_aggressiveness=*/0);
|
||||
TableCache::ReleaseObsolete(
|
||||
table_cache_, file.metadata->fd.GetNumber(),
|
||||
file.metadata->fd.pinned_reader.GetCacheHandle(),
|
||||
/*uncache_aggressiveness=*/0);
|
||||
file.DeleteMetadata();
|
||||
}
|
||||
obsolete_files_.clear();
|
||||
@@ -5663,6 +5780,8 @@ 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();
|
||||
}
|
||||
|
||||
@@ -6537,7 +6656,8 @@ 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);
|
||||
EpochNumberRequirement::kMightMissing,
|
||||
/*skip_load_table_files=*/db_options_->open_files_async);
|
||||
handler.Iterate(reader, &log_read_status);
|
||||
s = handler.status();
|
||||
if (s.ok()) {
|
||||
@@ -7590,7 +7710,6 @@ 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,
|
||||
@@ -7856,10 +7975,12 @@ 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,
|
||||
/*no_io=*/false, internal_stats->GetFileReadHist(level), false, level,
|
||||
&table_reader, /*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) {
|
||||
|
||||
@@ -1779,6 +1779,8 @@ 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.
|
||||
|
||||
@@ -58,6 +58,11 @@ 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;
|
||||
|
||||
+276
-12
@@ -1228,11 +1228,12 @@ 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(impl->GetDbIdentityFromIdentityFile(IOOptions(), &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();
|
||||
}
|
||||
new_db.SetDBId(db_id);
|
||||
}
|
||||
new_db.SetLogNumber(0);
|
||||
@@ -2407,6 +2408,267 @@ 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.
|
||||
@@ -3512,11 +3774,12 @@ 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(impl->GetDbIdentityFromIdentityFile(IOOptions(), &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();
|
||||
}
|
||||
new_db.SetDBId(db_id);
|
||||
}
|
||||
const std::string manifest_path = DescriptorFileName(dbname_, 1);
|
||||
@@ -3838,11 +4101,12 @@ 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(impl->GetDbIdentityFromIdentityFile(IOOptions(), &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();
|
||||
}
|
||||
new_db.SetDBId(db_id);
|
||||
}
|
||||
{
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// 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
|
||||
@@ -5,10 +5,34 @@
|
||||
|
||||
#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
|
||||
|
||||
@@ -113,6 +113,7 @@ 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);
|
||||
@@ -179,6 +180,9 @@ 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);
|
||||
@@ -256,6 +260,7 @@ 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);
|
||||
@@ -269,6 +274,7 @@ 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);
|
||||
@@ -282,8 +288,10 @@ 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)
|
||||
@@ -441,6 +449,7 @@ 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);
|
||||
|
||||
@@ -203,6 +203,9 @@ 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.");
|
||||
@@ -619,6 +622,20 @@ 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, "",
|
||||
@@ -1003,6 +1020,9 @@ 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", "");
|
||||
@@ -1060,6 +1080,11 @@ 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");
|
||||
@@ -1513,6 +1538,10 @@ 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.");
|
||||
@@ -1522,6 +1551,10 @@ 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.");
|
||||
@@ -1576,4 +1609,10 @@ 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
|
||||
@@ -226,6 +226,13 @@ 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.
|
||||
@@ -366,6 +373,8 @@ 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
|
||||
@@ -29,6 +29,7 @@
|
||||
#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"
|
||||
@@ -41,6 +42,7 @@
|
||||
#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 {
|
||||
|
||||
@@ -929,6 +931,22 @@ 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,
|
||||
@@ -985,6 +1003,9 @@ 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) {
|
||||
@@ -1034,6 +1055,9 @@ 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
|
||||
@@ -1692,6 +1716,14 @@ 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);
|
||||
|
||||
@@ -1976,7 +2008,19 @@ Status StressTest::TestIterateImpl(ThreadState* thread,
|
||||
|
||||
Slice key(key_str);
|
||||
|
||||
const bool support_seek_first_or_last = expect_total_order;
|
||||
// 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;
|
||||
|
||||
// Write-prepared and Write-unprepared and multi-cf-iterator do not support
|
||||
// Refresh() yet.
|
||||
@@ -1991,17 +2035,17 @@ Status StressTest::TestIterateImpl(ThreadState* thread,
|
||||
}
|
||||
|
||||
LastIterateOp last_op;
|
||||
if (support_seek_first_or_last && thread->rand.OneIn(100)) {
|
||||
if (support_seek_to_first && thread->rand.OneIn(100)) {
|
||||
iter->SeekToFirst();
|
||||
cmp_iter->SeekToFirst();
|
||||
last_op = kLastOpSeekToFirst;
|
||||
op_logs += "STF ";
|
||||
} else if (support_seek_first_or_last && thread->rand.OneIn(100)) {
|
||||
} else if (support_seek_to_last && thread->rand.OneIn(100)) {
|
||||
iter->SeekToLast();
|
||||
cmp_iter->SeekToLast();
|
||||
last_op = kLastOpSeekToLast;
|
||||
op_logs += "STL ";
|
||||
} else if (thread->rand.OneIn(8)) {
|
||||
} else if (support_seek_for_prev && thread->rand.OneIn(8)) {
|
||||
iter->SeekForPrev(key);
|
||||
cmp_iter->SeekForPrev(key);
|
||||
last_op = kLastOpSeekForPrev;
|
||||
@@ -2033,7 +2077,8 @@ Status StressTest::TestIterateImpl(ThreadState* thread,
|
||||
key, op_logs, verify_func, &diverged);
|
||||
|
||||
const bool no_reverse =
|
||||
(FLAGS_memtablerep == "prefix_hash" && !expect_total_order);
|
||||
(FLAGS_memtablerep == "prefix_hash" && !expect_total_order) ||
|
||||
!FLAGS_test_backward_scan;
|
||||
for (uint64_t i = 0; i < FLAGS_num_iterations && iter->Valid(); ++i) {
|
||||
if (no_reverse || thread->rand.OneIn(2)) {
|
||||
iter->Next();
|
||||
@@ -3630,8 +3675,11 @@ 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_, options_);
|
||||
InitializeOptionsFromFlags(cache_, filter_policy_, udi_factory_, options_);
|
||||
}
|
||||
InitializeOptionsGeneral(cache_, filter_policy_, sqfc_factory_, options_);
|
||||
DbStressCustomCompressionManager::Register();
|
||||
@@ -3669,13 +3717,6 @@ 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)) {
|
||||
@@ -4271,6 +4312,7 @@ 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 =
|
||||
@@ -4331,6 +4373,7 @@ 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);
|
||||
@@ -4353,6 +4396,9 @@ 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;
|
||||
@@ -4399,6 +4445,13 @@ 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();
|
||||
}
|
||||
@@ -4444,6 +4497,8 @@ 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;
|
||||
@@ -4472,6 +4527,8 @@ 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 =
|
||||
@@ -4483,9 +4540,13 @@ 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;
|
||||
@@ -4714,10 +4775,18 @@ void InitializeOptionsGeneral(
|
||||
"`preserve_unverified_changes` to keep all files\n");
|
||||
options.avoid_flush_during_recovery = true;
|
||||
}
|
||||
// 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.
|
||||
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.
|
||||
options.disable_auto_compactions = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#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 {
|
||||
@@ -33,6 +34,12 @@ 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() {}
|
||||
@@ -422,6 +429,7 @@ 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_;
|
||||
@@ -437,7 +445,9 @@ bool InitializeOptionsFromFile(Options& options);
|
||||
// input arguments.
|
||||
void InitializeOptionsFromFlags(
|
||||
const std::shared_ptr<Cache>& cache,
|
||||
const std::shared_ptr<const FilterPolicy>& filter_policy, Options& options);
|
||||
const std::shared_ptr<const FilterPolicy>& filter_policy,
|
||||
const std::shared_ptr<UserDefinedIndexFactory>& udi_factory,
|
||||
Options& options);
|
||||
|
||||
// Initialize `options` on which `InitializeOptionsFromFile()` and
|
||||
// `InitializeOptionsFromFlags()` have both been called already.
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#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"
|
||||
|
||||
@@ -92,6 +93,15 @@ 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 =
|
||||
@@ -219,6 +229,22 @@ 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) {
|
||||
@@ -240,6 +266,25 @@ 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;
|
||||
|
||||
@@ -543,7 +543,10 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
}
|
||||
std::unique_ptr<Iterator> iter(
|
||||
secondary_db_->NewIterator(read_opts, handle));
|
||||
uint32_t rnd = (thread->rand.Next()) % 4;
|
||||
// Skip SeekToFirst, SeekToLast, SeekForPrev, and Prev when backward
|
||||
// scan is disabled.
|
||||
uint32_t rnd =
|
||||
(!FLAGS_test_backward_scan) ? 2 : (thread->rand.Next()) % 4;
|
||||
if (0 == rnd) {
|
||||
// SeekToFirst() + Next()*5
|
||||
read_opts.total_order_seek = true;
|
||||
@@ -1905,13 +1908,7 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
} while (!s.ok() && IsErrorInjectedAndRetryable(s) &&
|
||||
initial_wal_write_may_succeed);
|
||||
|
||||
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
|
||||
if (IsExpectedTxnError(s)) {
|
||||
pending_expected_value.Rollback();
|
||||
return Status::OK();
|
||||
}
|
||||
@@ -2016,6 +2013,11 @@ 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)) {
|
||||
@@ -2083,6 +2085,11 @@ 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)) {
|
||||
@@ -2656,76 +2663,78 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
op_logs += "N";
|
||||
}
|
||||
|
||||
// backward scan
|
||||
key_str = Key(ub - 1);
|
||||
iter->SeekForPrev(key_str);
|
||||
// backward scan — skip when backward iteration is not supported
|
||||
if (FLAGS_test_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->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 (!iter->PrepareValue()) {
|
||||
assert(!iter->Valid());
|
||||
assert(!iter->status().ok());
|
||||
}
|
||||
}
|
||||
if (!check_no_key_in_range(lb, last_key)) {
|
||||
|
||||
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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
last_key = static_cast<int64_t>(curr);
|
||||
if (last_key <= lb) {
|
||||
break;
|
||||
iter->Prev();
|
||||
|
||||
op_logs += "P";
|
||||
}
|
||||
|
||||
iter->Prev();
|
||||
|
||||
op_logs += "P";
|
||||
}
|
||||
|
||||
// Write-prepared/write-unprepared transactions and multi-CF iterator do not
|
||||
@@ -2767,7 +2776,8 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
key_str = Key(mid);
|
||||
const Slice key(key_str);
|
||||
|
||||
if (thread->rand.OneIn(2)) {
|
||||
// Skip SeekForPrev and Prev when backward scan is not supported.
|
||||
if (!FLAGS_test_backward_scan || thread->rand.OneIn(2)) {
|
||||
iter->Seek(key);
|
||||
op_logs += " S " + key.ToString(true) + " ";
|
||||
if (!iter->Valid() && iter->status().ok()) {
|
||||
@@ -2836,8 +2846,14 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
iter->Next();
|
||||
op_logs += "N";
|
||||
} else if (static_cast<int64_t>(curr) >= ub) {
|
||||
iter->Prev();
|
||||
op_logs += "P";
|
||||
// Use Next when backward scan is not supported.
|
||||
if (!FLAGS_test_backward_scan) {
|
||||
iter->Next();
|
||||
op_logs += "N";
|
||||
} else {
|
||||
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);
|
||||
@@ -2864,7 +2880,7 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
break;
|
||||
}
|
||||
|
||||
if (thread->rand.OneIn(2)) {
|
||||
if (!FLAGS_test_backward_scan || thread->rand.OneIn(2)) {
|
||||
iter->Next();
|
||||
op_logs += "N";
|
||||
if (!iter->Valid()) {
|
||||
@@ -3209,15 +3225,7 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
if (IsExpectedTxnError(s)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
<!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">
|
||||
|
||||
Vendored
+5
-2
@@ -1940,7 +1940,10 @@ IOStatus PosixDirectory::FsyncWithDirOptions(
|
||||
assert(fd_ >= 0); // Check use after close
|
||||
IOStatus s = IOStatus::OK();
|
||||
#ifndef OS_AIX
|
||||
if (is_btrfs_) {
|
||||
bool test_is_btrfs = is_btrfs_;
|
||||
TEST_SYNC_POINT_CALLBACK("PosixDirectory::FsyncWithDirOptions:ForceBtrfs",
|
||||
&test_is_btrfs);
|
||||
if (test_is_btrfs) {
|
||||
// skip dir fsync for new file creation, which is not needed for btrfs
|
||||
if (dir_fsync_options.reason == DirFsyncOptions::kNewFileSynced) {
|
||||
return s;
|
||||
@@ -1959,7 +1962,7 @@ IOStatus PosixDirectory::FsyncWithDirOptions(
|
||||
} else if (fsync(fd) < 0) {
|
||||
s = IOError("While fsync renaming file", new_name, errno);
|
||||
}
|
||||
if (close(fd) < 0) {
|
||||
if (fd >= 0 && close(fd) < 0) {
|
||||
s = IOError("While closing file after fsync", new_name, errno);
|
||||
}
|
||||
return s;
|
||||
|
||||
Vendored
+43
@@ -3,11 +3,13 @@
|
||||
// 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 {
|
||||
|
||||
@@ -174,6 +176,47 @@ 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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <cinttypes>
|
||||
#include <future>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
@@ -856,6 +857,113 @@ 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) {
|
||||
|
||||
+10
-2
@@ -3221,7 +3221,11 @@ class FilePrefetchBufferTest : public testing::Test {
|
||||
stats_ = CreateDBStatistics();
|
||||
}
|
||||
|
||||
void TearDown() override { EXPECT_OK(DestroyDir(env_, test_dir_)); }
|
||||
void TearDown() override {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
EXPECT_OK(DestroyDir(env_, test_dir_));
|
||||
}
|
||||
|
||||
void Write(const std::string& fname, const std::string& content) {
|
||||
std::unique_ptr<FSWritableFile> f;
|
||||
@@ -3747,7 +3751,11 @@ class FSBufferPrefetchTest
|
||||
stats_ = CreateDBStatistics();
|
||||
}
|
||||
|
||||
void TearDown() override { EXPECT_OK(DestroyDir(env_, test_dir_)); }
|
||||
void TearDown() override {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
EXPECT_OK(DestroyDir(env_, test_dir_));
|
||||
}
|
||||
|
||||
void Write(const std::string& fname, const std::string& content) {
|
||||
std::unique_ptr<FSWritableFile> f;
|
||||
|
||||
@@ -98,7 +98,7 @@ endif # FMT_SOURCE_PATH
|
||||
PLATFORM_LDFLAGS += -lglog
|
||||
endif
|
||||
|
||||
FOLLY_COMMIT_HASH = 1e8ce1e5d35acff7b78fedbca3e7311b39f43529
|
||||
FOLLY_COMMIT_HASH = 3b0eed0a128c484a2b077546c3e08c824a311357
|
||||
|
||||
# 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
|
||||
|
||||
@@ -213,8 +213,7 @@ enum class VerifyOutputFlags : uint32_t {
|
||||
// by comparing the one inserted into a
|
||||
// file, and what is read back.
|
||||
|
||||
// TODO - Implement
|
||||
// kVerifyFileChecksum = 1 << 2, // Verify file-level checksum
|
||||
kVerifyFileChecksum = 1 << 2, // Verify file-level checksum
|
||||
|
||||
// Second set of bits: when to enable verification
|
||||
kEnableForLocalCompaction = 1 << 10, // Enable for local compaction
|
||||
@@ -1306,6 +1305,18 @@ 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
|
||||
|
||||
+10
-1
@@ -1479,6 +1479,10 @@ 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(
|
||||
@@ -2177,7 +2181,12 @@ enum {
|
||||
rocksdb_internal_range_del_reseek_count,
|
||||
rocksdb_block_read_cpu_time,
|
||||
rocksdb_internal_merge_point_lookup_count,
|
||||
rocksdb_total_metric_count = 80
|
||||
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
|
||||
};
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_set_perf_level(int);
|
||||
|
||||
@@ -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,
|
||||
std::string* value) = 0;
|
||||
PinnableSlice* 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<std::string>* values,
|
||||
std::vector<PinnableSlice>* values,
|
||||
std::vector<Status>* statuses) = 0;
|
||||
|
||||
// Allocate and return the contents of the properties block. If the builder
|
||||
|
||||
@@ -201,6 +201,7 @@ enum class BackgroundErrorReason {
|
||||
kManifestWrite,
|
||||
kFlushNoWAL,
|
||||
kManifestWriteNoWAL,
|
||||
kAsyncFileOpen,
|
||||
};
|
||||
|
||||
struct WriteStallInfo {
|
||||
@@ -553,6 +554,34 @@ 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
|
||||
@@ -869,6 +898,17 @@ 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 {}
|
||||
};
|
||||
|
||||
|
||||
@@ -211,6 +211,26 @@ 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;
|
||||
|
||||
@@ -783,6 +783,25 @@ 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
|
||||
@@ -975,6 +994,13 @@ 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
|
||||
@@ -1392,9 +1418,30 @@ 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.
|
||||
@@ -2235,9 +2282,10 @@ struct ReadOptions {
|
||||
// block based table index. The table_factory used for the column family
|
||||
// must support building/reading this index.
|
||||
//
|
||||
// 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.
|
||||
// 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.
|
||||
const UserDefinedIndexFactory* table_index_factory = nullptr;
|
||||
|
||||
// *** END options only relevant to iterators or scans ***
|
||||
|
||||
@@ -106,7 +106,8 @@ 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_time; // total nanos spent on block decompression
|
||||
uint64_t block_decompress_count; // total number of block decompressions
|
||||
|
||||
uint64_t get_read_bytes; // bytes for vals returned by Get
|
||||
uint64_t multiget_read_bytes; // bytes for vals returned by MultiGet
|
||||
@@ -290,6 +291,14 @@ 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 {
|
||||
|
||||
@@ -35,6 +35,19 @@ 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.
|
||||
|
||||
@@ -730,6 +730,11 @@ 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
|
||||
};
|
||||
|
||||
|
||||
+49
-3
@@ -264,6 +264,9 @@ 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,
|
||||
@@ -274,6 +277,16 @@ 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;
|
||||
@@ -620,6 +633,22 @@ 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
|
||||
@@ -710,17 +739,34 @@ 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. 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
|
||||
// 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
|
||||
// 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 =
|
||||
|
||||
@@ -16,30 +16,34 @@
|
||||
#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 const std::string kUserDefinedIndexPrefix =
|
||||
inline constexpr const char* 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:
|
||||
// Right now, we only support Puts. In the future, we may support merges,
|
||||
// deletions etc.
|
||||
enum ValueType {
|
||||
kValue,
|
||||
kTypeMax,
|
||||
// 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.
|
||||
};
|
||||
|
||||
// File offset and size of the data block
|
||||
@@ -48,41 +52,68 @@ 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 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.
|
||||
// Add a new index entry for a data block boundary.
|
||||
//
|
||||
// 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.
|
||||
// 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).
|
||||
//
|
||||
// Called before the OnKeyAdded() call for first_key_in_next_block.
|
||||
// @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.
|
||||
// @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
|
||||
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) = 0;
|
||||
std::string* separator_scratch,
|
||||
const IndexEntryContext& context) = 0;
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
//
|
||||
// The key will be a user key. RocksDB guarantees that there will only be
|
||||
// one entry for each key in the file/index.
|
||||
// @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.
|
||||
virtual void OnKeyAdded(const Slice& /*key*/, ValueType /*type*/,
|
||||
const Slice& /*value*/) {}
|
||||
|
||||
@@ -104,13 +135,36 @@ 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
|
||||
// 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.
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
@@ -119,8 +173,14 @@ 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.
|
||||
virtual Status SeekAndGetResult(const Slice& target,
|
||||
IterateResult* result) = 0;
|
||||
//
|
||||
// @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;
|
||||
|
||||
// Advance to the next index entry. The result must be populated similar
|
||||
// to SeekAndGetResult.
|
||||
@@ -152,7 +212,7 @@ struct UserDefinedIndexOption {
|
||||
// Factory for creating user-defined index builders.
|
||||
class UserDefinedIndexFactory : public Customizable {
|
||||
public:
|
||||
virtual ~UserDefinedIndexFactory() = default;
|
||||
~UserDefinedIndexFactory() override = default;
|
||||
|
||||
static const char* Type() { return "UserDefinedIndexFactory"; }
|
||||
|
||||
@@ -174,14 +234,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
Reference in new issue
Block a user