mirror of
https://github.com/facebook/rocksdb.git
synced 2026-07-07 14:47:40 +08:00
Compare commits
176 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19a1ebaa6d | |||
| 67dbbd79b9 | |||
| 8053b9414f | |||
| 5177a8e6c2 | |||
| 3883a8d05e | |||
| 768de6e50d | |||
| 62f05627be | |||
| 9ef369c606 | |||
| 30dba7f41a | |||
| 023fbb074a | |||
| 26a501b5d1 | |||
| e492562651 | |||
| a0db079fdd | |||
| 9270dc8149 | |||
| 638354e766 | |||
| c724aeb67e | |||
| 364eb88151 | |||
| d7afd3bb3b | |||
| ae30c71c6b | |||
| 91b31112ed | |||
| 7f32e2cabc | |||
| 81fa943ca0 | |||
| a3ba3e8a6e | |||
| e82af29adb | |||
| acfa68ccff | |||
| 8bf167a194 | |||
| 97551b72d7 | |||
| d1ab4bf12c | |||
| e42af37ea7 | |||
| 54d13c0af9 | |||
| 2904bc64dc | |||
| 88d7d2df75 | |||
| 367f2b0fdf | |||
| 07a5a0a804 | |||
| 01d8c7720f | |||
| cba33621bd | |||
| 554a123295 | |||
| 459236341c | |||
| c48b020e92 | |||
| 3daabe2db3 | |||
| 805a476a5f | |||
| 21723bbbef | |||
| a2c96df7d7 | |||
| 87c554b492 | |||
| cbd61a3165 | |||
| 59633e36ed | |||
| 4707775ae9 | |||
| 46ce1a03a9 | |||
| 795f3bd61f | |||
| 330962bff6 | |||
| e07ccc3528 | |||
| 224e849e8d | |||
| c734b7cc60 | |||
| 60b34f30c4 | |||
| 02a2b3501d | |||
| 1dc4813d97 | |||
| 4af61efaf4 | |||
| e0d549fbba | |||
| c25dcbccba | |||
| c99d5ec06e | |||
| a4045b0a5f | |||
| b874d2546e | |||
| c1dbe0a6ac | |||
| 52d8574629 | |||
| a12bd7d641 | |||
| a69e9fdb7e | |||
| fa8f4f1ef0 | |||
| d534974711 | |||
| cc8d9ea04d | |||
| 1fd2c202c7 | |||
| c28b4f0e12 | |||
| 4e2fe35bbb | |||
| b3b7668bf5 | |||
| b12d3a6da6 | |||
| 8fb8fdd349 | |||
| 959fa315b1 | |||
| f4fa2d2386 | |||
| ebaa925006 | |||
| 3ae314446b | |||
| 6842ba8455 | |||
| a2abe3cfd6 | |||
| 2e7cf42cda | |||
| 3cdf942192 | |||
| d8fc727592 | |||
| ff16ff23ed | |||
| 03e5a0b9a8 | |||
| 763401b595 | |||
| c4941760c9 | |||
| 809ed26be5 | |||
| 13ea89f66b | |||
| 4c1ffedb1e | |||
| 7f89e5fb09 | |||
| b374ba5968 | |||
| 6ff96384dd | |||
| a4139ef9e9 | |||
| 3ef8b1b743 | |||
| 4bc0bff0e4 | |||
| 49ec219955 | |||
| 790294f3ad | |||
| 9886d52fc4 | |||
| 36e50a61fc | |||
| 7b06a9ae7e | |||
| df5695cf24 | |||
| 820c4a77c1 | |||
| 94b6566505 | |||
| 1cc216a45b | |||
| 978bf67426 | |||
| 6b8ca89f45 | |||
| 50852b5c8d | |||
| 9f474a1034 | |||
| f3a20854e0 | |||
| c47e35395b | |||
| a9ac4df630 | |||
| 4254c705a2 | |||
| bad2d5b0af | |||
| 1fe1b1bd46 | |||
| f3f7feba92 | |||
| b7395b3fa4 | |||
| 3b38fde293 | |||
| 3070f73e97 | |||
| 9d609dd6fe | |||
| 4d8a0acd76 | |||
| 3efe9460ba | |||
| 4dd05023c0 | |||
| 6a618d0686 | |||
| ec832ff42c | |||
| 91fa765f3a | |||
| 92cedd58af | |||
| 3dd6d060e5 | |||
| 1b817cff45 | |||
| fc85a700cf | |||
| 06699cd0fb | |||
| 0186937710 | |||
| 8ec2177bd1 | |||
| 156328107d | |||
| 3f51c0a185 | |||
| 65e77282ce | |||
| a0ff5b143f | |||
| 64f8d5765b | |||
| 4cf28450a5 | |||
| ae0c9faaef | |||
| 51561fc2cd | |||
| ebd118711c | |||
| ffe58fa976 | |||
| d6b03d9c96 | |||
| abc1f7ced5 | |||
| 01ec15f328 | |||
| af4e32945b | |||
| b611aa7309 | |||
| 830347c765 | |||
| de6cdbb62d | |||
| b8bbee0f3e | |||
| 24fac74206 | |||
| f12465cb93 | |||
| 7832ee886f | |||
| ae322e0a73 | |||
| 1e23ee053f | |||
| effac3a918 | |||
| c7f0e1fa93 | |||
| 141a1f0abf | |||
| 41e5eb1a4d | |||
| 875b8544df | |||
| e96295a288 | |||
| f897c1b2ef | |||
| dae450b78e | |||
| 2af38206ad | |||
| 5db0603613 | |||
| a965706e73 | |||
| 1a4b1e42cc | |||
| 47de3a3a2c | |||
| 926e2b589f | |||
| 453ebd6f37 | |||
| 24dc8306f8 | |||
| 0921ec1200 | |||
| f620a6d039 | |||
| c78144e452 |
@@ -9,7 +9,16 @@ runs:
|
||||
steps:
|
||||
- name: Build folly and dependencies
|
||||
if: ${{ inputs.cache-hit != 'true' }}
|
||||
run: make build_folly
|
||||
run: |
|
||||
clean_path=()
|
||||
IFS=: read -ra path_entries <<< "$PATH"
|
||||
for entry in "${path_entries[@]}"; do
|
||||
if [[ "$entry" != "/usr/lib/ccache" ]]; then
|
||||
clean_path+=("$entry")
|
||||
fi
|
||||
done
|
||||
export PATH="$(IFS=:; echo "${clean_path[*]}")"
|
||||
make build_folly
|
||||
shell: bash
|
||||
- name: Skip folly build (using cached version)
|
||||
if: ${{ inputs.cache-hit == 'true' }}
|
||||
|
||||
@@ -2,6 +2,13 @@ name: pre-steps
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Dump ulimits for diagnostics
|
||||
run: |
|
||||
echo "=== ulimits (soft) ==="
|
||||
ulimit -a -S || true
|
||||
echo "=== ulimits (hard) ==="
|
||||
ulimit -a -H || true
|
||||
shell: bash
|
||||
- name: Install lld linker for faster builds
|
||||
run: apt-get update -y && apt-get install -y lld 2>/dev/null || true
|
||||
shell: bash
|
||||
|
||||
@@ -5,7 +5,11 @@ runs:
|
||||
steps:
|
||||
- name: Trim and print ccache stats
|
||||
run: |
|
||||
if [ -z "$CCACHE_DIR" ]; then
|
||||
echo "teardown-ccache: CCACHE_DIR not set, skipping (setup-ccache may not have run)"
|
||||
exit 0
|
||||
fi
|
||||
.github/scripts/ccache-trim.sh || true
|
||||
ccache -s
|
||||
ccache -s || echo "teardown-ccache: ccache not found, skipping stats"
|
||||
if: always()
|
||||
shell: bash
|
||||
|
||||
@@ -3,7 +3,7 @@ inputs:
|
||||
suite-run:
|
||||
description: Comma-separated list of test suites to run (empty to skip C++ tests)
|
||||
required: false
|
||||
default: arena_test,db_basic_test,db_test,db_test2,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test
|
||||
default: arena_test,db_basic_test,db_test,db_etc2_test,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test
|
||||
run-java:
|
||||
description: Whether to run Java tests
|
||||
required: false
|
||||
@@ -17,11 +17,15 @@ runs:
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
max-size: "10GB"
|
||||
key: ccache-windows-${{ github.workflow }}
|
||||
restore-keys: |
|
||||
ccache-windows-
|
||||
- name: Configure ccache
|
||||
shell: pwsh
|
||||
run: |
|
||||
ccache --set-config=base_dir=C:\a\rocksdb\rocksdb
|
||||
ccache --set-config=hash_dir=false
|
||||
ccache --set-config=compiler_check=content
|
||||
- name: Custom steps
|
||||
env:
|
||||
THIRDPARTY_HOME: ${{ github.workspace }}/thirdparty
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Shared markdown builder for AI review comment bodies.
|
||||
//
|
||||
// Usage:
|
||||
// const build = require('./build-ai-review-comment.js');
|
||||
// return build({ icon, headerTitle, triggerLine, responseBody, footerLines
|
||||
// });
|
||||
|
||||
module.exports = function buildAiReviewComment(
|
||||
{icon, headerTitle, triggerLine, responseBody, footerLines}) {
|
||||
return [
|
||||
`## ${icon} ${headerTitle}`,
|
||||
'',
|
||||
triggerLine,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
responseBody,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>ℹ️ About this response</summary>',
|
||||
'',
|
||||
...footerLines,
|
||||
'</details>',
|
||||
].join('\n');
|
||||
};
|
||||
@@ -7,11 +7,25 @@
|
||||
// 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 }
|
||||
// meta - { trigger, autoMode, headSha, reviewer, isQuery, isPartial
|
||||
// }
|
||||
|
||||
const fs = require('fs');
|
||||
const buildComment = require('./build-ai-review-comment.js');
|
||||
|
||||
module.exports = function parseClaude({executionFile, conclusion, meta}) {
|
||||
function getTriggerLine() {
|
||||
if (meta.trigger !== 'auto') {
|
||||
return `*Requested by @${meta.reviewer}*`;
|
||||
}
|
||||
const shortSha = meta.headSha ? meta.headSha.substring(0, 7) : 'unknown';
|
||||
if (meta.autoMode === 'early') {
|
||||
return `*Auto-triggered after CI reached the early-review threshold — reviewing commit ${
|
||||
shortSha}*`;
|
||||
}
|
||||
return `*Auto-triggered after CI passed — reviewing commit ${shortSha}*`;
|
||||
}
|
||||
|
||||
let responseBody = '';
|
||||
|
||||
try {
|
||||
@@ -23,6 +37,29 @@ module.exports = function parseClaude({executionFile, conclusion, meta}) {
|
||||
|
||||
const resultMessage = executionLog.find(m => m.type === 'result');
|
||||
|
||||
// Helper: extract the last substantial assistant text from the log.
|
||||
// Used as a fallback when Claude ran out of turns and the recovery
|
||||
// session also failed to produce a result.
|
||||
function getLastAssistantText(log, minLength = 200) {
|
||||
for (let i = log.length - 1; i >= 0; i--) {
|
||||
const m = log[i];
|
||||
if (m.type !== 'assistant') continue;
|
||||
const content = m.message && m.message.content;
|
||||
if (!Array.isArray(content)) continue;
|
||||
for (let j = content.length - 1; j >= 0; j--) {
|
||||
if (content[j].type === 'text' && content[j].text &&
|
||||
content[j].text.trim().length >= minLength) {
|
||||
const text = content[j].text.trim();
|
||||
// Truncate to avoid enormous PR comments
|
||||
return text.length > 50000 ? text.substring(0, 50000) +
|
||||
'\n\n*[Truncated — full output in execution log artifact]*' :
|
||||
text;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!resultMessage) {
|
||||
responseBody = '⚠️ No result message found in execution log.';
|
||||
} else if (resultMessage.subtype === 'success' && resultMessage.result) {
|
||||
@@ -31,6 +68,19 @@ module.exports = function parseClaude({executionFile, conclusion, meta}) {
|
||||
const errorInfo =
|
||||
resultMessage.result || resultMessage.error || 'Unknown error';
|
||||
responseBody = `❌ **Claude encountered an error:**\n\n${errorInfo}`;
|
||||
} else if (resultMessage.subtype === 'error_max_turns') {
|
||||
// The workflow runs a recovery session when this happens, so this
|
||||
// branch is typically only hit if recovery wasn't attempted (e.g.,
|
||||
// no findings file was written). Extract what we can.
|
||||
const partial = getLastAssistantText(executionLog);
|
||||
if (partial) {
|
||||
responseBody =
|
||||
`⚠️ **Review incomplete — Claude hit the turn limit.**\n\nBelow is the last partial output. You can request a fresh review with \`/claude-review\`.\n\n---\n\n${
|
||||
partial}`;
|
||||
} else {
|
||||
responseBody =
|
||||
'⚠️ **Review incomplete — Claude hit the turn limit before producing output.** You can request a fresh review with `/claude-review`.';
|
||||
}
|
||||
} else if (resultMessage.result) {
|
||||
responseBody = `⚠️ **Completed with status: ${
|
||||
resultMessage.subtype}**\n\n${resultMessage.result}`;
|
||||
@@ -41,38 +91,28 @@ module.exports = function parseClaude({executionFile, conclusion, meta}) {
|
||||
responseBody = `❌ Error parsing Claude response: ${error.message}`;
|
||||
}
|
||||
|
||||
const icon = conclusion === 'success' ? '✅' : '⚠️';
|
||||
const isPartial = !!meta.isPartial;
|
||||
const icon = isPartial ? '🟡' : (conclusion === 'success' ? '✅' : '⚠️');
|
||||
const headerTitle = meta.isQuery ? 'Claude Response' : 'Claude Code Review';
|
||||
const triggerLine = meta.trigger === 'auto' ?
|
||||
`*Auto-triggered after CI passed — reviewing commit ${
|
||||
meta.headSha.substring(0, 7)}*` :
|
||||
`*Requested by @${meta.reviewer}*`;
|
||||
const triggerLine = getTriggerLine();
|
||||
|
||||
return [
|
||||
`## ${icon} ${headerTitle}`,
|
||||
'',
|
||||
return buildComment({
|
||||
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');
|
||||
footerLines: [
|
||||
'Generated by [Claude Code](https://github.com/anthropics/claude-code-action).',
|
||||
'Review methodology: `claude_md/code_review.md`',
|
||||
'',
|
||||
'**Limitations:**',
|
||||
'- Claude may miss context from files not in the diff',
|
||||
'- Large PRs may be truncated',
|
||||
'- Always apply human judgment to AI suggestions',
|
||||
'',
|
||||
'**Commands:**',
|
||||
'- `/claude-review [context]` — Request a code review',
|
||||
'- `/claude-query <question>` — Ask about the PR or codebase',
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Parse Codex review artifacts and produce a markdown review comment.
|
||||
//
|
||||
// Usage from actions/github-script:
|
||||
// const parse = require('./.github/scripts/parse-codex-review.js');
|
||||
// const markdown = parse({ responseFile, recoveryFile, findingsFile, logFile,
|
||||
// exitCode, meta });
|
||||
//
|
||||
// Parameters:
|
||||
// responseFile - path to Codex final response output
|
||||
// recoveryFile - path to recovery output formatted from review-findings.md
|
||||
// findingsFile - path to incremental findings file written during review
|
||||
// logFile - path to Codex stdout/stderr log
|
||||
// exitCode - Codex process exit code
|
||||
// meta - { trigger, autoMode, headSha, reviewer, isQuery, isPartial }
|
||||
|
||||
const fs = require('fs');
|
||||
const buildComment = require('./build-ai-review-comment.js');
|
||||
|
||||
module.exports = function parseCodex(
|
||||
{responseFile, recoveryFile, findingsFile, logFile, exitCode, meta}) {
|
||||
function getTriggerLine() {
|
||||
if (meta.trigger !== 'auto') {
|
||||
return `*Requested by @${meta.reviewer}*`;
|
||||
}
|
||||
const shortSha = meta.headSha ? meta.headSha.substring(0, 7) : 'unknown';
|
||||
if (meta.autoMode === 'early') {
|
||||
return `*Auto-triggered after CI reached the early-review threshold — reviewing commit ${
|
||||
shortSha}*`;
|
||||
}
|
||||
return `*Auto-triggered after CI passed — reviewing commit ${shortSha}*`;
|
||||
}
|
||||
|
||||
function readIfPresent(path) {
|
||||
if (!path || !fs.existsSync(path)) {
|
||||
return '';
|
||||
}
|
||||
const text = fs.readFileSync(path, 'utf8').trim();
|
||||
return text;
|
||||
}
|
||||
|
||||
function tailFile(path, maxChars = 12000) {
|
||||
const text = readIfPresent(path);
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
if (text.length <= maxChars) {
|
||||
return text;
|
||||
}
|
||||
return text.slice(text.length - maxChars);
|
||||
}
|
||||
|
||||
let responseBody = '';
|
||||
const recovered = readIfPresent(recoveryFile);
|
||||
const direct = readIfPresent(responseFile);
|
||||
const findings = readIfPresent(findingsFile);
|
||||
|
||||
if (recovered) {
|
||||
responseBody = recovered;
|
||||
} else if (direct) {
|
||||
responseBody = direct;
|
||||
} else if (findings) {
|
||||
responseBody =
|
||||
'⚠️ **Review incomplete — Codex did not produce a final response.**\n\n' +
|
||||
'Below are the incremental findings recovered from `review-findings.md`.\n\n---\n\n' +
|
||||
findings;
|
||||
} else {
|
||||
const logTail = tailFile(logFile);
|
||||
responseBody = logTail ?
|
||||
`❌ **Codex review failed before producing findings.**\n\n\`\`\`\n${
|
||||
logTail}\n\`\`\`` :
|
||||
'❌ Codex review failed before producing any output.';
|
||||
}
|
||||
|
||||
const isPartial = !!meta.isPartial;
|
||||
const code = Number.parseInt(exitCode || '1', 10);
|
||||
const icon = isPartial ? '🟡' : (code === 0 ? '✅' : '⚠️');
|
||||
const triggerLine = getTriggerLine();
|
||||
|
||||
return buildComment({
|
||||
icon,
|
||||
headerTitle: meta.isQuery ? 'Codex Response' : 'Codex Code Review',
|
||||
triggerLine,
|
||||
responseBody,
|
||||
footerLines: [
|
||||
'Generated by Codex CLI.',
|
||||
'Review methodology: `claude_md/code_review.md`',
|
||||
'',
|
||||
'**Limitations:**',
|
||||
'- Codex may miss context from files not in the diff',
|
||||
'- Large PRs may be truncated',
|
||||
'- Always apply human judgment to AI suggestions',
|
||||
'',
|
||||
'**Commands:**',
|
||||
'- `/codex-review [context]` — Request a code review',
|
||||
'- `/codex-query <question>` — Ask about the PR or codebase',
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -14,9 +14,31 @@
|
||||
// marker - HTML comment marker for dedup (e.g. '<!-- claude-review -->')
|
||||
// If an existing comment with this marker is found, it is updated.
|
||||
// If not found, a new comment is created.
|
||||
// legacyMarkers - optional list of legacy markers that should be migrated by
|
||||
// being considered part of the same comment family.
|
||||
// prunePrefix - optional marker prefix whose older comments should be
|
||||
// superseded. If obsoleteTitle is set, older comments are
|
||||
// collapsed instead of deleted.
|
||||
// preserveLatest - optional count of active comments to keep when
|
||||
// prunePrefix is set.
|
||||
// obsoleteMarker - optional HTML marker used to detect already-obsolete
|
||||
// comments.
|
||||
// obsoleteTitle - optional heading to use when collapsing superseded
|
||||
// comments into a details block.
|
||||
|
||||
module.exports = async function postPrComment(
|
||||
{github, context, core, prNumber, body, marker}) {
|
||||
module.exports = async function postPrComment({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
prNumber,
|
||||
body,
|
||||
marker,
|
||||
legacyMarkers = [],
|
||||
prunePrefix = '',
|
||||
preserveLatest = 0,
|
||||
obsoleteMarker = '',
|
||||
obsoleteTitle = '',
|
||||
}) {
|
||||
if (!prNumber || !body) {
|
||||
core.warning('Missing prNumber or body; skipping comment.');
|
||||
return;
|
||||
@@ -28,30 +50,155 @@ module.exports = async function postPrComment(
|
||||
// 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({
|
||||
async function listComments() {
|
||||
return await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
comment_id: existing.id,
|
||||
body: markedBody,
|
||||
issue_number: prNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
core.info(`Updated existing comment ${existing.id}`);
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
}
|
||||
|
||||
function commentActivityTime(comment) {
|
||||
const timestamp =
|
||||
Date.parse(comment.updated_at || comment.created_at || '');
|
||||
return Number.isNaN(timestamp) ? 0 : timestamp;
|
||||
}
|
||||
|
||||
function commentSortDescending(left, right) {
|
||||
const timeDelta = commentActivityTime(right) - commentActivityTime(left);
|
||||
if (timeDelta !== 0) {
|
||||
return timeDelta;
|
||||
}
|
||||
return Number(right.id || 0) - Number(left.id || 0);
|
||||
}
|
||||
|
||||
function isObsoleteComment(comment) {
|
||||
return !!obsoleteMarker && typeof comment.body === 'string' &&
|
||||
comment.body.includes(obsoleteMarker);
|
||||
}
|
||||
|
||||
function buildObsoleteBody(comment) {
|
||||
const originalBody =
|
||||
typeof comment.body === 'string' && comment.body.trim() ?
|
||||
comment.body :
|
||||
'*No original review body preserved.*';
|
||||
const title = obsoleteTitle || 'AI Review - OBSOLETE';
|
||||
return `${obsoleteMarker}\n## ${
|
||||
title}\n\n<details>\n<summary>Superseded by a newer AI review. Expand to see the original review.</summary>\n\n${
|
||||
originalBody}\n\n</details>`;
|
||||
}
|
||||
|
||||
async function deleteCommentIfPresent(comment, reason) {
|
||||
try {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id,
|
||||
});
|
||||
core.info(`${reason} ${comment.id}`);
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
core.info(`Comment ${comment.id} was already deleted by another run.`);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function supersedeCommentIfPresent(comment, reason) {
|
||||
if (!obsoleteTitle) {
|
||||
await deleteCommentIfPresent(comment, reason);
|
||||
return;
|
||||
}
|
||||
if (isObsoleteComment(comment)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await github.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id,
|
||||
body: buildObsoleteBody(comment),
|
||||
});
|
||||
core.info(`${reason} ${comment.id}`);
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
core.info(`Comment ${comment.id} was already deleted by another run.`);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
let comments = await listComments();
|
||||
const existing = comments.find(
|
||||
comment =>
|
||||
typeof comment.body === 'string' && comment.body.includes(marker));
|
||||
let currentCommentId = null;
|
||||
|
||||
if (existing) {
|
||||
try {
|
||||
const response = await github.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: existing.id,
|
||||
body: markedBody,
|
||||
});
|
||||
currentCommentId = existing.id;
|
||||
core.info(`Updated existing comment ${existing.id}`);
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
core.info(
|
||||
`Comment ${existing.id} disappeared before update; ` +
|
||||
'creating a fresh comment instead.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentCommentId) {
|
||||
const response = await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: markedBody,
|
||||
});
|
||||
currentCommentId = response.data.id;
|
||||
core.info('Created new PR comment');
|
||||
}
|
||||
|
||||
if (prunePrefix || legacyMarkers.length > 0) {
|
||||
comments = await listComments();
|
||||
const relatedComments = comments.filter(
|
||||
comment => comment.id !== currentCommentId &&
|
||||
typeof comment.body === 'string' &&
|
||||
((prunePrefix && comment.body.includes(prunePrefix)) ||
|
||||
legacyMarkers.some(
|
||||
legacyMarker => comment.body.includes(legacyMarker))));
|
||||
const activeRelatedComments =
|
||||
comments
|
||||
.filter(
|
||||
comment => typeof comment.body === 'string' &&
|
||||
!isObsoleteComment(comment) &&
|
||||
(comment.id === currentCommentId ||
|
||||
relatedComments.some(
|
||||
relatedComment => relatedComment.id === comment.id)))
|
||||
.sort(commentSortDescending);
|
||||
|
||||
const keep =
|
||||
new Set(activeRelatedComments.slice(0, Math.max(preserveLatest, 1))
|
||||
.map(comment => comment.id));
|
||||
|
||||
const supersedeCandidates = obsoleteTitle ?
|
||||
activeRelatedComments.filter(comment => !keep.has(comment.id)) :
|
||||
relatedComments.filter(comment => !keep.has(comment.id));
|
||||
|
||||
for (const comment of supersedeCandidates) {
|
||||
if (keep.has(comment.id)) {
|
||||
continue;
|
||||
}
|
||||
await supersedeCommentIfPresent(comment, 'Superseded old AI review');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const postPrComment = require('./post-pr-comment.js');
|
||||
|
||||
const OBSOLETE_MARKER = '<!-- claude-review-obsolete -->';
|
||||
const OBSOLETE_TITLE = 'Claude Code Review - OBSOLETE';
|
||||
|
||||
function makeComment(id, body, createdAt, updatedAt) {
|
||||
return {
|
||||
id,
|
||||
body,
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt || createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
function createHarness(initialComments, options = {}) {
|
||||
let comments = initialComments.map(comment => ({...comment}));
|
||||
let nextCommentId = options.nextCommentId || 1000;
|
||||
let paginateCount = 0;
|
||||
|
||||
const calls = {
|
||||
update: [],
|
||||
create: [],
|
||||
delete: [],
|
||||
};
|
||||
|
||||
const github = {
|
||||
paginate: async () => {
|
||||
paginateCount++;
|
||||
if (options.onPaginate) {
|
||||
const updated = options.onPaginate({
|
||||
paginateCount,
|
||||
comments: comments.map(comment => ({...comment})),
|
||||
});
|
||||
if (updated) {
|
||||
comments = updated.map(comment => ({...comment}));
|
||||
}
|
||||
}
|
||||
return comments.map(comment => ({...comment}));
|
||||
},
|
||||
rest: {
|
||||
issues: {
|
||||
listComments: () => {
|
||||
throw new Error('listComments should only be used through paginate');
|
||||
},
|
||||
updateComment: async ({comment_id, body}) => {
|
||||
calls.update.push({comment_id, body});
|
||||
const error =
|
||||
options.updateErrors && options.updateErrors[comment_id];
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
const index =
|
||||
comments.findIndex(comment => comment.id === comment_id);
|
||||
if (index === -1) {
|
||||
const notFound = new Error(`Comment ${comment_id} not found`);
|
||||
notFound.status = 404;
|
||||
throw notFound;
|
||||
}
|
||||
const updated = {
|
||||
...comments[index],
|
||||
body,
|
||||
updated_at: options.updateTimestamp || '2026-04-24T00:00:00Z',
|
||||
};
|
||||
comments[index] = updated;
|
||||
return {data: {...updated}};
|
||||
},
|
||||
createComment: async ({issue_number, body}) => {
|
||||
calls.create.push({issue_number, body});
|
||||
const created = makeComment(
|
||||
nextCommentId++, body,
|
||||
options.createTimestamp || '2026-04-24T00:00:00Z');
|
||||
comments.push(created);
|
||||
return {data: {id: created.id}};
|
||||
},
|
||||
deleteComment: async ({comment_id}) => {
|
||||
calls.delete.push({comment_id});
|
||||
const error =
|
||||
options.deleteErrors && options.deleteErrors[comment_id];
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
comments = comments.filter(comment => comment.id !== comment_id);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
github,
|
||||
calls,
|
||||
getComments: () => comments.map(comment => ({...comment})),
|
||||
};
|
||||
}
|
||||
|
||||
function createCore() {
|
||||
return {
|
||||
info: () => {},
|
||||
warning: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function assertObsoleteComment(comment, originalBody) {
|
||||
assert.ok(comment);
|
||||
assert.match(comment.body, new RegExp(escapeRegExp(OBSOLETE_MARKER)));
|
||||
assert.match(comment.body, new RegExp(`## ${escapeRegExp(OBSOLETE_TITLE)}`));
|
||||
assert.match(comment.body, /<details>/);
|
||||
assert.match(comment.body, /Superseded by a newer AI review/);
|
||||
assert.match(comment.body, new RegExp(escapeRegExp(originalBody)));
|
||||
}
|
||||
|
||||
const context = {
|
||||
repo: {
|
||||
owner: 'facebook',
|
||||
repo: 'rocksdb',
|
||||
},
|
||||
};
|
||||
|
||||
test(
|
||||
'creates a fresh review comment and supersedes legacy comments',
|
||||
async () => {
|
||||
const harness = createHarness(
|
||||
[
|
||||
makeComment(
|
||||
1, '<!-- claude-review-auto -->\nold legacy', 'not-a-date'),
|
||||
makeComment(
|
||||
2, '<!-- claude-review-auto -->\nnew legacy',
|
||||
'2026-04-21T00:00:00Z'),
|
||||
],
|
||||
{
|
||||
updateTimestamp: '2026-04-24T00:00:00Z',
|
||||
});
|
||||
|
||||
await postPrComment({
|
||||
github: harness.github,
|
||||
context,
|
||||
core: createCore(),
|
||||
prNumber: 14659,
|
||||
body: 'review body',
|
||||
marker: '<!-- claude-review-auto-run-500 -->',
|
||||
legacyMarkers: ['<!-- claude-review-auto -->'],
|
||||
prunePrefix: '<!-- claude-review-auto-',
|
||||
preserveLatest: 1,
|
||||
obsoleteMarker: OBSOLETE_MARKER,
|
||||
obsoleteTitle: OBSOLETE_TITLE,
|
||||
});
|
||||
|
||||
assert.equal(harness.calls.create.length, 1);
|
||||
assert.deepEqual(
|
||||
harness.calls.update.map(call => call.comment_id), [2, 1]);
|
||||
assert.equal(harness.calls.delete.length, 0);
|
||||
|
||||
const comments = harness.getComments();
|
||||
assert.equal(comments.length, 3);
|
||||
assert.match(
|
||||
comments.find(comment => comment.id === 1000).body,
|
||||
/<!-- claude-review-auto-run-500 -->/);
|
||||
assertObsoleteComment(
|
||||
comments.find(comment => comment.id === 2),
|
||||
'<!-- claude-review-auto -->\nnew legacy');
|
||||
assertObsoleteComment(
|
||||
comments.find(comment => comment.id === 1),
|
||||
'<!-- claude-review-auto -->\nold legacy');
|
||||
});
|
||||
|
||||
test(
|
||||
'updates an exact-match comment and supersedes leftover legacy comments',
|
||||
async () => {
|
||||
const harness = createHarness(
|
||||
[
|
||||
makeComment(
|
||||
3, '<!-- claude-review-auto-abcdef0 -->\ncurrent body',
|
||||
'2026-04-24T00:00:00Z'),
|
||||
makeComment(
|
||||
4, '<!-- claude-review-auto -->\nlegacy body',
|
||||
'2026-04-20T00:00:00Z'),
|
||||
],
|
||||
{
|
||||
updateTimestamp: '2026-04-24T01:00:00Z',
|
||||
});
|
||||
|
||||
await postPrComment({
|
||||
github: harness.github,
|
||||
context,
|
||||
core: createCore(),
|
||||
prNumber: 14659,
|
||||
body: 'refreshed body',
|
||||
marker: '<!-- claude-review-auto-abcdef0 -->',
|
||||
legacyMarkers: ['<!-- claude-review-auto -->'],
|
||||
prunePrefix: '<!-- claude-review-auto-',
|
||||
preserveLatest: 1,
|
||||
obsoleteMarker: OBSOLETE_MARKER,
|
||||
obsoleteTitle: OBSOLETE_TITLE,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
harness.calls.update.map(call => call.comment_id), [3, 4]);
|
||||
assert.equal(harness.calls.create.length, 0);
|
||||
assert.equal(harness.calls.delete.length, 0);
|
||||
|
||||
const comments = harness.getComments();
|
||||
assert.match(
|
||||
comments.find(comment => comment.id === 3).body,
|
||||
/<!-- claude-review-auto-abcdef0 -->\nrefreshed body/);
|
||||
assertObsoleteComment(
|
||||
comments.find(comment => comment.id === 4),
|
||||
'<!-- claude-review-auto -->\nlegacy body');
|
||||
});
|
||||
|
||||
test(
|
||||
'creates a new comment if the target disappears before update',
|
||||
async () => {
|
||||
const missing = new Error('gone');
|
||||
missing.status = 404;
|
||||
const harness = createHarness(
|
||||
[
|
||||
makeComment(
|
||||
10, '<!-- claude-review-auto-abcdef0 -->\nold body',
|
||||
'2026-04-20T00:00:00Z'),
|
||||
],
|
||||
{
|
||||
updateErrors: {
|
||||
10: missing,
|
||||
},
|
||||
});
|
||||
|
||||
await postPrComment({
|
||||
github: harness.github,
|
||||
context,
|
||||
core: createCore(),
|
||||
prNumber: 14659,
|
||||
body: 'replacement body',
|
||||
marker: '<!-- claude-review-auto-abcdef0 -->',
|
||||
});
|
||||
|
||||
assert.equal(harness.calls.update.length, 1);
|
||||
assert.equal(harness.calls.create.length, 1);
|
||||
assert.match(
|
||||
harness.calls.create[0].body, /<!-- claude-review-auto-abcdef0 -->/);
|
||||
});
|
||||
|
||||
test(
|
||||
'ignores 404 when superseding a comment already deleted by another run',
|
||||
async () => {
|
||||
const missing = new Error('gone');
|
||||
missing.status = 404;
|
||||
const harness = createHarness(
|
||||
[
|
||||
makeComment(
|
||||
20, '<!-- claude-review-auto-oldest -->\noldest',
|
||||
'2026-04-20T00:00:00Z'),
|
||||
makeComment(
|
||||
21, '<!-- claude-review-auto-newer -->\nnewer',
|
||||
'2026-04-21T00:00:00Z'),
|
||||
],
|
||||
{
|
||||
nextCommentId: 30,
|
||||
createTimestamp: '2026-04-24T00:00:00Z',
|
||||
deleteErrors: {
|
||||
20: missing,
|
||||
},
|
||||
});
|
||||
|
||||
await postPrComment({
|
||||
github: harness.github,
|
||||
context,
|
||||
core: createCore(),
|
||||
prNumber: 14659,
|
||||
body: 'fresh body',
|
||||
marker: '<!-- claude-review-auto-latest -->',
|
||||
prunePrefix: '<!-- claude-review-auto-',
|
||||
preserveLatest: 1,
|
||||
obsoleteMarker: OBSOLETE_MARKER,
|
||||
obsoleteTitle: OBSOLETE_TITLE,
|
||||
});
|
||||
|
||||
assert.equal(harness.calls.create.length, 1);
|
||||
assert.equal(harness.calls.delete.length, 0);
|
||||
assert.deepEqual(
|
||||
harness.calls.update.map(call => call.comment_id), [21, 20]);
|
||||
|
||||
const comments = harness.getComments();
|
||||
assertObsoleteComment(
|
||||
comments.find(comment => comment.id === 21),
|
||||
'<!-- claude-review-auto-newer -->\nnewer');
|
||||
});
|
||||
|
||||
test(
|
||||
'supersedes the current review when a newer concurrent one appears',
|
||||
async () => {
|
||||
const harness = createHarness(
|
||||
[
|
||||
makeComment(
|
||||
40, '<!-- claude-review-auto-current -->\ncurrent',
|
||||
'2026-04-20T00:00:00Z'),
|
||||
makeComment(
|
||||
41, '<!-- claude-review-auto-older -->\nolder',
|
||||
'2026-04-19T00:00:00Z'),
|
||||
],
|
||||
{
|
||||
updateTimestamp: '2026-04-24T00:00:00Z',
|
||||
onPaginate: ({paginateCount, comments}) => {
|
||||
if (paginateCount !== 2) {
|
||||
return comments;
|
||||
}
|
||||
return comments.concat([
|
||||
makeComment(
|
||||
42, '<!-- claude-review-auto-newer -->\nnewer',
|
||||
'2026-04-25T00:00:00Z'),
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
await postPrComment({
|
||||
github: harness.github,
|
||||
context,
|
||||
core: createCore(),
|
||||
prNumber: 14659,
|
||||
body: 'updated current body',
|
||||
marker: '<!-- claude-review-auto-current -->',
|
||||
prunePrefix: '<!-- claude-review-auto-',
|
||||
preserveLatest: 1,
|
||||
obsoleteMarker: OBSOLETE_MARKER,
|
||||
obsoleteTitle: OBSOLETE_TITLE,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
harness.calls.update.map(call => call.comment_id), [40, 40, 41]);
|
||||
assert.equal(harness.calls.delete.length, 0);
|
||||
|
||||
const comments = harness.getComments();
|
||||
assert.match(
|
||||
comments.find(comment => comment.id === 42).body,
|
||||
/<!-- claude-review-auto-newer -->\nnewer/);
|
||||
assertObsoleteComment(
|
||||
comments.find(comment => comment.id === 40),
|
||||
'<!-- claude-review-auto-current -->\nupdated current body');
|
||||
assertObsoleteComment(
|
||||
comments.find(comment => comment.id === 41),
|
||||
'<!-- claude-review-auto-older -->\nolder');
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,185 @@
|
||||
# Shared comment-posting workflow for AI reviews.
|
||||
|
||||
name: AI Review Comment
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
provider:
|
||||
description: Provider key, for example "claude" or "codex"
|
||||
required: true
|
||||
type: string
|
||||
result_artifact_name:
|
||||
description: Artifact name produced by the analysis workflow
|
||||
required: true
|
||||
type: string
|
||||
comment_file:
|
||||
description: Markdown comment file produced by the analysis workflow
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: true
|
||||
|
||||
- name: Download review artifact
|
||||
id: download
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ inputs.result_artifact_name }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
continue-on-error: true
|
||||
|
||||
- name: Post or update PR comment
|
||||
if: steps.download.outcome == 'success'
|
||||
env:
|
||||
PROVIDER: ${{ inputs.provider }}
|
||||
COMMENT_FILE: ${{ inputs.comment_file }}
|
||||
RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync(process.env.COMMENT_FILE) ||
|
||||
!fs.existsSync('pr_number.txt')) {
|
||||
core.info('No review results found; skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
const post = require('./.github/scripts/post-pr-comment.js');
|
||||
const provider = process.env.PROVIDER;
|
||||
const providerTitle = provider === 'codex' ? 'Codex' : 'Claude';
|
||||
const body = fs.readFileSync(process.env.COMMENT_FILE, 'utf8');
|
||||
const prNumber = parseInt(
|
||||
fs.readFileSync('pr_number.txt', 'utf8').trim(),
|
||||
10
|
||||
);
|
||||
const trigger = fs.existsSync('trigger_type.txt') ?
|
||||
fs.readFileSync('trigger_type.txt', 'utf8').trim() :
|
||||
'auto';
|
||||
const headSha = fs.existsSync('head_sha.txt') ?
|
||||
fs.readFileSync('head_sha.txt', 'utf8').trim() :
|
||||
'';
|
||||
const shortSha = headSha ? headSha.substring(0, 7) : '';
|
||||
const runId = process.env.RUN_ID;
|
||||
|
||||
let marker = '';
|
||||
let legacyMarkers = [];
|
||||
let prunePrefix = '';
|
||||
let preserveLatest = 0;
|
||||
let obsoleteMarker = '';
|
||||
let obsoleteTitle = '';
|
||||
|
||||
if (trigger === 'auto') {
|
||||
if (!shortSha) {
|
||||
core.warning(
|
||||
'Missing head_sha.txt for auto review; skipping comment.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
marker = `<!-- ${provider}-review-auto-run-${runId} -->`;
|
||||
legacyMarkers = [`<!-- ${provider}-review-auto -->`];
|
||||
prunePrefix = `<!-- ${provider}-review-auto-`;
|
||||
preserveLatest = 1;
|
||||
obsoleteMarker = `<!-- ${provider}-review-obsolete -->`;
|
||||
obsoleteTitle = `${providerTitle} Code Review - OBSOLETE`;
|
||||
} else {
|
||||
marker = `<!-- ${provider}-review-manual-run-${runId} -->`;
|
||||
}
|
||||
|
||||
await post({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
prNumber,
|
||||
body,
|
||||
marker,
|
||||
legacyMarkers,
|
||||
prunePrefix,
|
||||
preserveLatest,
|
||||
obsoleteMarker,
|
||||
obsoleteTitle,
|
||||
});
|
||||
|
||||
- name: Add reaction to trigger comment
|
||||
if: steps.download.outcome == 'success'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync('trigger_type.txt')) return;
|
||||
const trigger = fs.readFileSync('trigger_type.txt', 'utf8').trim();
|
||||
if (trigger !== 'manual') return;
|
||||
if (!fs.existsSync('comment_id.txt')) return;
|
||||
const commentId = parseInt(
|
||||
fs.readFileSync('comment_id.txt', 'utf8').trim(),
|
||||
10
|
||||
);
|
||||
if (!commentId) return;
|
||||
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: commentId,
|
||||
content: 'rocket'
|
||||
});
|
||||
|
||||
failure-notice:
|
||||
if: github.event.workflow_run.conclusion == 'failure'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifact for PR number
|
||||
id: download
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ inputs.result_artifact_name }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
continue-on-error: true
|
||||
|
||||
- name: React with failure
|
||||
if: steps.download.outcome == 'success'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync('comment_id.txt')) return;
|
||||
const commentId = parseInt(
|
||||
fs.readFileSync('comment_id.txt', 'utf8').trim(),
|
||||
10
|
||||
);
|
||||
if (!commentId) return;
|
||||
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: commentId,
|
||||
content: 'confused'
|
||||
});
|
||||
|
||||
unauthorized-notice:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.workflow_run.event == 'issue_comment' &&
|
||||
github.event.workflow_run.conclusion == 'skipped'
|
||||
steps:
|
||||
- name: Log skipped unauthorized request
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
core.info(
|
||||
'Analysis workflow was skipped, which usually means the ' +
|
||||
'issue_comment requester was not in the authorized list.'
|
||||
);
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
- name: Download clang-tidy results
|
||||
id: download
|
||||
uses: actions/download-artifact@v4.0.0
|
||||
uses: actions/download-artifact@v4.1.3
|
||||
with:
|
||||
name: clang-tidy-result
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
with:
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
- name: Install clang-tidy
|
||||
if: steps.diff-base.outputs.skip != 'true'
|
||||
run: apt-get update && apt-get install -y clang-tidy
|
||||
run: apt-get update && apt-get install -y clang-tidy-21 && ln -sf /usr/bin/clang-tidy-21 /usr/local/bin/clang-tidy
|
||||
- name: Generate compile_commands.json
|
||||
if: steps.diff-base.outputs.skip != 'true'
|
||||
run: |
|
||||
mkdir build && cd build
|
||||
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
-DCMAKE_C_COMPILER=clang-18 \
|
||||
-DCMAKE_CXX_COMPILER=clang++-18 ..
|
||||
-DCMAKE_C_COMPILER=clang-21 \
|
||||
-DCMAKE_CXX_COMPILER=clang++-21 ..
|
||||
cd ..
|
||||
ln -sf build/compile_commands.json compile_commands.json
|
||||
- name: Run clang-tidy on changed files
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
# 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.
|
||||
# Thin wrapper around .github/workflows/ai-review-comment.yml so the provider
|
||||
# specific workflow name stays stable while the shared implementation lives in
|
||||
# one reusable workflow.
|
||||
|
||||
name: Post Claude Review Comment
|
||||
|
||||
@@ -20,127 +16,10 @@ permissions:
|
||||
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.');
|
||||
review-comment:
|
||||
uses: ./.github/workflows/ai-review-comment.yml
|
||||
with:
|
||||
provider: claude
|
||||
result_artifact_name: claude-review-result
|
||||
comment_file: claude-review-comment.md
|
||||
secrets: inherit
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
# 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
|
||||
# Thin wrapper around .github/workflows/ai-review-analysis.yml so provider
|
||||
# specific triggers and workflow_dispatch inputs stay stable while the shared
|
||||
# implementation lives in one reusable workflow.
|
||||
|
||||
name: Claude Code Review
|
||||
|
||||
@@ -22,6 +11,11 @@ on:
|
||||
workflows: ["facebook/rocksdb/pr-jobs"]
|
||||
types: [completed]
|
||||
|
||||
# The early pull_request_target path is limited to same-repo PRs by the job
|
||||
# condition below. Fork PRs skip this path and rely on workflow_run instead.
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
@@ -32,445 +26,44 @@ on:
|
||||
required: true
|
||||
type: number
|
||||
model:
|
||||
description: Claude model to use
|
||||
description: Claude model to use (defaults to latest Opus)
|
||||
required: false
|
||||
type: choice
|
||||
options:
|
||||
- claude-opus-4-6
|
||||
- claude-sonnet-4-20250514
|
||||
- claude-sonnet-4-6
|
||||
default: claude-opus-4-6
|
||||
thinking_budget:
|
||||
description: Override MAX_THINKING_TOKENS (blank = auto-classify, capped at 24000)
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# The shared analysis workflow polls check state, inspects prior runs, and
|
||||
# reads PR metadata. Keep those permissions read-only in the caller.
|
||||
actions: read
|
||||
checks: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
# ======================== Auto Review ======================== #
|
||||
auto-review:
|
||||
name: Auto Claude Review
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
review:
|
||||
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
|
||||
github.event_name != 'pull_request_target' ||
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: ./.github/workflows/ai-review-analysis.yml
|
||||
with:
|
||||
provider: claude
|
||||
display_name: Claude
|
||||
review_command: /claude-review
|
||||
query_command: /claude-query
|
||||
result_artifact_name: claude-review-result
|
||||
comment_file: claude-review-comment.md
|
||||
default_model: claude-opus-4-6
|
||||
selected_model: ${{ github.event_name == 'workflow_dispatch' && inputs.model || '' }}
|
||||
classifier_model: claude-sonnet-4-6
|
||||
recovery_model: claude-sonnet-4-6
|
||||
thinking_budget: ${{ github.event_name == 'workflow_dispatch' && inputs.thinking_budget || '' }}
|
||||
dispatch_pr_number: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || '' }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Codex Code Review — Comment Posting Workflow
|
||||
#
|
||||
# Thin wrapper around .github/workflows/ai-review-comment.yml so the provider
|
||||
# specific workflow name stays stable while the shared implementation lives in
|
||||
# one reusable workflow.
|
||||
|
||||
name: Post Codex Review Comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Codex Code Review"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
review-comment:
|
||||
uses: ./.github/workflows/ai-review-comment.yml
|
||||
with:
|
||||
provider: codex
|
||||
result_artifact_name: codex-review-result
|
||||
comment_file: codex-review-comment.md
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,68 @@
|
||||
# Codex Code Review — Analysis Workflow
|
||||
#
|
||||
# Thin wrapper around .github/workflows/ai-review-analysis.yml so provider
|
||||
# specific triggers and workflow_dispatch inputs stay stable while the shared
|
||||
# implementation lives in one reusable workflow.
|
||||
|
||||
name: Codex Code Review
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["facebook/rocksdb/pr-jobs"]
|
||||
types: [completed]
|
||||
|
||||
# The early pull_request_target path is limited to same-repo PRs by the job
|
||||
# condition below. Fork PRs skip this path and rely on workflow_run instead.
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: PR number to review
|
||||
required: true
|
||||
type: number
|
||||
model:
|
||||
description: Codex model to use
|
||||
required: false
|
||||
type: choice
|
||||
options:
|
||||
- gpt-5.5
|
||||
- gpt-5.3-codex
|
||||
- gpt-5.2-codex
|
||||
default: gpt-5.5
|
||||
thinking_budget:
|
||||
description: Override MAX_THINKING_TOKENS (blank = auto-classify)
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# The shared analysis workflow polls check state, inspects prior runs, and
|
||||
# reads PR metadata. Keep those permissions read-only in the caller.
|
||||
actions: read
|
||||
checks: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
review:
|
||||
if: >-
|
||||
github.event_name != 'pull_request_target' ||
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: ./.github/workflows/ai-review-analysis.yml
|
||||
with:
|
||||
provider: codex
|
||||
display_name: Codex
|
||||
review_command: /codex-review
|
||||
query_command: /codex-query
|
||||
result_artifact_name: codex-review-result
|
||||
comment_file: codex-review-comment.md
|
||||
default_model: gpt-5.5
|
||||
selected_model: ${{ github.event_name == 'workflow_dispatch' && inputs.model || '' }}
|
||||
thinking_budget: ${{ github.event_name == 'workflow_dispatch' && inputs.thinking_budget || '' }}
|
||||
dispatch_pr_number: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || '' }}
|
||||
secrets: inherit
|
||||
@@ -41,22 +41,41 @@ jobs:
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- run: make V=1 -j32 check
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-clang-18-asan-ubsan-with-folly:
|
||||
build-linux-clang-21-asan-ubsan-with-folly:
|
||||
if: ${{ github.repository_owner == 'facebook' }}
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
env:
|
||||
CC: clang-18
|
||||
CXX: clang++-18
|
||||
CC: clang-21
|
||||
CXX: clang++-21
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/cache-getdeps-downloads"
|
||||
- uses: "./.github/actions/setup-folly"
|
||||
- uses: "./.github/actions/build-folly"
|
||||
- name: Build folly and dependencies
|
||||
run: |
|
||||
clean_path=()
|
||||
IFS=: read -ra path_entries <<< "$PATH"
|
||||
for entry in "${path_entries[@]}"; do
|
||||
if [[ "$entry" != "/usr/lib/ccache" ]]; then
|
||||
clean_path+=("$entry")
|
||||
fi
|
||||
done
|
||||
export PATH="$(IFS=:; echo "${clean_path[*]}")"
|
||||
ccache_bin="$(command -v ccache || true)"
|
||||
if [[ -n "$ccache_bin" && -x "$ccache_bin" ]]; then
|
||||
mv "$ccache_bin" "${ccache_bin}.disabled"
|
||||
fi
|
||||
export CC=gcc
|
||||
export CXX=g++
|
||||
export USE_CCACHE=0
|
||||
export CCACHE_DISABLE=1
|
||||
make build_folly
|
||||
shell: bash
|
||||
- run: LIB_MODE=static USE_CLANG=1 USE_FOLLY=1 COMPILE_WITH_UBSAN=1 COMPILE_WITH_ASAN=1 make -j32 check
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-cmake-with-folly:
|
||||
@@ -145,13 +164,13 @@ jobs:
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- name: Build rocksdb lib
|
||||
run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 make -j4 static_lib
|
||||
run: CC=clang-21 CXX=clang++-21 USE_CLANG=1 make -j4 static_lib
|
||||
- name: Build fuzzers
|
||||
run: cd fuzz && make sst_file_writer_fuzzer db_fuzzer db_map_fuzzer
|
||||
- uses: "./.github/actions/post-steps"
|
||||
|
||||
@@ -72,6 +72,8 @@ jobs:
|
||||
run: make check-buck-targets
|
||||
- name: Simple source code checks
|
||||
run: make check-sources
|
||||
- name: Validate GitHub Actions YAML
|
||||
run: make check-workflow-yaml
|
||||
- name: Sanity check check_format_compatible.sh
|
||||
run: |-
|
||||
export TEST_TMPDIR=/dev/shm/rocksdb
|
||||
@@ -105,7 +107,7 @@ jobs:
|
||||
runs-on:
|
||||
labels: 4-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
@@ -295,23 +297,23 @@ jobs:
|
||||
- uses: "./.github/actions/teardown-ccache"
|
||||
if: always()
|
||||
- uses: "./.github/actions/post-steps"
|
||||
build-linux-clang-18-no_test_run:
|
||||
if: needs.config.outputs.only_job == 'build-linux-clang-18-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
build-linux-clang-21-no_test_run:
|
||||
if: needs.config.outputs.only_job == 'build-linux-clang-21-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- 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
|
||||
cache-key-prefix: clang-21
|
||||
- run: CC=clang-21 CXX=clang++-21 USE_CLANG=1 make -j32 all microbench
|
||||
- run: make clean
|
||||
- run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 DEBUG_LEVEL=0 make -j32 release
|
||||
- run: CC=clang-21 CXX=clang++-21 USE_CLANG=1 DEBUG_LEVEL=0 make -j32 release
|
||||
- uses: "./.github/actions/teardown-ccache"
|
||||
if: always()
|
||||
- uses: "./.github/actions/post-steps"
|
||||
@@ -321,7 +323,7 @@ jobs:
|
||||
runs-on:
|
||||
labels: 16-core-ubuntu
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
@@ -371,22 +373,25 @@ jobs:
|
||||
crash_duration: 240
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
|
||||
options: --shm-size=16gb
|
||||
options: --shm-size=16gb --ulimit nofile=1048576:1048576
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- uses: "./.github/actions/setup-ccache"
|
||||
with:
|
||||
cache-key-prefix: mini-crashtest
|
||||
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=${{ matrix.crash_duration }} --max_key=2500000' ${{ matrix.crash_test_target }}
|
||||
- run: |
|
||||
ulimit -S -n $(ulimit -Hn) # Raise open files soft limit to hard limit
|
||||
echo "Updated ulimit -Hn: $(ulimit -Hn) -Sn: $(ulimit -Sn)"
|
||||
make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=${{ matrix.crash_duration }} --max_key=2500000' ${{ matrix.crash_test_target }}
|
||||
- uses: "./.github/actions/teardown-ccache"
|
||||
if: always()
|
||||
- uses: "./.github/actions/post-steps"
|
||||
with:
|
||||
artifact-prefix: "${{ github.job }}-${{ matrix.crash_test_target }}"
|
||||
# ======================= Linux with Sanitizers ===================== #
|
||||
build-linux-clang18-asan-ubsan:
|
||||
if: needs.config.outputs.only_job == 'build-linux-clang18-asan-ubsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
build-linux-clang21-asan-ubsan:
|
||||
if: needs.config.outputs.only_job == 'build-linux-clang21-asan-ubsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 32-core-ubuntu
|
||||
@@ -395,15 +400,15 @@ jobs:
|
||||
matrix:
|
||||
shard: [0, 1, 2]
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- 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
|
||||
cache-key-prefix: clang21-asan-ubsan
|
||||
- run: COMPILE_WITH_ASAN=1 COMPILE_WITH_UBSAN=1 CC=clang-21 CXX=clang++-21 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j40 check
|
||||
env:
|
||||
CI_SHARD_INDEX: ${{ matrix.shard }}
|
||||
CI_TOTAL_SHARDS: 3
|
||||
@@ -412,8 +417,8 @@ jobs:
|
||||
- uses: "./.github/actions/post-steps"
|
||||
with:
|
||||
artifact-prefix: "${{ github.job }}-shard${{ matrix.shard }}"
|
||||
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')
|
||||
build-linux-clang21-mini-tsan:
|
||||
if: needs.config.outputs.only_job == 'build-linux-clang21-mini-tsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
|
||||
needs: config
|
||||
runs-on:
|
||||
labels: 32-core-ubuntu
|
||||
@@ -422,15 +427,15 @@ jobs:
|
||||
matrix:
|
||||
shard: [0, 1, 2]
|
||||
container:
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
options: --shm-size=16gb
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.0
|
||||
- uses: "./.github/actions/pre-steps"
|
||||
- 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
|
||||
cache-key-prefix: clang21-tsan
|
||||
- run: COMPILE_WITH_TSAN=1 CC=clang-21 CXX=clang++-21 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j32 check
|
||||
env:
|
||||
CI_SHARD_INDEX: ${{ matrix.shard }}
|
||||
CI_TOTAL_SHARDS: 3
|
||||
@@ -532,7 +537,7 @@ jobs:
|
||||
suite_run: db_test
|
||||
run_java: "false"
|
||||
- test_shard: other
|
||||
suite_run: arena_test,db_basic_test,db_test2,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test
|
||||
suite_run: arena_test,db_basic_test,db_etc2_test,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test
|
||||
run_java: "false"
|
||||
- test_shard: java
|
||||
suite_run: ""
|
||||
|
||||
+9
-1
@@ -52,7 +52,7 @@ GRTAGS
|
||||
GTAGS
|
||||
rocksdb_dump
|
||||
rocksdb_undump
|
||||
db_test2
|
||||
db_etc2_test
|
||||
trace_analyzer
|
||||
block_cache_trace_analyzer
|
||||
io_tracer_parser
|
||||
@@ -102,3 +102,11 @@ cmake-build-*
|
||||
third-party/folly/
|
||||
.cache
|
||||
*.sublime-*
|
||||
|
||||
# Claude Code local settings
|
||||
.claude/settings.local.json
|
||||
|
||||
tools/__pycache__/
|
||||
# Keep documentation trackable even if broader ignores match names like "*_test".
|
||||
!docs/
|
||||
!docs/**
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Agent Instructions
|
||||
|
||||
This repository's authoritative agent instructions live in `CLAUDE.md`.
|
||||
|
||||
Read and follow [`CLAUDE.md`](./CLAUDE.md) in full before making changes or
|
||||
reviewing code in this checkout.
|
||||
|
||||
If there is any ambiguity between this file and `CLAUDE.md`, `CLAUDE.md` takes
|
||||
precedence.
|
||||
@@ -32,12 +32,14 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/blob/blob_file_cache.cc",
|
||||
"db/blob/blob_file_garbage.cc",
|
||||
"db/blob/blob_file_meta.cc",
|
||||
"db/blob/blob_file_partition_manager.cc",
|
||||
"db/blob/blob_file_reader.cc",
|
||||
"db/blob/blob_garbage_meter.cc",
|
||||
"db/blob/blob_log_format.cc",
|
||||
"db/blob/blob_log_sequential_reader.cc",
|
||||
"db/blob/blob_log_writer.cc",
|
||||
"db/blob/blob_source.cc",
|
||||
"db/blob/blob_write_batch_transformer.cc",
|
||||
"db/blob/prefetch_buffer_collection.cc",
|
||||
"db/builder.cc",
|
||||
"db/c.cc",
|
||||
@@ -109,6 +111,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/version_util.cc",
|
||||
"db/wal_edit.cc",
|
||||
"db/wal_manager.cc",
|
||||
"db/wide/read_path_blob_resolver.cc",
|
||||
"db/wide/wide_column_serialization.cc",
|
||||
"db/wide/wide_columns.cc",
|
||||
"db/wide/wide_columns_helper.cc",
|
||||
@@ -331,6 +334,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"utilities/secondary_index/simple_secondary_index.cc",
|
||||
"utilities/simulator_cache/cache_simulator.cc",
|
||||
"utilities/simulator_cache/sim_cache.cc",
|
||||
"utilities/sorted_run_builder/sorted_run_builder.cc",
|
||||
"utilities/table_properties_collectors/compact_for_tiering_collector.cc",
|
||||
"utilities/table_properties_collectors/compact_on_deletion_collector.cc",
|
||||
"utilities/trace/file_trace_reader_writer.cc",
|
||||
@@ -374,10 +378,10 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"utilities/write_batch_with_index/write_batch_with_index_internal.cc",
|
||||
], deps=[
|
||||
"//folly/container:f14_hash",
|
||||
"//folly/experimental/coro:blocking_wait",
|
||||
"//folly/experimental/coro:collect",
|
||||
"//folly/experimental/coro:coroutine",
|
||||
"//folly/experimental/coro:task",
|
||||
"//folly/coro:blocking_wait",
|
||||
"//folly/coro:collect",
|
||||
"//folly/coro:coroutine",
|
||||
"//folly/coro:task",
|
||||
"//folly/synchronization:distributed_mutex",
|
||||
], headers=glob(["**/*.h"]), link_whole=False, extra_test_libs=False)
|
||||
|
||||
@@ -4804,6 +4808,12 @@ cpp_unittest_wrapper(name="db_blob_corruption_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_blob_direct_write_test",
|
||||
srcs=["db/blob/db_blob_direct_write_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_blob_index_test",
|
||||
srcs=["db/blob/db_blob_index_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -4858,6 +4868,12 @@ cpp_unittest_wrapper(name="db_encryption_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_etc2_test",
|
||||
srcs=["db/db_etc2_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_etc3_test",
|
||||
srcs=["db/db_etc3_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -4942,6 +4958,12 @@ cpp_unittest_wrapper(name="db_merge_operator_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_open_with_config_test",
|
||||
srcs=["db/db_open_with_config_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_options_test",
|
||||
srcs=["db/db_options_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -5008,12 +5030,6 @@ cpp_unittest_wrapper(name="db_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_test2",
|
||||
srcs=["db/db_test2.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_universal_compaction_test",
|
||||
srcs=["db/db_universal_compaction_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -5032,6 +5048,12 @@ cpp_unittest_wrapper(name="db_wide_basic_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_wide_blob_direct_write_test",
|
||||
srcs=["db/wide/db_wide_blob_direct_write_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_with_timestamp_basic_test",
|
||||
srcs=["db/db_with_timestamp_basic_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -5532,6 +5554,12 @@ cpp_unittest_wrapper(name="slice_transform_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="sorted_run_builder_test",
|
||||
srcs=["utilities/sorted_run_builder/sorted_run_builder_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="sst_dump_test",
|
||||
srcs=["tools/sst_dump_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
|
||||
@@ -209,14 +209,48 @@ The following patterns emerged as frequent sources of review feedback:
|
||||
## Important tips
|
||||
|
||||
### Build system
|
||||
* There are 3 build system. Make, CMake, BUCK(meta internal).
|
||||
* There are 3 build system. Make for git clones, BUCK (meta internal) for hg
|
||||
clones, and CMake for some special cases.
|
||||
* When a new .cc file is added, update Makefile, CMakeLists.txt, src.mk, BUCK.
|
||||
* Don't manually edit BUCK file, after updating src.mk, run
|
||||
/usr/local/bin/python3 buckifier/buckify_rocksdb.py to update it
|
||||
* Use make to build and run the test. CMake and BUCK are not used locally.
|
||||
* Use `make dbg` command to build all of the unit test in debug mode.
|
||||
* For -j in make command, use the number of CPU cores to decide it.
|
||||
|
||||
### When to run `make clean` (avoid mixing build modes)
|
||||
|
||||
The Makefile does **not** track build mode, so object files from a prior
|
||||
build are silently reused even when compiled with different flags, leading
|
||||
to confusing linker errors, sanitizer false negatives, ODR violations, or
|
||||
"phantom" bugs.
|
||||
|
||||
Run `make clean` before switching any of these:
|
||||
* **`ASSERT_STATUS_CHECKED=1` ↔ unset** — changes the `Status` class layout (ABI break).
|
||||
* **Sanitizer builds** — toggling any of `COMPILE_WITH_ASAN=1`,
|
||||
`COMPILE_WITH_UBSAN=1`, `COMPILE_WITH_TSAN=1` on/off.
|
||||
* `DEBUG_LEVEL=0` (release) ↔ `DEBUG_LEVEL=1` (debug, default for `make dbg`).
|
||||
* Different compilers, `OPT` levels, or other flags affecting codegen/ABI.
|
||||
|
||||
**Notable exception:** `DEBUG_LEVEL=2` can be safely mixed with
|
||||
`DEBUG_LEVEL=1` — rebuild a subset of files with `DEBUG_LEVEL=2` to get
|
||||
extra/more accurate runtime checks for those files without a full clean.
|
||||
|
||||
When in doubt, `make clean` is cheap insurance compared to chasing a
|
||||
phantom bug.
|
||||
|
||||
### Source checks
|
||||
* Run `make check-sources` before committing. This catches non-ASCII
|
||||
characters in source files and other source-level issues that CI will
|
||||
reject. In particular, **do not use Unicode characters** (em dashes,
|
||||
smart quotes, etc.) in comments or strings -- use ASCII equivalents
|
||||
(`--` instead of em dash, `'` instead of smart quote, etc.).
|
||||
|
||||
### RTTI and dynamic_cast
|
||||
* Production code and `db_stress` must build in **release mode
|
||||
(`-fno-rtti`)**. Do not use `dynamic_cast` anywhere except unit tests.
|
||||
Use `static_cast_with_check` from `util/cast_util.h` (validates with
|
||||
`dynamic_cast` in debug builds, plain `static_cast` in release).
|
||||
* Unit tests (`*_test.cc`) are built in debug mode with RTTI enabled.
|
||||
|
||||
### Unit Test
|
||||
* After all of the unit tests are added, review them and try to extract common
|
||||
reusable utility functions to reduce code duplication due to copy past between
|
||||
@@ -227,6 +261,11 @@ The following patterns emerged as frequent sources of review feedback:
|
||||
* When there are multiple unit tests need to be executed, try to use
|
||||
gtest_parallel.py if available. E.g.
|
||||
python3 ${GTEST_PARALLEL}/gtest_parallel.py ./table_test
|
||||
* After writing a test, stress-test for flakiness:
|
||||
```bash
|
||||
COERCE_CONTEXT_SWITCH=1 make {test_binary}
|
||||
./{test_binary} --gtest_filter="*YourTestName*" --gtest_repeat=5
|
||||
```
|
||||
|
||||
### Unit test dedup guidelines
|
||||
* Extract helper functions for repeated patterns such as object
|
||||
@@ -261,6 +300,14 @@ The following patterns emerged as frequent sources of review feedback:
|
||||
### Stress test
|
||||
* When adding a new feature, make sure stress test covers the new option.
|
||||
|
||||
### Component docs
|
||||
* For component-level design notes and implementation walkthroughs, start with
|
||||
`docs/components/index.md`.
|
||||
* Documentation under `docs/components/` is organized by subsystem in
|
||||
`docs/components/<area>/`.
|
||||
* Each subsystem directory should have an `index.md` entry point plus focused
|
||||
chapter files for deeper topics.
|
||||
|
||||
### DB bench update
|
||||
* When adding a performance related feature, support it in db_bench
|
||||
|
||||
@@ -274,6 +321,9 @@ The following patterns emerged as frequent sources of review feedback:
|
||||
* Execute make clean to clean all of the changes.
|
||||
* Execute make check to build all of the changes and execute all of the tests.
|
||||
Note that executing all of the tests could take multiple minutes.
|
||||
* Run `ASSERT_STATUS_CHECKED=1 make check` to verify all Status objects are
|
||||
properly checked. This catches missing error handling that can lead to
|
||||
silent data corruption.
|
||||
|
||||
### Monitoring make check progress
|
||||
* Use `make check-progress` to get machine-parseable JSON progress while
|
||||
|
||||
+88
-2
@@ -654,6 +654,8 @@ if(USE_FOLLY)
|
||||
FMT_INST_PATH)
|
||||
exec_program(ls ARGS -d ${FOLLY_INST_PATH}/../gflags* OUTPUT_VARIABLE
|
||||
GFLAGS_INST_PATH)
|
||||
exec_program(ls ARGS -d ${FOLLY_INST_PATH}/../glog* OUTPUT_VARIABLE
|
||||
GLOG_INST_PATH)
|
||||
set(Boost_DIR ${BOOST_INST_PATH}/lib/cmake/Boost-1.83.0)
|
||||
if(EXISTS ${FMT_INST_PATH}/lib64)
|
||||
set(fmt_DIR ${FMT_INST_PATH}/lib64/cmake/fmt)
|
||||
@@ -661,6 +663,42 @@ if(USE_FOLLY)
|
||||
set(fmt_DIR ${FMT_INST_PATH}/lib/cmake/fmt)
|
||||
endif()
|
||||
set(gflags_DIR ${GFLAGS_INST_PATH}/lib/cmake/gflags)
|
||||
if(EXISTS ${GLOG_INST_PATH}/lib64/cmake/glog)
|
||||
set(GLOG_CMAKE_DIR ${GLOG_INST_PATH}/lib64/cmake/glog)
|
||||
elseif(EXISTS ${GLOG_INST_PATH}/lib/cmake/glog)
|
||||
set(GLOG_CMAKE_DIR ${GLOG_INST_PATH}/lib/cmake/glog)
|
||||
endif()
|
||||
if(NOT GLOG_CMAKE_DIR)
|
||||
find_library(GETDEPS_GLOG_LIBRARY NAMES glogd glog
|
||||
PATHS ${GLOG_INST_PATH}/lib64 ${GLOG_INST_PATH}/lib
|
||||
NO_DEFAULT_PATH)
|
||||
if(NOT GETDEPS_GLOG_LIBRARY OR
|
||||
NOT EXISTS ${GLOG_INST_PATH}/include/glog/logging.h)
|
||||
message(FATAL_ERROR "Could not find getdeps glog under "
|
||||
"${GLOG_INST_PATH}")
|
||||
endif()
|
||||
set(GLOG_CMAKE_DIR ${CMAKE_CURRENT_BINARY_DIR}/getdeps-glog-cmake)
|
||||
file(MAKE_DIRECTORY ${GLOG_CMAKE_DIR})
|
||||
file(WRITE ${GLOG_CMAKE_DIR}/glog-config.cmake
|
||||
"if(NOT TARGET glog::glog)\n"
|
||||
" add_library(glog::glog UNKNOWN IMPORTED)\n"
|
||||
" set_target_properties(glog::glog PROPERTIES\n"
|
||||
" IMPORTED_LOCATION \"${GETDEPS_GLOG_LIBRARY}\"\n"
|
||||
" INTERFACE_INCLUDE_DIRECTORIES \"${GLOG_INST_PATH}/include\")\n"
|
||||
"endif()\n"
|
||||
"set(glog_FOUND TRUE)\n"
|
||||
"set(Glog_FOUND TRUE)\n"
|
||||
"set(GLOG_FOUND TRUE)\n"
|
||||
"set(GLOG_LIBRARY \"${GETDEPS_GLOG_LIBRARY}\")\n"
|
||||
"set(GLOG_LIBRARIES \"${GETDEPS_GLOG_LIBRARY}\")\n"
|
||||
"set(GLOG_INCLUDE_DIR \"${GLOG_INST_PATH}/include\")\n")
|
||||
endif()
|
||||
set(glog_DIR ${GLOG_CMAKE_DIR})
|
||||
set(Glog_DIR ${GLOG_CMAKE_DIR})
|
||||
# Unlike gflags, glog may also be installed system-wide. Pre-resolve the
|
||||
# getdeps copy so folly-config.cmake cannot fall back to another version.
|
||||
find_package(glog CONFIG REQUIRED PATHS ${GLOG_CMAKE_DIR}
|
||||
NO_DEFAULT_PATH)
|
||||
|
||||
exec_program(sed ARGS -i 's/gflags_shared//g'
|
||||
${FOLLY_INST_PATH}/lib/cmake/folly/folly-targets.cmake)
|
||||
@@ -674,6 +712,31 @@ if(USE_FOLLY)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Folly itself uses gflags transitively, but RocksDB tools and benchmarks
|
||||
# also call gflags APIs directly, so they need an explicit link dependency.
|
||||
if(WITH_GFLAGS)
|
||||
if(NOT TARGET gflags::gflags AND NOT TARGET gflags_shared AND
|
||||
NOT gflags_LIBRARIES)
|
||||
find_package(gflags CONFIG QUIET)
|
||||
if(NOT gflags_FOUND)
|
||||
find_package(gflags REQUIRED)
|
||||
endif()
|
||||
endif()
|
||||
if(TARGET gflags::gflags)
|
||||
set(GFLAGS_LIB gflags::gflags)
|
||||
elseif(TARGET gflags_shared)
|
||||
set(GFLAGS_LIB gflags_shared)
|
||||
elseif(DEFINED GFLAGS_TARGET AND TARGET ${GFLAGS_TARGET})
|
||||
set(GFLAGS_LIB ${GFLAGS_TARGET})
|
||||
elseif(gflags_LIBRARIES)
|
||||
set(GFLAGS_LIB ${gflags_LIBRARIES})
|
||||
else()
|
||||
message(FATAL_ERROR
|
||||
"WITH_GFLAGS is enabled, but no gflags library could be resolved")
|
||||
endif()
|
||||
list(APPEND THIRDPARTY_LIBS ${GFLAGS_LIB})
|
||||
endif()
|
||||
|
||||
add_compile_definitions(USE_FOLLY FOLLY_NO_CONFIG HAVE_CXX11_ATOMIC)
|
||||
list(APPEND THIRDPARTY_LIBS Folly::folly)
|
||||
set(FOLLY_LIBS Folly::folly)
|
||||
@@ -707,6 +770,7 @@ set(SOURCES
|
||||
db/blob/blob_file_addition.cc
|
||||
db/blob/blob_file_builder.cc
|
||||
db/blob/blob_file_cache.cc
|
||||
db/blob/blob_file_partition_manager.cc
|
||||
db/blob/blob_file_garbage.cc
|
||||
db/blob/blob_file_meta.cc
|
||||
db/blob/blob_file_reader.cc
|
||||
@@ -715,6 +779,7 @@ set(SOURCES
|
||||
db/blob/blob_log_sequential_reader.cc
|
||||
db/blob/blob_log_writer.cc
|
||||
db/blob/blob_source.cc
|
||||
db/blob/blob_write_batch_transformer.cc
|
||||
db/blob/prefetch_buffer_collection.cc
|
||||
db/builder.cc
|
||||
db/c.cc
|
||||
@@ -786,6 +851,7 @@ set(SOURCES
|
||||
db/version_util.cc
|
||||
db/wal_edit.cc
|
||||
db/wal_manager.cc
|
||||
db/wide/read_path_blob_resolver.cc
|
||||
db/wide/wide_column_serialization.cc
|
||||
db/wide/wide_columns.cc
|
||||
db/wide/wide_columns_helper.cc
|
||||
@@ -967,6 +1033,7 @@ set(SOURCES
|
||||
utilities/cassandra/merge_operator.cc
|
||||
utilities/checkpoint/checkpoint_impl.cc
|
||||
utilities/compaction_filters.cc
|
||||
utilities/sorted_run_builder/sorted_run_builder.cc
|
||||
utilities/compaction_filters/remove_emptyvalue_compactionfilter.cc
|
||||
utilities/counted_fs.cc
|
||||
utilities/debug.cc
|
||||
@@ -1136,8 +1203,23 @@ if(USE_FOLLY_LITE)
|
||||
FMT_INCLUDE_DIR)
|
||||
include_directories(${FMT_INCLUDE_DIR})
|
||||
|
||||
exec_program(python3 ${PROJECT_SOURCE_DIR}/third-party/folly ARGS
|
||||
build/fbcode_builder/getdeps.py show-inst-dir glog OUTPUT_VARIABLE
|
||||
GLOG_INST_PATH)
|
||||
if(EXISTS ${GLOG_INST_PATH}/lib64)
|
||||
set(GLOG_LIB_DIR ${GLOG_INST_PATH}/lib64)
|
||||
else()
|
||||
set(GLOG_LIB_DIR ${GLOG_INST_PATH}/lib)
|
||||
endif()
|
||||
|
||||
add_definitions(-DUSE_FOLLY -DFOLLY_NO_CONFIG)
|
||||
list(APPEND THIRDPARTY_LIBS glog)
|
||||
find_library(GLOG_LIBRARY NAMES glog PATHS ${GLOG_LIB_DIR} NO_DEFAULT_PATH)
|
||||
if(NOT GLOG_LIBRARY)
|
||||
find_library(GLOG_LIBRARY NAMES glog)
|
||||
endif()
|
||||
if(GLOG_LIBRARY)
|
||||
list(APPEND THIRDPARTY_LIBS ${GLOG_LIBRARY})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(ROCKSDB_STATIC_LIB rocksdb${ARTIFACT_SUFFIX})
|
||||
@@ -1383,6 +1465,7 @@ if(WITH_TESTS)
|
||||
db/blob/blob_garbage_meter_test.cc
|
||||
db/blob/blob_source_test.cc
|
||||
db/blob/db_blob_basic_test.cc
|
||||
db/blob/db_blob_direct_write_test.cc
|
||||
db/blob/db_blob_compaction_test.cc
|
||||
db/blob/db_blob_corruption_test.cc
|
||||
db/blob/db_blob_index_test.cc
|
||||
@@ -1408,6 +1491,7 @@ if(WITH_TESTS)
|
||||
db/db_clip_test.cc
|
||||
db/db_dynamic_level_test.cc
|
||||
db/db_encryption_test.cc
|
||||
db/db_etc2_test.cc
|
||||
db/db_etc3_test.cc
|
||||
db/db_flush_test.cc
|
||||
db/db_inplace_update_test.cc
|
||||
@@ -1420,6 +1504,7 @@ if(WITH_TESTS)
|
||||
db/db_memtable_test.cc
|
||||
db/db_merge_operator_test.cc
|
||||
db/db_merge_operand_test.cc
|
||||
db/db_open_with_config_test.cc
|
||||
db/db_options_test.cc
|
||||
db/db_properties_test.cc
|
||||
db/db_range_del_test.cc
|
||||
@@ -1430,7 +1515,6 @@ if(WITH_TESTS)
|
||||
db/db_table_properties_test.cc
|
||||
db/db_tailing_iter_test.cc
|
||||
db/db_test.cc
|
||||
db/db_test2.cc
|
||||
db/db_logical_block_size_cache_test.cc
|
||||
db/db_universal_compaction_test.cc
|
||||
db/db_wal_test.cc
|
||||
@@ -1472,6 +1556,7 @@ if(WITH_TESTS)
|
||||
db/wal_manager_test.cc
|
||||
db/wal_edit_test.cc
|
||||
db/wide/db_wide_basic_test.cc
|
||||
db/wide/db_wide_blob_direct_write_test.cc
|
||||
db/wide/wide_column_serialization_test.cc
|
||||
db/wide/wide_columns_helper_test.cc
|
||||
db/write_batch_test.cc
|
||||
@@ -1552,6 +1637,7 @@ if(WITH_TESTS)
|
||||
utilities/cassandra/cassandra_row_merge_test.cc
|
||||
utilities/cassandra/cassandra_serialize_test.cc
|
||||
utilities/checkpoint/checkpoint_test.cc
|
||||
utilities/sorted_run_builder/sorted_run_builder_test.cc
|
||||
utilities/env_timed_test.cc
|
||||
utilities/fault_injection_fs_test.cc
|
||||
utilities/memory/memory_test.cc
|
||||
|
||||
+81
@@ -1,6 +1,87 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 11.4.0 (06/02/2026)
|
||||
### Public API Changes
|
||||
* Added `rocksdb_options_set_memtable_batch_lookup_optimization()` and `rocksdb_options_get_memtable_batch_lookup_optimization()` to the C API, exposing the existing `AdvancedColumnFamilyOptions::memtable_batch_lookup_optimization` field. This allows C API users (and downstream language bindings) to enable the skip-list memtable's batch-lookup optimization for `MultiGet`, which caches the search path between consecutive keys and reduces per-key cost from O(log N) to O(log d) where d is the distance between consecutive keys.
|
||||
* Added `rocksdb_readoptions_set_optimize_multiget_for_io()` and `rocksdb_readoptions_get_optimize_multiget_for_io()` to the C API, exposing the existing `ReadOptions::optimize_multiget_for_io` field. This allows C API users (and downstream language bindings) to opt out of the multi-level parallel MultiGet path, which only takes effect when the library is built with `USE_COROUTINES`.
|
||||
* Added `rocksdb_block_based_table_index_block_search_type_auto` enum constant and the `rocksdb_block_based_options_set_uniform_cv_threshold()` setter to the C API. The constant exposes `BlockSearchType::kAuto`, and the setter exposes `BlockBasedTableOptions::uniform_cv_threshold`. Both are required for `kAuto` index-block search to take effect from the C API: without setting `uniform_cv_threshold >= 0`, the per-block `is_uniform` footer bit is never written, and `kAuto` falls back to binary search at read time.
|
||||
* Added `EventListener::OnDBShutdownBegin`, a callback that fires once when a DB begins shutdown, including when RocksDB cleans up a failed `DB::Open()` attempt.
|
||||
* Added a new PerfContext counter `blob_cache_read_byte` for blob cache bytes read.
|
||||
|
||||
### Behavior Changes
|
||||
* Remote compaction now falls back to local compaction when the primary cannot deserialize a successful `CompactionServiceResult` before installing any remote output files.
|
||||
* StringToMap (rocksdb/convenience.h) now preserves the outer braces of nested values so each map entry is in self-contained form and can be embedded directly into another `key=value;` string (e.g. SetOptions) without losing inner ';' delimiters. A new symmetric MapToString utility is provided.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a bug where `AbortAllCompactions()` could leave automatic compaction work unscheduled when aborting before the background worker picked it, causing compaction and write stalls until DB restart.
|
||||
* Fix a bug where db had a false positive compaction corruption error due to remote compaction service result with `has_accurate_num_input_records=false` not being serialized.
|
||||
* Fixed WritePrepared TransactionDB cleanup after retryable commit or rollback write failures so prepared transactions remain rollbackable instead of leaving unresolved prepared state.
|
||||
* Fix a rare corruption bug for tiered compaction that incorrectly moved last level range tombstones into proximal level. This corruption error is surfaced when force_consistency_checks is enabled.
|
||||
|
||||
### Performance Improvements
|
||||
* Reduce the likelihood of user-facing metadata operations blocking on MANIFEST rotation when file creation is slow, such as on remote storage. MANIFEST write batches containing foreground edits from `CreateColumnFamily()`, `DropColumnFamily()`, `CreateColumnFamilyWithImport()`, `IngestExternalFile()`, and `DeleteFilesInRanges()` now get a relaxed file size threshold for triggering MANIFEST rotation; background-only MANIFEST write batches, such as compaction and flush, continue to use the normal threshold. This change should make auto-tuning manifest file size more attractive (see `max_manifest_file_size` and `max_manifest_space_amp_pct` options).
|
||||
|
||||
|
||||
## 11.3.0 (05/15/2026)
|
||||
### New Features
|
||||
* Add experimental DB option `async_wal_precreate` to precreate the next WAL file in a background thread and reduce foreground WAL rotation latency. The option is sanitized to false when WAL recycling is enabled.
|
||||
* Added a new `EventListener::OnCompactionPreCommit` callback that fires after a compaction job finishes but before its input files are released (i.e. while `FileMetaData::being_compacted` is still true). Listeners that maintain bookkeeping of which files are currently being compacted can clean up such state in this new callback to avoid races with concurrent compaction picking, where another thread might pick up the same files for a new compaction immediately after `being_compacted` is flipped back to false but before `OnCompactionCompleted` fires. The default implementation is a no-op so this is not a breaking change.
|
||||
* Add mutable DBOption `optimize_manifest_for_recovery` (default false). When enabled, RocksDB can reduce recovery work after a clean shutdown, which may lower DB::Open latency on warm reopens.
|
||||
* Added public utility APIs `ParseCompressionNameForDisplay()` to convert `TableProperties::compression_name` into a human-readable compression name for both legacy and format_version 7+ SST metadata, including custom `CompressionManager`-provided display names for custom compression types.
|
||||
* Add `reuse_manifest_on_open` DBOption (default false). When enabled, DB::Open reuses the existing MANIFEST file for append instead of creating a fresh one, avoiding the cost of serializing the entire database state into a new MANIFEST on the first post-open write. To prevent this feature from interfering with manifest file size auto-tuning, an extra forward-compatible field is now always added to the MANIFEST (to track the last "compacted" size).
|
||||
|
||||
### Behavior Changes
|
||||
* Read-only open with `error_if_wal_file_exists=true` now tolerates empty WAL files so empty precreated WALs do not prevent inspection.
|
||||
* WriteCommitted TransactionDB now matches WritePrepared and WriteUnprepared compaction filtering in both single- and two-write-queue modes: a compaction filter's FilterMergeOperand will not be invoked on merge operands at or below the latest published sequence number.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed blob-backed wide-column merge reads to preserve correct status
|
||||
propagation and resolution across memtable, read-only, and secondary DB
|
||||
paths.
|
||||
* Fixed a bug where `DB::GetCreationTimeOfOldestFile()` could return inaccurate results instead of the real creation time when called shortly after opening a legacy DB (one whose manifest lacks `file_creation_time`) with `open_files_async = true`. The API now waits for background SST file loading to complete only when needed; modern DBs are unaffected.
|
||||
* Fixed merge reads against wide-column/blob-backed base values to preserve precise failure statuses, including `GetMergeOperands()` and direct-write memtable reads.
|
||||
* Fix bug in range tombstone synthesis that covers live keys added during an IngestExternalFile
|
||||
* Reject the empty string as a column family name in `DB::CreateColumnFamily` / `DB::CreateColumnFamilies`. Previously such calls returned OK and a usable handle, but the column family was not persisted in the manifest, so any data written to it was silently lost on DB reopen.
|
||||
* Fixed a bug where a WriteCommitted TransactionDB using commit-bypass WBWI ingestion could drop an entry that is still visible at the published sequence boundary.
|
||||
|
||||
## 11.2.0 (04/18/2026)
|
||||
### New Features
|
||||
* Added experimental `DBOptions::fast_sst_open` option. When enabled, RocksDB retrieves opaque file system metadata for SST files after flush, compaction, and external file ingestion, persists it in the MANIFEST, and passes it back to the file system on subsequent file opens to accelerate DB open time.
|
||||
* Added new option `min_tombstones_for_range_conversion` in `AdvancedColumnFamilyOptions`. When set to a non-zero value N, forward or reverse iteration will convert N or more contiguous point tombstones into a range tombstone in the mutable memtable. Future read operations will then be able to benefit from range tombstone optimizations. There are some limitations when it comes to table_filters, prefix_filters, and UDTs. See header comments for more details.
|
||||
* Added read-triggered compaction: a new column family option `read_triggered_compaction_threshold` (default 0, disabled) that marks SST files for compaction when their read frequency (`num_collapsible_entry_reads_sampled / file_size`) exceeds the threshold. This helps reduce read amplification for frequently-read ("hot") keys. A new DB option `max_compaction_trigger_wakeup_seconds` (default 43200s / 12 hours) controls the maximum interval for periodic compaction score re-evaluation, which is necessary for this feature to work on quiet (no-write) databases.
|
||||
|
||||
### Public API Changes
|
||||
* Added new `WideColumnBlobResolver` interface and `CompactionFilter::FilterV4()` method, including `ResolveColumn()` / `ResolveColumns()` helpers for lazy loading blob column values in wide-column entities during compaction. This allows compaction filters to resolve blob values on-demand, avoiding unnecessary I/O for blob columns they don't need to access.
|
||||
* Changed experimental feature `ExternalTableReader::Get` and `ExternalTableReader::MultiGet` to use `PinnableSlice` instead of `std::string` for output values, enabling zero-copy pinning. This will break existing implementations.
|
||||
* Added `SstFileReader::Get` and `SstFileReader::MultiGet` overloads that accept `PinnableSlice`/`std::vector<PinnableSlice>*`, enabling zero-copy reads when the underlying `TableReader` supports pinning.
|
||||
|
||||
### Behavior Changes
|
||||
* Prefix filter changes - when seeking to a key that is out of domain, and total_order_seek is false, total_order_seek is treated as if it were true. When prefix_same_as_start = true, now iterating past a key that is out of domain invalidates the iterator. The existing behavior does not check for InDomain in the DBIter, so Transform() can produce undefined behavior (e.g. key of size 3 on FixedPrefixTransform(4)).
|
||||
* Wide-column entities with blob-backed columns now use a new V2 on-disk encoding; older RocksDB versions that do not support wide-column blob separation will reject DBs or SSTs containing those entities.
|
||||
|
||||
### Bug Fixes
|
||||
* Fix blob garbage accounting for blob direct-write flushes so flush-time filtering and overwrite elision correctly register obsolete blob bytes in blob metadata.
|
||||
* Fix a memory accounting leak in IODispatcher where ReadIndex() moved block values out of ReadSet without releasing the associated prefetch memory, causing subsequent prefetches to be blocked when max_prefetch_memory_bytes was set.
|
||||
* Fix MultiScan to fall back to synchronous coalesced reads when async I/O is unsupported at runtime.
|
||||
|
||||
|
||||
## 11.1.0 (03/23/2026)
|
||||
### New Features
|
||||
* Add a new option `open_files_async`. The existing behavior is on DB open, we open all sst files and do basic validations. For very large DBs on remote filesystems with many ssts, this may take very long. This option performs these validations instead in the background. Open errors found by this async background task are surfaced as a new background error kAsyncFileOpen.
|
||||
* Added `BlockBasedTableOptions::kAuto` index block search type that automatically selects between binary and interpolation search on a per-index-block basis. During SST construction, each index block's key distribution uniformity is analyzed using the coefficient of variation of key gaps, and index blocks with uniform keys use interpolation search while others fall back to binary search. The uniformity threshold is configurable via `BlockBasedTableOptions::uniform_cv_threshold` (default: 0.2).
|
||||
* Introduced enforce_write_buffer_manager_during_recovery option to allow WriteBufferManager to be enforced during WAL recovery. (Default: true)
|
||||
* Add `memtable_batch_lookup_optimization` option to use batch lookup optimization for memtable MultiGet. For skip list memtables, after each key lookup, the search path is cached and reused for the next key, reducing per-key cost from O(log N) to O(log d) where d is the distance between consecutive keys. Benchmarks show ~7% improvement in memtable-resident MultiGet throughput.
|
||||
* Added `BlockBasedTableOptions::PrepopulateBlockCache::kFlushAndCompaction` to prepopulate the block cache during both flush and compaction. Compaction-warmed blocks are inserted at `BOTTOM` priority (vs `LOW` for flush) so they are evicted first under cache pressure. Recommended only for use cases where most or all of the database is expected to reside in cache (e.g., tiered or remote storage where the working set fits in cache).
|
||||
* Added new mutable DB option `verify_manifest_content_on_close` (default: false). When enabled, on DB close the MANIFEST file is read back and all records are validated (CRC checksums and logical content). If corruption is detected, a fresh MANIFEST is written from in-memory state.
|
||||
|
||||
### Behavior Changes
|
||||
* num_reads_sampled now factors in re-seeks and next/prev() on file iterators for files in L1+. next/prev() is discounted by 64x compared to seek due to being a much cheaper call.
|
||||
* Remote compaction workers now skip WAL recovery when opening the secondary DB instance, since only the LSM state from MANIFEST is needed for compaction. This reduces I/O and speeds up remote compaction startup.
|
||||
|
||||
### Bug Fixes
|
||||
* Fix a bug in round-robin compaction that missed selecting input files that are needed to guarantee data correctness and cause crashing in debug builds or silent data corruption in release builds for Get().
|
||||
|
||||
## 11.0.0 (02/23/2026)
|
||||
### New Features
|
||||
* Added support for storing wide-column entity column values in blob files. When `min_blob_size` is configured, large column values in wide-column entities will be stored in blob files, reducing SST file size and improving read performance.
|
||||
|
||||
@@ -327,6 +327,9 @@ missing_make_config_paths := $(shell \
|
||||
$(foreach path, $(missing_make_config_paths), \
|
||||
$(warning Warning: $(path) does not exist))
|
||||
|
||||
# This (the first rule) must depend on "all".
|
||||
default: all
|
||||
|
||||
ifeq ($(PLATFORM), OS_AIX)
|
||||
# no debug info
|
||||
else ifneq ($(PLATFORM), IOS)
|
||||
@@ -384,6 +387,10 @@ endif
|
||||
# TSAN doesn't work well with jemalloc. If we're compiling with TSAN, we should use regular malloc.
|
||||
ifdef COMPILE_WITH_TSAN
|
||||
DISABLE_JEMALLOC=1
|
||||
# Use a suppressions file instead of the process-wide TSAN default
|
||||
# suppressions hook, which belongs to the final application.
|
||||
TSAN_OPTIONS?=suppressions=$(CURDIR)/tools/tsan_suppressions.txt
|
||||
export TSAN_OPTIONS
|
||||
EXEC_LDFLAGS += -fsanitize=thread
|
||||
PLATFORM_CCFLAGS += -fsanitize=thread -fPIC -DFOLLY_SANITIZE_THREAD
|
||||
PLATFORM_CXXFLAGS += -fsanitize=thread -fPIC -DFOLLY_SANITIZE_THREAD
|
||||
@@ -481,9 +488,6 @@ ifdef ROCKSDB_MODIFY_NPHASH
|
||||
PLATFORM_CXXFLAGS += -DROCKSDB_MODIFY_NPHASH=1
|
||||
endif
|
||||
|
||||
# This (the first rule) must depend on "all".
|
||||
default: all
|
||||
|
||||
WARNING_FLAGS = -W -Wextra -Wall -Wsign-compare -Wshadow \
|
||||
-Wunused-parameter
|
||||
|
||||
@@ -638,6 +642,7 @@ PARALLEL_TEST = $(filter-out $(NON_PARALLEL_TEST), $(ROCKSDBTESTS_SUBSET))
|
||||
TESTS_PLATFORM_DEPENDENT := \
|
||||
db_basic_test \
|
||||
db_blob_basic_test \
|
||||
db_blob_direct_write_test \
|
||||
db_encryption_test \
|
||||
external_sst_file_basic_test \
|
||||
auto_roll_logger_test \
|
||||
@@ -645,6 +650,7 @@ TESTS_PLATFORM_DEPENDENT := \
|
||||
dynamic_bloom_test \
|
||||
c_test \
|
||||
checkpoint_test \
|
||||
sorted_run_builder_test \
|
||||
crc32c_test \
|
||||
coding_test \
|
||||
inlineskiplist_test \
|
||||
@@ -805,7 +811,18 @@ endif # PLATFORM_SHARED_EXT
|
||||
rocksdbjavastatic rocksdbjava install install-static install-shared \
|
||||
uninstall analyze tools tools_lib check-headers checkout_folly clang-tidy
|
||||
|
||||
all: $(LIBRARY) $(BENCHMARKS) tools tools_lib test_libs $(TESTS)
|
||||
# Auto-configure git hooks on first build so developers do not need to run
|
||||
# "make install-hooks" manually. This is a no-op if already set.
|
||||
setup-hooks:
|
||||
@if [ -d .git ] && [ -d githooks ]; then \
|
||||
cur=$$(git config core.hooksPath 2>/dev/null); \
|
||||
if [ "$$cur" != "githooks" ]; then \
|
||||
git config core.hooksPath githooks; \
|
||||
echo "git hooks: configured core.hooksPath = githooks"; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
all: setup-hooks $(LIBRARY) $(BENCHMARKS) tools tools_lib test_libs $(TESTS)
|
||||
|
||||
all_but_some_tests: $(LIBRARY) $(BENCHMARKS) tools tools_lib test_libs $(ROCKSDBTESTS_SUBSET)
|
||||
|
||||
@@ -870,23 +887,43 @@ coverage: clean
|
||||
parallel_tests = $(patsubst %,parallel_%,$(PARALLEL_TEST))
|
||||
.PHONY: gen_parallel_tests $(parallel_tests)
|
||||
# Shard size controls how many test cases run per process. The actual number
|
||||
# of shards per binary is: min(ceil(test_count / GTEST_SHARD_SIZE), NCORES * 8)
|
||||
# of shards per binary is: min(ceil(test_count / shard_size), NCORES * 8)
|
||||
# This adapts to machine size: many small shards on beefy machines, fewer
|
||||
# larger shards on CI (typically 2-4 cores).
|
||||
GTEST_SHARD_SIZE ?= 10
|
||||
NCORES ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
|
||||
|
||||
# Per-binary overrides for GTEST_SHARD_SIZE, used to chop up binaries whose
|
||||
# individual test cases are slow enough that the default of 10 leaves one
|
||||
# shard on the critical path of "make check". Format: whitespace-separated
|
||||
# tokens "BINARY:SIZE". Sizes were tuned from `make suggest-slow-tests`
|
||||
# output to keep max shard time under ~20s on a beefy box. Reducing too far
|
||||
# adds per-shard fixed overhead, so don't use 1 unless individual tests are
|
||||
# >5s.
|
||||
SHARD_SIZE_OVERRIDES ?= \
|
||||
point_lock_manager_stress_test:1 \
|
||||
external_sst_file_test:1 \
|
||||
external_sst_file_basic_test:2 \
|
||||
db_test:3 \
|
||||
compaction_service_test:1
|
||||
|
||||
$(parallel_tests):
|
||||
$(AM_V_at)TEST_BINARY=$(patsubst parallel_%,%,$@); \
|
||||
TEST_COUNT=` \
|
||||
(./$$TEST_BINARY --gtest_list_tests 2>/dev/null || echo " list_failure") \
|
||||
| grep -c '^ '`; \
|
||||
if [ "$$TEST_COUNT" -le 0 ]; then TEST_COUNT=1; fi; \
|
||||
SHARD_SIZE=$(GTEST_SHARD_SIZE); \
|
||||
for o in $(SHARD_SIZE_OVERRIDES); do \
|
||||
case "$$o" in \
|
||||
"$$TEST_BINARY":*) SHARD_SIZE=$${o#*:} ;; \
|
||||
esac; \
|
||||
done; \
|
||||
MAX_SHARDS=$$(( $(NCORES) * 8 )); \
|
||||
NUM_SHARDS=$$(( (TEST_COUNT + $(GTEST_SHARD_SIZE) - 1) / $(GTEST_SHARD_SIZE) )); \
|
||||
NUM_SHARDS=$$(( (TEST_COUNT + SHARD_SIZE - 1) / SHARD_SIZE )); \
|
||||
if [ "$$NUM_SHARDS" -gt "$$MAX_SHARDS" ]; then NUM_SHARDS=$$MAX_SHARDS; fi; \
|
||||
if [ "$$NUM_SHARDS" -le 0 ]; then NUM_SHARDS=1; fi; \
|
||||
echo " Generating $$NUM_SHARDS shards for $$TEST_BINARY ($$TEST_COUNT tests)"; \
|
||||
echo " Generating $$NUM_SHARDS shards for $$TEST_BINARY ($$TEST_COUNT tests, shard_size=$$SHARD_SIZE)"; \
|
||||
SHARD_IDX=0; \
|
||||
while [ "$$SHARD_IDX" -lt "$$NUM_SHARDS" ]; do \
|
||||
if [ -n "$(CI_TOTAL_SHARDS)" ] && [ $$(( $$SHARD_IDX % $(CI_TOTAL_SHARDS) )) -ne $(CI_SHARD_INDEX) ]; then \
|
||||
@@ -912,31 +949,64 @@ gen_parallel_tests:
|
||||
$(AM_V_at)$(FIND) t -type f -name 'run-*' -exec rm -f {} \;
|
||||
$(MAKE) $(parallel_tests)
|
||||
|
||||
# Reorder input lines (which are one per test) so that the
|
||||
# longest-running tests appear first in the output.
|
||||
# Do this by prefixing each selected name with its duration,
|
||||
# sort the resulting names, and remove the leading numbers.
|
||||
# FIXME: the "100" we prepend is a fake time, for now.
|
||||
# FIXME: squirrel away timings from each run and use them
|
||||
# (when present) on subsequent runs to order these tests.
|
||||
# Reorder input lines (one per test or per shard) so the longest-running
|
||||
# tests/shards start first under "make check" parallel scheduling. The trick
|
||||
# is to prefix matched names with a fake duration (100), sort numerically
|
||||
# descending, then strip the prefix. Slow-matched tests therefore come ahead
|
||||
# of all unmatched tests; ordering within each group is unspecified.
|
||||
#
|
||||
# Without this reordering, these two tests would happen to start only
|
||||
# after almost all other tests had completed, thus adding 100 seconds
|
||||
# to the duration of parallel "make check". That's the difference
|
||||
# between 4 minutes (old) and 2m20s (new).
|
||||
# Why this matters: gnu_parallel hands out jobs in input order. If a slow
|
||||
# test starts only after most others have finished, the run is bottlenecked
|
||||
# on its tail. Front-loading slow tests overlaps them with the bulk of
|
||||
# faster ones for much better wall-clock time.
|
||||
#
|
||||
# 152.120 PASS t/DBTest.FileCreationRandomFailure
|
||||
# 107.816 PASS t/DBTest.EncodeDecompressedBlockSizeTest
|
||||
# Maintenance:
|
||||
# 1) Run `make check` so LOG is up-to-date.
|
||||
# 2) Run `make suggest-slow-tests` to see candidate slow binaries.
|
||||
# 3) Add binaries with high per-shard time (slow critical path) or high
|
||||
# total time across many shards (need early queueing for fan-out).
|
||||
# Each alternative is wrapped in `^.*NAME.*$$` so the *whole input line* is
|
||||
# captured into $$1; then `s,(...),100 $$1,` prepends "100 " to the line.
|
||||
#
|
||||
# With sharded test execution, prioritize binaries known to be slow.
|
||||
# These generate many shards and should start early for good load balancing.
|
||||
# Tiers below are based on observed timings (see suggest-slow-tests).
|
||||
# Tier 1: max single-shard time >= 30s (critical-path bottlenecks).
|
||||
# Tier 2: max single-shard time 15-30s.
|
||||
# Tier 3: huge total time across many tiny shards; front-loading them keeps
|
||||
# the tail of the run busy while big shards finish.
|
||||
slow_test_regexp = \
|
||||
^.*block_based_table_reader_test.*$$|^.*table_test.*$$|^.*block_test.*$$|^.*write_prepared_transaction_test.*$$|^.*transaction_test.*$$|^.*external_sst_file_test.*$$|^.*db_wal_test.*$$|^.*db_with_timestamp_basic_test.*$$|^.*db_test-.*$$
|
||||
^.*point_lock_manager_stress_test.*$$|^.*db_test.*$$|^.*external_sst_file_test.*$$|^.*compaction_service_test.*$$|^.*corruption_test.*$$|^.*comparator_db_test.*$$|^.*external_sst_file_basic_test.*$$|^.*rate_limiter_test.*$$|^.*db_compaction_test.*$$|^.*write_prepared_transaction_test.*$$|^.*db_merge_operator_test.*$$|\
|
||||
^.*db_dynamic_level_test.*$$|^.*db_bloom_filter_test.*$$|^.*error_handler_fs_test.*$$|^.*merge_helper_test.*$$|^.*transaction_test.*$$|^.*db_kv_checksum_test.*$$|^.*inlineskiplist_test.*$$|\
|
||||
^.*db_with_timestamp_basic_test.*$$|^.*table_test.*$$|^.*db_wal_test.*$$|^.*block_based_table_reader_test.*$$|^.*block_test.*$$
|
||||
prioritize_long_running_tests = \
|
||||
perl -pe 's,($(slow_test_regexp)),100 $$1,' \
|
||||
| sort -k1,1gr \
|
||||
| sed 's/^[.0-9]* //'
|
||||
|
||||
# Helper: print binaries observed to be slow in the last `make check` run.
|
||||
# Use this to decide whether `slow_test_regexp` above needs updating.
|
||||
# Aggregates per-binary across shards; flags any binary whose max single-shard
|
||||
# time is >= 20s OR whose total time is >= 200s.
|
||||
.PHONY: suggest-slow-tests
|
||||
suggest-slow-tests:
|
||||
@if [ ! -s LOG ]; then \
|
||||
echo "No (or empty) LOG file. Run 'make check' first." >&2; exit 1; \
|
||||
fi
|
||||
@bash -c '$(quoted_perl_command)' < LOG | awk '\
|
||||
{ \
|
||||
t=$$1; name=$$3; \
|
||||
sub(/^t\/run-/, "", name); \
|
||||
sub(/-shard-[0-9]+$$/, "", name); \
|
||||
total[name] += t; \
|
||||
if (t > max[name]) max[name] = t; \
|
||||
count[name]++; \
|
||||
} \
|
||||
END { \
|
||||
printf "%8s %8s %5s %s\n", "TOTAL_s", "MAX_s", "SHRDS", "BINARY"; \
|
||||
for (n in total) \
|
||||
if (max[n] >= 20 || total[n] >= 200) \
|
||||
printf "%8.1f %8.1f %5d %s\n", total[n], max[n], count[n], n; \
|
||||
}' | (read header; echo "$$header"; sort -k2,2gr)
|
||||
|
||||
# "make check" uses
|
||||
# Run with "make J=1 check" to disable parallelism in "make check".
|
||||
# Run with "make J=200% check" to run two parallel jobs per core.
|
||||
@@ -973,7 +1043,7 @@ check_0:
|
||||
awk -v s=$(CI_SHARD_INDEX) -v n=$(CI_TOTAL_SHARDS) '(NR-1)%n==s'; \
|
||||
else cat; fi; \
|
||||
fi; \
|
||||
find t -name 'run-*' -print; \
|
||||
$(FIND) t -name 'run-*' -print; \
|
||||
} \
|
||||
| $(prioritize_long_running_tests) \
|
||||
| grep -E '$(tests-regexp)' \
|
||||
@@ -995,7 +1065,7 @@ valgrind_check_0:
|
||||
' run "make watch-log" in a separate window' ''; \
|
||||
{ \
|
||||
printf './%s\n' $(filter-out $(PARALLEL_TEST) %skiplist_test options_settable_test, $(TESTS)); \
|
||||
find t -name 'run-*' -print; \
|
||||
$(FIND) t -name 'run-*' -print; \
|
||||
} \
|
||||
| $(prioritize_long_running_tests) \
|
||||
| grep -E '$(tests-regexp)' \
|
||||
@@ -1048,6 +1118,7 @@ ifneq ($(PLATFORM), OS_AIX)
|
||||
$(PYTHON) tools/check_all_python.py
|
||||
ifndef ASSERT_STATUS_CHECKED # not yet working with these tests
|
||||
$(PYTHON) tools/ldb_test.py
|
||||
$(PYTHON) tools/db_crashtest_test.py
|
||||
sh tools/rocksdb_dump_test.sh
|
||||
endif
|
||||
endif
|
||||
@@ -1055,6 +1126,7 @@ ifndef SKIP_FORMAT_BUCK_CHECKS
|
||||
$(MAKE) check-format
|
||||
$(MAKE) check-buck-targets
|
||||
$(MAKE) check-sources
|
||||
$(MAKE) check-workflow-yaml
|
||||
endif
|
||||
|
||||
# TODO add ldb_tests
|
||||
@@ -1065,6 +1137,10 @@ check_some: $(ROCKSDBTESTS_SUBSET)
|
||||
ldb_tests: ldb
|
||||
$(PYTHON) tools/ldb_test.py
|
||||
|
||||
.PHONY: db_crashtest_tests
|
||||
db_crashtest_tests:
|
||||
$(PYTHON) tools/db_crashtest_test.py
|
||||
|
||||
include crash_test.mk
|
||||
|
||||
asan_check: clean
|
||||
@@ -1207,11 +1283,17 @@ clean-not-downloaded: clean-ext-libraries-bin clean-rocks clean-not-downloaded-r
|
||||
|
||||
clean-rocks:
|
||||
# Not practical to exactly match all versions/variants in naming (e.g. debug or not)
|
||||
rm -f ${LIBNAME}*.so* ${LIBNAME}*.a
|
||||
rm -f $(BENCHMARKS) $(TOOLS) $(TESTS) $(PARALLEL_TEST) $(MICROBENCHS)
|
||||
rm -rf $(CLEAN_FILES) ios-x86 ios-arm scan_build_report
|
||||
$(FIND) . -name "*.[oda]" -exec rm -f {} \;
|
||||
$(FIND) . -type f \( -name "*.gcda" -o -name "*.gcno" \) -exec rm -f {} \;
|
||||
@echo Removing link targets
|
||||
@rm -f ${LIBNAME}*.so* ${LIBNAME}*.a $(BENCHMARKS) $(TOOLS) $(TESTS) $(PARALLEL_TEST) $(MICROBENCHS)
|
||||
@echo Finding and cleaning other files
|
||||
@rm -rf $(CLEAN_FILES) ios-x86 ios-arm scan_build_report
|
||||
@if $(FIND) Makefile -regextype awk &> /dev/null; then \
|
||||
$(FIND) . -regextype awk \
|
||||
-regex '.*/([.].*|third-party)' -prune -o \
|
||||
-type f -regex '.*[.]([oda]|gcda|gcno)' -exec rm -f {} \; ; \
|
||||
else \
|
||||
$(FIND) . -name "*.[oda]" -exec rm -f {} \; ; \
|
||||
fi
|
||||
|
||||
clean-rocksjava: clean-rocks
|
||||
rm -rf jl jls
|
||||
@@ -1224,7 +1306,7 @@ clean-ext-libraries-all:
|
||||
rm -rf bzip2* snappy* zlib* lz4* zstd*
|
||||
|
||||
clean-ext-libraries-bin:
|
||||
find . -maxdepth 1 -type d \( -name bzip2\* -or -name snappy\* -or -name zlib\* -or -name lz4\* -or -name zstd\* \) -prune -exec rm -rf {} \;
|
||||
$(FIND) . -maxdepth 1 -type d \( -name bzip2\* -or -name snappy\* -or -name zlib\* -or -name lz4\* -or -name zstd\* \) -prune -exec rm -rf {} \;
|
||||
|
||||
tags:
|
||||
ctags -R .
|
||||
@@ -1248,12 +1330,43 @@ format-auto:
|
||||
check-format:
|
||||
build_tools/format-diff.sh -c
|
||||
|
||||
|
||||
# Crude alternative to setup-hooks: copies hooks into .git/hooks/ instead of
|
||||
# using core.hooksPath. The copies won't track changes to githooks/.
|
||||
install-hooks:
|
||||
@echo "Installing git hooks from githooks/..."
|
||||
@if [ -d githooks ]; then \
|
||||
for hook in githooks/*; do \
|
||||
hook_name=$$(basename "$$hook"); \
|
||||
cp "$$hook" .git/hooks/"$$hook_name"; \
|
||||
chmod +x .git/hooks/"$$hook_name"; \
|
||||
echo " Installed $$hook_name"; \
|
||||
done; \
|
||||
echo "Done. Hooks installed to .git/hooks/"; \
|
||||
else \
|
||||
echo "Error: githooks/ directory not found"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# Reverse of install-hooks (not needed if using setup-hooks / core.hooksPath).
|
||||
uninstall-hooks:
|
||||
@echo "Removing installed git hooks..."
|
||||
@for hook in githooks/*; do \
|
||||
hook_name=$$(basename "$$hook"); \
|
||||
rm -f .git/hooks/"$$hook_name"; \
|
||||
echo " Removed $$hook_name"; \
|
||||
done
|
||||
@echo "Done."
|
||||
|
||||
check-buck-targets:
|
||||
buckifier/check_buck_targets.sh
|
||||
|
||||
check-sources:
|
||||
build_tools/check-sources.sh
|
||||
|
||||
check-workflow-yaml:
|
||||
build_tools/check-workflow-yaml.sh
|
||||
|
||||
# Run clang-tidy on locally changed files, filtered to changed lines only.
|
||||
# Requires compile_commands.json (generate with cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON).
|
||||
# Override CLANG_TIDY_BINARY and CLANG_TIDY_JOBS as needed:
|
||||
@@ -1441,6 +1554,9 @@ db_basic_test: $(OBJ_DIR)/db/db_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
db_blob_basic_test: $(OBJ_DIR)/db/blob/db_blob_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_blob_direct_write_test: $(OBJ_DIR)/db/blob/db_blob_direct_write_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_blob_compaction_test: $(OBJ_DIR)/db/blob/db_blob_compaction_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -1450,6 +1566,9 @@ db_readonly_with_timestamp_test: $(OBJ_DIR)/db/db_readonly_with_timestamp_test.o
|
||||
db_wide_basic_test: $(OBJ_DIR)/db/wide/db_wide_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_wide_blob_direct_write_test: $(OBJ_DIR)/db/wide/db_wide_blob_direct_write_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_with_timestamp_basic_test: $(OBJ_DIR)/db/db_with_timestamp_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -1459,10 +1578,13 @@ db_with_timestamp_compaction_test: db/db_with_timestamp_compaction_test.o $(TEST
|
||||
db_encryption_test: $(OBJ_DIR)/db/db_encryption_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_open_with_config_test: $(OBJ_DIR)/db/db_open_with_config_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_test: $(OBJ_DIR)/db/db_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_test2: $(OBJ_DIR)/db/db_test2.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
db_etc2_test: $(OBJ_DIR)/db/db_etc2_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_etc3_test: $(OBJ_DIR)/db/db_etc3_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
@@ -1603,6 +1725,9 @@ backup_engine_test: $(OBJ_DIR)/utilities/backup/backup_engine_test.o $(TEST_LIBR
|
||||
checkpoint_test: $(OBJ_DIR)/utilities/checkpoint/checkpoint_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
sorted_run_builder_test: $(OBJ_DIR)/utilities/sorted_run_builder/sorted_run_builder_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
cache_simulator_test: $(OBJ_DIR)/utilities/simulator_cache/cache_simulator_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -2602,7 +2727,7 @@ list_all_tests:
|
||||
|
||||
# Remove the rules for which dependencies should not be generated and see if any are left.
|
||||
#If so, include the dependencies; if not, do not include the dependency files
|
||||
ROCKS_DEP_RULES=$(filter-out clean format check-format check-buck-targets check-headers check-sources clang-tidy jclean jtest package analyze tags rocksdbjavastatic% unity.% unity_test checkout_folly, $(MAKECMDGOALS))
|
||||
ROCKS_DEP_RULES=$(filter-out clean format check-format check-buck-targets check-headers check-sources check-workflow-yaml clang-tidy jclean jtest package analyze tags rocksdbjavastatic% unity.% unity_test checkout_folly, $(MAKECMDGOALS))
|
||||
ifneq ("$(ROCKS_DEP_RULES)", "")
|
||||
-include $(DEPFILES)
|
||||
endif
|
||||
|
||||
@@ -146,10 +146,10 @@ def generate_buck(repo_path, deps_map):
|
||||
src_mk["RANGE_TREE_SOURCES"] + src_mk["TOOL_LIB_SOURCES"],
|
||||
deps=[
|
||||
"//folly/container:f14_hash",
|
||||
"//folly/experimental/coro:blocking_wait",
|
||||
"//folly/experimental/coro:collect",
|
||||
"//folly/experimental/coro:coroutine",
|
||||
"//folly/experimental/coro:task",
|
||||
"//folly/coro:blocking_wait",
|
||||
"//folly/coro:collect",
|
||||
"//folly/coro:coroutine",
|
||||
"//folly/coro:task",
|
||||
"//folly/synchronization:distributed_mutex",
|
||||
],
|
||||
headers=LiteralValue("glob([\"**/*.h\"])")
|
||||
|
||||
@@ -37,7 +37,7 @@ if [ "$?" != "1" ]; then
|
||||
BAD=1
|
||||
fi
|
||||
|
||||
git grep -n -P "[\x80-\xFF]" -- ':!docs' ':!*.md'
|
||||
LC_ALL=C git grep -n $'[\x80-\xff]' -- ':!docs' ':!*.md' ':!.github'
|
||||
if [ "$?" != "1" ]; then
|
||||
echo '^^^^ Use only ASCII characters in source files'
|
||||
BAD=1
|
||||
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
|
||||
#
|
||||
# Validate GitHub Actions workflow YAML before it reaches CI runtime.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v ruby >/dev/null 2>&1; then
|
||||
echo "ruby is required to validate GitHub Actions workflow YAML"
|
||||
echo "On CentOS Stream: sudo dnf install ruby rubygems rubygem-psych"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! ruby -e 'require "psych"' 2>/dev/null; then
|
||||
echo "ruby is installed but cannot load required library 'psych'"
|
||||
echo "On CentOS Stream: sudo dnf install rubygems rubygem-psych"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ruby <<'RUBY'
|
||||
require "psych"
|
||||
|
||||
bad = false
|
||||
workflow_files = Dir[".github/workflows/*.{yml,yaml}"].sort
|
||||
|
||||
if workflow_files.empty?
|
||||
warn "No workflow YAML files found under .github/workflows"
|
||||
exit 1
|
||||
end
|
||||
|
||||
workflow_files.each do |path|
|
||||
begin
|
||||
Psych.parse_file(path)
|
||||
puts "OK #{path}"
|
||||
rescue Psych::Exception => e
|
||||
warn "Invalid YAML in #{path}: #{e.message}"
|
||||
bad = true
|
||||
end
|
||||
end
|
||||
|
||||
exit(bad ? 1 : 0)
|
||||
RUBY
|
||||
@@ -1,62 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
# 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 the Apache 2.0 License (found in the LICENSE.Apache file in the root directory).
|
||||
"""
|
||||
Pre-download packages with unreliable mirrors using fallback mirrors.
|
||||
Reads package info from folly's getdeps manifest files.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
import subprocess
|
||||
import configparser
|
||||
import hashlib
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
DOWNLOAD_TIMEOUT_SECONDS = 120
|
||||
DOWNLOAD_CHUNK_BYTES = 64 * 1024
|
||||
MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024
|
||||
|
||||
MIRROR_FALLBACKS = {
|
||||
"ftpmirror.gnu.org/gnu/": [
|
||||
"https://mirrors.kernel.org/gnu/",
|
||||
"https://ftpmirror.gnu.org/gnu/",
|
||||
"https://ftp.gnu.org/gnu/",
|
||||
],
|
||||
"ftp.gnu.org/gnu/": [
|
||||
"https://mirrors.kernel.org/gnu/",
|
||||
"https://ftpmirror.gnu.org/gnu/",
|
||||
"https://ftp.gnu.org/gnu/",
|
||||
],
|
||||
}
|
||||
|
||||
# These packages must have URLs matching MIRROR_FALLBACKS; other packages are
|
||||
# left for getdeps.py's normal download path.
|
||||
PACKAGES_TO_CHECK = ("autoconf", "automake", "libtool", "libiberty")
|
||||
|
||||
|
||||
def sha256_file(path):
|
||||
"""Calculate SHA256 hash of a file."""
|
||||
h = hashlib.sha256()
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
for chunk in iter(lambda: f.read(65536), b''):
|
||||
with open(path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(65536), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def parse_manifest(manifest_path):
|
||||
"""Parse a getdeps manifest file to extract download info."""
|
||||
config = configparser.ConfigParser()
|
||||
# folly manifests can contain bare keys in sections unrelated to downloads.
|
||||
config = configparser.ConfigParser(allow_no_value=True, interpolation=None)
|
||||
try:
|
||||
config.read(manifest_path)
|
||||
if 'download' in config:
|
||||
return {
|
||||
'url': config['download'].get('url', ''),
|
||||
'sha256': config['download'].get('sha256', ''),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
with open(manifest_path, encoding="utf-8") as manifest_file:
|
||||
config.read_file(manifest_file)
|
||||
except Exception as ex:
|
||||
print(f" {os.path.basename(manifest_path)}: WARNING - parse failed: {ex}")
|
||||
return None
|
||||
|
||||
if "download" in config:
|
||||
return {
|
||||
"url": config["download"].get("url", ""),
|
||||
"sha256": config["download"].get("sha256", ""),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def file_size(path):
|
||||
try:
|
||||
return os.path.getsize(path)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def get_fallback_mirrors(url):
|
||||
"""Get fallback mirror URLs for a given URL."""
|
||||
# Fallback mirror patterns for known unreliable hosts
|
||||
mirror_fallbacks = {
|
||||
"ftp.gnu.org/gnu/": [
|
||||
"https://mirrors.kernel.org/gnu/",
|
||||
"https://ftpmirror.gnu.org/gnu/",
|
||||
"https://ftp.gnu.org/gnu/",
|
||||
],
|
||||
"ftpmirror.gnu.org/gnu/": [
|
||||
"https://mirrors.kernel.org/gnu/",
|
||||
"https://ftpmirror.gnu.org/gnu/",
|
||||
"https://ftp.gnu.org/gnu/",
|
||||
],
|
||||
}
|
||||
|
||||
for pattern, mirrors in mirror_fallbacks.items():
|
||||
for pattern, mirrors in MIRROR_FALLBACKS.items():
|
||||
if pattern in url:
|
||||
# Extract the path after the pattern
|
||||
path_start = url.find(pattern) + len(pattern)
|
||||
path = url[path_start:]
|
||||
return [mirror + path for mirror in mirrors]
|
||||
return [url] # No fallback, use original
|
||||
return []
|
||||
|
||||
|
||||
def download_url(url, filepath):
|
||||
"""Download URL to filepath without leaving partial files behind."""
|
||||
tmp_filepath = filepath + ".tmp"
|
||||
if os.path.exists(tmp_filepath):
|
||||
os.remove(tmp_filepath)
|
||||
|
||||
request = urllib.request.Request(
|
||||
url, headers={"User-Agent": "rocksdb-getdeps-fallback/1.0"}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(
|
||||
request, timeout=DOWNLOAD_TIMEOUT_SECONDS
|
||||
) as response, open(tmp_filepath, "wb") as output:
|
||||
copied = 0
|
||||
while True:
|
||||
chunk = response.read(DOWNLOAD_CHUNK_BYTES)
|
||||
if not chunk:
|
||||
break
|
||||
|
||||
copied += len(chunk)
|
||||
if copied > MAX_DOWNLOAD_BYTES:
|
||||
raise Exception(
|
||||
f"download exceeds {MAX_DOWNLOAD_BYTES} bytes"
|
||||
)
|
||||
output.write(chunk)
|
||||
os.replace(tmp_filepath, filepath)
|
||||
finally:
|
||||
if os.path.exists(tmp_filepath):
|
||||
os.remove(tmp_filepath)
|
||||
|
||||
|
||||
def prepare_download(package, info, download_dir, cache_dir):
|
||||
url = info["url"]
|
||||
expected_sha256 = info["sha256"]
|
||||
mirrors = get_fallback_mirrors(url)
|
||||
if not mirrors:
|
||||
return False
|
||||
|
||||
if not expected_sha256:
|
||||
print(f" {package}: WARNING - skipped fallback without sha256")
|
||||
return False
|
||||
|
||||
# getdeps uses format: {package}-{filename}
|
||||
filename = f"{package}-{os.path.basename(url)}"
|
||||
filepath = os.path.join(download_dir, filename)
|
||||
cache_path = os.path.join(cache_dir, filename)
|
||||
|
||||
# Check if already valid.
|
||||
actual_sha256 = sha256_file(filepath) if os.path.exists(filepath) else None
|
||||
if actual_sha256 == expected_sha256:
|
||||
print(f" {filename}: OK (already downloaded)")
|
||||
return True
|
||||
if actual_sha256 is not None:
|
||||
print(
|
||||
f" {filename}: WARNING - removing invalid download "
|
||||
f"sha256={actual_sha256}"
|
||||
)
|
||||
os.remove(filepath)
|
||||
|
||||
# The cache is only an opportunistic single-build accelerator; callers
|
||||
# should not share it across concurrent builds without external locking.
|
||||
actual_sha256 = sha256_file(cache_path) if os.path.exists(cache_path) else None
|
||||
if actual_sha256 == expected_sha256:
|
||||
print(f" {filename}: OK (from cache)")
|
||||
shutil.copy2(cache_path, filepath)
|
||||
return True
|
||||
if actual_sha256 is not None:
|
||||
print(
|
||||
f" {filename}: WARNING - removing invalid cache "
|
||||
f"sha256={actual_sha256}"
|
||||
)
|
||||
os.remove(cache_path)
|
||||
|
||||
# Try fallback mirrors.
|
||||
for mirror_url in mirrors:
|
||||
print(f" {filename}: trying {mirror_url}...")
|
||||
try:
|
||||
download_url(mirror_url, filepath)
|
||||
except Exception as ex:
|
||||
print(f" {filename}: WARNING - download failed: {ex}")
|
||||
continue
|
||||
|
||||
actual_sha256 = sha256_file(filepath)
|
||||
if actual_sha256 == expected_sha256:
|
||||
size = file_size(filepath)
|
||||
print(f" {filename}: OK (downloaded, {size} bytes)")
|
||||
shutil.copy2(filepath, cache_path)
|
||||
return True
|
||||
|
||||
size = file_size(filepath)
|
||||
print(
|
||||
f" {filename}: WARNING - sha256 mismatch from {mirror_url}: "
|
||||
f"expected={expected_sha256} actual={actual_sha256} size={size}"
|
||||
)
|
||||
os.remove(filepath)
|
||||
|
||||
print(f" {filename}: WARNING - all mirrors failed")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 4:
|
||||
@@ -64,60 +190,39 @@ def main():
|
||||
sys.exit(1)
|
||||
|
||||
download_dir, cache_dir, manifests_dir = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
os.makedirs(download_dir, exist_ok=True)
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
|
||||
# Packages known to have unreliable mirrors
|
||||
packages_to_check = ["autoconf", "automake", "libtool"]
|
||||
|
||||
for package in packages_to_check:
|
||||
checked = 0
|
||||
ready = 0
|
||||
for package in PACKAGES_TO_CHECK:
|
||||
manifest_path = os.path.join(manifests_dir, package)
|
||||
if not os.path.exists(manifest_path):
|
||||
if not os.path.isfile(manifest_path):
|
||||
continue
|
||||
|
||||
info = parse_manifest(manifest_path)
|
||||
if not info or not info['url'] or not info['sha256']:
|
||||
if not info or not info["url"]:
|
||||
continue
|
||||
|
||||
# Determine filename from URL
|
||||
url = info['url']
|
||||
expected_sha256 = info['sha256']
|
||||
url_filename = os.path.basename(url)
|
||||
|
||||
# getdeps uses format: {package}-{filename}
|
||||
filename = f"{package}-{url_filename}"
|
||||
filepath = os.path.join(download_dir, filename)
|
||||
cache_path = os.path.join(cache_dir, filename)
|
||||
|
||||
# Check if already valid
|
||||
if os.path.exists(filepath) and sha256_file(filepath) == expected_sha256:
|
||||
print(f" {filename}: OK (already downloaded)")
|
||||
if not info["sha256"]:
|
||||
print(f" {package}: WARNING - skipped fallback without sha256")
|
||||
continue
|
||||
|
||||
# Check cache
|
||||
if os.path.exists(cache_path) and sha256_file(cache_path) == expected_sha256:
|
||||
print(f" {filename}: OK (from cache)")
|
||||
subprocess.run(['cp', cache_path, filepath], check=True)
|
||||
if not get_fallback_mirrors(info["url"]):
|
||||
print(
|
||||
f" {package}: WARNING - skipped fallback without known mirror "
|
||||
f"for {info['url']}"
|
||||
)
|
||||
continue
|
||||
|
||||
# Try fallback mirrors
|
||||
mirrors = get_fallback_mirrors(url)
|
||||
downloaded = False
|
||||
for mirror_url in mirrors:
|
||||
print(f" {filename}: trying {mirror_url}...")
|
||||
try:
|
||||
subprocess.run(['wget', '-q', '-O', filepath, mirror_url], check=True, timeout=120)
|
||||
if sha256_file(filepath) == expected_sha256:
|
||||
print(f" {filename}: OK (downloaded)")
|
||||
subprocess.run(['cp', filepath, cache_path], check=False)
|
||||
downloaded = True
|
||||
break
|
||||
else:
|
||||
os.remove(filepath)
|
||||
except Exception:
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
checked += 1
|
||||
try:
|
||||
if prepare_download(package, info, download_dir, cache_dir):
|
||||
ready += 1
|
||||
except Exception as ex:
|
||||
print(f" {package}: WARNING - fallback preparation failed: {ex}")
|
||||
|
||||
if not downloaded:
|
||||
print(f" {filename}: WARNING - all mirrors failed")
|
||||
print(f" fallback mirror downloads ready: {ready}/{checked}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# the docker and docker-registry groups, and logging out and back in to pick
|
||||
# those up.)
|
||||
#
|
||||
# Meta employees: see https://fburl.com/rocksdb-docker to build this on a devvm.
|
||||
#
|
||||
# Follow https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic
|
||||
# to login with your GitHub credentials, as in
|
||||
#
|
||||
@@ -15,8 +17,8 @@
|
||||
# Then in the build_tools/ubuntu22_image directory, (bump minor version for
|
||||
# random docker file updates, major version tracks Ubuntu release)
|
||||
#
|
||||
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:22.0
|
||||
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:22.0
|
||||
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:22.2
|
||||
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:22.2
|
||||
#
|
||||
# Might need to change visibility to public through
|
||||
# https://github.com/orgs/facebook/packages/container/rocksdb_ubuntu/settings
|
||||
@@ -28,7 +30,7 @@ FROM ubuntu:22.04
|
||||
RUN apt-get update
|
||||
RUN apt-get upgrade -y
|
||||
# install basic tools
|
||||
RUN apt-get install -y vim wget curl
|
||||
RUN apt-get install -y vim wget curl ccache
|
||||
# install tzdata noninteractive
|
||||
RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install tzdata
|
||||
# install git and default compilers
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# the docker and docker-registry groups, and logging out and back in to pick
|
||||
# those up.)
|
||||
#
|
||||
# Meta employees: see https://fburl.com/rocksdb-docker to build this on a devvm.
|
||||
#
|
||||
# Follow https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic
|
||||
# to login with your GitHub credentials, as in
|
||||
#
|
||||
@@ -15,8 +17,8 @@
|
||||
# Then in the build_tools/ubuntu24_image directory, (bump minor version for
|
||||
# random docker file updates, major version tracks Ubuntu release)
|
||||
#
|
||||
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:24.0
|
||||
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:24.1
|
||||
#
|
||||
# Might need to change visibility to public through
|
||||
# https://github.com/orgs/facebook/packages/container/rocksdb_ubuntu/settings
|
||||
@@ -28,11 +30,15 @@ FROM ubuntu:24.04
|
||||
RUN apt-get update
|
||||
RUN apt-get upgrade -y
|
||||
# install basic tools
|
||||
RUN apt-get install -y vim wget curl
|
||||
RUN apt-get install -y vim wget curl ccache
|
||||
# install tzdata noninteractive
|
||||
RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install tzdata
|
||||
# install git and default compilers
|
||||
RUN apt-get install -y git gcc g++ clang clang-tools
|
||||
# install clang-21 from LLVM snapshot repo
|
||||
RUN curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/llvm.gpg && \
|
||||
echo "deb https://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" > /etc/apt/sources.list.d/llvm-21.list && \
|
||||
apt-get update && apt-get install -y clang-21
|
||||
# install basic package
|
||||
RUN apt-get install -y lsb-release software-properties-common gnupg
|
||||
# install gflags, tbb
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# PR Complexity Classifier — CI Triage Prompt
|
||||
|
||||
## Purpose
|
||||
Quickly classify a RocksDB pull request as **simple** or **complex** so the
|
||||
downstream reviewer can pick an appropriate thinking budget.
|
||||
|
||||
## Output Contract (STRICT)
|
||||
Your final response MUST be exactly one of these two lowercase tokens, on a
|
||||
line by itself, with no other text:
|
||||
|
||||
```
|
||||
simple
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
complex
|
||||
```
|
||||
|
||||
If you are unsure, output `complex`. The downstream review will use this token
|
||||
to set its thinking budget — when in doubt, prefer the more thorough budget.
|
||||
|
||||
## Classification Heuristics
|
||||
|
||||
Treat a PR as **simple** when ALL of the following hold:
|
||||
- Diff touches < ~200 lines of non-test code
|
||||
- No changes to public headers under `include/rocksdb/`
|
||||
- No changes to on-disk format (SST block layout, WAL record layout,
|
||||
manifest, options file format, version edit encoding)
|
||||
- No changes to compaction picker, write-path locking, recovery, or
|
||||
WAL/memtable lifecycle
|
||||
- No new public option, no new statistics counter, no new RPC/serialized
|
||||
message
|
||||
- Tests-only or docs-only changes
|
||||
- Trivial refactors (rename, comment, dead-code removal, lint fixes)
|
||||
- One-line bug fix with obvious root cause and a focused regression test
|
||||
|
||||
Treat a PR as **complex** when ANY of the following hold:
|
||||
- Touches files in `db/`, `db/compaction/`, `db/blob/`, `cache/`,
|
||||
`table/block_based/`, `file/`, `env/io_*` with > ~50 lines of logic change
|
||||
- Modifies thread-safety/synchronization (mutexes, atomics, memory ordering,
|
||||
refcounting, condvars)
|
||||
- Changes serialization or on-disk format
|
||||
- Changes public API surface in `include/rocksdb/`
|
||||
- Adds a new feature flag, option, or compaction style
|
||||
- Touches transactions (`utilities/transactions/`), backup/checkpoint,
|
||||
user-defined timestamps, or remote compaction
|
||||
- Cross-cutting refactor spanning > 5 directories
|
||||
|
||||
## How To Decide
|
||||
1. Read the diff summary and changed-file list provided below.
|
||||
2. Skim the diff hunks — focus on which subsystems are touched and whether
|
||||
the change is mechanical vs. semantic.
|
||||
3. Apply the heuristics above. Default to `complex` if the rules conflict.
|
||||
4. Output the single token. No preamble. No explanation.
|
||||
|
||||
## Tool Use
|
||||
You may use `View`, `GlobTool`, and `GrepTool` to peek at one or two files
|
||||
if the diff snippet alone is ambiguous, but keep this fast — this is a
|
||||
triage step, not a review. Do NOT spawn sub-agents.
|
||||
@@ -0,0 +1,3 @@
|
||||
You are an expert C++ engineer for RocksDB.
|
||||
Read CLAUDE.md in the repo root and claude_md/ files for project context.
|
||||
Answer thoroughly with exact file paths and line numbers.
|
||||
@@ -0,0 +1,54 @@
|
||||
Read review-findings.md. This file contains partial code review findings
|
||||
from a session that ran out of turns before finishing. Reformat them into
|
||||
a clean final review comment.
|
||||
|
||||
## Required Output Structure
|
||||
|
||||
Use this exact structure so the PR page stays scrollable:
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
|
||||
> **Partial review** — the analysis was interrupted before all perspectives
|
||||
> were completed. Findings below cover only the perspectives that finished.
|
||||
> You can request a fresh full review with `/claude-review`.
|
||||
|
||||
<!-- One or two sentences of overall assessment. -->
|
||||
|
||||
**High-severity findings (N):**
|
||||
- **[file.cc:123]** One-line description. <!-- repeat per HIGH finding -->
|
||||
|
||||
<!-- If no HIGH findings were salvaged, write: -->
|
||||
<!-- _No high-severity findings recovered._ -->
|
||||
|
||||
<details>
|
||||
<summary>Recovered findings (click to expand)</summary>
|
||||
|
||||
### Findings
|
||||
|
||||
#### :red_circle: HIGH
|
||||
... (H1, H2, ...)
|
||||
|
||||
#### :yellow_circle: MEDIUM
|
||||
... (M1, M2, ...)
|
||||
|
||||
#### :green_circle: LOW / NIT
|
||||
... (L1, L2, ...)
|
||||
|
||||
### Cross-Component Analysis
|
||||
<!-- Whatever cross-component results were captured. -->
|
||||
|
||||
### Positive Observations
|
||||
<!-- Optional. -->
|
||||
|
||||
</details>
|
||||
```
|
||||
|
||||
## Rules
|
||||
- The `## Summary`, the partial-review notice, and the HIGH bullet list MUST
|
||||
stay outside the `<details>` block.
|
||||
- All per-finding detail and analysis MUST live inside the `<details>` block.
|
||||
- Do NOT nest `<details>` blocks.
|
||||
- Do NOT add any text after the closing `</details>` — the comment-builder
|
||||
appends its own footer.
|
||||
- Output ONLY the formatted review. No commentary, no explanation.
|
||||
@@ -0,0 +1,661 @@
|
||||
# Code Review Workflow — CI Prompt
|
||||
|
||||
## Purpose
|
||||
Thorough multi-agent code review for RocksDB commits following CLAUDE.md guidelines,
|
||||
with structured codebase exploration, inter-agent debate, and verification.
|
||||
|
||||
IMPORTANT — Incremental output: After completing EACH major phase (Setup,
|
||||
Codebase Context, Initial Review, Debate, Synthesis), append your findings
|
||||
to review-findings.md using the Write tool. This ensures partial results are
|
||||
saved even if the review is interrupted by the turn limit. After the final
|
||||
synthesis, write the complete review to review-findings.md, replacing
|
||||
previous content. Also output the final review as your response text.
|
||||
|
||||
## Workflow Steps
|
||||
|
||||
### 1. Setup Phase
|
||||
- Read CLAUDE.md to understand review guidelines
|
||||
- Identify the commit/diff/PR to review
|
||||
- The PR diff is provided below in this prompt. The repository is already
|
||||
checked out — use local file search tools (Grep, Glob, Read) to explore.
|
||||
- Parse the changed files and categorize them (API, core logic, tests, etc.)
|
||||
- Read all changed files to build full context
|
||||
|
||||
### 2. Codebase Context Phase (CRITICAL — Do Not Skip)
|
||||
|
||||
**Why this matters**: Review agents that only see the diff miss systemic bugs.
|
||||
For example, a change to an iterator's return value might look correct in
|
||||
isolation but break multi-SST iteration 5 layers up the call stack. A change
|
||||
to error handling might silently drop errors that a caller 3 levels up relies
|
||||
on for recovery. This phase builds the context that prevents those blind spots.
|
||||
|
||||
Go deep. Surface-level reading leads to reviews that miss caching layers,
|
||||
existing helper functions, or concurrency requirements. The most expensive
|
||||
failure mode is findings that look correct in isolation but miss how the
|
||||
change breaks the surrounding system.
|
||||
|
||||
The team lead spawns one or more research agents to perform the following
|
||||
analyses using local file search tools (Grep, Glob, Read). The research
|
||||
must be written to `context.md` BEFORE any review agent is spawned.
|
||||
|
||||
#### 2a. Subsystem Deep-Read
|
||||
Read the changed files AND their surrounding subsystem **in depth** — not just
|
||||
signatures, but actual logic, edge cases, data flows, locking protocols,
|
||||
concurrency patterns, and interactions between components.
|
||||
|
||||
For each changed file, also read:
|
||||
- The header that declares its interface (if the change is in a .cc file)
|
||||
- The sibling implementation that does the same thing the "standard" way
|
||||
(e.g., for a UDI iterator change, read how `IndexBlockIter` handles the
|
||||
same scenario — it's the reference implementation)
|
||||
- The wrapper/adapter layer that sits between this code and its callers
|
||||
- Any existing tests to understand what behaviors are currently guaranteed
|
||||
|
||||
#### 2b. Caller-Chain Analysis (upstream impact)
|
||||
For each changed function/method, trace the call chain UPWARD 3-5 levels to
|
||||
understand who consumes the changed behavior and what invariants they rely on.
|
||||
**Focus on control flow decisions** — if/else branches, while loop conditions,
|
||||
early returns — that depend on the changed return values or state.
|
||||
|
||||
**How to do this**:
|
||||
- Search for callers of each changed function (search for the function name, the
|
||||
class method, virtual overrides)
|
||||
- For each caller, search for ITS callers, repeating 3-5 levels up
|
||||
- At each level, read the actual code to understand the control flow decision
|
||||
- Stop when you reach a "terminal" caller (e.g., user-facing API, top-level
|
||||
iterator) or when the propagation is clearly safe
|
||||
|
||||
#### 2c. Callee-Chain Analysis (downstream dependencies AND side effects)
|
||||
For each changed function, trace what it calls and whether those calls have
|
||||
new preconditions or changed semantics. **Critically, trace SIDE EFFECTS —
|
||||
not just return values.** Many bugs hide in side effects on shared state
|
||||
(counters, sequence numbers, flags) that are invisible if you only check
|
||||
return values.
|
||||
|
||||
**For each callee, ask:**
|
||||
1. What does it RETURN? (the obvious check)
|
||||
2. What shared state does it MUTATE? (the non-obvious check)
|
||||
3. Under what PRECONDITIONS does it operate correctly?
|
||||
4. Are those preconditions met in the new calling context?
|
||||
|
||||
#### 2d. Sibling Implementation Comparison
|
||||
Identify the "reference" or "standard" implementation that does the same thing
|
||||
the changed code does, and compare their behavior. This is critical for plugin/
|
||||
extension APIs where the implementation must match an implicit contract.
|
||||
|
||||
#### 2e. Invariant Documentation
|
||||
Document the key invariants that the changed code must maintain. Read existing
|
||||
comments, assertions, and test expectations to discover these invariants.
|
||||
|
||||
#### 2f. Related Functionality and Existing Conventions
|
||||
Identify related functionality that already exists in the codebase:
|
||||
- Are there existing helper functions the change should use (or is duplicating)?
|
||||
- Are there existing patterns for the same kind of change (e.g., how other
|
||||
Customizable subclasses handle deprecation)?
|
||||
- Are there existing tests that test similar scenarios (e.g., multi-SST
|
||||
iteration tests that could be extended)?
|
||||
|
||||
#### 2g. Cross-Component Data Consumer Analysis (CRITICAL — common blind spot)
|
||||
|
||||
**Why this is separate from caller-chain analysis:** Sections 2b-2c trace
|
||||
who CALLS the changed functions. This section traces who CONSUMES the data
|
||||
that the changed code PRODUCES. These are often completely different
|
||||
subsystems.
|
||||
|
||||
When the change writes data to a shared structure (memtable, SST block, cache
|
||||
entry, statistics counter), ask: **"Who reads this data, under what rules,
|
||||
and do those rules match the writer's assumptions?"**
|
||||
|
||||
**How to do this:**
|
||||
1. Identify every piece of data the change WRITES (e.g., a range tombstone
|
||||
entry, a counter update, a metadata field)
|
||||
2. For each, search the codebase for ALL readers of that data structure
|
||||
3. For each reader, check:
|
||||
- Does the reader apply visibility rules (seqno, read_callback, snapshot)?
|
||||
- Are those rules compatible with the writer's assumptions?
|
||||
- Does the reader assume invariants about the data (e.g., seqno ordering,
|
||||
monotonicity) that the writer might violate?
|
||||
|
||||
#### 2h. Alternative Execution Contexts
|
||||
|
||||
The same code path may run in different contexts with different assumptions.
|
||||
**Enumerate all contexts and check each one.** This is where "works in the
|
||||
common case but breaks in edge configurations" bugs hide.
|
||||
|
||||
For RocksDB, always check whether the changed code interacts differently with:
|
||||
|
||||
| Context | Key difference | Common failure mode |
|
||||
|---------|---------------|-------------------|
|
||||
| WritePreparedTxnDB / WriteUnpreparedTxnDB | `read_callback_` controls visibility, not just seqno | Visibility bypass |
|
||||
| ReadOnly DB / SecondaryInstance | No mutable memtable, writes not allowed | Null pointer or no-op needed |
|
||||
| CompactionService / Remote compaction | Different process, serialized state | Serialization mismatch |
|
||||
| User-defined timestamps | Extra dimension in key comparison and visibility | Wrong ordering |
|
||||
| MemPurge | Memtable-to-memtable, not memtable-to-SST | Missing data |
|
||||
| Column family with BlobDB | Values may be in blob files, not inline | Missing dereference |
|
||||
| Snapshots (old, held long-term) | Snapshot seqno may be far behind current state | Metadata corruption |
|
||||
| Concurrent writers (allow_concurrent_memtable_write) | Lock-free paths vs locked paths | Lost updates |
|
||||
| FIFO / Universal compaction | Different compaction invariants than Level | Wrong assumptions |
|
||||
| Prefix seek / total order seek | Different iterator behavior | Wrong iteration results |
|
||||
|
||||
**For each context, ask:**
|
||||
1. Does the code execute in this context? (Check constructor, feature flags)
|
||||
2. If yes, do the assumptions still hold?
|
||||
3. If not, should the feature be disabled in this context?
|
||||
|
||||
#### 2i. Assumption Stress-Testing (the "logically X" audit)
|
||||
|
||||
When the change claims a property (e.g., "logically redundant," "no-op in
|
||||
this case," "safe because X"), **systematically break it:**
|
||||
|
||||
1. **State the claim precisely.** E.g., "The inserted range tombstone is
|
||||
logically redundant — it covers only keys already deleted by point
|
||||
tombstones."
|
||||
2. **List the preconditions** that must be true for the claim to hold.
|
||||
3. **For each precondition, construct a counterexample** — a concrete scenario
|
||||
where the precondition is violated.
|
||||
4. **Verify each counterexample against the code.** Does the code guard
|
||||
against it? If not, it's a bug.
|
||||
|
||||
**Anti-pattern: treating asserts as proofs.** When you see an assert, do NOT
|
||||
conclude "the invariant holds." Instead ask: "Can I construct an input where
|
||||
this assert fires?" If yes, it's a bug (the assert catches it in debug but
|
||||
release builds silently corrupt). If you cannot construct a counterexample
|
||||
after trying, document WHY it's impossible.
|
||||
|
||||
#### 2j. Write Context Document
|
||||
Write the complete analysis to `context.md` in the review folder. This document
|
||||
is provided to ALL review agents as part of their prompt.
|
||||
|
||||
**Context document template**:
|
||||
```markdown
|
||||
# Codebase Context for Review
|
||||
|
||||
## How the Relevant Subsystem Works Today
|
||||
[Detailed description of the subsystem architecture, data flows,
|
||||
and component interactions. Not just "what" but "how" and "why."]
|
||||
|
||||
## Changed Functions and Their Call Chains
|
||||
|
||||
### function_name() (file:line)
|
||||
**What changed**: [brief description of the behavioral change]
|
||||
|
||||
**Upstream callers** (who depends on this):
|
||||
1. CallerA::method() (file:line) — uses return value to decide X
|
||||
2. CallerB::method() (file:line) — passes result to CallerC
|
||||
3. CallerC::method() (file:line) — THE CRITICAL DECISION POINT: ...
|
||||
|
||||
**Downstream callees** (what this depends on):
|
||||
1. calleeA() — behavior unchanged
|
||||
2. calleeB() — NEW dependency, requires X
|
||||
|
||||
**Sibling implementation** (how the "standard" version handles this):
|
||||
- StandardImpl::method() does Y at file:line, which ensures invariant Z
|
||||
- The changed code must match or document any deviation
|
||||
|
||||
**Key invariants**:
|
||||
- Must never return X when Y is true because CallerC will...
|
||||
- Must always set Z before returning because CallerB assumes...
|
||||
|
||||
## System Architecture Context
|
||||
[How the changed components fit into the overall system]
|
||||
|
||||
## Known Invariants That Must Be Preserved
|
||||
1. ...
|
||||
2. ...
|
||||
|
||||
## Existing Conventions and Related Code
|
||||
- [helper functions, patterns, existing tests that are relevant]
|
||||
|
||||
## Cross-Component Data Consumers
|
||||
For each piece of data the change WRITES to a shared structure:
|
||||
### [data item] written to [structure]
|
||||
**Writer assumptions**: [what the writer assumes about how this data is consumed]
|
||||
**All readers**:
|
||||
1. ReaderA::method() (file:line) — reads under [visibility rules]
|
||||
- Compatible with writer? YES/NO. If NO, explain the mismatch.
|
||||
2. ReaderB::method() (file:line) — reads under [different rules]
|
||||
- Compatible with writer? YES/NO.
|
||||
|
||||
## Alternative Execution Contexts
|
||||
| Context | Does code execute? | Assumptions hold? | Action needed? |
|
||||
|---------|-------------------|-------------------|----------------|
|
||||
| WritePreparedTxnDB | YES/NO | YES/NO | [disable/guard/safe] |
|
||||
| Old snapshots | YES/NO | YES/NO | ... |
|
||||
| User-defined timestamps | YES/NO | YES/NO | ... |
|
||||
| ReadOnly DB | YES/NO | YES/NO | ... |
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
## Assumption Stress Test
|
||||
### Claim: "[the design claim, e.g., logically redundant]"
|
||||
**Preconditions for claim to hold:**
|
||||
1. [precondition] — counterexample: [scenario]. Guarded? YES/NO.
|
||||
2. [precondition] — counterexample: [scenario]. Guarded? YES/NO.
|
||||
|
||||
## Potential Pitfalls
|
||||
- [specific scenarios where the change could interact badly with
|
||||
the surrounding system, identified from the caller-chain analysis]
|
||||
```
|
||||
|
||||
### 3. Initial Review Phase (Parallel)
|
||||
Create a team and spawn 5 review agents in parallel. **Include the context
|
||||
document from Phase 2 in each agent's prompt.** Each agent writes findings
|
||||
to its own file and sends a summary message to the team lead.
|
||||
|
||||
Each agent's prompt should include:
|
||||
```
|
||||
## Codebase Context (READ THIS FIRST)
|
||||
[paste or reference the context.md file]
|
||||
|
||||
You MUST consider how your findings interact with the upstream callers
|
||||
and system invariants documented above. A finding that looks correct
|
||||
in isolation may be a critical bug when you consider the full call chain.
|
||||
```
|
||||
|
||||
#### Agent: Design & Approach Reviewer
|
||||
- Read the commit message deeply to understand the problem being solved
|
||||
- Analyze whether this is the right approach to the problem
|
||||
- Consider alternative designs and trade-offs:
|
||||
- Are there simpler solutions that achieve the same goal?
|
||||
- Does the approach introduce unnecessary complexity?
|
||||
- Would a different data structure or algorithm be more appropriate?
|
||||
- Evaluate whether the change follows existing architectural patterns or
|
||||
introduces new patterns that may be inconsistent
|
||||
- Assess the scope: is the change too large? Should it be split into
|
||||
smaller, independently reviewable pieces?
|
||||
- Check if the approach has precedent in the codebase or in the academic
|
||||
literature it references (e.g., SuRF paper)
|
||||
|
||||
#### Agent: Correctness Reviewer
|
||||
- Thread safety and concurrency analysis
|
||||
- Error handling and propagation via Status type
|
||||
- Edge cases (empty inputs, overflow, boundary conditions)
|
||||
- Data corruption scenarios
|
||||
- Logic correctness of new algorithms
|
||||
- **Behavioral contract changes**: Do return value semantics match what
|
||||
upstream callers expect? (Use the caller-chain analysis from context.md)
|
||||
- **Callee side effects**: Does the change call functions that mutate shared
|
||||
state (counters, seqnos, metadata)? Are the mutations correct for the new
|
||||
calling context? (Use the callee-chain analysis from context.md)
|
||||
|
||||
#### Agent: Cross-Component & Adversarial Reviewer
|
||||
|
||||
This agent exists because the most critical bugs hide at component boundaries,
|
||||
not within a single component. It uses a fundamentally different methodology
|
||||
than the correctness reviewer: instead of verifying "does the code do what it
|
||||
intends?", it asks "what breaks when we change the assumptions?"
|
||||
|
||||
**Data-flow analysis:** Perform the cross-component data consumer analysis
|
||||
described in section 2g. For every piece of data the change WRITES to a shared
|
||||
structure, trace ALL READERS and verify their visibility rules are compatible
|
||||
with the writer's assumptions. This is a DATA-FLOW question, not a
|
||||
CONTROL-FLOW question — readers may be in completely different subsystems.
|
||||
|
||||
**Alternative execution contexts:** Use the canonical table from section 2h.
|
||||
Enumerate all contexts where the changed code executes and verify assumptions
|
||||
hold in each.
|
||||
|
||||
**Assumption stress-testing and assert-breaking:** Follow the methodology from
|
||||
section 2i. Identify every design claim, enumerate preconditions, construct
|
||||
counterexamples. Treat asserts as hypotheses to break, not proofs.
|
||||
**Red flag words**: "logically redundant", "safe because", "no-op",
|
||||
"always true", "cannot happen", "invariant holds"
|
||||
|
||||
**Callee side-effect audit:** Perform the callee side-effect audit described
|
||||
in section 2c. For every callee, ask what it RETURNS and what it MUTATES.
|
||||
|
||||
Write findings to `findings-cross-component.md`.
|
||||
|
||||
#### Agent: Invariant Adversary
|
||||
|
||||
This agent does NOT read the diff in detail. It takes the feature summary and
|
||||
systematically tries to break it. While other agents verify "does the code do
|
||||
what it says?", this agent asks "what existing system invariants does this
|
||||
feature violate?" and "under what inputs do the design claims fail?"
|
||||
|
||||
**Steps 1-4: Assumption stress-testing.** Follow the methodology from section
|
||||
2i: extract design claims, enumerate preconditions, construct counterexamples,
|
||||
and attempt to break every assert. Be exhaustive about input parameter ranges,
|
||||
shared state configurations, and concurrent interleaving.
|
||||
|
||||
**Step 5: Callee side-effect audit.** Perform the callee side-effect audit
|
||||
described in section 2c. For every function the changed code calls, list ALL
|
||||
mutations to shared state (atomic CAS loops, counter increments, flag/metadata
|
||||
updates, cache invalidation). A function returning `Status::OK()` does NOT
|
||||
mean its side effects are correct.
|
||||
|
||||
Write findings to `findings-invariant-adversary.md`.
|
||||
|
||||
#### Agent: Caller-Context Auditor
|
||||
|
||||
This agent traces BACKWARD from every entry point the changed code hooks into.
|
||||
While other agents read the changed code forward ("what does it do?"), this
|
||||
agent enumerates WHO invokes it and WITH WHAT parameter ranges.
|
||||
|
||||
The key insight: a function that is correct for all *typical* inputs may be
|
||||
catastrophically wrong for *atypical-but-reachable* inputs. This agent's job
|
||||
is to find the atypical inputs.
|
||||
|
||||
**Step 1: Identify entry points.** List every function/constructor/method in
|
||||
the changed code that receives external input (parameters, config values,
|
||||
pointers to shared state). Include:
|
||||
- Constructor parameters (e.g., `active_mem`, `read_callback`, `sequence`)
|
||||
- Virtual method overrides that callers invoke polymorphically
|
||||
- Functions called from multiple subsystems
|
||||
|
||||
**Step 2: Enumerate all callers (3-5 levels up).** For each entry point,
|
||||
search the codebase for ALL callers. Do NOT stop at the first caller — trace
|
||||
the full call chain. Use `Grep` and `Glob` to find:
|
||||
- Direct callers of the function
|
||||
- Callers of wrapper/adapter functions that forward to it
|
||||
- Factory functions that construct the object
|
||||
- Virtual dispatch sites (search for the base class method name)
|
||||
|
||||
**Step 3: Parameter range analysis.** For each caller, determine:
|
||||
- What values does it pass for each parameter?
|
||||
- Under what conditions does it call this function?
|
||||
- Are there callers that pass unusual values? (e.g., `kMaxSequenceNumber`,
|
||||
`nullptr`, old snapshot seqnos, non-null `read_callback_`)
|
||||
|
||||
Build a parameter range table for each entry point, listing all callers
|
||||
and the values they pass for each parameter.
|
||||
|
||||
**Step 4: Configuration matrix.** Enumerate option/config combinations that
|
||||
affect the changed code path:
|
||||
- Which options enable/disable the feature?
|
||||
- Which options change the execution context? (e.g.,
|
||||
`allow_concurrent_memtable_write`, `use_trie_index`)
|
||||
- Are there option combinations that create contradictions?
|
||||
|
||||
**Step 5: Cross-reference with context.md.** Compare your caller analysis
|
||||
with the invariants and execution contexts documented in context.md. Flag
|
||||
any caller that violates an assumed precondition.
|
||||
|
||||
Write findings to `findings-caller-audit.md`.
|
||||
|
||||
#### Agent: Performance Reviewer
|
||||
- Memory allocation on hot paths
|
||||
- Unnecessary copies (string, buffer)
|
||||
- Cache efficiency and data locality
|
||||
- Loop optimization opportunities
|
||||
- Branch prediction (LIKELY/UNLIKELY)
|
||||
- Zero-overhead design verification
|
||||
|
||||
#### Agent: API & Compatibility Reviewer
|
||||
- Public API backwards compatibility
|
||||
- API consistency with existing patterns
|
||||
- Documentation completeness
|
||||
- Deprecation policy compliance
|
||||
- Forward compatibility (struct extensibility)
|
||||
- **Behavioral compatibility**: Do changed semantics (e.g., kOutOfBound vs
|
||||
kUnknown) break implicit contracts with callers?
|
||||
|
||||
#### Agent: Serialization & Deserialization Reviewer
|
||||
- Format correctness and versioning
|
||||
- Alignment requirements
|
||||
- Integer overflow in size computations
|
||||
- Bounds checking on untrusted data
|
||||
- Crafted input attack vectors
|
||||
|
||||
#### Agent: Test Coverage Reviewer
|
||||
- Edge case coverage
|
||||
- Failure mode testing (corruption, truncation)
|
||||
- Round-trip testing (build → serialize → deserialize → verify)
|
||||
- Integration test coverage
|
||||
- **System-level test coverage**: Are the caller-chain interactions tested?
|
||||
(e.g., multi-SST iteration with UDI, level compaction with new behavior)
|
||||
- Test quality (no flaky patterns, good assertions)
|
||||
|
||||
### 4. Round-Robin Debate Phase
|
||||
After all 5 agents complete their initial review, the team lead orchestrates
|
||||
a structured debate:
|
||||
|
||||
#### Step 4a: Share findings
|
||||
- Team lead sends each agent a summary of ALL other agents' findings
|
||||
(or instructs each agent to read the other agents' findings files)
|
||||
|
||||
#### Step 4b: Critique round (2 rounds)
|
||||
Each agent reviews the findings from the other agents and sends messages to
|
||||
challenge, support, or refine them:
|
||||
|
||||
- **Challenge**: "I disagree with [agent] [finding] because [evidence]."
|
||||
- **Support**: "I independently found the same issue — this confirms it."
|
||||
- **Refine**: "[Finding A] is actually a consequence of [Finding B].
|
||||
The root cause is the same."
|
||||
|
||||
The debate assignment follows a round-robin pattern:
|
||||
```
|
||||
correctness-reviewer → critiques → invariant-adversary, serialization
|
||||
cross-component-reviewer → critiques → correctness, caller-audit
|
||||
invariant-adversary → critiques → correctness, cross-component
|
||||
caller-audit → critiques → invariant-adversary, cross-component
|
||||
performance-reviewer → critiques → api, cross-component
|
||||
api-reviewer → critiques → correctness, performance
|
||||
serialization-reviewer → critiques → correctness, invariant-adversary
|
||||
test-reviewer → critiques → serialization, caller-audit
|
||||
design-reviewer → critiques → cross-component, invariant-adversary
|
||||
```
|
||||
|
||||
**Note:** The invariant-adversary and caller-audit agents are deliberately
|
||||
cross-linked with correctness and cross-component because their findings
|
||||
often reveal the same underlying bug from different angles (invariant
|
||||
violation vs reachable bad input vs data-flow mismatch).
|
||||
|
||||
Each agent should:
|
||||
1. Read the assigned agents' findings files
|
||||
2. Send a critique message to each assigned agent
|
||||
3. Respond to critiques received from other agents
|
||||
4. Update their own findings file with any revisions (upgraded/downgraded severity,
|
||||
withdrawn findings, new findings inspired by others)
|
||||
|
||||
#### Step 4c: Team lead collects debate results
|
||||
- Read all updated findings files
|
||||
- Review the debate messages
|
||||
- Build consensus: findings supported by 2+ agents → HIGH confidence
|
||||
- Write consensus document with final severity classifications
|
||||
|
||||
### 5. Consensus Phase
|
||||
Team lead synthesizes the debate into a consensus document:
|
||||
- Cross-reference overlapping findings across agents
|
||||
- Count agreement (majority = 2+ agents independently flagging or supporting)
|
||||
- Note disagreements and how they were resolved
|
||||
- Classify findings as HIGH/MEDIUM/LOW severity
|
||||
- Write consensus document
|
||||
|
||||
### 6. Summary Phase
|
||||
- Compile all validated findings ordered by severity
|
||||
- Include detailed bug analysis (root cause, vulnerable code paths)
|
||||
- Include suggested fixes
|
||||
- Note which findings were debated and the outcome
|
||||
- Write the complete review to review-findings.md and output as response
|
||||
|
||||
**Final report quality rules:**
|
||||
- The final report must be CLEAN and POLISHED. No stream-of-consciousness,
|
||||
no "Wait, actually...", no "on closer re-examination". Those belong in
|
||||
the working notes, not the output.
|
||||
- If a finding was raised during review but later disproven during debate
|
||||
or deeper analysis, REMOVE IT entirely from the final report. Do not
|
||||
include it with a retraction — just drop it.
|
||||
- If a finding was downgraded (e.g., Critical → Suggestion), present it
|
||||
at the final severity only. Do not narrate the severity change.
|
||||
- Each finding in the final report should read as a confident, verified
|
||||
conclusion — not a record of the analysis process.
|
||||
- The reader should never see the reviewer arguing with itself.
|
||||
|
||||
**REQUIRED output structure (so the PR page stays scrollable):**
|
||||
|
||||
The final response (and contents of `review-findings.md`) MUST follow this
|
||||
exact structure. The summary appears first so reviewers can see HIGH findings
|
||||
at a glance; everything else is hidden behind a `<details>` block.
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
|
||||
<!-- One or two sentences of overall assessment. -->
|
||||
|
||||
**High-severity findings (N):**
|
||||
- **[file.cc:123]** One-line description of the issue. <!-- repeat per HIGH finding -->
|
||||
|
||||
<!-- If there are NO high-severity findings, write exactly: -->
|
||||
<!-- _No high-severity findings._ -->
|
||||
|
||||
<details>
|
||||
<summary>Full review (click to expand)</summary>
|
||||
|
||||
### Findings
|
||||
|
||||
#### :red_circle: HIGH
|
||||
|
||||
##### H1. <Title> — `file.cc:123`
|
||||
- **Issue:** ...
|
||||
- **Root cause:** ...
|
||||
- **Suggested fix:** ...
|
||||
|
||||
#### :yellow_circle: MEDIUM
|
||||
... (same structure: M1, M2, ...)
|
||||
|
||||
#### :green_circle: LOW / NIT
|
||||
... (same structure: L1, L2, ...)
|
||||
|
||||
### Cross-Component Analysis
|
||||
<!-- Execution-context table and assumption stress-test results. -->
|
||||
|
||||
### Positive Observations
|
||||
<!-- Optional: good patterns, clever optimizations. -->
|
||||
|
||||
</details>
|
||||
```
|
||||
|
||||
Rules for this structure:
|
||||
- The top-level `## Summary` and the bullet list of HIGH findings MUST stay
|
||||
outside the `<details>` block — they are always visible.
|
||||
- Every detail (per-finding root cause, fix, debate outcomes, cross-component
|
||||
analysis, positive observations) MUST live inside the `<details>` block.
|
||||
- Do NOT nest a `<details>` inside another `<details>`.
|
||||
- Do NOT add any text after the closing `</details>` — the comment-builder
|
||||
appends its own footer.
|
||||
- If the review is partial (recovery path), still produce the summary block
|
||||
first, and put whatever was salvaged inside the `<details>` block.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Context Phase (must be completed before agents spawn)
|
||||
- [ ] Subsystem deep-read — read changed files AND surrounding subsystem
|
||||
in depth (logic, edge cases, data flows, locking, concurrency)
|
||||
- [ ] Caller chain for every changed public/virtual method (3-5 levels up)
|
||||
- [ ] Critical decision points where callers branch on changed behavior
|
||||
- [ ] Callee chain with SIDE EFFECTS — downstream dependencies, new
|
||||
preconditions, AND mutations to shared state (section 2c)
|
||||
- [ ] Sibling implementations — how does the standard version handle same scenarios?
|
||||
- [ ] Invariants that callers rely on
|
||||
- [ ] Related functionality — existing helpers, patterns, conventions
|
||||
- [ ] Cross-component data consumers — for every data WRITTEN, find ALL readers
|
||||
and verify visibility rules match (section 2g)
|
||||
- [ ] Alternative execution contexts verified (section 2h table)
|
||||
- [ ] Assumption stress-test — for every design claim, list preconditions
|
||||
and construct counterexamples (section 2i)
|
||||
- [ ] Multi-component interactions (compaction, recovery, snapshots, iterators)
|
||||
- [ ] Configuration dependencies and unexpected option combinations
|
||||
- [ ] Potential pitfalls from caller-chain analysis
|
||||
|
||||
### Review Phase (verified by agents)
|
||||
- [ ] Database semantics preserved (snapshot isolation, key ordering)
|
||||
- [ ] All error cases handled
|
||||
- [ ] Thread-safe with correct synchronization
|
||||
- [ ] No data races or deadlocks
|
||||
- [ ] Appropriate test coverage (edge cases, failure modes, system-level)
|
||||
- [ ] No unnecessary allocations or copies in hot paths
|
||||
- [ ] Backwards compatible
|
||||
- [ ] New APIs consistent with existing patterns and documented
|
||||
- [ ] Code follows RocksDB style conventions
|
||||
- [ ] Behavioral contracts with upstream callers preserved
|
||||
- [ ] All callers enumerated with parameter range table
|
||||
|
||||
## Output Structure
|
||||
Write all review artifacts to the working directory root:
|
||||
- `review-findings.md` — Incremental findings (appended after each phase),
|
||||
then replaced with the final synthesized review at the end
|
||||
- `context.md` — Codebase context (call chains, invariants)
|
||||
- `findings-*.md` — Per-agent findings (design, correctness, cross-component,
|
||||
invariant-adversary, caller-audit, performance, api, serialization, tests)
|
||||
- `consensus.md` — Cross-review consensus (post-debate)
|
||||
|
||||
|
||||
|
||||
## Team Structure
|
||||
```
|
||||
Team Lead (you)
|
||||
│
|
||||
├── Phase 2: Codebase Context (team lead or dedicated research agent)
|
||||
│ └── context-researcher (general-purpose agent)
|
||||
│ ├── Trace caller chains (3-5 levels up)
|
||||
│ ├── Trace callee chains (dependencies AND side effects)
|
||||
│ ├── Trace data consumers (who reads what the change writes?)
|
||||
│ └── Document invariants
|
||||
│
|
||||
├── Phase 3: Initial Review (parallel, run_in_background)
|
||||
│ │ (all agents receive context.md in their prompt)
|
||||
│ ├── design-reviewer (general-purpose agent)
|
||||
│ ├── correctness-reviewer (general-purpose agent)
|
||||
│ ├── cross-component-reviewer (general-purpose agent)
|
||||
│ ├── invariant-adversary (general-purpose agent) ← NEW
|
||||
│ ├── caller-audit (general-purpose agent) ← NEW
|
||||
│ ├── performance-reviewer (general-purpose agent)
|
||||
│ ├── api-reviewer (general-purpose agent)
|
||||
│ ├── serialization-reviewer (general-purpose agent)
|
||||
│ └── test-reviewer (general-purpose agent)
|
||||
│
|
||||
├── Phase 4: Debate (agents message each other)
|
||||
│ ├── correctness ↔ invariant-adversary, serialization
|
||||
│ ├── cross-component ↔ correctness, caller-audit
|
||||
│ ├── invariant-adversary ↔ correctness, cross-component
|
||||
│ ├── caller-audit ↔ invariant-adversary, cross-component
|
||||
│ ├── performance ↔ api, cross-component
|
||||
│ ├── api ↔ correctness, performance
|
||||
│ ├── serialization ↔ correctness, invariant-adversary
|
||||
│ ├── test-coverage ↔ serialization, caller-audit
|
||||
│ └── design ↔ cross-component, invariant-adversary
|
||||
│
|
||||
```
|
||||
|
||||
|
||||
## Agent Communication Protocol
|
||||
|
||||
### During Initial Review (Phase 3)
|
||||
- Each agent writes findings to its own file
|
||||
- Each agent sends a summary message to team lead when done
|
||||
- Agents do NOT communicate with each other yet
|
||||
|
||||
### During Debate (Phase 4)
|
||||
- Team lead sends each agent a message with instructions to:
|
||||
1. Read the assigned agents' findings files
|
||||
2. Send critique messages to those agents
|
||||
3. Respond to incoming critiques
|
||||
4. Update their own findings file with revisions
|
||||
- Each critique message should include:
|
||||
- Which finding they're addressing (e.g., "Correctness F1")
|
||||
- Whether they AGREE, DISAGREE, or want to REFINE
|
||||
- Their reasoning with code evidence
|
||||
- Suggested severity adjustment (if any)
|
||||
|
||||
### Message format example
|
||||
```
|
||||
To: correctness-reviewer
|
||||
Re: Your Finding F1
|
||||
|
||||
AGREE/DISAGREE/REFINE - [reasoning with code evidence].
|
||||
[Suggested severity adjustment if any.]
|
||||
```
|
||||
|
||||
## Review Anti-Patterns
|
||||
|
||||
These recurring failure modes lead to missed bugs. Each is detailed in the
|
||||
referenced section; this table is a quick-reference checklist.
|
||||
|
||||
| Anti-Pattern | Fix | Reference |
|
||||
|---|---|---|
|
||||
| Return-Value Tunnel Vision | Trace callee MUTATIONS, not just returns | Section 2c |
|
||||
| Default-Configuration Bias | Enumerate all execution contexts | Section 2h |
|
||||
| Assert-as-Proof | Try to BREAK every assert | Section 2i |
|
||||
| Write-Path-Only Analysis | Trace data readers, not just writers | Section 2g |
|
||||
| Confirmation-Seeking Research | Use adversarial prompts ("find where X fails") | Invariant Adversary agent |
|
||||
| Data-Flow vs Control-Flow Confusion | Separate who CALLS from who READS the data | Section 2g |
|
||||
@@ -7,6 +7,13 @@ DB_STRESS_CMD?=./db_stress
|
||||
|
||||
include common.mk
|
||||
|
||||
ifdef COMPILE_WITH_TSAN
|
||||
# Keep direct `make -f crash_test.mk COMPILE_WITH_TSAN=1 ...` runs
|
||||
# aligned with the main Makefile's TSAN runtime options.
|
||||
TSAN_OPTIONS?=suppressions=$(CURDIR)/tools/tsan_suppressions.txt
|
||||
export TSAN_OPTIONS
|
||||
endif
|
||||
|
||||
CRASHTEST_MAKE=$(MAKE) -f crash_test.mk
|
||||
CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --cleanup_cmd='$(DB_CLEANUP_CMD)' --destroy_db_initially=1
|
||||
|
||||
|
||||
+18
-7
@@ -5,6 +5,8 @@
|
||||
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
|
||||
#include "db/blob/blob_file_partition_manager.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/version_set.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -14,10 +16,13 @@ Status BlobFetcher::FetchBlob(const Slice& user_key,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
assert(version_);
|
||||
|
||||
return version_->GetBlob(read_options_, user_key, blob_index_slice,
|
||||
prefetch_buffer, blob_value, bytes_read);
|
||||
BlobIndex blob_index;
|
||||
Status status = blob_index.DecodeFrom(blob_index_slice);
|
||||
if (status.ok()) {
|
||||
status = FetchBlob(user_key, blob_index, prefetch_buffer, blob_value,
|
||||
bytes_read);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
Status BlobFetcher::FetchBlob(const Slice& user_key,
|
||||
@@ -25,10 +30,16 @@ Status BlobFetcher::FetchBlob(const Slice& user_key,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
assert(version_);
|
||||
if (!allow_write_path_fallback_) {
|
||||
assert(version_);
|
||||
|
||||
return version_->GetBlob(read_options_, user_key, blob_index, prefetch_buffer,
|
||||
blob_value, bytes_read);
|
||||
return version_->GetBlob(read_options_, user_key, blob_index,
|
||||
prefetch_buffer, blob_value, bytes_read);
|
||||
}
|
||||
|
||||
return BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
|
||||
read_options_, user_key, blob_index, version_, blob_file_cache_,
|
||||
prefetch_buffer, blob_value, bytes_read);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
+13
-3
@@ -10,17 +10,25 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class BlobFileCache;
|
||||
class Version;
|
||||
class Slice;
|
||||
class FilePrefetchBuffer;
|
||||
class PinnableSlice;
|
||||
class BlobIndex;
|
||||
|
||||
// A thin wrapper around the blob retrieval functionality of Version.
|
||||
// A thin wrapper around blob retrieval. By default it reads through Version,
|
||||
// and it can optionally fall back to direct-write blob files that are not yet
|
||||
// manifest-visible.
|
||||
class BlobFetcher {
|
||||
public:
|
||||
BlobFetcher(const Version* version, const ReadOptions& read_options)
|
||||
: version_(version), read_options_(read_options) {}
|
||||
BlobFetcher(const Version* version, const ReadOptions& read_options,
|
||||
BlobFileCache* blob_file_cache = nullptr,
|
||||
bool allow_write_path_fallback = false)
|
||||
: version_(version),
|
||||
read_options_(read_options),
|
||||
blob_file_cache_(blob_file_cache),
|
||||
allow_write_path_fallback_(allow_write_path_fallback) {}
|
||||
|
||||
Status FetchBlob(const Slice& user_key, const Slice& blob_index_slice,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
@@ -33,5 +41,7 @@ class BlobFetcher {
|
||||
private:
|
||||
const Version* version_;
|
||||
ReadOptions read_options_;
|
||||
BlobFileCache* blob_file_cache_;
|
||||
bool allow_write_path_fallback_;
|
||||
};
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
@@ -67,13 +67,11 @@ BlobFileBuilder::BlobFileBuilder(
|
||||
min_blob_size_(mutable_cf_options->min_blob_size),
|
||||
blob_file_size_(mutable_cf_options->blob_file_size),
|
||||
blob_compression_type_(mutable_cf_options->blob_compression_type),
|
||||
// TODO: support most CompressionOptions with a new CF option
|
||||
// blob_compression_opts
|
||||
// TODO with schema change: support custom compression manager and options
|
||||
// such as max_compressed_bytes_per_kb
|
||||
// NOTE: returns nullptr for kNoCompression
|
||||
blob_compressor_(GetBuiltinV2CompressionManager()->GetCompressor(
|
||||
CompressionOptions{}, blob_compression_type_)),
|
||||
mutable_cf_options->blob_compression_opts, blob_compression_type_)),
|
||||
blob_compressor_wa_(blob_compressor_
|
||||
? blob_compressor_->ObtainWorkingArea()
|
||||
: Compressor::ManagedWorkingArea{}),
|
||||
@@ -111,7 +109,7 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
|
||||
assert(blob_index);
|
||||
assert(blob_index->empty());
|
||||
|
||||
if (value.size() < min_blob_size_) {
|
||||
if (value.empty() || value.size() < min_blob_size_) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
|
||||
+125
-3
@@ -9,6 +9,7 @@
|
||||
#include <memory>
|
||||
|
||||
#include "db/blob/blob_file_reader.h"
|
||||
#include "logging/logging.h"
|
||||
#include "options/cf_options.h"
|
||||
#include "rocksdb/cache.h"
|
||||
#include "rocksdb/slice.h"
|
||||
@@ -38,7 +39,8 @@ BlobFileCache::BlobFileCache(Cache* cache,
|
||||
|
||||
Status BlobFileCache::GetBlobFileReader(
|
||||
const ReadOptions& read_options, uint64_t blob_file_number,
|
||||
CacheHandleGuard<BlobFileReader>* blob_file_reader) {
|
||||
CacheHandleGuard<BlobFileReader>* blob_file_reader,
|
||||
bool allow_footer_skip_retry) {
|
||||
assert(blob_file_reader);
|
||||
assert(blob_file_reader->IsEmpty());
|
||||
|
||||
@@ -73,10 +75,22 @@ Status BlobFileCache::GetBlobFileReader(
|
||||
|
||||
{
|
||||
assert(file_options_);
|
||||
const Status s = BlobFileReader::Create(
|
||||
Status s = BlobFileReader::Create(
|
||||
*immutable_options_, read_options, *file_options_, column_family_id_,
|
||||
blob_file_read_hist_, blob_file_number, io_tracer_, &reader);
|
||||
blob_file_read_hist_, blob_file_number, io_tracer_,
|
||||
/*skip_footer_validation=*/false, &reader);
|
||||
if (!s.ok() && s.IsCorruption() && allow_footer_skip_retry) {
|
||||
reader.reset();
|
||||
s = BlobFileReader::Create(
|
||||
*immutable_options_, read_options, *file_options_, column_family_id_,
|
||||
blob_file_read_hist_, blob_file_number, io_tracer_,
|
||||
/*skip_footer_validation=*/true, &reader);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
ROCKS_LOG_WARN(immutable_options_->logger,
|
||||
"BlobFileCache open failed for blob file %" PRIu64
|
||||
" in CF %u: %s",
|
||||
blob_file_number, column_family_id_, s.ToString().c_str());
|
||||
RecordTick(statistics, NO_FILE_ERRORS);
|
||||
return s;
|
||||
}
|
||||
@@ -99,12 +113,120 @@ Status BlobFileCache::GetBlobFileReader(
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlobFileCache::OpenBlobFileReaderUncached(
|
||||
const ReadOptions& read_options, uint64_t blob_file_number,
|
||||
std::unique_ptr<BlobFileReader>* blob_file_reader,
|
||||
bool allow_footer_skip_retry) {
|
||||
assert(blob_file_reader);
|
||||
assert(!*blob_file_reader);
|
||||
|
||||
Statistics* const statistics = immutable_options_->stats;
|
||||
RecordTick(statistics, NO_FILE_OPENS);
|
||||
|
||||
Status s = BlobFileReader::Create(
|
||||
*immutable_options_, read_options, *file_options_, column_family_id_,
|
||||
blob_file_read_hist_, blob_file_number, io_tracer_,
|
||||
/*skip_footer_validation=*/false, blob_file_reader);
|
||||
if (!s.ok() && s.IsCorruption() && allow_footer_skip_retry) {
|
||||
blob_file_reader->reset();
|
||||
s = BlobFileReader::Create(
|
||||
*immutable_options_, read_options, *file_options_, column_family_id_,
|
||||
blob_file_read_hist_, blob_file_number, io_tracer_,
|
||||
/*skip_footer_validation=*/true, blob_file_reader);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
RecordTick(statistics, NO_FILE_ERRORS);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status BlobFileCache::InsertBlobFileReader(
|
||||
uint64_t blob_file_number,
|
||||
std::unique_ptr<BlobFileReader>* blob_file_reader,
|
||||
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader) {
|
||||
assert(blob_file_reader);
|
||||
assert(*blob_file_reader);
|
||||
assert(cached_blob_file_reader);
|
||||
assert(cached_blob_file_reader->IsEmpty());
|
||||
|
||||
const Slice key = GetSliceForKey(&blob_file_number);
|
||||
// Serialize refreshes for the same blob file. The cache API does not expose
|
||||
// a conditional replace, so refresh is intentionally modeled as
|
||||
// Lookup/optional Erase/Insert under this per-key mutex.
|
||||
MutexLock lock(&mutex_.Get(key));
|
||||
|
||||
TypedHandle* handle = cache_.Lookup(key);
|
||||
if (handle) {
|
||||
*cached_blob_file_reader = cache_.Guard(handle);
|
||||
blob_file_reader->reset();
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
constexpr size_t charge = 1;
|
||||
Status s = cache_.Insert(key, blob_file_reader->get(), charge, &handle);
|
||||
if (!s.ok()) {
|
||||
RecordTick(immutable_options_->stats, NO_FILE_ERRORS);
|
||||
return s;
|
||||
}
|
||||
|
||||
// Ownership transferred to the cache.
|
||||
[[maybe_unused]] BlobFileReader* released_reader =
|
||||
blob_file_reader->release();
|
||||
*cached_blob_file_reader = cache_.Guard(handle);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlobFileCache::RefreshBlobFileReader(
|
||||
uint64_t blob_file_number,
|
||||
std::unique_ptr<BlobFileReader>* blob_file_reader,
|
||||
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader) {
|
||||
assert(blob_file_reader);
|
||||
assert(*blob_file_reader);
|
||||
assert(cached_blob_file_reader);
|
||||
assert(cached_blob_file_reader->IsEmpty());
|
||||
|
||||
const Slice key = GetSliceForKey(&blob_file_number);
|
||||
MutexLock lock(&mutex_.Get(key));
|
||||
|
||||
TypedHandle* handle = cache_.Lookup(key);
|
||||
if (handle) {
|
||||
BlobFileReader* const cached_reader = cache_.Value(handle);
|
||||
assert(cached_reader != nullptr);
|
||||
|
||||
// Active direct-write blob files can grow between refresh attempts. Keep
|
||||
// whichever reader observed the larger on-disk size so an older refresh
|
||||
// cannot overwrite a newer one that another thread already installed.
|
||||
if (cached_reader->GetFileSize() >= (*blob_file_reader)->GetFileSize()) {
|
||||
*cached_blob_file_reader = cache_.Guard(handle);
|
||||
blob_file_reader->reset();
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
cache_.Release(handle);
|
||||
cache_.get()->Erase(key);
|
||||
}
|
||||
|
||||
constexpr size_t charge = 1;
|
||||
Status s = cache_.Insert(key, blob_file_reader->get(), charge, &handle);
|
||||
if (!s.ok()) {
|
||||
RecordTick(immutable_options_->stats, NO_FILE_ERRORS);
|
||||
return s;
|
||||
}
|
||||
|
||||
// Ownership transferred to the cache.
|
||||
[[maybe_unused]] BlobFileReader* released_reader =
|
||||
blob_file_reader->release();
|
||||
*cached_blob_file_reader = cache_.Guard(handle);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void BlobFileCache::Evict(uint64_t blob_file_number) {
|
||||
// NOTE: sharing same Cache with table_cache
|
||||
const Slice key = GetSliceForKey(&blob_file_number);
|
||||
|
||||
assert(cache_);
|
||||
|
||||
MutexLock lock(&mutex_.Get(key));
|
||||
cache_.get()->Erase(key);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,35 @@ class BlobFileCache {
|
||||
BlobFileCache(const BlobFileCache&) = delete;
|
||||
BlobFileCache& operator=(const BlobFileCache&) = delete;
|
||||
|
||||
// Returns a cached reader for `blob_file_number`, opening and caching it on
|
||||
// miss. If `allow_footer_skip_retry` is true, a footer-validation corruption
|
||||
// retries once without requiring a footer.
|
||||
Status GetBlobFileReader(const ReadOptions& read_options,
|
||||
uint64_t blob_file_number,
|
||||
CacheHandleGuard<BlobFileReader>* blob_file_reader);
|
||||
CacheHandleGuard<BlobFileReader>* blob_file_reader,
|
||||
bool allow_footer_skip_retry = false);
|
||||
|
||||
// Opens a blob file reader without inserting it into the cache.
|
||||
Status OpenBlobFileReaderUncached(
|
||||
const ReadOptions& read_options, uint64_t blob_file_number,
|
||||
std::unique_ptr<BlobFileReader>* blob_file_reader,
|
||||
bool allow_footer_skip_retry = false);
|
||||
|
||||
// Inserts a freshly opened uncached reader unless another thread already
|
||||
// cached the same blob file.
|
||||
Status InsertBlobFileReader(
|
||||
uint64_t blob_file_number,
|
||||
std::unique_ptr<BlobFileReader>* blob_file_reader,
|
||||
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader);
|
||||
|
||||
// Installs a refresh-opened reader into the cache. If another thread has
|
||||
// already cached a reader opened on at least as large a file size, keep that
|
||||
// reader instead so a racing refresh cannot reintroduce an older active-file
|
||||
// view after the blob file grows.
|
||||
Status RefreshBlobFileReader(
|
||||
uint64_t blob_file_number,
|
||||
std::unique_ptr<BlobFileReader>* blob_file_reader,
|
||||
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader);
|
||||
|
||||
// Called when a blob file is obsolete to ensure it is removed from the cache
|
||||
// to avoid effectively leaking the open file and assicated memory
|
||||
|
||||
@@ -192,6 +192,102 @@ TEST_F(BlobFileCacheTest, GetBlobFileReader_Race) {
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(BlobFileCacheTest, RefreshBlobFileReaderPrefersLargestObservedFileSize) {
|
||||
Options options;
|
||||
options.env = mock_env_.get();
|
||||
options.statistics = CreateDBStatistics();
|
||||
options.cf_paths.emplace_back(
|
||||
test::PerThreadDBPath(
|
||||
mock_env_.get(),
|
||||
"BlobFileCacheTest_"
|
||||
"RefreshBlobFileReaderPrefersLargestObservedFileSize"),
|
||||
0);
|
||||
options.enable_blob_files = true;
|
||||
|
||||
constexpr uint32_t column_family_id = 1;
|
||||
ImmutableOptions immutable_options(options);
|
||||
constexpr uint64_t blob_file_number = 123;
|
||||
|
||||
const std::string blob_file_path =
|
||||
BlobFileName(immutable_options.cf_paths.front().path, blob_file_number);
|
||||
|
||||
std::unique_ptr<FSWritableFile> file;
|
||||
ASSERT_OK(NewWritableFile(immutable_options.fs.get(), blob_file_path, &file,
|
||||
FileOptions()));
|
||||
|
||||
std::unique_ptr<WritableFileWriter> file_writer(new WritableFileWriter(
|
||||
std::move(file), blob_file_path, FileOptions(), immutable_options.clock));
|
||||
|
||||
constexpr Statistics* statistics = nullptr;
|
||||
constexpr bool use_fsync = false;
|
||||
constexpr bool do_flush = false;
|
||||
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
|
||||
statistics, blob_file_number, use_fsync,
|
||||
do_flush);
|
||||
|
||||
constexpr bool has_ttl = false;
|
||||
constexpr ExpirationRange expiration_range;
|
||||
BlobLogHeader header(column_family_id, kNoCompression, has_ttl,
|
||||
expiration_range);
|
||||
ASSERT_OK(blob_log_writer.WriteHeader(WriteOptions(), header));
|
||||
|
||||
uint64_t key_offset = 0;
|
||||
uint64_t blob_offset = 0;
|
||||
ASSERT_OK(blob_log_writer.AddRecord(WriteOptions(), "key0", "blob0",
|
||||
&key_offset, &blob_offset));
|
||||
ASSERT_OK(blob_log_writer.Sync(WriteOptions()));
|
||||
|
||||
constexpr size_t capacity = 10;
|
||||
std::shared_ptr<Cache> backing_cache = NewLRUCache(capacity);
|
||||
|
||||
FileOptions file_options;
|
||||
constexpr HistogramImpl* blob_file_read_hist = nullptr;
|
||||
BlobFileCache blob_file_cache(backing_cache.get(), &immutable_options,
|
||||
&file_options, column_family_id,
|
||||
blob_file_read_hist, nullptr /*IOTracer*/);
|
||||
|
||||
const ReadOptions read_options;
|
||||
std::unique_ptr<BlobFileReader> stale_reader;
|
||||
ASSERT_OK(blob_file_cache.OpenBlobFileReaderUncached(
|
||||
read_options, blob_file_number, &stale_reader,
|
||||
/*allow_footer_skip_retry=*/true));
|
||||
|
||||
std::unique_ptr<BlobFileReader> initial_cached_reader;
|
||||
ASSERT_OK(blob_file_cache.OpenBlobFileReaderUncached(
|
||||
read_options, blob_file_number, &initial_cached_reader,
|
||||
/*allow_footer_skip_retry=*/true));
|
||||
|
||||
CacheHandleGuard<BlobFileReader> cached_reader;
|
||||
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(
|
||||
blob_file_number, &initial_cached_reader, &cached_reader));
|
||||
ASSERT_NE(cached_reader.GetValue(), nullptr);
|
||||
const uint64_t initial_file_size = cached_reader.GetValue()->GetFileSize();
|
||||
|
||||
ASSERT_OK(blob_log_writer.AddRecord(WriteOptions(), "key1", "blob1",
|
||||
&key_offset, &blob_offset));
|
||||
ASSERT_OK(blob_log_writer.Sync(WriteOptions()));
|
||||
|
||||
std::unique_ptr<BlobFileReader> fresh_reader;
|
||||
ASSERT_OK(blob_file_cache.OpenBlobFileReaderUncached(
|
||||
read_options, blob_file_number, &fresh_reader,
|
||||
/*allow_footer_skip_retry=*/true));
|
||||
ASSERT_GT(fresh_reader->GetFileSize(), initial_file_size);
|
||||
|
||||
CacheHandleGuard<BlobFileReader> refreshed_reader;
|
||||
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(
|
||||
blob_file_number, &fresh_reader, &refreshed_reader));
|
||||
ASSERT_NE(refreshed_reader.GetValue(), nullptr);
|
||||
ASSERT_GT(refreshed_reader.GetValue()->GetFileSize(), initial_file_size);
|
||||
BlobFileReader* const largest_reader = refreshed_reader.GetValue();
|
||||
|
||||
CacheHandleGuard<BlobFileReader> preserved_reader;
|
||||
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(
|
||||
blob_file_number, &stale_reader, &preserved_reader));
|
||||
ASSERT_NE(preserved_reader.GetValue(), nullptr);
|
||||
ASSERT_EQ(preserved_reader.GetValue(), largest_reader);
|
||||
ASSERT_EQ(stale_reader.get(), nullptr);
|
||||
}
|
||||
|
||||
TEST_F(BlobFileCacheTest, GetBlobFileReader_IOError) {
|
||||
Options options;
|
||||
options.env = mock_env_.get();
|
||||
|
||||
@@ -0,0 +1,851 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "db/blob/blob_file_partition_manager.h"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cinttypes>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "cache/cache_key.h"
|
||||
#include "cache/typed_cache.h"
|
||||
#include "db/blob/blob_contents.h"
|
||||
#include "db/blob/blob_file_cache.h"
|
||||
#include "db/blob/blob_file_completion_callback.h"
|
||||
#include "db/blob/blob_file_reader.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/blob/blob_log_writer.h"
|
||||
#include "db/version_set.h"
|
||||
#include "file/filename.h"
|
||||
#include "file/read_write_util.h"
|
||||
#include "file/writable_file_writer.h"
|
||||
#include "logging/logging.h"
|
||||
#include "util/aligned_buffer.h"
|
||||
#include "util/compression.h"
|
||||
#include "util/mutexlock.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace {
|
||||
|
||||
class RoundRobinBlobFilePartitionStrategy : public BlobFilePartitionStrategy {
|
||||
public:
|
||||
using BlobFilePartitionStrategy::SelectPartition;
|
||||
|
||||
const char* Name() const override {
|
||||
return "RoundRobinBlobFilePartitionStrategy";
|
||||
}
|
||||
|
||||
uint32_t SelectPartition(uint32_t num_partitions,
|
||||
uint32_t /*column_family_id*/, const Slice& /*key*/,
|
||||
const Slice& /*value*/) override {
|
||||
assert(num_partitions > 0);
|
||||
return static_cast<uint32_t>(
|
||||
next_partition_.fetch_add(1, std::memory_order_relaxed) %
|
||||
static_cast<uint64_t>(num_partitions));
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<uint64_t> next_partition_{0};
|
||||
};
|
||||
|
||||
struct DirectWriteCompressionState {
|
||||
CompressionOptions compression_opts;
|
||||
// `working_area` must be released before its owning compressor.
|
||||
std::unique_ptr<Compressor> compressor;
|
||||
Compressor::ManagedWorkingArea working_area;
|
||||
};
|
||||
|
||||
DirectWriteCompressionState& GetDirectWriteCompressionState(
|
||||
CompressionType compression, const CompressionOptions& compression_opts) {
|
||||
assert(compression <= kLastBuiltinCompression);
|
||||
|
||||
static thread_local std::array<DirectWriteCompressionState,
|
||||
static_cast<size_t>(kLastBuiltinCompression) +
|
||||
1>
|
||||
compression_states;
|
||||
|
||||
auto& compression_state =
|
||||
compression_states[static_cast<size_t>(compression)];
|
||||
if (compression != kNoCompression &&
|
||||
(compression_state.compressor == nullptr ||
|
||||
compression_state.compression_opts != compression_opts)) {
|
||||
// BDW compression settings are mutable, so rebuild the per-thread cached
|
||||
// compressor when the latest published opts for this type change.
|
||||
compression_state.working_area = Compressor::ManagedWorkingArea{};
|
||||
compression_state.compressor.reset();
|
||||
compression_state.compression_opts = compression_opts;
|
||||
compression_state.compressor =
|
||||
GetBuiltinV2CompressionManager()->GetCompressor(compression_opts,
|
||||
compression);
|
||||
if (compression_state.compressor != nullptr) {
|
||||
compression_state.working_area =
|
||||
compression_state.compressor->ObtainWorkingArea();
|
||||
}
|
||||
}
|
||||
|
||||
return compression_state;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BlobFilePartitionManager::BlobFilePartitionManager(
|
||||
uint32_t num_partitions,
|
||||
std::shared_ptr<BlobFilePartitionStrategy> strategy,
|
||||
FileNumberAllocator file_number_allocator, FileSystem* fs,
|
||||
SystemClock* clock, Statistics* statistics, const FileOptions& file_options,
|
||||
std::string db_path, std::string column_family_name,
|
||||
uint64_t blob_file_size, bool use_fsync, BlobFileCache* blob_file_cache,
|
||||
BlobFileCompletionCallback* blob_callback,
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners,
|
||||
FileChecksumGenFactory* file_checksum_gen_factory,
|
||||
const FileTypeSet& checksum_handoff_file_types,
|
||||
const std::shared_ptr<IOTracer>& io_tracer, std::string db_id,
|
||||
std::string db_session_id, Logger* info_log)
|
||||
: num_partitions_(num_partitions == 0 ? 1 : num_partitions),
|
||||
strategy_(strategy != nullptr
|
||||
? std::move(strategy)
|
||||
: std::make_shared<RoundRobinBlobFilePartitionStrategy>()),
|
||||
file_number_allocator_(std::move(file_number_allocator)),
|
||||
fs_(fs),
|
||||
clock_(clock),
|
||||
statistics_(statistics),
|
||||
file_options_(file_options),
|
||||
db_path_(std::move(db_path)),
|
||||
column_family_name_(std::move(column_family_name)),
|
||||
blob_file_size_(blob_file_size),
|
||||
use_fsync_(use_fsync),
|
||||
blob_file_cache_(blob_file_cache),
|
||||
blob_callback_(blob_callback),
|
||||
listeners_(listeners),
|
||||
file_checksum_gen_factory_(file_checksum_gen_factory),
|
||||
checksum_handoff_file_types_(checksum_handoff_file_types),
|
||||
io_tracer_(io_tracer),
|
||||
db_id_(std::move(db_id)),
|
||||
db_session_id_(std::move(db_session_id)),
|
||||
info_log_(info_log) {
|
||||
partitions_.reserve(num_partitions_);
|
||||
for (uint32_t i = 0; i < num_partitions_; ++i) {
|
||||
partitions_.emplace_back(new Partition());
|
||||
}
|
||||
}
|
||||
|
||||
BlobFilePartitionManager::~BlobFilePartitionManager() {
|
||||
if (blob_file_cache_ != nullptr) {
|
||||
std::vector<uint64_t> tracked_file_numbers;
|
||||
{
|
||||
ReadLock lock(&file_partition_mutex_);
|
||||
tracked_file_numbers.reserve(file_to_partition_.size());
|
||||
for (const auto& entry : file_to_partition_) {
|
||||
tracked_file_numbers.push_back(entry.first);
|
||||
}
|
||||
}
|
||||
|
||||
for (uint64_t file_number : tracked_file_numbers) {
|
||||
// Dropping the CF or closing the DB can destroy the manager while active
|
||||
// direct-write readers are still cached. Evict them before the CF-owned
|
||||
// blob cache goes away so Close() does not retain obsolete footer-less
|
||||
// readers for files that are no longer live.
|
||||
blob_file_cache_->Evict(file_number);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BlobFilePartitionManager::ResetPartitionState(Partition* partition) {
|
||||
partition->writer.reset();
|
||||
partition->file_number = 0;
|
||||
partition->file_size = 0;
|
||||
partition->blob_count = 0;
|
||||
partition->total_blob_bytes = 0;
|
||||
partition->garbage_blob_count = 0;
|
||||
partition->garbage_blob_bytes = 0;
|
||||
partition->column_family_id = 0;
|
||||
partition->compression = kNoCompression;
|
||||
partition->sync_required = false;
|
||||
}
|
||||
|
||||
void BlobFilePartitionManager::AddFilePartitionMapping(uint64_t file_number,
|
||||
uint32_t partition_idx) {
|
||||
WriteLock lock(&file_partition_mutex_);
|
||||
file_to_partition_[file_number] = partition_idx;
|
||||
}
|
||||
|
||||
void BlobFilePartitionManager::RemoveFilePartitionMapping(
|
||||
uint64_t file_number) {
|
||||
WriteLock lock(&file_partition_mutex_);
|
||||
file_to_partition_.erase(file_number);
|
||||
}
|
||||
|
||||
Status BlobFilePartitionManager::OpenNewBlobFile(Partition* partition,
|
||||
uint32_t column_family_id,
|
||||
CompressionType compression,
|
||||
uint32_t partition_idx) {
|
||||
assert(partition != nullptr);
|
||||
assert(!partition->writer);
|
||||
|
||||
const uint64_t blob_file_number = file_number_allocator_();
|
||||
const std::string blob_file_path = BlobFileName(db_path_, blob_file_number);
|
||||
|
||||
AddFilePartitionMapping(blob_file_number, partition_idx);
|
||||
|
||||
if (blob_callback_ != nullptr) {
|
||||
blob_callback_->OnBlobFileCreationStarted(
|
||||
blob_file_path, column_family_name_,
|
||||
/*job_id=*/0, BlobFileCreationReason::kFlush);
|
||||
}
|
||||
|
||||
std::unique_ptr<FSWritableFile> file;
|
||||
Status s = NewWritableFile(fs_, blob_file_path, &file, file_options_);
|
||||
if (!s.ok()) {
|
||||
RemoveFilePartitionMapping(blob_file_number);
|
||||
return s;
|
||||
}
|
||||
|
||||
const bool perform_data_verification =
|
||||
checksum_handoff_file_types_.Contains(FileType::kBlobFile);
|
||||
auto file_writer = std::make_unique<WritableFileWriter>(
|
||||
std::move(file), blob_file_path, file_options_, clock_, io_tracer_,
|
||||
statistics_, Histograms::BLOB_DB_BLOB_FILE_WRITE_MICROS, listeners_,
|
||||
file_checksum_gen_factory_, perform_data_verification);
|
||||
|
||||
// This only drains WritableFileWriter's buffered bytes so readers can see
|
||||
// each appended record promptly. Durability still comes from SyncAllOpenFiles
|
||||
// or AppendFooter(), both of which call Sync().
|
||||
constexpr bool kDoFlushEachRecord = true;
|
||||
auto blob_log_writer = std::make_unique<BlobLogWriter>(
|
||||
std::move(file_writer), clock_, statistics_, blob_file_number, use_fsync_,
|
||||
kDoFlushEachRecord);
|
||||
|
||||
constexpr bool has_ttl = false;
|
||||
constexpr ExpirationRange expiration_range{};
|
||||
BlobLogHeader header(column_family_id, compression, has_ttl,
|
||||
expiration_range);
|
||||
s = blob_log_writer->WriteHeader(WriteOptions(), header);
|
||||
if (!s.ok()) {
|
||||
RemoveFilePartitionMapping(blob_file_number);
|
||||
return s;
|
||||
}
|
||||
|
||||
partition->writer = std::move(blob_log_writer);
|
||||
partition->file_number = blob_file_number;
|
||||
partition->file_size = BlobLogHeader::kSize;
|
||||
partition->blob_count = 0;
|
||||
partition->total_blob_bytes = 0;
|
||||
partition->garbage_blob_count = 0;
|
||||
partition->garbage_blob_bytes = 0;
|
||||
partition->column_family_id = column_family_id;
|
||||
partition->compression = compression;
|
||||
partition->sync_required = false;
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlobFilePartitionManager::SealActiveBlobFile(
|
||||
const WriteOptions& write_options, Partition* partition,
|
||||
SealedFile* sealed_file) {
|
||||
assert(partition != nullptr);
|
||||
assert(partition->writer);
|
||||
assert(sealed_file != nullptr);
|
||||
|
||||
const uint64_t file_number = partition->file_number;
|
||||
Status s =
|
||||
FinalizeBlobFile(write_options, partition->writer.get(), file_number,
|
||||
partition->blob_count, partition->total_blob_bytes,
|
||||
&sealed_file->addition);
|
||||
if (!s.ok()) {
|
||||
RemoveFilePartitionMapping(file_number);
|
||||
ResetPartitionState(partition);
|
||||
return s;
|
||||
}
|
||||
|
||||
sealed_file->garbage_blob_count = partition->garbage_blob_count;
|
||||
sealed_file->garbage_blob_bytes = partition->garbage_blob_bytes;
|
||||
ResetPartitionState(partition);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlobFilePartitionManager::SealDeferredFile(
|
||||
const WriteOptions& write_options, DeferredFile* deferred,
|
||||
SealedFile* sealed_file) {
|
||||
assert(deferred != nullptr);
|
||||
assert(deferred->writer);
|
||||
assert(sealed_file != nullptr);
|
||||
|
||||
Status s = FinalizeBlobFile(
|
||||
write_options, deferred->writer.get(), deferred->file_number,
|
||||
deferred->blob_count, deferred->total_blob_bytes, &sealed_file->addition);
|
||||
if (!s.ok()) {
|
||||
RemoveFilePartitionMapping(deferred->file_number);
|
||||
return s;
|
||||
}
|
||||
|
||||
sealed_file->garbage_blob_count = deferred->garbage_blob_count;
|
||||
sealed_file->garbage_blob_bytes = deferred->garbage_blob_bytes;
|
||||
deferred->writer.reset();
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlobFilePartitionManager::FinalizeBlobFile(
|
||||
const WriteOptions& write_options, BlobLogWriter* writer,
|
||||
uint64_t file_number, uint64_t blob_count, uint64_t total_blob_bytes,
|
||||
BlobFileAddition* addition) {
|
||||
assert(writer != nullptr);
|
||||
assert(addition != nullptr);
|
||||
|
||||
BlobLogFooter footer;
|
||||
footer.blob_count = blob_count;
|
||||
|
||||
std::string checksum_method;
|
||||
std::string checksum_value;
|
||||
Status s = writer->AppendFooter(write_options, footer, &checksum_method,
|
||||
&checksum_value);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (blob_callback_ != nullptr) {
|
||||
const std::string blob_file_path = BlobFileName(db_path_, file_number);
|
||||
s = blob_callback_->OnBlobFileCompleted(
|
||||
blob_file_path, column_family_name_, /*job_id=*/0, file_number,
|
||||
BlobFileCreationReason::kFlush, s, checksum_value, checksum_method,
|
||||
blob_count, total_blob_bytes);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
*addition =
|
||||
BlobFileAddition(file_number, blob_count, total_blob_bytes,
|
||||
std::move(checksum_method), std::move(checksum_value));
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void BlobFilePartitionManager::AddSealedFileGarbage(
|
||||
const SealedFile& sealed_file, std::vector<BlobFileGarbage>* garbages) {
|
||||
assert(garbages != nullptr);
|
||||
|
||||
if (sealed_file.garbage_blob_count == 0) {
|
||||
assert(sealed_file.garbage_blob_bytes == 0);
|
||||
return;
|
||||
}
|
||||
|
||||
garbages->emplace_back(sealed_file.addition.GetBlobFileNumber(),
|
||||
sealed_file.garbage_blob_count,
|
||||
sealed_file.garbage_blob_bytes);
|
||||
}
|
||||
|
||||
bool BlobFilePartitionManager::MarkPartitionGarbage(Partition* partition,
|
||||
uint64_t file_number,
|
||||
uint64_t blob_count,
|
||||
uint64_t blob_bytes) {
|
||||
assert(partition != nullptr);
|
||||
|
||||
if (!partition->writer || partition->file_number != file_number) {
|
||||
return false;
|
||||
}
|
||||
|
||||
partition->garbage_blob_count += blob_count;
|
||||
partition->garbage_blob_bytes += blob_bytes;
|
||||
assert(partition->garbage_blob_count <= partition->blob_count);
|
||||
assert(partition->garbage_blob_bytes <= partition->total_blob_bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BlobFilePartitionManager::MarkDeferredFileGarbage(DeferredFile* deferred,
|
||||
uint64_t file_number,
|
||||
uint64_t blob_count,
|
||||
uint64_t blob_bytes) {
|
||||
assert(deferred != nullptr);
|
||||
|
||||
if (deferred->file_number != file_number) {
|
||||
return false;
|
||||
}
|
||||
|
||||
deferred->garbage_blob_count += blob_count;
|
||||
deferred->garbage_blob_bytes += blob_bytes;
|
||||
assert(deferred->garbage_blob_count <= deferred->blob_count);
|
||||
assert(deferred->garbage_blob_bytes <= deferred->total_blob_bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BlobFilePartitionManager::MarkSealedFileGarbage(
|
||||
std::vector<SealedFile>* sealed_files, uint64_t file_number,
|
||||
uint64_t blob_count, uint64_t blob_bytes) {
|
||||
assert(sealed_files != nullptr);
|
||||
|
||||
for (auto& sealed_file : *sealed_files) {
|
||||
if (sealed_file.addition.GetBlobFileNumber() != file_number) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sealed_file.garbage_blob_count += blob_count;
|
||||
sealed_file.garbage_blob_bytes += blob_bytes;
|
||||
assert(sealed_file.garbage_blob_count <=
|
||||
sealed_file.addition.GetTotalBlobCount());
|
||||
assert(sealed_file.garbage_blob_bytes <=
|
||||
sealed_file.addition.GetTotalBlobBytes());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Status BlobFilePartitionManager::MaybePrepopulateBlobCache(
|
||||
const BlobDirectWriteSettings& settings, const Slice& original_value,
|
||||
uint64_t blob_file_number, uint64_t blob_offset) {
|
||||
if (settings.blob_cache == nullptr ||
|
||||
settings.prepopulate_blob_cache != PrepopulateBlobCache::kFlushOnly) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
FullTypedCacheInterface<BlobContents, BlobContentsCreator> blob_cache{
|
||||
settings.blob_cache};
|
||||
const OffsetableCacheKey base_cache_key(db_id_, db_session_id_,
|
||||
blob_file_number);
|
||||
const CacheKey cache_key = base_cache_key.WithOffset(blob_offset);
|
||||
return blob_cache.InsertSaved(cache_key.AsSlice(), original_value, nullptr,
|
||||
Cache::Priority::BOTTOM,
|
||||
CacheTier::kVolatileTier);
|
||||
}
|
||||
|
||||
uint32_t BlobFilePartitionManager::SelectWideColumnPartition(
|
||||
uint32_t column_family_id, const Slice& key,
|
||||
const WideColumns& columns) const {
|
||||
return strategy_->SelectPartition(num_partitions_, column_family_id, key,
|
||||
columns) %
|
||||
num_partitions_;
|
||||
}
|
||||
|
||||
Status BlobFilePartitionManager::WriteBlob(
|
||||
const WriteOptions& write_options, uint32_t column_family_id,
|
||||
CompressionType compression, const Slice& key, const Slice& value,
|
||||
uint64_t* blob_file_number, uint64_t* blob_offset, uint64_t* blob_size,
|
||||
const BlobDirectWriteSettings& settings, const uint32_t* partition_idx) {
|
||||
assert(blob_file_number != nullptr);
|
||||
assert(blob_offset != nullptr);
|
||||
assert(blob_size != nullptr);
|
||||
|
||||
// Do compression before taking the partition mutex so large-value CPU work
|
||||
// does not serialize writers. A concurrent file rollover can still cause
|
||||
// this compressed buffer to be discarded below, which is acceptable on this
|
||||
// non-hot failure/configuration-change path.
|
||||
GrowableBuffer compressed_value;
|
||||
Slice write_value = value;
|
||||
if (compression != kNoCompression) {
|
||||
auto& compression_state =
|
||||
GetDirectWriteCompressionState(compression, settings.compression_opts);
|
||||
if (compression_state.compressor == nullptr) {
|
||||
return Status::NotSupported(
|
||||
"Blob direct write compression type not supported");
|
||||
}
|
||||
Status s = LegacyForceBuiltinCompression(*compression_state.compressor,
|
||||
&compression_state.working_area,
|
||||
value, &compressed_value);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
write_value = Slice(compressed_value);
|
||||
}
|
||||
|
||||
// Partition selection is based on the logical write inputs. In particular,
|
||||
// strategies that inspect value contents or size see the original
|
||||
// uncompressed value rather than `write_value`. The modulo here is
|
||||
// intentional so custom strategies can return arbitrary hashed or sentinel
|
||||
// values without violating the partition bounds.
|
||||
const uint32_t selected_partition_idx =
|
||||
partition_idx != nullptr
|
||||
? (*partition_idx % num_partitions_)
|
||||
: (strategy_->SelectPartition(num_partitions_, column_family_id, key,
|
||||
value) %
|
||||
num_partitions_);
|
||||
|
||||
{
|
||||
MutexLock lock(&mutex_);
|
||||
Partition* partition = partitions_[selected_partition_idx].get();
|
||||
|
||||
auto seal_current_file = [&]() -> Status {
|
||||
if (!partition->writer) {
|
||||
return Status::OK();
|
||||
}
|
||||
SealedFile sealed_file;
|
||||
Status s = SealActiveBlobFile(write_options, partition, &sealed_file);
|
||||
if (s.ok()) {
|
||||
current_generation_sealed_files_.push_back(std::move(sealed_file));
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
const uint64_t record_size =
|
||||
BlobLogRecord::kHeaderSize + key.size() + write_value.size();
|
||||
const uint64_t future_file_size =
|
||||
partition->file_size + record_size + BlobLogFooter::kSize;
|
||||
|
||||
if (partition->writer &&
|
||||
(partition->column_family_id != column_family_id ||
|
||||
partition->compression != compression ||
|
||||
(partition->blob_count > 0 && future_file_size > blob_file_size_))) {
|
||||
Status s = seal_current_file();
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
if (!partition->writer) {
|
||||
Status s = OpenNewBlobFile(partition, column_family_id, compression,
|
||||
selected_partition_idx);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t key_offset = 0;
|
||||
Status s = partition->writer->AddRecord(write_options, key, write_value,
|
||||
&key_offset, blob_offset);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
partition->sync_required = true;
|
||||
partition->blob_count += 1;
|
||||
partition->total_blob_bytes += record_size;
|
||||
partition->file_size = BlobLogHeader::kSize + partition->total_blob_bytes;
|
||||
|
||||
*blob_file_number = partition->file_number;
|
||||
*blob_size = write_value.size();
|
||||
}
|
||||
|
||||
Status prepopulate_s = MaybePrepopulateBlobCache(
|
||||
settings, value, *blob_file_number, *blob_offset);
|
||||
if (!prepopulate_s.ok() && info_log_ != nullptr) {
|
||||
ROCKS_LOG_WARN(info_log_,
|
||||
"Failed to pre-populate direct-write blob cache entry: %s",
|
||||
prepopulate_s.ToString().c_str());
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void BlobFilePartitionManager::RotateCurrentGeneration() {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
GenerationBatch batch;
|
||||
batch.sealed_files = std::move(current_generation_sealed_files_);
|
||||
current_generation_sealed_files_.clear();
|
||||
|
||||
for (auto& partition : partitions_) {
|
||||
if (!partition->writer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DeferredFile deferred;
|
||||
deferred.writer = std::move(partition->writer);
|
||||
deferred.file_number = partition->file_number;
|
||||
deferred.blob_count = partition->blob_count;
|
||||
deferred.total_blob_bytes = partition->total_blob_bytes;
|
||||
deferred.garbage_blob_count = partition->garbage_blob_count;
|
||||
deferred.garbage_blob_bytes = partition->garbage_blob_bytes;
|
||||
ResetPartitionState(partition.get());
|
||||
batch.deferred_files.emplace_back(std::move(deferred));
|
||||
}
|
||||
|
||||
pending_generations_.emplace_back(std::move(batch));
|
||||
}
|
||||
|
||||
Status BlobFilePartitionManager::PrepareFlushAdditions(
|
||||
const WriteOptions& write_options, size_t num_generations,
|
||||
std::vector<BlobFileAddition>* additions,
|
||||
std::vector<BlobFileGarbage>* garbages,
|
||||
std::vector<std::vector<uint64_t>>* generation_blob_file_numbers) {
|
||||
assert(additions != nullptr);
|
||||
assert(garbages != nullptr);
|
||||
additions->clear();
|
||||
garbages->clear();
|
||||
if (generation_blob_file_numbers != nullptr) {
|
||||
generation_blob_file_numbers->clear();
|
||||
}
|
||||
|
||||
MutexLock lock(&mutex_);
|
||||
if (num_generations > pending_generations_.size()) {
|
||||
return Status::Corruption(
|
||||
"Missing blob direct write generation metadata for flush");
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < num_generations; ++i) {
|
||||
GenerationBatch& batch = pending_generations_[i];
|
||||
while (!batch.deferred_files.empty()) {
|
||||
DeferredFile deferred = std::move(batch.deferred_files.front());
|
||||
batch.deferred_files.pop_front();
|
||||
|
||||
SealedFile sealed_file;
|
||||
Status s = SealDeferredFile(write_options, &deferred, &sealed_file);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
// Keep each successfully sealed file attached to the generation
|
||||
// immediately. If a later seal or the flush job itself fails, retry must
|
||||
// reuse these exact on-disk files instead of finalizing replacements.
|
||||
batch.sealed_files.push_back(std::move(sealed_file));
|
||||
}
|
||||
|
||||
std::vector<uint64_t>* generation_file_numbers = nullptr;
|
||||
if (generation_blob_file_numbers != nullptr) {
|
||||
generation_blob_file_numbers->emplace_back();
|
||||
generation_file_numbers = &generation_blob_file_numbers->back();
|
||||
generation_file_numbers->reserve(batch.sealed_files.size());
|
||||
}
|
||||
|
||||
for (const auto& sealed_file : batch.sealed_files) {
|
||||
additions->push_back(sealed_file.addition);
|
||||
AddSealedFileGarbage(sealed_file, garbages);
|
||||
if (generation_file_numbers != nullptr) {
|
||||
generation_file_numbers->push_back(
|
||||
sealed_file.addition.GetBlobFileNumber());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlobFilePartitionManager::MarkBlobWriteAsGarbage(uint64_t file_number,
|
||||
uint64_t blob_count,
|
||||
uint64_t blob_bytes) {
|
||||
if (blob_count == 0) {
|
||||
assert(blob_bytes == 0);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
MutexLock lock(&mutex_);
|
||||
for (auto& partition : partitions_) {
|
||||
if (MarkPartitionGarbage(partition.get(), file_number, blob_count,
|
||||
blob_bytes)) {
|
||||
return Status::OK();
|
||||
}
|
||||
}
|
||||
|
||||
if (MarkSealedFileGarbage(¤t_generation_sealed_files_, file_number,
|
||||
blob_count, blob_bytes)) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
for (auto& batch : pending_generations_) {
|
||||
for (auto& deferred : batch.deferred_files) {
|
||||
if (MarkDeferredFileGarbage(&deferred, file_number, blob_count,
|
||||
blob_bytes)) {
|
||||
return Status::OK();
|
||||
}
|
||||
}
|
||||
if (MarkSealedFileGarbage(&batch.sealed_files, file_number, blob_count,
|
||||
blob_bytes)) {
|
||||
return Status::OK();
|
||||
}
|
||||
}
|
||||
|
||||
const std::string message =
|
||||
"Could not match failed blob direct-write rollback for file #" +
|
||||
std::to_string(file_number);
|
||||
if (info_log_ != nullptr) {
|
||||
ROCKS_LOG_ERROR(info_log_, "%s", message.c_str());
|
||||
}
|
||||
return Status::Corruption(message);
|
||||
}
|
||||
|
||||
void BlobFilePartitionManager::CommitPreparedGenerations(
|
||||
size_t num_generations) {
|
||||
MutexLock lock(&mutex_);
|
||||
while (num_generations-- > 0 && !pending_generations_.empty()) {
|
||||
pending_generations_.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
Status BlobFilePartitionManager::SyncAllOpenFiles(
|
||||
const WriteOptions& write_options) {
|
||||
MutexLock lock(&mutex_);
|
||||
for (const auto& partition : partitions_) {
|
||||
if (!partition->writer || !partition->sync_required) {
|
||||
continue;
|
||||
}
|
||||
Status s = partition->writer->Sync(write_options);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
partition->sync_required = false;
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void BlobFilePartitionManager::GetActiveBlobFileNumbers(
|
||||
UnorderedSet<uint64_t>* file_numbers) const {
|
||||
assert(file_numbers != nullptr);
|
||||
ReadLock lock(&file_partition_mutex_);
|
||||
for (const auto& entry : file_to_partition_) {
|
||||
file_numbers->insert(entry.first);
|
||||
}
|
||||
}
|
||||
|
||||
void BlobFilePartitionManager::GetProtectedBlobFileNumbers(
|
||||
UnorderedSet<uint64_t>* file_numbers) const {
|
||||
assert(file_numbers != nullptr);
|
||||
ReadLock lock(&file_partition_mutex_);
|
||||
for (const auto& entry : protected_blob_file_refs_) {
|
||||
file_numbers->insert(entry.first);
|
||||
}
|
||||
}
|
||||
|
||||
bool BlobFilePartitionManager::IsTrackedBlobFileNumber(
|
||||
uint64_t file_number) const {
|
||||
ReadLock lock(&file_partition_mutex_);
|
||||
return file_to_partition_.find(file_number) != file_to_partition_.end() ||
|
||||
protected_blob_file_refs_.find(file_number) !=
|
||||
protected_blob_file_refs_.end();
|
||||
}
|
||||
|
||||
void BlobFilePartitionManager::ProtectSealedBlobFileNumbers(
|
||||
const std::vector<uint64_t>& file_numbers) {
|
||||
if (file_numbers.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
WriteLock lock(&file_partition_mutex_);
|
||||
for (uint64_t file_number : file_numbers) {
|
||||
++protected_blob_file_refs_[file_number];
|
||||
}
|
||||
}
|
||||
|
||||
void BlobFilePartitionManager::UnprotectSealedBlobFileNumbers(
|
||||
const std::vector<uint64_t>& file_numbers) {
|
||||
if (file_numbers.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
WriteLock lock(&file_partition_mutex_);
|
||||
for (uint64_t file_number : file_numbers) {
|
||||
auto it = protected_blob_file_refs_.find(file_number);
|
||||
if (it == protected_blob_file_refs_.end()) {
|
||||
if (info_log_ != nullptr) {
|
||||
ROCKS_LOG_ERROR(info_log_,
|
||||
"Memtable blob protection underflow for file #%" PRIu64,
|
||||
file_number);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
assert(it->second > 0);
|
||||
--it->second;
|
||||
if (it->second == 0) {
|
||||
protected_blob_file_refs_.erase(it);
|
||||
if (blob_file_cache_ != nullptr) {
|
||||
// Once the last memtable-backed reference goes away, any cached reader
|
||||
// for this file is either obsolete or can be reopened through the
|
||||
// manifest-visible path. Evict it here so delayed protection does not
|
||||
// leave an obsolete blob reader behind until DB close.
|
||||
blob_file_cache_->Evict(file_number);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BlobFilePartitionManager::RemoveFilePartitionMappings(
|
||||
const std::vector<uint64_t>& file_numbers) {
|
||||
if (file_numbers.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
WriteLock lock(&file_partition_mutex_);
|
||||
for (uint64_t file_number : file_numbers) {
|
||||
file_to_partition_.erase(file_number);
|
||||
if (blob_file_cache_ != nullptr) {
|
||||
// In-flight direct-write reads may have cached a footer-less reader for
|
||||
// this file before it was sealed. Drop it now so future manifest-visible
|
||||
// reads reopen against the finalized on-disk size and footer state.
|
||||
blob_file_cache_->Evict(file_number);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Status BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
|
||||
const ReadOptions& read_options, const Slice& user_key,
|
||||
const BlobIndex& blob_idx, const Version* version,
|
||||
BlobFileCache* blob_file_cache, FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value, uint64_t* bytes_read) {
|
||||
assert(blob_value != nullptr);
|
||||
|
||||
if (version != nullptr) {
|
||||
// Only fall back when the blob file is still owned exclusively by the
|
||||
// write path and therefore absent from Version metadata. Once Version
|
||||
// knows the file number, every result from Version::GetBlob(), including
|
||||
// corruption, must propagate directly.
|
||||
if (blob_idx.HasTTL() || blob_idx.IsInlined() ||
|
||||
version->storage_info()->GetBlobFileMetaData(blob_idx.file_number()) !=
|
||||
nullptr) {
|
||||
return version->GetBlob(read_options, user_key, blob_idx, prefetch_buffer,
|
||||
blob_value, bytes_read);
|
||||
}
|
||||
}
|
||||
|
||||
if (blob_file_cache == nullptr) {
|
||||
return version != nullptr ? Status::Corruption("Invalid blob file number")
|
||||
: Status::NotFound();
|
||||
}
|
||||
|
||||
if (read_options.read_tier == kBlockCacheTier) {
|
||||
// The direct-write fallback below may need to open the blob file reader,
|
||||
// which `kBlockCacheTier` forbids. Keep the normal Version-backed path
|
||||
// above eligible for cache-only hits.
|
||||
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
}
|
||||
|
||||
Status s;
|
||||
CacheHandleGuard<BlobFileReader> reader;
|
||||
s = blob_file_cache->GetBlobFileReader(read_options, blob_idx.file_number(),
|
||||
&reader,
|
||||
/*allow_footer_skip_retry=*/true);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
std::unique_ptr<BlobContents> blob_contents;
|
||||
s = reader.GetValue()->GetBlob(read_options, user_key, blob_idx.offset(),
|
||||
blob_idx.size(), blob_idx.compression(),
|
||||
prefetch_buffer, nullptr, &blob_contents,
|
||||
bytes_read);
|
||||
if (s.ok()) {
|
||||
blob_value->PinSelf(blob_contents->data());
|
||||
return s;
|
||||
}
|
||||
|
||||
if (!s.IsCorruption()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
reader.Reset();
|
||||
blob_file_cache->Evict(blob_idx.file_number());
|
||||
|
||||
std::unique_ptr<BlobFileReader> fresh_reader;
|
||||
s = blob_file_cache->OpenBlobFileReaderUncached(
|
||||
read_options, blob_idx.file_number(), &fresh_reader,
|
||||
/*allow_footer_skip_retry=*/true);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
std::unique_ptr<BlobContents> fresh_contents;
|
||||
s = fresh_reader->GetBlob(read_options, user_key, blob_idx.offset(),
|
||||
blob_idx.size(), blob_idx.compression(),
|
||||
prefetch_buffer, nullptr, &fresh_contents,
|
||||
bytes_read);
|
||||
if (s.ok()) {
|
||||
blob_value->PinSelf(fresh_contents->data());
|
||||
CacheHandleGuard<BlobFileReader> ignored;
|
||||
blob_file_cache
|
||||
->RefreshBlobFileReader(blob_idx.file_number(), &fresh_reader, &ignored)
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,294 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "db/blob/blob_file_addition.h"
|
||||
#include "db/blob/blob_file_garbage.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "db/blob/blob_write_batch_transformer.h"
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/compression_type.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/status.h"
|
||||
#include "util/hash_containers.h"
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class BlobFileCache;
|
||||
class BlobFileCompletionCallback;
|
||||
class BlobIndex;
|
||||
class BlobLogWriter;
|
||||
class FilePrefetchBuffer;
|
||||
class IOTracer;
|
||||
class Logger;
|
||||
class PinnableSlice;
|
||||
class Version;
|
||||
class WritableFileWriter;
|
||||
struct FileOptions;
|
||||
struct ReadOptions;
|
||||
|
||||
// Manages per-partition blob files for write-path blob direct write.
|
||||
//
|
||||
// The v1 design keeps each memtable switch as one FIFO generation batch.
|
||||
// Direct-write files for that memtable are sealed and registered when the
|
||||
// matching flush commits. The current implementation still serializes blob
|
||||
// appends through one manager mutex; follow-up PRs can add independent
|
||||
// per-partition locks to allow concurrent blob writes without changing the
|
||||
// generation/flush contract. Follow-up PRs can also broaden compatibility as
|
||||
// the feature matures.
|
||||
class BlobFilePartitionManager {
|
||||
public:
|
||||
using FileNumberAllocator = std::function<uint64_t()>;
|
||||
|
||||
// Creates the per-column-family manager for write-path blob files.
|
||||
BlobFilePartitionManager(
|
||||
uint32_t num_partitions,
|
||||
std::shared_ptr<BlobFilePartitionStrategy> strategy,
|
||||
FileNumberAllocator file_number_allocator, FileSystem* fs,
|
||||
SystemClock* clock, Statistics* statistics,
|
||||
const FileOptions& file_options, std::string db_path,
|
||||
std::string column_family_name, uint64_t blob_file_size, bool use_fsync,
|
||||
BlobFileCache* blob_file_cache, BlobFileCompletionCallback* blob_callback,
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners,
|
||||
FileChecksumGenFactory* file_checksum_gen_factory,
|
||||
const FileTypeSet& checksum_handoff_file_types,
|
||||
const std::shared_ptr<IOTracer>& io_tracer, std::string db_id,
|
||||
std::string db_session_id, Logger* info_log);
|
||||
|
||||
// Evicts cached readers for manager-owned files during shutdown.
|
||||
~BlobFilePartitionManager();
|
||||
|
||||
// Appends one blob record to a partition file and returns the resulting
|
||||
// BlobIndex location metadata.
|
||||
Status WriteBlob(const WriteOptions& write_options, uint32_t column_family_id,
|
||||
CompressionType compression, const Slice& key,
|
||||
const Slice& value, uint64_t* blob_file_number,
|
||||
uint64_t* blob_offset, uint64_t* blob_size,
|
||||
const BlobDirectWriteSettings& settings,
|
||||
const uint32_t* partition_idx = nullptr);
|
||||
|
||||
// Selects the partition to use for all blob-backed columns of one PutEntity
|
||||
// operation. The return value is already normalized to [0, num_partitions_).
|
||||
uint32_t SelectWideColumnPartition(uint32_t column_family_id,
|
||||
const Slice& key,
|
||||
const WideColumns& columns) const;
|
||||
|
||||
// Move the current active partition files into the next immutable
|
||||
// memtable-generation batch. Called from SwitchMemtable() while DB mutex is
|
||||
// held. No I/O is performed here.
|
||||
void RotateCurrentGeneration();
|
||||
|
||||
// Seal the first `num_generations` queued immutable generations and return
|
||||
// all blob additions and initial-garbage updates that must be registered
|
||||
// with the matching flush. Generations stay queued until
|
||||
// CommitPreparedGenerations() is called. If `generation_blob_file_numbers`
|
||||
// is non-null, it receives the sealed blob file numbers for each prepared
|
||||
// generation in the same FIFO order.
|
||||
Status PrepareFlushAdditions(const WriteOptions& write_options,
|
||||
size_t num_generations,
|
||||
std::vector<BlobFileAddition>* additions,
|
||||
std::vector<BlobFileGarbage>* garbages,
|
||||
std::vector<std::vector<uint64_t>>*
|
||||
generation_blob_file_numbers = nullptr);
|
||||
|
||||
// Remove the first `num_generations` prepared immutable generations after
|
||||
// their blob-file additions and garbage edits were committed to MANIFEST.
|
||||
void CommitPreparedGenerations(size_t num_generations);
|
||||
|
||||
// In v1 flush-on-write mode each AddRecord flushes to the OS immediately, so
|
||||
// there is nothing extra to do for the non-sync write path.
|
||||
Status FlushAllOpenFiles(const WriteOptions& /*write_options*/) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
// Sync the active open blob files before a sync WAL write.
|
||||
Status SyncAllOpenFiles(const WriteOptions& write_options);
|
||||
|
||||
// Returns blob files still owned by the manager and therefore not yet safe
|
||||
// to consider obsolete.
|
||||
void GetActiveBlobFileNumbers(UnorderedSet<uint64_t>* file_numbers) const;
|
||||
|
||||
// Returns sealed blob files that are still reachable through live memtables
|
||||
// or old SuperVersions and therefore must not be purged yet.
|
||||
void GetProtectedBlobFileNumbers(UnorderedSet<uint64_t>* file_numbers) const;
|
||||
|
||||
// Returns true when the blob file is still owned by the write path or
|
||||
// protected by a live memtable / old SuperVersion.
|
||||
bool IsTrackedBlobFileNumber(uint64_t file_number) const;
|
||||
|
||||
// Increments / decrements memtable-held protection on sealed blob files.
|
||||
void ProtectSealedBlobFileNumbers(const std::vector<uint64_t>& file_numbers);
|
||||
void UnprotectSealedBlobFileNumbers(
|
||||
const std::vector<uint64_t>& file_numbers);
|
||||
|
||||
// Stops protecting file numbers that are now registered in MANIFEST.
|
||||
void RemoveFilePartitionMappings(const std::vector<uint64_t>& file_numbers);
|
||||
|
||||
// Marks blob records from a failed transformed write as initial garbage for
|
||||
// the target blob file while keeping the physical bytes in place.
|
||||
// Returns Corruption if the manager can no longer match the blob file.
|
||||
Status MarkBlobWriteAsGarbage(uint64_t file_number, uint64_t blob_count,
|
||||
uint64_t blob_bytes);
|
||||
|
||||
// Resolves a direct-write BlobIndex by consulting manifest-visible state
|
||||
// first, then falling back to direct blob-file reads only when the target
|
||||
// blob file is still write-path-owned and therefore not yet tracked by
|
||||
// Version. Existing manifest-visible read results, including I/O failures,
|
||||
// are returned directly rather than masked by fallback logic.
|
||||
static Status ResolveBlobDirectWriteIndex(
|
||||
const ReadOptions& read_options, const Slice& user_key,
|
||||
const BlobIndex& blob_idx, const Version* version,
|
||||
BlobFileCache* blob_file_cache, FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value, uint64_t* bytes_read);
|
||||
|
||||
private:
|
||||
struct Partition {
|
||||
// Active writer for this partition, or null when the partition is idle.
|
||||
std::unique_ptr<BlobLogWriter> writer;
|
||||
// Metadata tracked while the active file remains open.
|
||||
uint64_t file_number = 0;
|
||||
uint64_t file_size = 0;
|
||||
uint64_t blob_count = 0;
|
||||
uint64_t total_blob_bytes = 0;
|
||||
// Failed transformed writes already appended to this physical file.
|
||||
uint64_t garbage_blob_count = 0;
|
||||
uint64_t garbage_blob_bytes = 0;
|
||||
uint32_t column_family_id = 0;
|
||||
CompressionType compression = kNoCompression;
|
||||
bool sync_required = false;
|
||||
};
|
||||
|
||||
struct DeferredFile {
|
||||
// Blob writer moved out of an active partition at memtable rotation.
|
||||
std::unique_ptr<BlobLogWriter> writer;
|
||||
// Metadata preserved until flush preparation seals the file.
|
||||
uint64_t file_number = 0;
|
||||
uint64_t blob_count = 0;
|
||||
uint64_t total_blob_bytes = 0;
|
||||
// Failed transformed writes already appended before the file is sealed.
|
||||
uint64_t garbage_blob_count = 0;
|
||||
uint64_t garbage_blob_bytes = 0;
|
||||
};
|
||||
|
||||
struct SealedFile {
|
||||
// Blob-file metadata ready to be added to MANIFEST.
|
||||
BlobFileAddition addition;
|
||||
// Initial unreachable bytes that should be registered as garbage
|
||||
// alongside the file addition.
|
||||
uint64_t garbage_blob_count = 0;
|
||||
uint64_t garbage_blob_bytes = 0;
|
||||
};
|
||||
|
||||
struct GenerationBatch {
|
||||
// Files that were still open when the memtable became immutable.
|
||||
std::deque<DeferredFile> deferred_files;
|
||||
// Files already sealed for this memtable generation.
|
||||
std::vector<SealedFile> sealed_files;
|
||||
};
|
||||
|
||||
// Clears all active-file bookkeeping from a partition after sealing.
|
||||
void ResetPartitionState(Partition* partition);
|
||||
// Marks a blob file as manager-owned until it is committed or abandoned.
|
||||
void AddFilePartitionMapping(uint64_t file_number, uint32_t partition_idx);
|
||||
// Stops tracking a blob file after open/seal failure.
|
||||
void RemoveFilePartitionMapping(uint64_t file_number);
|
||||
|
||||
// Opens a new active blob file for one partition.
|
||||
Status OpenNewBlobFile(Partition* partition, uint32_t column_family_id,
|
||||
CompressionType compression, uint32_t partition_idx);
|
||||
// Finalizes one active partition file and returns the MANIFEST addition.
|
||||
Status SealActiveBlobFile(const WriteOptions& write_options,
|
||||
Partition* partition, SealedFile* sealed_file);
|
||||
// Finalizes one deferred file that was carried over to flush preparation.
|
||||
Status SealDeferredFile(const WriteOptions& write_options,
|
||||
DeferredFile* deferred, SealedFile* sealed_file);
|
||||
// Appends the footer, reports file completion, and builds the resulting
|
||||
// BlobFileAddition for one sealed blob file.
|
||||
Status FinalizeBlobFile(const WriteOptions& write_options,
|
||||
BlobLogWriter* writer, uint64_t file_number,
|
||||
uint64_t blob_count, uint64_t total_blob_bytes,
|
||||
BlobFileAddition* addition);
|
||||
// Appends one initial-garbage record to the flush output if any failed
|
||||
// transformed writes targeted the sealed blob file.
|
||||
static void AddSealedFileGarbage(const SealedFile& sealed_file,
|
||||
std::vector<BlobFileGarbage>* garbages);
|
||||
// Returns true if the active open file matches `file_number` and consumes
|
||||
// the failed-write garbage accounting.
|
||||
static bool MarkPartitionGarbage(Partition* partition, uint64_t file_number,
|
||||
uint64_t blob_count, uint64_t blob_bytes);
|
||||
// Returns true if the deferred flush-preparation file matches `file_number`
|
||||
// and consumes the failed-write garbage accounting.
|
||||
static bool MarkDeferredFileGarbage(DeferredFile* deferred,
|
||||
uint64_t file_number, uint64_t blob_count,
|
||||
uint64_t blob_bytes);
|
||||
// Returns true if one already-sealed file matches `file_number` and
|
||||
// consumes the failed-write garbage accounting.
|
||||
static bool MarkSealedFileGarbage(std::vector<SealedFile>* sealed_files,
|
||||
uint64_t file_number, uint64_t blob_count,
|
||||
uint64_t blob_bytes);
|
||||
// Seeds the blob cache with the original uncompressed value when configured.
|
||||
Status MaybePrepopulateBlobCache(const BlobDirectWriteSettings& settings,
|
||||
const Slice& original_value,
|
||||
uint64_t blob_file_number,
|
||||
uint64_t blob_offset);
|
||||
|
||||
// Configured partition fanout and write-selection strategy.
|
||||
const uint32_t num_partitions_;
|
||||
std::shared_ptr<BlobFilePartitionStrategy> strategy_;
|
||||
// Shared services needed to create, track, and cache blob files.
|
||||
FileNumberAllocator file_number_allocator_;
|
||||
FileSystem* fs_;
|
||||
SystemClock* clock_;
|
||||
Statistics* statistics_;
|
||||
FileOptions file_options_;
|
||||
// Column-family context used for file naming and callbacks.
|
||||
std::string db_path_;
|
||||
std::string column_family_name_;
|
||||
// File creation policy and shared blob-file infrastructure.
|
||||
uint64_t blob_file_size_;
|
||||
bool use_fsync_;
|
||||
BlobFileCache* blob_file_cache_;
|
||||
BlobFileCompletionCallback* blob_callback_;
|
||||
std::vector<std::shared_ptr<EventListener>> listeners_;
|
||||
FileChecksumGenFactory* file_checksum_gen_factory_;
|
||||
FileTypeSet checksum_handoff_file_types_;
|
||||
std::shared_ptr<IOTracer> io_tracer_;
|
||||
// DB identity used when constructing cache keys.
|
||||
std::string db_id_;
|
||||
std::string db_session_id_;
|
||||
Logger* info_log_;
|
||||
|
||||
// One active writer slot per configured partition.
|
||||
std::vector<std::unique_ptr<Partition>> partitions_;
|
||||
|
||||
// Protects partition state and generation queues.
|
||||
mutable port::Mutex mutex_;
|
||||
// Files already sealed while the current mutable memtable was active.
|
||||
std::vector<SealedFile> current_generation_sealed_files_;
|
||||
// FIFO immutable memtable generations waiting to be flushed.
|
||||
std::deque<GenerationBatch> pending_generations_;
|
||||
|
||||
// Tracks blob files still owned by active or deferred write-path state until
|
||||
// MANIFEST commit publishes them.
|
||||
std::unordered_map<uint64_t, uint32_t> file_to_partition_;
|
||||
// Sealed direct-write files that remain reachable from live memtables, such
|
||||
// as old SuperVersions serving lazy iterator reads after flush commit.
|
||||
std::unordered_map<uint64_t, uint32_t> protected_blob_file_refs_;
|
||||
// Protects file_to_partition_ and protected_blob_file_refs_.
|
||||
mutable port::RWMutex file_partition_mutex_;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+35
-13
@@ -29,7 +29,7 @@ Status BlobFileReader::Create(
|
||||
const ImmutableOptions& immutable_options, const ReadOptions& read_options,
|
||||
const FileOptions& file_options, uint32_t column_family_id,
|
||||
HistogramImpl* blob_file_read_hist, uint64_t blob_file_number,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer, bool skip_footer_validation,
|
||||
std::unique_ptr<BlobFileReader>* blob_file_reader) {
|
||||
assert(blob_file_reader);
|
||||
assert(!*blob_file_reader);
|
||||
@@ -40,7 +40,8 @@ Status BlobFileReader::Create(
|
||||
{
|
||||
const Status s =
|
||||
OpenFile(immutable_options, file_options, blob_file_read_hist,
|
||||
blob_file_number, io_tracer, &file_size, &file_reader);
|
||||
blob_file_number, io_tracer, &file_size, &file_reader,
|
||||
/*skip_footer_size_check=*/skip_footer_validation);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -61,7 +62,7 @@ Status BlobFileReader::Create(
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if (!skip_footer_validation) {
|
||||
const Status s =
|
||||
ReadFooter(file_reader.get(), read_options, file_size, statistics);
|
||||
if (!s.ok()) {
|
||||
@@ -76,9 +77,10 @@ Status BlobFileReader::Create(
|
||||
compression_type);
|
||||
}
|
||||
|
||||
blob_file_reader->reset(new BlobFileReader(
|
||||
std::move(file_reader), file_size, compression_type,
|
||||
std::move(decompressor), immutable_options.clock, statistics));
|
||||
blob_file_reader->reset(
|
||||
new BlobFileReader(std::move(file_reader), file_size, compression_type,
|
||||
std::move(decompressor), immutable_options.clock,
|
||||
statistics, !skip_footer_validation));
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
@@ -87,7 +89,8 @@ Status BlobFileReader::OpenFile(
|
||||
const ImmutableOptions& immutable_options, const FileOptions& file_opts,
|
||||
HistogramImpl* blob_file_read_hist, uint64_t blob_file_number,
|
||||
const std::shared_ptr<IOTracer>& io_tracer, uint64_t* file_size,
|
||||
std::unique_ptr<RandomAccessFileReader>* file_reader) {
|
||||
std::unique_ptr<RandomAccessFileReader>* file_reader,
|
||||
bool skip_footer_size_check) {
|
||||
assert(file_size);
|
||||
assert(file_reader);
|
||||
|
||||
@@ -112,17 +115,26 @@ Status BlobFileReader::OpenFile(
|
||||
}
|
||||
}
|
||||
|
||||
if (*file_size < BlobLogHeader::kSize + BlobLogFooter::kSize) {
|
||||
if (!skip_footer_size_check &&
|
||||
*file_size < BlobLogHeader::kSize + BlobLogFooter::kSize) {
|
||||
return Status::Corruption("Malformed blob file");
|
||||
}
|
||||
if (skip_footer_size_check && *file_size < BlobLogHeader::kSize) {
|
||||
return Status::Corruption("Malformed blob file");
|
||||
}
|
||||
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
FileOptions reader_file_opts = file_opts;
|
||||
|
||||
if (skip_footer_size_check && reader_file_opts.use_direct_reads) {
|
||||
reader_file_opts.use_direct_reads = false;
|
||||
}
|
||||
|
||||
{
|
||||
TEST_SYNC_POINT("BlobFileReader::OpenFile:NewRandomAccessFile");
|
||||
|
||||
const Status s =
|
||||
fs->NewRandomAccessFile(blob_file_path, file_opts, &file, dbg);
|
||||
fs->NewRandomAccessFile(blob_file_path, reader_file_opts, &file, dbg);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -291,13 +303,14 @@ BlobFileReader::BlobFileReader(
|
||||
std::unique_ptr<RandomAccessFileReader>&& file_reader, uint64_t file_size,
|
||||
CompressionType compression_type,
|
||||
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
|
||||
Statistics* statistics)
|
||||
Statistics* statistics, bool has_footer)
|
||||
: file_reader_(std::move(file_reader)),
|
||||
file_size_(file_size),
|
||||
compression_type_(compression_type),
|
||||
decompressor_(std::move(decompressor)),
|
||||
clock_(clock),
|
||||
statistics_(statistics) {
|
||||
statistics_(statistics),
|
||||
has_footer_(has_footer) {
|
||||
assert(file_reader_);
|
||||
}
|
||||
|
||||
@@ -312,7 +325,8 @@ Status BlobFileReader::GetBlob(
|
||||
|
||||
const uint64_t key_size = user_key.size();
|
||||
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_)) {
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
|
||||
has_footer_)) {
|
||||
return Status::Corruption("Invalid blob offset");
|
||||
}
|
||||
|
||||
@@ -428,7 +442,8 @@ void BlobFileReader::MultiGetBlob(
|
||||
const uint64_t offset = req->offset;
|
||||
const uint64_t value_size = req->len;
|
||||
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_)) {
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
|
||||
has_footer_)) {
|
||||
*req->status = Status::Corruption("Invalid blob offset");
|
||||
continue;
|
||||
}
|
||||
@@ -454,6 +469,13 @@ void BlobFileReader::MultiGetBlob(
|
||||
|
||||
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, total_len);
|
||||
|
||||
if (read_reqs.empty()) {
|
||||
if (bytes_read) {
|
||||
*bytes_read = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Buffer buf;
|
||||
AlignedBuf aligned_buf;
|
||||
|
||||
|
||||
@@ -36,7 +36,20 @@ class BlobFileReader {
|
||||
HistogramImpl* blob_file_read_hist,
|
||||
uint64_t blob_file_number,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
std::unique_ptr<BlobFileReader>* reader);
|
||||
std::unique_ptr<BlobFileReader>* reader) {
|
||||
return Create(immutable_options, read_options, file_options,
|
||||
column_family_id, blob_file_read_hist, blob_file_number,
|
||||
io_tracer, /*skip_footer_validation=*/false, reader);
|
||||
}
|
||||
|
||||
// Allows opening in-flight direct-write blob files by optionally skipping
|
||||
// footer validation.
|
||||
static Status Create(
|
||||
const ImmutableOptions& immutable_options,
|
||||
const ReadOptions& read_options, const FileOptions& file_options,
|
||||
uint32_t column_family_id, HistogramImpl* blob_file_read_hist,
|
||||
uint64_t blob_file_number, const std::shared_ptr<IOTracer>& io_tracer,
|
||||
bool skip_footer_validation, std::unique_ptr<BlobFileReader>* reader);
|
||||
|
||||
BlobFileReader(const BlobFileReader&) = delete;
|
||||
BlobFileReader& operator=(const BlobFileReader&) = delete;
|
||||
@@ -63,18 +76,22 @@ class BlobFileReader {
|
||||
uint64_t GetFileSize() const { return file_size_; }
|
||||
|
||||
private:
|
||||
// `has_footer` tracks whether offset validation should reserve footer space.
|
||||
BlobFileReader(std::unique_ptr<RandomAccessFileReader>&& file_reader,
|
||||
uint64_t file_size, CompressionType compression_type,
|
||||
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
|
||||
Statistics* statistics);
|
||||
Statistics* statistics, bool has_footer);
|
||||
|
||||
// `skip_footer_size_check` is used for direct-write files that are still
|
||||
// missing their footer at open time.
|
||||
static Status OpenFile(const ImmutableOptions& immutable_options,
|
||||
const FileOptions& file_opts,
|
||||
HistogramImpl* blob_file_read_hist,
|
||||
uint64_t blob_file_number,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
uint64_t* file_size,
|
||||
std::unique_ptr<RandomAccessFileReader>* file_reader);
|
||||
std::unique_ptr<RandomAccessFileReader>* file_reader,
|
||||
bool skip_footer_size_check);
|
||||
|
||||
static Status ReadHeader(const RandomAccessFileReader* file_reader,
|
||||
const ReadOptions& read_options,
|
||||
@@ -110,6 +127,8 @@ class BlobFileReader {
|
||||
std::shared_ptr<Decompressor> decompressor_;
|
||||
SystemClock* clock_;
|
||||
Statistics* statistics_;
|
||||
// False when the reader was opened before the blob file footer was written.
|
||||
bool has_footer_;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
@@ -8,70 +8,48 @@
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "db/dbformat.h"
|
||||
#include "db/wide/wide_column_serialization.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
Status BlobGarbageMeter::ProcessInFlow(const Slice& key, const Slice& value) {
|
||||
uint64_t blob_file_number = kInvalidBlobFileNumber;
|
||||
uint64_t bytes = 0;
|
||||
|
||||
const Status s = Parse(key, value, &blob_file_number, &bytes);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (blob_file_number == kInvalidBlobFileNumber) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
flows_[blob_file_number].AddInFlow(bytes);
|
||||
|
||||
return Status::OK();
|
||||
return ProcessFlow(key, value, /*is_inflow=*/true);
|
||||
}
|
||||
|
||||
Status BlobGarbageMeter::ProcessOutFlow(const Slice& key, const Slice& value) {
|
||||
uint64_t blob_file_number = kInvalidBlobFileNumber;
|
||||
uint64_t bytes = 0;
|
||||
|
||||
const Status s = Parse(key, value, &blob_file_number, &bytes);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (blob_file_number == kInvalidBlobFileNumber) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
// Note: in order to measure the amount of additional garbage, we only need to
|
||||
// track the outflow for preexisting files, i.e. those that also had inflow.
|
||||
// (Newly written files would only have outflow.)
|
||||
auto it = flows_.find(blob_file_number);
|
||||
if (it == flows_.end()) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
it->second.AddOutFlow(bytes);
|
||||
|
||||
return Status::OK();
|
||||
return ProcessFlow(key, value, /*is_inflow=*/false);
|
||||
}
|
||||
|
||||
Status BlobGarbageMeter::Parse(const Slice& key, const Slice& value,
|
||||
uint64_t* blob_file_number, uint64_t* bytes) {
|
||||
Status BlobGarbageMeter::GetBlobReferenceDetails(const ParsedInternalKey& ikey,
|
||||
const BlobIndex& blob_index,
|
||||
uint64_t* blob_file_number,
|
||||
uint64_t* bytes) {
|
||||
assert(blob_file_number);
|
||||
assert(*blob_file_number == kInvalidBlobFileNumber);
|
||||
assert(bytes);
|
||||
assert(*bytes == 0);
|
||||
|
||||
ParsedInternalKey ikey;
|
||||
|
||||
{
|
||||
constexpr bool log_err_key = false;
|
||||
const Status s = ParseInternalKey(key, &ikey, log_err_key);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (blob_index.IsInlined() || blob_index.HasTTL()) {
|
||||
return Status::Corruption("Unexpected TTL/inlined blob index");
|
||||
}
|
||||
|
||||
*blob_file_number = blob_index.file_number();
|
||||
*bytes =
|
||||
blob_index.size() +
|
||||
BlobLogRecord::CalculateAdjustmentForRecordHeader(ikey.user_key.size());
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlobGarbageMeter::ParseBlobIndexReference(const ParsedInternalKey& ikey,
|
||||
const Slice& value,
|
||||
uint64_t* blob_file_number,
|
||||
uint64_t* bytes) {
|
||||
assert(blob_file_number);
|
||||
assert(*blob_file_number == kInvalidBlobFileNumber);
|
||||
assert(bytes);
|
||||
assert(*bytes == 0);
|
||||
|
||||
if (ikey.type != kTypeBlobIndex) {
|
||||
return Status::OK();
|
||||
}
|
||||
@@ -85,16 +63,71 @@ Status BlobGarbageMeter::Parse(const Slice& key, const Slice& value,
|
||||
}
|
||||
}
|
||||
|
||||
if (blob_index.IsInlined() || blob_index.HasTTL()) {
|
||||
return Status::Corruption("Unexpected TTL/inlined blob index");
|
||||
return GetBlobReferenceDetails(ikey, blob_index, blob_file_number, bytes);
|
||||
}
|
||||
|
||||
void BlobGarbageMeter::AddFlow(uint64_t blob_file_number, uint64_t bytes,
|
||||
bool is_inflow) {
|
||||
if (is_inflow) {
|
||||
flows_[blob_file_number].AddInFlow(bytes);
|
||||
return;
|
||||
}
|
||||
|
||||
*blob_file_number = blob_index.file_number();
|
||||
*bytes =
|
||||
blob_index.size() +
|
||||
BlobLogRecord::CalculateAdjustmentForRecordHeader(ikey.user_key.size());
|
||||
// Note: in order to measure the amount of additional garbage, we only need to
|
||||
// track the outflow for preexisting files, i.e. those that also had inflow.
|
||||
// (Newly written files would only have outflow.)
|
||||
auto it = flows_.find(blob_file_number);
|
||||
if (it != flows_.end()) {
|
||||
it->second.AddOutFlow(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
Status BlobGarbageMeter::ProcessFlow(const Slice& key, const Slice& value,
|
||||
bool is_inflow) {
|
||||
ParsedInternalKey ikey;
|
||||
|
||||
{
|
||||
constexpr bool log_err_key = false;
|
||||
const Status s = ParseInternalKey(key, &ikey, log_err_key);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t blob_file_number = kInvalidBlobFileNumber;
|
||||
uint64_t bytes = 0;
|
||||
if (Status s =
|
||||
ParseBlobIndexReference(ikey, value, &blob_file_number, &bytes);
|
||||
!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (blob_file_number != kInvalidBlobFileNumber) {
|
||||
AddFlow(blob_file_number, bytes, is_inflow);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
return ProcessEntityBlobReferences(ikey, value, is_inflow);
|
||||
}
|
||||
|
||||
Status BlobGarbageMeter::ProcessEntityBlobReferences(
|
||||
const ParsedInternalKey& ikey, const Slice& value, bool is_inflow) {
|
||||
if (ikey.type != kTypeWideColumnEntity) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
return WideColumnSerialization::ForEachBlobFileNumber(
|
||||
value, [&](const BlobIndex& blob_index) -> Status {
|
||||
uint64_t file_number = kInvalidBlobFileNumber;
|
||||
uint64_t blob_bytes = 0;
|
||||
if (const Status s = GetBlobReferenceDetails(ikey, blob_index,
|
||||
&file_number, &blob_bytes);
|
||||
!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
AddFlow(file_number, blob_bytes, is_inflow);
|
||||
return Status::OK();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class BlobIndex;
|
||||
struct ParsedInternalKey;
|
||||
class Slice;
|
||||
|
||||
// A class that can be used to compute the amount of additional garbage
|
||||
@@ -93,8 +95,19 @@ class BlobGarbageMeter {
|
||||
}
|
||||
|
||||
private:
|
||||
static Status Parse(const Slice& key, const Slice& value,
|
||||
uint64_t* blob_file_number, uint64_t* bytes);
|
||||
static Status GetBlobReferenceDetails(const ParsedInternalKey& ikey,
|
||||
const BlobIndex& blob_index,
|
||||
uint64_t* blob_file_number,
|
||||
uint64_t* bytes);
|
||||
static Status ParseBlobIndexReference(const ParsedInternalKey& ikey,
|
||||
const Slice& value,
|
||||
uint64_t* blob_file_number,
|
||||
uint64_t* bytes);
|
||||
|
||||
void AddFlow(uint64_t blob_file_number, uint64_t bytes, bool is_inflow);
|
||||
Status ProcessFlow(const Slice& key, const Slice& value, bool is_inflow);
|
||||
Status ProcessEntityBlobReferences(const ParsedInternalKey& ikey,
|
||||
const Slice& value, bool is_inflow);
|
||||
|
||||
std::unordered_map<uint64_t, BlobInOutFlow> flows_;
|
||||
};
|
||||
|
||||
@@ -11,10 +11,23 @@
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "db/dbformat.h"
|
||||
#include "db/wide/wide_column_serialization.h"
|
||||
#include "test_util/testharness.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace {
|
||||
|
||||
BlobIndex MakeBlobIndex(uint64_t file_number, uint64_t offset, uint64_t size) {
|
||||
std::string encoded;
|
||||
BlobIndex::EncodeBlob(&encoded, file_number, offset, size, kNoCompression);
|
||||
BlobIndex blob_index;
|
||||
EXPECT_OK(blob_index.DecodeFrom(encoded));
|
||||
return blob_index;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(BlobGarbageMeterTest, MeasureGarbage) {
|
||||
BlobGarbageMeter blob_garbage_meter;
|
||||
|
||||
@@ -188,6 +201,67 @@ TEST(BlobGarbageMeterTest, InlinedTTLBlobIndex) {
|
||||
ASSERT_NOK(blob_garbage_meter.ProcessOutFlow(key_slice, value_slice));
|
||||
}
|
||||
|
||||
TEST(BlobGarbageMeterTest, WideColumnEntity) {
|
||||
constexpr char user_key[] = "entity_key";
|
||||
constexpr SequenceNumber seq = 123;
|
||||
const InternalKey key(user_key, seq, kTypeWideColumnEntity);
|
||||
const Slice key_slice = key.Encode();
|
||||
|
||||
const std::vector<std::pair<std::string, std::string>> columns = {
|
||||
{"", "default_inline"},
|
||||
{"blob_attr", "blob_inline"},
|
||||
{"ttl", "00000001"},
|
||||
};
|
||||
|
||||
const BlobIndex default_blob = MakeBlobIndex(4, 123, 555);
|
||||
const BlobIndex attr_blob = MakeBlobIndex(5, 456, 777);
|
||||
|
||||
std::string inflow_value;
|
||||
ASSERT_OK(WideColumnSerialization::SerializeV2(
|
||||
columns, {{0, default_blob}, {1, attr_blob}}, inflow_value));
|
||||
|
||||
std::string outflow_value;
|
||||
ASSERT_OK(WideColumnSerialization::SerializeV2(columns, {{0, default_blob}},
|
||||
outflow_value));
|
||||
|
||||
BlobGarbageMeter blob_garbage_meter;
|
||||
ASSERT_OK(blob_garbage_meter.ProcessInFlow(key_slice, inflow_value));
|
||||
ASSERT_OK(blob_garbage_meter.ProcessOutFlow(key_slice, outflow_value));
|
||||
|
||||
const auto& flows = blob_garbage_meter.flows();
|
||||
ASSERT_EQ(flows.size(), 2);
|
||||
|
||||
constexpr size_t kUserKeySize = sizeof(user_key) - 1;
|
||||
const uint64_t default_bytes =
|
||||
default_blob.size() +
|
||||
BlobLogRecord::CalculateAdjustmentForRecordHeader(kUserKeySize);
|
||||
const uint64_t attr_bytes =
|
||||
attr_blob.size() +
|
||||
BlobLogRecord::CalculateAdjustmentForRecordHeader(kUserKeySize);
|
||||
|
||||
{
|
||||
const auto it = flows.find(default_blob.file_number());
|
||||
ASSERT_NE(it, flows.end());
|
||||
ASSERT_EQ(it->second.GetInFlow().GetCount(), 1);
|
||||
ASSERT_EQ(it->second.GetInFlow().GetBytes(), default_bytes);
|
||||
ASSERT_EQ(it->second.GetOutFlow().GetCount(), 1);
|
||||
ASSERT_EQ(it->second.GetOutFlow().GetBytes(), default_bytes);
|
||||
ASSERT_FALSE(it->second.HasGarbage());
|
||||
}
|
||||
|
||||
{
|
||||
const auto it = flows.find(attr_blob.file_number());
|
||||
ASSERT_NE(it, flows.end());
|
||||
ASSERT_EQ(it->second.GetInFlow().GetCount(), 1);
|
||||
ASSERT_EQ(it->second.GetInFlow().GetBytes(), attr_bytes);
|
||||
ASSERT_EQ(it->second.GetOutFlow().GetCount(), 0);
|
||||
ASSERT_EQ(it->second.GetOutFlow().GetBytes(), 0);
|
||||
ASSERT_TRUE(it->second.HasGarbage());
|
||||
ASSERT_EQ(it->second.GetGarbageCount(), 1);
|
||||
ASSERT_EQ(it->second.GetGarbageBytes(), attr_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -148,13 +148,26 @@ struct BlobLogRecord {
|
||||
|
||||
// Checks whether a blob offset is potentially valid or not.
|
||||
inline bool IsValidBlobOffset(uint64_t value_offset, uint64_t key_size,
|
||||
uint64_t value_size, uint64_t file_size) {
|
||||
if (value_offset <
|
||||
BlobLogHeader::kSize + BlobLogRecord::kHeaderSize + key_size) {
|
||||
uint64_t value_size, uint64_t file_size,
|
||||
bool has_footer = true) {
|
||||
constexpr uint64_t kMinPrefix =
|
||||
BlobLogHeader::kSize + BlobLogRecord::kHeaderSize;
|
||||
if (value_offset < kMinPrefix) {
|
||||
return false;
|
||||
}
|
||||
if (value_offset - kMinPrefix < key_size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value_offset + value_size + BlobLogFooter::kSize > file_size) {
|
||||
const uint64_t footer_size = has_footer ? BlobLogFooter::kSize : 0;
|
||||
if (file_size < footer_size) {
|
||||
return false;
|
||||
}
|
||||
const uint64_t max_value_end = file_size - footer_size;
|
||||
if (value_size > max_value_end) {
|
||||
return false;
|
||||
}
|
||||
if (value_offset > max_value_end - value_size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,21 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace {
|
||||
|
||||
Status AppendBlobRefreshRetryFailure(const Status& stale_status,
|
||||
const Status& retry_status) {
|
||||
assert(stale_status.IsCorruption());
|
||||
assert(!retry_status.ok());
|
||||
if (retry_status.IsCorruption()) {
|
||||
return retry_status;
|
||||
}
|
||||
return Status::CopyAppendMessage(
|
||||
stale_status, "; refresh retry failed: ", retry_status.ToString());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BlobSource::BlobSource(const ImmutableOptions& immutable_options,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
const std::string& db_id,
|
||||
@@ -59,6 +74,7 @@ Status BlobSource::GetBlobFromCache(
|
||||
assert(cached_blob->GetValue());
|
||||
|
||||
PERF_COUNTER_ADD(blob_cache_hit_count, 1);
|
||||
PERF_COUNTER_ADD(blob_cache_read_byte, cached_blob->GetValue()->size());
|
||||
RecordTick(statistics_, BLOB_DB_CACHE_HIT);
|
||||
RecordTick(statistics_, BLOB_DB_CACHE_BYTES_READ,
|
||||
cached_blob->GetValue()->size());
|
||||
@@ -231,6 +247,38 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
s = blob_file_reader.GetValue()->GetBlob(
|
||||
read_options, user_key, offset, value_size, compression_type,
|
||||
prefetch_buffer, allocator, &blob_contents, &read_size);
|
||||
if (s.IsCorruption()) {
|
||||
const Status stale_status = s;
|
||||
blob_file_reader.Reset();
|
||||
blob_file_cache_->Evict(file_number);
|
||||
|
||||
std::unique_ptr<BlobFileReader> fresh_reader;
|
||||
s = blob_file_cache_->OpenBlobFileReaderUncached(
|
||||
read_options, file_number, &fresh_reader,
|
||||
/*allow_footer_skip_retry=*/false);
|
||||
if (!s.ok()) {
|
||||
return AppendBlobRefreshRetryFailure(stale_status, s);
|
||||
}
|
||||
|
||||
if (compression_type != fresh_reader->GetCompressionType()) {
|
||||
return Status::Corruption(
|
||||
"Compression type mismatch when reading blob");
|
||||
}
|
||||
|
||||
blob_contents.reset();
|
||||
read_size = 0;
|
||||
s = fresh_reader->GetBlob(read_options, user_key, offset, value_size,
|
||||
compression_type, prefetch_buffer, allocator,
|
||||
&blob_contents, &read_size);
|
||||
if (!s.ok()) {
|
||||
s = AppendBlobRefreshRetryFailure(stale_status, s);
|
||||
} else {
|
||||
CacheHandleGuard<BlobFileReader> ignored_reader;
|
||||
blob_file_cache_
|
||||
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -397,6 +445,89 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
blob_file_reader.GetValue()->MultiGetBlob(read_options, allocator,
|
||||
_blob_reqs, &_bytes_read);
|
||||
|
||||
bool needs_reader_refresh = false;
|
||||
for (const auto& blob_req : _blob_reqs) {
|
||||
BlobReadRequest* const req = blob_req.first;
|
||||
assert(req != nullptr);
|
||||
assert(req->status != nullptr);
|
||||
if (req->status->IsCorruption()) {
|
||||
needs_reader_refresh = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (needs_reader_refresh) {
|
||||
blob_file_reader.Reset();
|
||||
blob_file_cache_->Evict(file_number);
|
||||
|
||||
std::unique_ptr<BlobFileReader> fresh_reader;
|
||||
s = blob_file_cache_->OpenBlobFileReaderUncached(
|
||||
read_options, file_number, &fresh_reader,
|
||||
/*allow_footer_skip_retry=*/false);
|
||||
if (!s.ok()) {
|
||||
for (const auto& blob_req : _blob_reqs) {
|
||||
BlobReadRequest* const req = blob_req.first;
|
||||
assert(req != nullptr);
|
||||
assert(req->status != nullptr);
|
||||
if (req->status->IsCorruption()) {
|
||||
*req->status = AppendBlobRefreshRetryFailure(*req->status, s);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
|
||||
retry_blob_reqs;
|
||||
autovector<Status> stale_statuses;
|
||||
for (auto& blob_req : _blob_reqs) {
|
||||
BlobReadRequest* const req = blob_req.first;
|
||||
assert(req != nullptr);
|
||||
assert(req->status != nullptr);
|
||||
if (!req->status->IsCorruption()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
stale_statuses.emplace_back(*req->status);
|
||||
*req->status = Status::OK();
|
||||
blob_req.second.reset();
|
||||
retry_blob_reqs.emplace_back(req, std::unique_ptr<BlobContents>());
|
||||
}
|
||||
|
||||
uint64_t refreshed_bytes_read = 0;
|
||||
fresh_reader->MultiGetBlob(read_options, allocator, retry_blob_reqs,
|
||||
&refreshed_bytes_read);
|
||||
_bytes_read += refreshed_bytes_read;
|
||||
|
||||
bool install_fresh_reader = false;
|
||||
for (size_t i = 0; i < retry_blob_reqs.size(); ++i) {
|
||||
auto& retried_blob_req = retry_blob_reqs[i];
|
||||
BlobReadRequest* const retried_req = retried_blob_req.first;
|
||||
assert(retried_req != nullptr);
|
||||
if (retried_req->status->ok()) {
|
||||
install_fresh_reader = true;
|
||||
} else {
|
||||
*retried_req->status = AppendBlobRefreshRetryFailure(
|
||||
stale_statuses[i], *retried_req->status);
|
||||
}
|
||||
|
||||
for (auto& blob_req : _blob_reqs) {
|
||||
if (blob_req.first != retried_req) {
|
||||
continue;
|
||||
}
|
||||
|
||||
blob_req.second = std::move(retried_blob_req.second);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (install_fresh_reader) {
|
||||
CacheHandleGuard<BlobFileReader> ignored_reader;
|
||||
blob_file_cache_
|
||||
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
|
||||
if (blob_cache_ && read_options.fill_cache) {
|
||||
// If filling cache is allowed and a cache is configured, try to put
|
||||
// the blob(s) to the cache.
|
||||
|
||||
@@ -229,6 +229,7 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
// Retrieved the blob cache num_blobs * 3 times via TEST_BlobInCache,
|
||||
// GetBlob, and TEST_BlobInCache.
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, 0);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_read_byte, 0);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count, num_blobs);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte, total_bytes);
|
||||
ASSERT_GE((int)get_perf_context()->blob_checksum_time, 0);
|
||||
@@ -262,6 +263,8 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
blob_bytes += blob_sizes[i];
|
||||
total_bytes += bytes_read;
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, i);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_read_byte,
|
||||
blob_bytes - blob_sizes[i]);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count, i + 1);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte, total_bytes);
|
||||
|
||||
@@ -269,11 +272,13 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
blob_offsets[i]));
|
||||
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, i + 1);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_read_byte, blob_bytes);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count, i + 1);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte, total_bytes);
|
||||
}
|
||||
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, num_blobs);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_read_byte, blob_bytes);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count, num_blobs);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte, total_bytes);
|
||||
|
||||
@@ -312,6 +317,7 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
// Retrieved the blob cache num_blobs * 3 times via TEST_BlobInCache,
|
||||
// GetBlob, and TEST_BlobInCache.
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, num_blobs * 3);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_read_byte, blob_bytes * 3);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count, 0); // without i/o
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte, 0); // without i/o
|
||||
|
||||
@@ -690,6 +696,8 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromMultiFiles) {
|
||||
// TEST_BlobInCache.
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count,
|
||||
num_blobs * blob_files);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_read_byte,
|
||||
blob_value_bytes * blob_files);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count,
|
||||
num_blobs * blob_files); // blocking i/o
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte,
|
||||
@@ -750,6 +758,8 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromMultiFiles) {
|
||||
// via MultiGetBlob and TEST_BlobInCache.
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count,
|
||||
num_blobs * blob_files * 2);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_read_byte,
|
||||
blob_value_bytes * blob_files * 2);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count,
|
||||
0); // blocking i/o
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte,
|
||||
@@ -1044,6 +1054,156 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromCache) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BlobSourceTest, GetBlobPreservesCorruptionDetailsWhenRefreshOpenFails) {
|
||||
options_.cf_paths.emplace_back(
|
||||
test::PerThreadDBPath(env_,
|
||||
"BlobSourceTest_GetBlobPreservesCorruptionDetails"),
|
||||
0);
|
||||
|
||||
DestroyAndReopen(options_);
|
||||
|
||||
ImmutableOptions immutable_options(options_);
|
||||
MutableCFOptions mutable_cf_options(options_);
|
||||
|
||||
constexpr uint32_t column_family_id = 1;
|
||||
constexpr bool has_ttl = false;
|
||||
constexpr ExpirationRange expiration_range;
|
||||
constexpr uint64_t blob_file_number = 1;
|
||||
|
||||
const std::string key_str = "key0";
|
||||
const std::string blob_str = "blob0";
|
||||
std::vector<Slice> keys{Slice(key_str)};
|
||||
std::vector<Slice> blobs{Slice(blob_str)};
|
||||
std::vector<uint64_t> blob_offsets(1);
|
||||
std::vector<uint64_t> blob_sizes(1);
|
||||
|
||||
const uint64_t file_size = BlobLogHeader::kSize + BlobLogRecord::kHeaderSize +
|
||||
key_str.size() + blob_str.size() +
|
||||
BlobLogFooter::kSize;
|
||||
|
||||
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
|
||||
expiration_range, blob_file_number, keys, blobs, kNoCompression,
|
||||
blob_offsets, blob_sizes);
|
||||
|
||||
constexpr size_t capacity = 1024;
|
||||
std::shared_ptr<Cache> backing_cache = NewLRUCache(capacity);
|
||||
|
||||
FileOptions file_options;
|
||||
std::unique_ptr<BlobFileCache> blob_file_cache =
|
||||
std::make_unique<BlobFileCache>(
|
||||
backing_cache.get(), &immutable_options, &file_options,
|
||||
column_family_id, nullptr /*HistogramImpl*/, nullptr /*IOTracer*/);
|
||||
|
||||
BlobSource blob_source(immutable_options, mutable_cf_options, db_id_,
|
||||
db_session_id_, blob_file_cache.get());
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.verify_checksums = true;
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
|
||||
PinnableSlice value;
|
||||
uint64_t bytes_read = 0;
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys[0], blob_file_number, blob_offsets[0], file_size,
|
||||
blob_sizes[0], kNoCompression, prefetch_buffer, &value, &bytes_read));
|
||||
ASSERT_EQ(value, blobs[0]);
|
||||
|
||||
const std::string blob_file_path =
|
||||
BlobFileName(immutable_options.cf_paths.front().path, blob_file_number);
|
||||
ASSERT_OK(env_->DeleteFile(blob_file_path));
|
||||
|
||||
PinnableSlice invalid_value;
|
||||
bytes_read = 0;
|
||||
Status s =
|
||||
blob_source.GetBlob(read_options, keys[0], blob_file_number, file_size,
|
||||
file_size, blob_sizes[0], kNoCompression,
|
||||
prefetch_buffer, &invalid_value, &bytes_read);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
|
||||
const std::string status_str = s.ToString();
|
||||
ASSERT_NE(status_str.find("Invalid blob offset"), std::string::npos);
|
||||
ASSERT_NE(status_str.find("refresh retry failed"), std::string::npos);
|
||||
ASSERT_NE(status_str.find("IO error"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(BlobSourceTest,
|
||||
MultiGetBlobPreservesCorruptionDetailsWhenRefreshOpenFails) {
|
||||
options_.cf_paths.emplace_back(
|
||||
test::PerThreadDBPath(
|
||||
env_, "BlobSourceTest_MultiGetBlobPreservesCorruptionDetails"),
|
||||
0);
|
||||
|
||||
DestroyAndReopen(options_);
|
||||
|
||||
ImmutableOptions immutable_options(options_);
|
||||
MutableCFOptions mutable_cf_options(options_);
|
||||
|
||||
constexpr uint32_t column_family_id = 1;
|
||||
constexpr bool has_ttl = false;
|
||||
constexpr ExpirationRange expiration_range;
|
||||
constexpr uint64_t blob_file_number = 1;
|
||||
|
||||
const std::string key_str = "key0";
|
||||
const std::string blob_str = "blob0";
|
||||
std::vector<Slice> keys{Slice(key_str)};
|
||||
std::vector<Slice> blobs{Slice(blob_str)};
|
||||
std::vector<uint64_t> blob_offsets(1);
|
||||
std::vector<uint64_t> blob_sizes(1);
|
||||
|
||||
const uint64_t file_size = BlobLogHeader::kSize + BlobLogRecord::kHeaderSize +
|
||||
key_str.size() + blob_str.size() +
|
||||
BlobLogFooter::kSize;
|
||||
|
||||
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
|
||||
expiration_range, blob_file_number, keys, blobs, kNoCompression,
|
||||
blob_offsets, blob_sizes);
|
||||
|
||||
constexpr size_t capacity = 1024;
|
||||
std::shared_ptr<Cache> backing_cache = NewLRUCache(capacity);
|
||||
|
||||
FileOptions file_options;
|
||||
std::unique_ptr<BlobFileCache> blob_file_cache =
|
||||
std::make_unique<BlobFileCache>(
|
||||
backing_cache.get(), &immutable_options, &file_options,
|
||||
column_family_id, nullptr /*HistogramImpl*/, nullptr /*IOTracer*/);
|
||||
|
||||
BlobSource blob_source(immutable_options, mutable_cf_options, db_id_,
|
||||
db_session_id_, blob_file_cache.get());
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.verify_checksums = true;
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
|
||||
PinnableSlice value;
|
||||
uint64_t bytes_read = 0;
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys[0], blob_file_number, blob_offsets[0], file_size,
|
||||
blob_sizes[0], kNoCompression, prefetch_buffer, &value, &bytes_read));
|
||||
ASSERT_EQ(value, blobs[0]);
|
||||
|
||||
const std::string blob_file_path =
|
||||
BlobFileName(immutable_options.cf_paths.front().path, blob_file_number);
|
||||
ASSERT_OK(env_->DeleteFile(blob_file_path));
|
||||
|
||||
std::array<Status, 1> statuses;
|
||||
std::array<PinnableSlice, 1> values;
|
||||
autovector<BlobReadRequest> blob_reqs;
|
||||
blob_reqs.emplace_back(keys[0], file_size, blob_sizes[0], kNoCompression,
|
||||
&values[0], &statuses[0]);
|
||||
|
||||
bytes_read = 0;
|
||||
blob_source.MultiGetBlobFromOneFile(read_options, blob_file_number, file_size,
|
||||
blob_reqs, &bytes_read);
|
||||
ASSERT_TRUE(statuses[0].IsCorruption());
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
|
||||
const std::string status_str = statuses[0].ToString();
|
||||
ASSERT_NE(status_str.find("Invalid blob offset"), std::string::npos);
|
||||
ASSERT_NE(status_str.find("refresh retry failed"), std::string::npos);
|
||||
ASSERT_NE(status_str.find("IO error"), std::string::npos);
|
||||
}
|
||||
|
||||
class BlobSecondaryCacheTest : public DBTestBase {
|
||||
protected:
|
||||
public:
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "db/blob/blob_write_batch_transformer.h"
|
||||
|
||||
#include "db/blob/blob_file_partition_manager.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "db/wide/wide_column_serialization.h"
|
||||
#include "db/write_batch_internal.h"
|
||||
#include "port/likely.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
BlobWriteBatchTransformer::BlobWriteBatchTransformer(
|
||||
const BlobPartitionManagerProvider& partition_mgr_provider,
|
||||
WriteBatch* output_batch,
|
||||
const BlobDirectWriteSettingsProvider& settings_provider,
|
||||
const WriteOptions& write_options)
|
||||
: partition_mgr_provider_(partition_mgr_provider),
|
||||
output_batch_(output_batch),
|
||||
settings_provider_(settings_provider),
|
||||
write_options_(write_options) {
|
||||
assert(partition_mgr_provider_);
|
||||
assert(output_batch_);
|
||||
assert(settings_provider_);
|
||||
}
|
||||
|
||||
Status BlobWriteBatchTransformer::TransformBatch(
|
||||
const WriteOptions& write_options, WriteBatch* input_batch,
|
||||
WriteBatch* output_batch,
|
||||
const BlobPartitionManagerProvider& partition_mgr_provider,
|
||||
const BlobDirectWriteSettingsProvider& settings_provider, bool* transformed,
|
||||
std::vector<BlobFilePartitionManager*>* used_managers,
|
||||
std::vector<RollbackInfo>* rollback_infos) {
|
||||
assert(input_batch);
|
||||
assert(output_batch);
|
||||
assert(transformed);
|
||||
|
||||
output_batch->Clear();
|
||||
*transformed = false;
|
||||
|
||||
BlobWriteBatchTransformer transformer(partition_mgr_provider, output_batch,
|
||||
settings_provider, write_options);
|
||||
|
||||
Status s = input_batch->Iterate(&transformer);
|
||||
*transformed = transformer.HasTransformed();
|
||||
|
||||
if (used_managers) {
|
||||
used_managers->assign(transformer.used_managers_.begin(),
|
||||
transformer.used_managers_.end());
|
||||
}
|
||||
|
||||
if (rollback_infos) {
|
||||
*rollback_infos = std::move(transformer.rollback_infos_);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
Status BlobWriteBatchTransformer::MaybePreprocessWideColumns(
|
||||
const WriteOptions& write_options, uint32_t column_family_id,
|
||||
const Slice& key, const WideColumns& columns,
|
||||
BlobFilePartitionManager* partition_mgr,
|
||||
const BlobDirectWriteSettings& settings, bool serialize_inline_entity,
|
||||
std::string* entity, bool* transformed,
|
||||
std::vector<RollbackInfo>* rollback_infos) {
|
||||
assert(entity != nullptr);
|
||||
assert(transformed != nullptr);
|
||||
|
||||
*transformed = false;
|
||||
|
||||
if (partition_mgr == nullptr || !settings.enable_blob_direct_write) {
|
||||
if (!serialize_inline_entity) {
|
||||
return Status::OK();
|
||||
}
|
||||
entity->clear();
|
||||
return WideColumnSerialization::Serialize(columns, *entity);
|
||||
}
|
||||
|
||||
std::vector<std::pair<size_t, BlobIndex>> new_blob_columns;
|
||||
new_blob_columns.reserve(columns.size());
|
||||
std::string blob_index_buf;
|
||||
bool has_entity_partition = false;
|
||||
uint32_t entity_partition = 0;
|
||||
|
||||
for (size_t column_idx = 0; column_idx < columns.size(); ++column_idx) {
|
||||
const Slice& column_value = columns[column_idx].value();
|
||||
if (column_value.size() < settings.min_blob_size) {
|
||||
continue;
|
||||
}
|
||||
if (!has_entity_partition) {
|
||||
entity_partition = partition_mgr->SelectWideColumnPartition(
|
||||
column_family_id, key, columns);
|
||||
has_entity_partition = true;
|
||||
}
|
||||
|
||||
uint64_t blob_file_number = 0;
|
||||
uint64_t blob_offset = 0;
|
||||
uint64_t blob_size = 0;
|
||||
Status s = partition_mgr->WriteBlob(
|
||||
write_options, column_family_id, settings.compression_type, key,
|
||||
column_value, &blob_file_number, &blob_offset, &blob_size, settings,
|
||||
&entity_partition);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (rollback_infos != nullptr) {
|
||||
const uint64_t record_bytes =
|
||||
BlobLogRecord::kHeaderSize + key.size() + blob_size;
|
||||
rollback_infos->push_back(
|
||||
{partition_mgr, blob_file_number, /*count=*/1, record_bytes});
|
||||
}
|
||||
|
||||
BlobIndex blob_index;
|
||||
BlobIndex::EncodeBlob(&blob_index_buf, blob_file_number, blob_offset,
|
||||
blob_size, settings.compression_type);
|
||||
s = blob_index.DecodeFrom(blob_index_buf);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
new_blob_columns.emplace_back(column_idx, blob_index);
|
||||
}
|
||||
|
||||
if (new_blob_columns.empty()) {
|
||||
if (!serialize_inline_entity) {
|
||||
return Status::OK();
|
||||
}
|
||||
entity->clear();
|
||||
return WideColumnSerialization::Serialize(columns, *entity);
|
||||
}
|
||||
|
||||
entity->clear();
|
||||
Status s =
|
||||
WideColumnSerialization::SerializeV2(columns, new_blob_columns, *entity);
|
||||
if (s.ok()) {
|
||||
*transformed = true;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status BlobWriteBatchTransformer::PutCF(uint32_t column_family_id,
|
||||
const Slice& key, const Slice& value) {
|
||||
// Use cached settings/manager for the same CF to avoid per-entry lookup.
|
||||
if (column_family_id != cached_cf_id_) {
|
||||
cached_settings_ = settings_provider_(column_family_id);
|
||||
cached_partition_mgr_ = partition_mgr_provider_(column_family_id);
|
||||
cached_cf_id_ = column_family_id;
|
||||
}
|
||||
const auto& settings = cached_settings_;
|
||||
|
||||
if (!cached_partition_mgr_ || !settings.enable_blob_direct_write ||
|
||||
value.size() < settings.min_blob_size) {
|
||||
return WriteBatchInternal::Put(output_batch_, column_family_id, key, value);
|
||||
}
|
||||
|
||||
uint64_t blob_file_number = 0;
|
||||
uint64_t blob_offset = 0;
|
||||
uint64_t blob_size = 0;
|
||||
|
||||
Status s = cached_partition_mgr_->WriteBlob(
|
||||
write_options_, column_family_id, settings.compression_type, key, value,
|
||||
&blob_file_number, &blob_offset, &blob_size, settings);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
used_managers_.insert(cached_partition_mgr_);
|
||||
|
||||
// Track the exact file so stale transformed attempts can rollback
|
||||
// per-file rather than smearing bytes across all partitions at seal time.
|
||||
const uint64_t record_bytes =
|
||||
BlobLogRecord::kHeaderSize + key.size() + blob_size;
|
||||
rollback_infos_.push_back(
|
||||
{cached_partition_mgr_, blob_file_number, /*count=*/1, record_bytes});
|
||||
|
||||
BlobIndex::EncodeBlob(&blob_index_buf_, blob_file_number, blob_offset,
|
||||
blob_size, settings.compression_type);
|
||||
|
||||
has_transformed_ = true;
|
||||
return WriteBatchInternal::PutBlobIndex(output_batch_, column_family_id, key,
|
||||
blob_index_buf_);
|
||||
}
|
||||
|
||||
Status BlobWriteBatchTransformer::TimedPutCF(uint32_t column_family_id,
|
||||
const Slice& key,
|
||||
const Slice& value,
|
||||
uint64_t write_time) {
|
||||
// TimedPut: pass through without blob separation for now.
|
||||
return WriteBatchInternal::TimedPut(output_batch_, column_family_id, key,
|
||||
value, write_time);
|
||||
}
|
||||
|
||||
Status BlobWriteBatchTransformer::PutEntityCF(uint32_t column_family_id,
|
||||
const Slice& key,
|
||||
const Slice& entity) {
|
||||
if (column_family_id != cached_cf_id_) {
|
||||
cached_settings_ = settings_provider_(column_family_id);
|
||||
cached_partition_mgr_ = partition_mgr_provider_(column_family_id);
|
||||
cached_cf_id_ = column_family_id;
|
||||
}
|
||||
const auto& settings = cached_settings_;
|
||||
|
||||
if (!cached_partition_mgr_ || !settings.enable_blob_direct_write) {
|
||||
return WriteBatchInternal::PutEntitySerialized(
|
||||
output_batch_, column_family_id, key, entity);
|
||||
}
|
||||
|
||||
Slice entity_ref = entity;
|
||||
std::vector<WideColumn> columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> existing_blob_columns;
|
||||
Status status = WideColumnSerialization::DeserializeV2(entity_ref, columns,
|
||||
existing_blob_columns);
|
||||
if (status.ok()) {
|
||||
// Reject pre-serialized entities that already contain blob references.
|
||||
// Passing them through would preserve references to blob files outside this
|
||||
// batch's direct-write lifetime tracking, which can later turn into stale
|
||||
// reads after blob GC.
|
||||
if (UNLIKELY(!existing_blob_columns.empty())) {
|
||||
return Status::NotSupported(
|
||||
"Blob direct write does not support pre-serialized wide entities "
|
||||
"with blob references");
|
||||
}
|
||||
|
||||
std::string rewritten_entity;
|
||||
bool transformed = false;
|
||||
status = MaybePreprocessWideColumns(
|
||||
write_options_, column_family_id, key, columns, cached_partition_mgr_,
|
||||
settings, /*serialize_inline_entity=*/false, &rewritten_entity,
|
||||
&transformed, &rollback_infos_);
|
||||
if (status.ok()) {
|
||||
if (!transformed) {
|
||||
return WriteBatchInternal::PutEntitySerialized(
|
||||
output_batch_, column_family_id, key, entity);
|
||||
}
|
||||
|
||||
has_transformed_ = true;
|
||||
used_managers_.insert(cached_partition_mgr_);
|
||||
return WriteBatchInternal::PutEntitySerialized(
|
||||
output_batch_, column_family_id, key, rewritten_entity);
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
Status BlobWriteBatchTransformer::DeleteCF(uint32_t column_family_id,
|
||||
const Slice& key) {
|
||||
return WriteBatchInternal::Delete(output_batch_, column_family_id, key);
|
||||
}
|
||||
|
||||
Status BlobWriteBatchTransformer::SingleDeleteCF(uint32_t column_family_id,
|
||||
const Slice& key) {
|
||||
return WriteBatchInternal::SingleDelete(output_batch_, column_family_id, key);
|
||||
}
|
||||
|
||||
Status BlobWriteBatchTransformer::DeleteRangeCF(uint32_t column_family_id,
|
||||
const Slice& begin_key,
|
||||
const Slice& end_key) {
|
||||
return WriteBatchInternal::DeleteRange(output_batch_, column_family_id,
|
||||
begin_key, end_key);
|
||||
}
|
||||
|
||||
Status BlobWriteBatchTransformer::MergeCF(uint32_t column_family_id,
|
||||
const Slice& key,
|
||||
const Slice& value) {
|
||||
return WriteBatchInternal::Merge(output_batch_, column_family_id, key, value);
|
||||
}
|
||||
|
||||
Status BlobWriteBatchTransformer::PutBlobIndexCF(uint32_t column_family_id,
|
||||
const Slice& key,
|
||||
const Slice& value) {
|
||||
// Already a blob index -- pass through unchanged.
|
||||
return WriteBatchInternal::PutBlobIndex(output_batch_, column_family_id, key,
|
||||
value);
|
||||
}
|
||||
|
||||
void BlobWriteBatchTransformer::LogData(const Slice& blob) {
|
||||
output_batch_->PutLogData(blob).PermitUncheckedError();
|
||||
}
|
||||
|
||||
Status BlobWriteBatchTransformer::MarkBeginPrepare(bool unprepared) {
|
||||
return WriteBatchInternal::InsertBeginPrepare(
|
||||
output_batch_, !unprepared /* write_after_commit */, unprepared);
|
||||
}
|
||||
|
||||
Status BlobWriteBatchTransformer::MarkEndPrepare(const Slice& xid) {
|
||||
return WriteBatchInternal::InsertEndPrepare(output_batch_, xid);
|
||||
}
|
||||
|
||||
Status BlobWriteBatchTransformer::MarkCommit(const Slice& xid) {
|
||||
return WriteBatchInternal::MarkCommit(output_batch_, xid);
|
||||
}
|
||||
|
||||
Status BlobWriteBatchTransformer::MarkCommitWithTimestamp(const Slice& xid,
|
||||
const Slice& ts) {
|
||||
return WriteBatchInternal::MarkCommitWithTimestamp(output_batch_, xid, ts);
|
||||
}
|
||||
|
||||
Status BlobWriteBatchTransformer::MarkRollback(const Slice& xid) {
|
||||
return WriteBatchInternal::MarkRollback(output_batch_, xid);
|
||||
}
|
||||
|
||||
Status BlobWriteBatchTransformer::MarkNoop(bool /*empty_batch*/) {
|
||||
return WriteBatchInternal::InsertNoop(output_batch_);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,176 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "rocksdb/advanced_options.h"
|
||||
#include "rocksdb/compression_type.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/status.h"
|
||||
#include "rocksdb/write_batch.h"
|
||||
#include "util/hash_containers.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class BlobFilePartitionManager;
|
||||
class Cache;
|
||||
|
||||
// Callback to look up per-CF blob settings.
|
||||
struct BlobDirectWriteSettings {
|
||||
// Whether this column family should attempt direct-write blob separation.
|
||||
bool enable_blob_direct_write = false;
|
||||
// Minimum value size eligible for direct-write blob separation.
|
||||
uint64_t min_blob_size = 0;
|
||||
// Compression to use for newly written blob records.
|
||||
CompressionType compression_type = kNoCompression;
|
||||
// Compression options for newly written blob records.
|
||||
CompressionOptions compression_opts;
|
||||
// Raw pointer -- the Cache is owned by ColumnFamilyOptions and outlives all
|
||||
// settings snapshots. Using raw avoids 2 atomic ref-count ops per Put().
|
||||
Cache* blob_cache = nullptr;
|
||||
// Blob-cache prepopulation policy for direct-write records.
|
||||
PrepopulateBlobCache prepopulate_blob_cache = PrepopulateBlobCache::kDisable;
|
||||
};
|
||||
|
||||
using BlobDirectWriteSettingsProvider =
|
||||
std::function<BlobDirectWriteSettings(uint32_t column_family_id)>;
|
||||
|
||||
// Callback to look up per-CF partition manager.
|
||||
using BlobPartitionManagerProvider =
|
||||
std::function<BlobFilePartitionManager*(uint32_t column_family_id)>;
|
||||
|
||||
// Transforms a WriteBatch by writing large values directly to blob files
|
||||
// and replacing them with BlobIndex entries. Non-qualifying entries
|
||||
// (small values, deletes, merges, etc.) are passed through unchanged.
|
||||
class BlobWriteBatchTransformer : public WriteBatch::Handler {
|
||||
public:
|
||||
struct RollbackInfo {
|
||||
// Manager that owns the file updated during transform.
|
||||
BlobFilePartitionManager* partition_mgr = nullptr;
|
||||
// Blob file number written during transform.
|
||||
uint64_t file_number = 0;
|
||||
// Number of blob records appended to that file.
|
||||
uint64_t count = 0;
|
||||
// Number of bytes appended to that file.
|
||||
uint64_t bytes = 0;
|
||||
};
|
||||
|
||||
// Builds the final serialized entity for one PutEntity write.
|
||||
//
|
||||
// `columns` must already be sorted by column name. When direct-write blob
|
||||
// separation is enabled and one or more columns meet `min_blob_size`, this
|
||||
// writes those columns to blob files and returns a V2 entity containing
|
||||
// BlobIndex references. Otherwise, if `serialize_inline_entity` is true, it
|
||||
// returns a normal V1 entity with all columns inline.
|
||||
//
|
||||
// `rollback_infos`, when provided, is appended with the exact blob writes
|
||||
// performed before the method returns, even if it later fails, so callers
|
||||
// can account those bytes as initial garbage.
|
||||
static Status MaybePreprocessWideColumns(
|
||||
const WriteOptions& write_options, uint32_t column_family_id,
|
||||
const Slice& key, const WideColumns& columns,
|
||||
BlobFilePartitionManager* partition_mgr,
|
||||
const BlobDirectWriteSettings& settings, bool serialize_inline_entity,
|
||||
std::string* entity, bool* transformed,
|
||||
std::vector<RollbackInfo>* rollback_infos = nullptr);
|
||||
|
||||
// Constructs a per-batch transformer. Most callers should use
|
||||
// TransformBatch() instead of driving the handler directly.
|
||||
BlobWriteBatchTransformer(
|
||||
const BlobPartitionManagerProvider& partition_mgr_provider,
|
||||
WriteBatch* output_batch,
|
||||
const BlobDirectWriteSettingsProvider& settings_provider,
|
||||
const WriteOptions& write_options);
|
||||
|
||||
// Transform a WriteBatch. If no values qualify for blob separation,
|
||||
// output_batch will be empty and the caller should use the original batch.
|
||||
// If any values are separated, output_batch contains the full transformed
|
||||
// batch. used_managers (if non-null) receives the set of partition managers
|
||||
// that had data written to them, so the caller can flush/sync them.
|
||||
// rollback_infos (if non-null) receives the exact file/count/byte writes so
|
||||
// a failed transformed attempt can be accounted as initial garbage. Partial
|
||||
// results are returned even if this method fails after some blob writes.
|
||||
static Status TransformBatch(
|
||||
const WriteOptions& write_options, WriteBatch* input_batch,
|
||||
WriteBatch* output_batch,
|
||||
const BlobPartitionManagerProvider& partition_mgr_provider,
|
||||
const BlobDirectWriteSettingsProvider& settings_provider,
|
||||
bool* transformed,
|
||||
std::vector<BlobFilePartitionManager*>* used_managers = nullptr,
|
||||
std::vector<RollbackInfo>* rollback_infos = nullptr);
|
||||
|
||||
// WriteBatch::Handler overrides
|
||||
Status PutCF(uint32_t column_family_id, const Slice& key,
|
||||
const Slice& value) override;
|
||||
|
||||
Status TimedPutCF(uint32_t column_family_id, const Slice& key,
|
||||
const Slice& value, uint64_t write_time) override;
|
||||
|
||||
Status PutEntityCF(uint32_t column_family_id, const Slice& key,
|
||||
const Slice& entity) override;
|
||||
|
||||
Status DeleteCF(uint32_t column_family_id, const Slice& key) override;
|
||||
|
||||
Status SingleDeleteCF(uint32_t column_family_id, const Slice& key) override;
|
||||
|
||||
Status DeleteRangeCF(uint32_t column_family_id, const Slice& begin_key,
|
||||
const Slice& end_key) override;
|
||||
|
||||
Status MergeCF(uint32_t column_family_id, const Slice& key,
|
||||
const Slice& value) override;
|
||||
|
||||
Status PutBlobIndexCF(uint32_t column_family_id, const Slice& key,
|
||||
const Slice& value) override;
|
||||
|
||||
void LogData(const Slice& blob) override;
|
||||
|
||||
Status MarkBeginPrepare(bool unprepared = false) override;
|
||||
Status MarkEndPrepare(const Slice& xid) override;
|
||||
Status MarkCommit(const Slice& xid) override;
|
||||
Status MarkCommitWithTimestamp(const Slice& xid, const Slice& ts) override;
|
||||
Status MarkRollback(const Slice& xid) override;
|
||||
Status MarkNoop(bool empty_batch) override;
|
||||
|
||||
// Returns true once at least one value has been rewritten to a BlobIndex.
|
||||
bool HasTransformed() const { return has_transformed_; }
|
||||
|
||||
private:
|
||||
// Callback to look up the partition manager for a given column family ID.
|
||||
BlobPartitionManagerProvider partition_mgr_provider_;
|
||||
// Output batch that receives transformed entries (BlobIndex for qualifying
|
||||
// values, passthrough for everything else).
|
||||
WriteBatch* output_batch_;
|
||||
// Callback to look up blob direct write settings for a given CF ID.
|
||||
BlobDirectWriteSettingsProvider settings_provider_;
|
||||
// Write options from the caller, forwarded to WriteBlob calls.
|
||||
const WriteOptions& write_options_;
|
||||
// True once at least one value has been separated into a blob file.
|
||||
bool has_transformed_ = false;
|
||||
// Reusable buffer for encoding BlobIndex entries (avoids per-Put alloc).
|
||||
std::string blob_index_buf_;
|
||||
// Per-batch cache of the last CF's settings and manager, avoiding
|
||||
// redundant provider lookups when consecutive entries share the same CF.
|
||||
uint32_t cached_cf_id_ = UINT32_MAX;
|
||||
// Cached settings for `cached_cf_id_`.
|
||||
BlobDirectWriteSettings cached_settings_;
|
||||
// Cached partition manager for `cached_cf_id_`.
|
||||
BlobFilePartitionManager* cached_partition_mgr_ = nullptr;
|
||||
// Set of partition managers that received data during this batch,
|
||||
// returned to the caller so it can flush/sync them.
|
||||
UnorderedSet<BlobFilePartitionManager*> used_managers_;
|
||||
// Exact blob writes performed during this batch. We only aggregate these
|
||||
// entries if rollback is needed so the normal path keeps minimal overhead.
|
||||
std::vector<RollbackInfo> rollback_infos_;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -3,17 +3,29 @@
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "cache/compressed_secondary_cache.h"
|
||||
#include "db/blob/blob_file_partition_manager.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "db/blob/blob_log_sequential_reader.h"
|
||||
#include "db/column_family.h"
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/db_with_timestamp_test_util.h"
|
||||
#include "file/filename.h"
|
||||
#include "file/random_access_file_reader.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
#include "rocksdb/trace_reader_writer.h"
|
||||
#include "rocksdb/trace_record.h"
|
||||
#include "rocksdb/utilities/replayer.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/compression.h"
|
||||
#include "utilities/fault_injection_env.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -51,6 +63,43 @@ TEST_F(DBBlobBasicTest, GetBlob) {
|
||||
.IsIncomplete());
|
||||
}
|
||||
|
||||
TEST_F(DBBlobBasicTest, EmptyValueNotStoredAsBlob) {
|
||||
// Regression test for crash when empty blob value is evicted to
|
||||
// CompressedSecondaryCache (T261142690). Empty values should always be
|
||||
// stored inline in the SST, never as blobs, even with min_blob_size=0.
|
||||
// A BlobIndex for an empty value is strictly larger than the value itself,
|
||||
// so storing it as a blob is pure overhead.
|
||||
Options options = GetDefaultOptions();
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 0;
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
Reopen(options);
|
||||
|
||||
// Write an empty value and a non-empty value.
|
||||
ASSERT_OK(Put("empty_key", ""));
|
||||
constexpr char blob_value[] = "blob_value";
|
||||
ASSERT_OK(Put("nonempty_key", blob_value));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Both values should be readable.
|
||||
ASSERT_EQ(Get("empty_key"), "");
|
||||
ASSERT_EQ(Get("nonempty_key"), blob_value);
|
||||
|
||||
// The empty value should be stored inline (readable from block cache
|
||||
// without blob file I/O), while the non-empty value requires blob I/O.
|
||||
ReadOptions ro;
|
||||
ro.read_tier = kBlockCacheTier;
|
||||
|
||||
PinnableSlice result;
|
||||
ASSERT_OK(db_->Get(ro, db_->DefaultColumnFamily(), "empty_key", &result));
|
||||
ASSERT_EQ(result, "");
|
||||
|
||||
result.Reset();
|
||||
ASSERT_TRUE(db_->Get(ro, db_->DefaultColumnFamily(), "nonempty_key", &result)
|
||||
.IsIncomplete());
|
||||
}
|
||||
|
||||
TEST_F(DBBlobBasicTest, GetBlobFromCache) {
|
||||
Options options = GetDefaultOptions();
|
||||
|
||||
@@ -1571,6 +1620,47 @@ TEST_P(DBBlobBasicIOErrorTest, GetBlob_IOError) {
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_P(DBBlobBasicIOErrorTest, GetEntityMergeWithBlobBaseIOError) {
|
||||
// Goal: verify GetEntity preserves injected blob-read IOErrors when merge
|
||||
// reads a blob-backed base value, instead of laundering them into Corruption.
|
||||
// The test writes a blob-backed base value plus a merge operand, then injects
|
||||
// an IOError at blob read time and checks both GetEntity and Get see it.
|
||||
Options options;
|
||||
options.env = fault_injection_env_.get();
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 0;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
|
||||
Reopen(options);
|
||||
|
||||
constexpr char key[] = "key";
|
||||
constexpr char base_value[] = "base_value";
|
||||
|
||||
ASSERT_OK(Put(key, base_value));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(Merge(key, "merge_operand"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(sync_point_, [this](void* /* arg */) {
|
||||
fault_injection_env_->SetFilesystemActive(false,
|
||||
Status::IOError(sync_point_));
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
PinnableWideColumns entity_result;
|
||||
Status s = db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), key,
|
||||
&entity_result);
|
||||
ASSERT_TRUE(s.IsIOError()) << "Expected IOError but got: " << s.ToString();
|
||||
|
||||
PinnableSlice get_result;
|
||||
s = db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &get_result);
|
||||
ASSERT_TRUE(s.IsIOError()) << "Expected IOError but got: " << s.ToString();
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_P(DBBlobBasicIOErrorMultiGetTest, MultiGetBlobs_IOError) {
|
||||
Options options = GetDefaultOptions();
|
||||
options.env = fault_injection_env_.get();
|
||||
@@ -2459,6 +2549,138 @@ TEST_F(DBBlobWithTimestampTest, IterateBlobs) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBBlobBasicTest, GetApproximateSizesIncludingBlobFiles) {
|
||||
Options options = GetDefaultOptions();
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 0;
|
||||
|
||||
Reopen(options);
|
||||
|
||||
// Write some key-value pairs with blob values and flush to create blob files.
|
||||
constexpr int kNumKeys = 1000;
|
||||
constexpr int kValueSize = 1024;
|
||||
|
||||
Random rnd(301);
|
||||
for (int i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(Put(Key(i), rnd.RandomString(kValueSize)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Verify blob files exist.
|
||||
std::vector<std::string> files;
|
||||
ASSERT_OK(env_->GetChildren(dbname_, &files));
|
||||
bool has_blob_files = false;
|
||||
for (const auto& f : files) {
|
||||
if (f.size() > 5 && f.substr(f.size() - 5) == ".blob") {
|
||||
has_blob_files = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(has_blob_files);
|
||||
|
||||
// Query the full range - all keys are covered.
|
||||
std::string start = Key(0);
|
||||
std::string end = Key(kNumKeys);
|
||||
Range r(start, end);
|
||||
|
||||
// Without include_blob_files (default behavior): should not include blob
|
||||
// file sizes.
|
||||
uint64_t size_without_blobs = 0;
|
||||
{
|
||||
SizeApproximationOptions size_approx_options;
|
||||
size_approx_options.include_files = true;
|
||||
size_approx_options.include_blob_files = false;
|
||||
ASSERT_OK(db_->GetApproximateSizes(size_approx_options,
|
||||
db_->DefaultColumnFamily(), &r, 1,
|
||||
&size_without_blobs));
|
||||
ASSERT_GT(size_without_blobs, 0);
|
||||
}
|
||||
|
||||
// With include_blob_files: should be strictly larger.
|
||||
{
|
||||
SizeApproximationOptions size_approx_options;
|
||||
size_approx_options.include_files = true;
|
||||
size_approx_options.include_blob_files = true;
|
||||
uint64_t size_with_blobs = 0;
|
||||
ASSERT_OK(db_->GetApproximateSizes(size_approx_options,
|
||||
db_->DefaultColumnFamily(), &r, 1,
|
||||
&size_with_blobs));
|
||||
ASSERT_GT(size_with_blobs, size_without_blobs);
|
||||
}
|
||||
|
||||
// Range that doesn't overlap any data should return 0.
|
||||
{
|
||||
std::string no_start = Key(kNumKeys + 100);
|
||||
std::string no_end = Key(kNumKeys + 200);
|
||||
Range no_r(no_start, no_end);
|
||||
SizeApproximationOptions size_approx_options;
|
||||
size_approx_options.include_files = true;
|
||||
size_approx_options.include_blob_files = true;
|
||||
uint64_t no_size = 0;
|
||||
ASSERT_OK(db_->GetApproximateSizes(
|
||||
size_approx_options, db_->DefaultColumnFamily(), &no_r, 1, &no_size));
|
||||
ASSERT_EQ(no_size, 0);
|
||||
}
|
||||
|
||||
// Partial range should return proportionally less blob size than full range.
|
||||
{
|
||||
SizeApproximationOptions size_approx_options;
|
||||
size_approx_options.include_files = true;
|
||||
size_approx_options.include_blob_files = true;
|
||||
|
||||
uint64_t full_size = 0;
|
||||
ASSERT_OK(db_->GetApproximateSizes(
|
||||
size_approx_options, db_->DefaultColumnFamily(), &r, 1, &full_size));
|
||||
|
||||
// Query roughly the first half of keys.
|
||||
std::string half_end = Key(kNumKeys / 2);
|
||||
Range half_r(start, half_end);
|
||||
uint64_t half_size = 0;
|
||||
ASSERT_OK(db_->GetApproximateSizes(size_approx_options,
|
||||
db_->DefaultColumnFamily(), &half_r, 1,
|
||||
&half_size));
|
||||
ASSERT_GT(half_size, 0);
|
||||
ASSERT_LT(half_size, full_size);
|
||||
}
|
||||
|
||||
// Via SizeApproximationFlags API.
|
||||
{
|
||||
uint64_t size_flags = 0;
|
||||
ASSERT_OK(db_->GetApproximateSizes(
|
||||
db_->DefaultColumnFamily(), &r, 1, &size_flags,
|
||||
DB::SizeApproximationFlags::INCLUDE_FILES |
|
||||
DB::SizeApproximationFlags::INCLUDE_BLOB_FILES));
|
||||
ASSERT_GT(size_flags, size_without_blobs);
|
||||
}
|
||||
|
||||
// Multi-range query: two non-overlapping sub-ranges should sum to
|
||||
// approximately the full-range result.
|
||||
{
|
||||
SizeApproximationOptions size_approx_options;
|
||||
size_approx_options.include_files = true;
|
||||
size_approx_options.include_blob_files = true;
|
||||
|
||||
std::string mid = Key(kNumKeys / 2);
|
||||
std::string r1_start = Key(0);
|
||||
std::string r1_end = mid;
|
||||
std::string r2_start = mid;
|
||||
std::string r2_end = Key(kNumKeys);
|
||||
Range ranges[2] = {Range(r1_start, r1_end), Range(r2_start, r2_end)};
|
||||
uint64_t sizes[2] = {0, 0};
|
||||
ASSERT_OK(db_->GetApproximateSizes(
|
||||
size_approx_options, db_->DefaultColumnFamily(), ranges, 2, sizes));
|
||||
// Each sub-range should return a positive size.
|
||||
ASSERT_GT(sizes[0], 0);
|
||||
ASSERT_GT(sizes[1], 0);
|
||||
// Sum of sub-ranges should be close to the full-range result.
|
||||
uint64_t full_size = 0;
|
||||
ASSERT_OK(db_->GetApproximateSizes(
|
||||
size_approx_options, db_->DefaultColumnFamily(), &r, 1, &full_size));
|
||||
ASSERT_NEAR(static_cast<double>(sizes[0] + sizes[1]),
|
||||
static_cast<double>(full_size), full_size * 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+64
-8
@@ -13,7 +13,10 @@
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
|
||||
#include "db/blob/blob_counting_iterator.h"
|
||||
#include "db/blob/blob_file_builder.h"
|
||||
#include "db/blob/blob_garbage_meter.h"
|
||||
#include "db/column_family.h"
|
||||
#include "db/compaction/compaction_iterator.h"
|
||||
#include "db/dbformat.h"
|
||||
#include "db/event_helpers.h"
|
||||
@@ -87,7 +90,8 @@ Status BuildTable(
|
||||
Env::WriteLifeTimeHint write_hint, const std::string* full_history_ts_low,
|
||||
BlobFileCompletionCallback* blob_callback, Version* version,
|
||||
uint64_t* memtable_payload_bytes, uint64_t* memtable_garbage_bytes,
|
||||
InternalStats::CompactionStats* flush_stats) {
|
||||
InternalStats::CompactionStats* flush_stats,
|
||||
std::vector<BlobFileGarbage>* blob_file_garbages, bool fast_sst_open) {
|
||||
assert((tboptions.column_family_id ==
|
||||
TablePropertiesCollectorFactory::Context::kUnknownColumnFamily) ==
|
||||
tboptions.column_family_name.empty());
|
||||
@@ -99,7 +103,24 @@ Status BuildTable(
|
||||
/*enable_hash=*/paranoid_file_checks);
|
||||
Status s;
|
||||
meta->fd.file_size = 0;
|
||||
iter->SeekToFirst();
|
||||
if (blob_file_garbages != nullptr) {
|
||||
blob_file_garbages->clear();
|
||||
}
|
||||
|
||||
InternalIterator* input = iter;
|
||||
std::unique_ptr<BlobGarbageMeter> blob_garbage_meter;
|
||||
std::unique_ptr<BlobCountingIterator> blob_counting_iter;
|
||||
if (blob_file_garbages != nullptr) {
|
||||
// Flush can drop blob-backed records via overwrite elision and flush-time
|
||||
// compaction filters. Track input and surviving output refs just like the
|
||||
// compaction path so the manifest learns about the new garbage.
|
||||
blob_garbage_meter.reset(new BlobGarbageMeter());
|
||||
blob_counting_iter.reset(
|
||||
new BlobCountingIterator(iter, blob_garbage_meter.get()));
|
||||
input = blob_counting_iter.get();
|
||||
}
|
||||
|
||||
input->SeekToFirst();
|
||||
std::unique_ptr<CompactionRangeDelAggregator> range_del_agg(
|
||||
new CompactionRangeDelAggregator(&tboptions.internal_comparator,
|
||||
snapshots, full_history_ts_low));
|
||||
@@ -128,7 +149,7 @@ Status BuildTable(
|
||||
|
||||
TableProperties tp;
|
||||
bool table_file_created = false;
|
||||
if (iter->Valid() || !range_del_agg->IsEmpty()) {
|
||||
if (input->Valid() || !range_del_agg->IsEmpty()) {
|
||||
std::unique_ptr<CompactionFilter> compaction_filter;
|
||||
if (ioptions.compaction_filter_factory != nullptr &&
|
||||
ioptions.compaction_filter_factory->ShouldFilterTableFileCreation(
|
||||
@@ -211,7 +232,7 @@ Status BuildTable(
|
||||
|
||||
const std::atomic<bool> kManualCompactionCanceledFalse{false};
|
||||
CompactionIterator c_iter(
|
||||
iter, ucmp, &merge, kMaxSequenceNumber, &snapshots, earliest_snapshot,
|
||||
input, ucmp, &merge, kMaxSequenceNumber, &snapshots, earliest_snapshot,
|
||||
earliest_write_conflict_snapshot, job_snapshot, snapshot_checker, env,
|
||||
ShouldReportDetailedTime(env, ioptions.stats), range_del_agg.get(),
|
||||
blob_file_builder.get(), ioptions.allow_data_in_errors,
|
||||
@@ -219,7 +240,17 @@ Status BuildTable(
|
||||
/*manual_compaction_canceled=*/kManualCompactionCanceledFalse,
|
||||
true /* must_count_input_entries */,
|
||||
/*compaction=*/nullptr, compaction_filter.get(),
|
||||
/*shutting_down=*/nullptr, db_options.info_log, full_history_ts_low);
|
||||
/*shutting_down=*/nullptr, db_options.info_log, full_history_ts_low,
|
||||
std::nullopt, version, tboptions.read_options.io_activity);
|
||||
|
||||
if (version != nullptr) {
|
||||
ColumnFamilyData* const cfd = version->cfd();
|
||||
c_iter.SetBlobFetcher(
|
||||
version, cfd != nullptr ? cfd->blob_file_cache() : nullptr,
|
||||
tboptions.read_options.io_activity,
|
||||
tboptions.reason == TableFileCreationReason::kFlush ||
|
||||
tboptions.reason == TableFileCreationReason::kRecovery);
|
||||
}
|
||||
|
||||
SequenceNumber smallest_preferred_seqno = kMaxSequenceNumber;
|
||||
std::string key_after_flush_buf;
|
||||
@@ -268,6 +299,14 @@ Status BuildTable(
|
||||
flush_stats->num_output_records++;
|
||||
}
|
||||
|
||||
if (blob_garbage_meter != nullptr) {
|
||||
s = blob_garbage_meter->ProcessOutFlow(key_after_flush,
|
||||
value_after_flush);
|
||||
if (!s.ok()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
s = meta->UpdateBoundaries(key_after_flush, value_after_flush,
|
||||
ikey.sequence, ikey.type);
|
||||
if (!s.ok()) {
|
||||
@@ -457,6 +496,8 @@ Status BuildTable(
|
||||
// here because this is a special case after we finish the table building.
|
||||
// No matter whether use_direct_io_for_flush_and_compaction is true,
|
||||
// the goal is to cache it here for further user reads.
|
||||
std::string* file_open_metadata_ptr =
|
||||
fast_sst_open ? &meta->file_open_metadata : nullptr;
|
||||
std::unique_ptr<InternalIterator> it(table_cache->NewIterator(
|
||||
tboptions.read_options, file_options, tboptions.internal_comparator,
|
||||
*meta, nullptr /* range_del_agg */, mutable_cf_options, nullptr,
|
||||
@@ -467,7 +508,10 @@ Status BuildTable(
|
||||
MaxFileSizeForL0MetaPin(mutable_cf_options),
|
||||
/*smallest_compaction_key=*/nullptr,
|
||||
/*largest_compaction_key*/ nullptr,
|
||||
/*allow_unprepared_value*/ false));
|
||||
/*allow_unprepared_value*/ false,
|
||||
/*range_del_read_seqno=*/nullptr,
|
||||
/*range_del_iter=*/nullptr,
|
||||
/*maybe_pin_table_handle=*/false, file_open_metadata_ptr));
|
||||
s = it->status();
|
||||
if (s.ok() && paranoid_file_checks) {
|
||||
OutputValidator file_validator(tboptions.internal_comparator,
|
||||
@@ -485,8 +529,20 @@ Status BuildTable(
|
||||
}
|
||||
|
||||
// Check for input iterator errors
|
||||
if (!iter->status().ok()) {
|
||||
s = iter->status();
|
||||
if (!input->status().ok()) {
|
||||
s = input->status();
|
||||
}
|
||||
|
||||
if (s.ok() && blob_file_garbages != nullptr &&
|
||||
blob_garbage_meter != nullptr) {
|
||||
for (const auto& pair : blob_garbage_meter->flows()) {
|
||||
const uint64_t blob_file_number = pair.first;
|
||||
const BlobGarbageMeter::BlobInOutFlow& flow = pair.second;
|
||||
if (flow.HasGarbage()) {
|
||||
blob_file_garbages->emplace_back(
|
||||
blob_file_number, flow.GetGarbageCount(), flow.GetGarbageBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.ok() || meta->fd.GetFileSize() == 0) {
|
||||
|
||||
+4
-1
@@ -31,6 +31,7 @@ struct FileMetaData;
|
||||
|
||||
class VersionSet;
|
||||
class BlobFileAddition;
|
||||
class BlobFileGarbage;
|
||||
class SnapshotChecker;
|
||||
class TableCache;
|
||||
class TableBuilder;
|
||||
@@ -79,6 +80,8 @@ Status BuildTable(
|
||||
BlobFileCompletionCallback* blob_callback = nullptr,
|
||||
Version* version = nullptr, uint64_t* memtable_payload_bytes = nullptr,
|
||||
uint64_t* memtable_garbage_bytes = nullptr,
|
||||
InternalStats::CompactionStats* flush_stats = nullptr);
|
||||
InternalStats::CompactionStats* flush_stats = nullptr,
|
||||
std::vector<BlobFileGarbage>* blob_file_garbages = nullptr,
|
||||
bool fast_sst_open = false);
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
@@ -3056,6 +3056,7 @@ class H : public WriteBatch::Handler {
|
||||
(*log_data_)(state_, blob.data(), blob.size());
|
||||
}
|
||||
}
|
||||
Status MarkNoop(bool /* empty_batch */) override { return Status::OK(); }
|
||||
};
|
||||
|
||||
class HCF : public WriteBatch::Handler {
|
||||
@@ -3089,6 +3090,7 @@ class HCF : public WriteBatch::Handler {
|
||||
(*log_data_)(state_, blob.data(), blob.size());
|
||||
}
|
||||
}
|
||||
Status MarkNoop(bool /* empty_batch */) override { return Status::OK(); }
|
||||
};
|
||||
|
||||
void rocksdb_writebatch_iterate(rocksdb_writebatch_t* b, void* state,
|
||||
@@ -3769,6 +3771,11 @@ void rocksdb_block_based_options_set_index_block_search_type(
|
||||
static_cast<BlockBasedTableOptions::BlockSearchType>(v);
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_uniform_cv_threshold(
|
||||
rocksdb_block_based_table_options_t* options, double v) {
|
||||
options->rep.uniform_cv_threshold = v;
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_data_block_hash_ratio(
|
||||
rocksdb_block_based_table_options_t* options, double v) {
|
||||
options->rep.data_block_hash_table_util_ratio = v;
|
||||
@@ -4648,6 +4655,16 @@ uint32_t rocksdb_options_get_memtable_avg_op_scan_flush_trigger(
|
||||
return opt->rep.memtable_avg_op_scan_flush_trigger;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_min_tombstones_for_range_conversion(
|
||||
rocksdb_options_t* opt, uint32_t n) {
|
||||
opt->rep.min_tombstones_for_range_conversion = n;
|
||||
}
|
||||
|
||||
uint32_t rocksdb_options_get_min_tombstones_for_range_conversion(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.min_tombstones_for_range_conversion;
|
||||
}
|
||||
|
||||
void rocksdb_options_enable_statistics(rocksdb_options_t* opt) {
|
||||
opt->rep.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
}
|
||||
@@ -4747,6 +4764,26 @@ double rocksdb_options_get_blob_gc_force_threshold(rocksdb_options_t* opt) {
|
||||
return opt->rep.blob_garbage_collection_force_threshold;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_read_triggered_compaction_threshold(
|
||||
rocksdb_options_t* opt, double val) {
|
||||
opt->rep.read_triggered_compaction_threshold = val;
|
||||
}
|
||||
|
||||
double rocksdb_options_get_read_triggered_compaction_threshold(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.read_triggered_compaction_threshold;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_max_compaction_trigger_wakeup_seconds(
|
||||
rocksdb_options_t* opt, uint64_t val) {
|
||||
opt->rep.max_compaction_trigger_wakeup_seconds = val;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_options_get_max_compaction_trigger_wakeup_seconds(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.max_compaction_trigger_wakeup_seconds;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_blob_compaction_readahead_size(rocksdb_options_t* opt,
|
||||
uint64_t val) {
|
||||
opt->rep.blob_compaction_readahead_size = val;
|
||||
@@ -5254,6 +5291,15 @@ size_t rocksdb_options_get_recycle_log_file_num(rocksdb_options_t* opt) {
|
||||
return opt->rep.recycle_log_file_num;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_async_wal_precreate(rocksdb_options_t* opt,
|
||||
unsigned char v) {
|
||||
opt->rep.async_wal_precreate = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_async_wal_precreate(rocksdb_options_t* opt) {
|
||||
return opt->rep.async_wal_precreate;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_soft_pending_compaction_bytes_limit(
|
||||
rocksdb_options_t* opt, size_t v) {
|
||||
opt->rep.soft_pending_compaction_bytes_limit = v;
|
||||
@@ -5357,6 +5403,16 @@ size_t rocksdb_options_get_memtable_huge_page_size(rocksdb_options_t* opt) {
|
||||
return opt->rep.memtable_huge_page_size;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_memtable_batch_lookup_optimization(
|
||||
rocksdb_options_t* opt, unsigned char v) {
|
||||
opt->rep.memtable_batch_lookup_optimization = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_memtable_batch_lookup_optimization(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.memtable_batch_lookup_optimization;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_hash_skip_list_rep(rocksdb_options_t* opt,
|
||||
size_t bucket_count,
|
||||
int32_t skiplist_height,
|
||||
@@ -5788,6 +5844,8 @@ uint64_t rocksdb_perfcontext_metric(rocksdb_perfcontext_t* context,
|
||||
return rep->number_async_seek;
|
||||
case rocksdb_blob_cache_hit_count:
|
||||
return rep->blob_cache_hit_count;
|
||||
case rocksdb_blob_cache_read_byte:
|
||||
return rep->blob_cache_read_byte;
|
||||
case rocksdb_blob_read_count:
|
||||
return rep->blob_read_count;
|
||||
case rocksdb_blob_read_byte:
|
||||
@@ -6206,6 +6264,16 @@ unsigned char rocksdb_readoptions_get_async_io(rocksdb_readoptions_t* opt) {
|
||||
return opt->rep.async_io;
|
||||
}
|
||||
|
||||
void rocksdb_readoptions_set_optimize_multiget_for_io(
|
||||
rocksdb_readoptions_t* opt, unsigned char v) {
|
||||
opt->rep.optimize_multiget_for_io = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_readoptions_get_optimize_multiget_for_io(
|
||||
rocksdb_readoptions_t* opt) {
|
||||
return opt->rep.optimize_multiget_for_io;
|
||||
}
|
||||
|
||||
void rocksdb_readoptions_set_timestamp(rocksdb_readoptions_t* opt,
|
||||
const char* ts, size_t tslen) {
|
||||
if (ts == nullptr) {
|
||||
@@ -8284,6 +8352,11 @@ void rocksdb_transaction_delete_cf(
|
||||
SaveError(errptr, txn->rep->Delete(column_family->rep, Slice(key, klen)));
|
||||
}
|
||||
|
||||
void rocksdb_transaction_put_log_data(rocksdb_transaction_t* txn,
|
||||
const char* blob, size_t len) {
|
||||
txn->rep->PutLogData(Slice(blob, len));
|
||||
}
|
||||
|
||||
// Delete a key outside a transaction
|
||||
void rocksdb_transactiondb_delete(rocksdb_transactiondb_t* txn_db,
|
||||
const rocksdb_writeoptions_t* options,
|
||||
|
||||
+87
@@ -205,6 +205,27 @@ static void CheckDel(void* ptr, const char* k, size_t klen) {
|
||||
(*state)++;
|
||||
}
|
||||
|
||||
static void NopPut(void* ptr, const char* k, size_t klen, const char* v,
|
||||
size_t vlen) {
|
||||
(void)ptr;
|
||||
(void)k;
|
||||
(void)klen;
|
||||
(void)v;
|
||||
(void)vlen;
|
||||
}
|
||||
|
||||
static void NopDel(void* ptr, const char* k, size_t klen) {
|
||||
(void)ptr;
|
||||
(void)k;
|
||||
(void)klen;
|
||||
}
|
||||
|
||||
static void CheckLogData(void* ptr, const char* blob, size_t blen) {
|
||||
CheckEqual("log_blob", blob, blen);
|
||||
int* found = (int*)ptr;
|
||||
*found = 1;
|
||||
}
|
||||
|
||||
// Callback from rocksdb_writebatch_iterate_cf()
|
||||
static void CheckPutCF(void* ptr, uint32_t cfid, const char* k, size_t klen,
|
||||
const char* v, size_t vlen) {
|
||||
@@ -867,6 +888,9 @@ int main(int argc, char** argv) {
|
||||
rocksdb_block_based_options_set_block_cache(table_options, cache);
|
||||
rocksdb_block_based_options_set_data_block_index_type(table_options, 1);
|
||||
rocksdb_block_based_options_set_data_block_hash_ratio(table_options, 0.75);
|
||||
rocksdb_block_based_options_set_index_block_search_type(
|
||||
table_options, rocksdb_block_based_table_index_block_search_type_auto);
|
||||
rocksdb_block_based_options_set_uniform_cv_threshold(table_options, 0.2);
|
||||
rocksdb_block_based_options_set_top_level_index_pinning_tier(table_options,
|
||||
1);
|
||||
rocksdb_block_based_options_set_partition_pinning_tier(table_options, 2);
|
||||
@@ -2643,6 +2667,10 @@ int main(int argc, char** argv) {
|
||||
CheckCondition(150 ==
|
||||
rocksdb_options_get_memtable_avg_op_scan_flush_trigger(o));
|
||||
|
||||
rocksdb_options_set_min_tombstones_for_range_conversion(o, 200);
|
||||
CheckCondition(200 ==
|
||||
rocksdb_options_get_min_tombstones_for_range_conversion(o));
|
||||
|
||||
rocksdb_options_set_ttl(o, 5000);
|
||||
CheckCondition(5000 == rocksdb_options_get_ttl(o));
|
||||
|
||||
@@ -2787,6 +2815,15 @@ int main(int argc, char** argv) {
|
||||
rocksdb_options_set_memtable_huge_page_size(o, 25);
|
||||
CheckCondition(25 == rocksdb_options_get_memtable_huge_page_size(o));
|
||||
|
||||
// memtable_batch_lookup_optimization defaults to 0; flip to 1 to verify
|
||||
// the setter is wired through to the underlying C++ ColumnFamilyOptions
|
||||
// field.
|
||||
CheckCondition(0 ==
|
||||
rocksdb_options_get_memtable_batch_lookup_optimization(o));
|
||||
rocksdb_options_set_memtable_batch_lookup_optimization(o, 1);
|
||||
CheckCondition(1 ==
|
||||
rocksdb_options_get_memtable_batch_lookup_optimization(o));
|
||||
|
||||
rocksdb_options_set_max_successive_merges(o, 26);
|
||||
CheckCondition(26 == rocksdb_options_get_max_successive_merges(o));
|
||||
|
||||
@@ -2844,6 +2881,9 @@ int main(int argc, char** argv) {
|
||||
rocksdb_options_set_track_and_verify_wals_in_manifest(o, 42);
|
||||
CheckCondition(1 ==
|
||||
rocksdb_options_get_track_and_verify_wals_in_manifest(o));
|
||||
CheckCondition(0 == rocksdb_options_get_async_wal_precreate(o));
|
||||
rocksdb_options_set_async_wal_precreate(o, 1);
|
||||
CheckCondition(1 == rocksdb_options_get_async_wal_precreate(o));
|
||||
|
||||
/* Blob Options */
|
||||
rocksdb_options_set_enable_blob_files(o, 1);
|
||||
@@ -2867,6 +2907,14 @@ int main(int argc, char** argv) {
|
||||
rocksdb_options_set_blob_gc_force_threshold(o, 0.75);
|
||||
CheckCondition(0.75 == rocksdb_options_get_blob_gc_force_threshold(o));
|
||||
|
||||
rocksdb_options_set_read_triggered_compaction_threshold(o, 0.001);
|
||||
CheckCondition(0.001 ==
|
||||
rocksdb_options_get_read_triggered_compaction_threshold(o));
|
||||
|
||||
rocksdb_options_set_max_compaction_trigger_wakeup_seconds(o, 3600);
|
||||
CheckCondition(
|
||||
3600 == rocksdb_options_get_max_compaction_trigger_wakeup_seconds(o));
|
||||
|
||||
rocksdb_options_set_blob_compaction_readahead_size(o, 262144);
|
||||
CheckCondition(262144 ==
|
||||
rocksdb_options_get_blob_compaction_readahead_size(o));
|
||||
@@ -2962,6 +3010,8 @@ int main(int argc, char** argv) {
|
||||
rocksdb_options_get_memtable_prefix_bloom_size_ratio(copy));
|
||||
CheckCondition(24 == rocksdb_options_get_max_compaction_bytes(copy));
|
||||
CheckCondition(25 == rocksdb_options_get_memtable_huge_page_size(copy));
|
||||
CheckCondition(
|
||||
1 == rocksdb_options_get_memtable_batch_lookup_optimization(copy));
|
||||
CheckCondition(26 == rocksdb_options_get_max_successive_merges(copy));
|
||||
CheckCondition(27 == rocksdb_options_get_bloom_locality(copy));
|
||||
CheckCondition(1 == rocksdb_options_get_inplace_update_support(copy));
|
||||
@@ -3086,6 +3136,12 @@ int main(int argc, char** argv) {
|
||||
CheckCondition(150 ==
|
||||
rocksdb_options_get_memtable_avg_op_scan_flush_trigger(o));
|
||||
|
||||
rocksdb_options_set_min_tombstones_for_range_conversion(copy, 1000);
|
||||
CheckCondition(
|
||||
1000 == rocksdb_options_get_min_tombstones_for_range_conversion(copy));
|
||||
CheckCondition(200 ==
|
||||
rocksdb_options_get_min_tombstones_for_range_conversion(o));
|
||||
|
||||
rocksdb_options_set_ttl(copy, 8000);
|
||||
CheckCondition(8000 == rocksdb_options_get_ttl(copy));
|
||||
CheckCondition(5000 == rocksdb_options_get_ttl(o));
|
||||
@@ -3390,6 +3446,12 @@ int main(int argc, char** argv) {
|
||||
rocksdb_readoptions_set_async_io(ro, 1);
|
||||
CheckCondition(1 == rocksdb_readoptions_get_async_io(ro));
|
||||
|
||||
// optimize_multiget_for_io defaults to true (1); flip to 0 to verify the
|
||||
// setter is wired through to the underlying C++ ReadOptions field.
|
||||
CheckCondition(1 == rocksdb_readoptions_get_optimize_multiget_for_io(ro));
|
||||
rocksdb_readoptions_set_optimize_multiget_for_io(ro, 0);
|
||||
CheckCondition(0 == rocksdb_readoptions_get_optimize_multiget_for_io(ro));
|
||||
|
||||
rocksdb_readoptions_destroy(ro);
|
||||
}
|
||||
|
||||
@@ -3850,10 +3912,35 @@ int main(int argc, char** argv) {
|
||||
CheckMultiGetValues(3, vals, vals_sizes, errs, expected);
|
||||
}
|
||||
|
||||
rocksdb_transaction_put_log_data(txn, "log_blob", 8);
|
||||
// record sequence number before commit so we can scan the WAL after
|
||||
rocksdb_t* base_db_ld = rocksdb_transactiondb_get_base_db(txn_db);
|
||||
uint64_t seq_before = rocksdb_get_latest_sequence_number(base_db_ld);
|
||||
|
||||
// commit
|
||||
rocksdb_transaction_commit(txn, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
// verify log data was written to WAL by scanning batches since seq_before
|
||||
{
|
||||
rocksdb_wal_iterator_t* wal_iter =
|
||||
rocksdb_get_updates_since(base_db_ld, seq_before, NULL, &err);
|
||||
CheckNoError(err);
|
||||
int log_found = 0;
|
||||
for (; rocksdb_wal_iter_valid(wal_iter) && !log_found;
|
||||
rocksdb_wal_iter_next(wal_iter)) {
|
||||
uint64_t seq;
|
||||
rocksdb_writebatch_t* wal_wb =
|
||||
rocksdb_wal_iter_get_batch(wal_iter, &seq);
|
||||
rocksdb_writebatch_iterate_ld(wal_wb, &log_found, NopPut, NopDel,
|
||||
CheckLogData);
|
||||
rocksdb_writebatch_destroy(wal_wb);
|
||||
}
|
||||
CheckCondition(log_found == 1);
|
||||
rocksdb_wal_iter_destroy(wal_iter);
|
||||
}
|
||||
rocksdb_transactiondb_close_base_db(base_db_ld);
|
||||
|
||||
// read from outside transaction, after commit
|
||||
CheckTxnDBGet(txn_db, roptions, "foo", "hello");
|
||||
CheckTxnDBPinGet(txn_db, roptions, "foo", "hello");
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace ROCKSDB_NAMESPACE {
|
||||
void CoalescingIterator::Coalesce(
|
||||
const autovector<MultiCfIteratorInfo>& items) {
|
||||
assert(wide_columns_.empty());
|
||||
assert(owned_columns_.empty());
|
||||
MinHeap heap;
|
||||
for (const auto& item : items) {
|
||||
assert(item.iterator);
|
||||
@@ -22,13 +23,22 @@ void CoalescingIterator::Coalesce(
|
||||
if (heap.empty()) {
|
||||
return;
|
||||
}
|
||||
// Own the coalesced bytes so the result stays valid even if a child iterator
|
||||
// refreshes or reuses its internal buffers.
|
||||
owned_columns_.reserve(heap.size());
|
||||
wide_columns_.reserve(heap.size());
|
||||
auto add_column = [this](const WideColumn& column) {
|
||||
owned_columns_.emplace_back(column.name().ToString(),
|
||||
column.value().ToString());
|
||||
const auto& owned = owned_columns_.back();
|
||||
wide_columns_.emplace_back(owned.first, owned.second);
|
||||
};
|
||||
auto current = heap.top();
|
||||
heap.pop();
|
||||
while (!heap.empty()) {
|
||||
int comparison = current.column->name().compare(heap.top().column->name());
|
||||
if (comparison < 0) {
|
||||
wide_columns_.push_back(*current.column);
|
||||
add_column(*current.column);
|
||||
} else if (comparison > 0) {
|
||||
// Shouldn't reach here.
|
||||
// Current item in the heap is greater than the top item in the min heap
|
||||
@@ -37,7 +47,7 @@ void CoalescingIterator::Coalesce(
|
||||
current = heap.top();
|
||||
heap.pop();
|
||||
}
|
||||
wide_columns_.push_back(*current.column);
|
||||
add_column(*current.column);
|
||||
|
||||
if (WideColumnsHelper::HasDefaultColumn(wide_columns_)) {
|
||||
value_ = WideColumnsHelper::GetDefaultColumn(wide_columns_);
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "db/multi_cf_iterator_impl.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -45,6 +49,7 @@ class CoalescingIterator : public Iterator {
|
||||
void Reset() {
|
||||
value_.clear();
|
||||
wide_columns_.clear();
|
||||
owned_columns_.clear();
|
||||
}
|
||||
|
||||
bool PrepareValue() override { return impl_.PrepareValue(); }
|
||||
@@ -79,6 +84,7 @@ class CoalescingIterator : public Iterator {
|
||||
MultiCfIteratorImpl<ResetFunc, PopulateFunc> impl_;
|
||||
Slice value_;
|
||||
WideColumns wide_columns_;
|
||||
std::vector<std::pair<std::string, std::string>> owned_columns_;
|
||||
|
||||
struct WideColumnWithOrder {
|
||||
const WideColumn* column;
|
||||
|
||||
+63
-14
@@ -11,12 +11,14 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "db/blob/blob_file_cache.h"
|
||||
#include "db/blob/blob_file_partition_manager.h"
|
||||
#include "db/blob/blob_source.h"
|
||||
#include "db/compaction/compaction_picker.h"
|
||||
#include "db/compaction/compaction_picker_fifo.h"
|
||||
@@ -495,6 +497,15 @@ ColumnFamilyOptions SanitizeCfOptions(const ImmutableDBOptions& db_options,
|
||||
result.memtable_avg_op_scan_flush_trigger = 0;
|
||||
}
|
||||
}
|
||||
if (result.enable_blob_direct_write && !result.enable_blob_files) {
|
||||
ROCKS_LOG_WARN(db_options.info_log.get(),
|
||||
"enable_blob_direct_write requires enable_blob_files=true. "
|
||||
"Disabling blob direct write.");
|
||||
result.enable_blob_direct_write = false;
|
||||
}
|
||||
if (result.blob_direct_write_partitions == 0) {
|
||||
result.blob_direct_write_partitions = 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -601,7 +612,7 @@ ColumnFamilyData::ColumnFamilyData(
|
||||
const FileOptions* file_options, ColumnFamilySet* column_family_set,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
|
||||
const std::string& db_session_id, bool read_only)
|
||||
const std::string& db_session_id, bool read_only, bool fast_sst_open)
|
||||
: id_(id),
|
||||
name_(name),
|
||||
dummy_versions_(_dummy_versions),
|
||||
@@ -661,7 +672,7 @@ ColumnFamilyData::ColumnFamilyData(
|
||||
new InternalStats(ioptions_.num_levels, ioptions_.clock, this));
|
||||
table_cache_.reset(new TableCache(ioptions_, file_options, _table_cache,
|
||||
block_cache_tracer, io_tracer,
|
||||
db_session_id));
|
||||
db_session_id, fast_sst_open));
|
||||
blob_file_cache_.reset(
|
||||
new BlobFileCache(_table_cache, &ioptions(), soptions(), id_,
|
||||
internal_stats_->GetBlobFileReadHist(), io_tracer));
|
||||
@@ -782,6 +793,11 @@ ColumnFamilyData::~ColumnFamilyData() {
|
||||
}
|
||||
}
|
||||
|
||||
void ColumnFamilyData::SetBlobPartitionManager(
|
||||
std::shared_ptr<BlobFilePartitionManager> mgr) {
|
||||
blob_partition_manager_ = std::move(mgr);
|
||||
}
|
||||
|
||||
bool ColumnFamilyData::UnrefAndTryDelete() {
|
||||
int old_refs = refs_.fetch_sub(1);
|
||||
assert(old_refs > 0);
|
||||
@@ -1523,6 +1539,32 @@ Status ColumnFamilyData::ValidateOptions(
|
||||
|
||||
const auto* ucmp = cf_options.comparator;
|
||||
assert(ucmp);
|
||||
if (cf_options.enable_blob_direct_write) {
|
||||
if (db_options.enable_pipelined_write) {
|
||||
return Status::NotSupported(
|
||||
"Blob direct write v1 does not support pipelined writes.");
|
||||
}
|
||||
if (db_options.allow_concurrent_memtable_write) {
|
||||
return Status::NotSupported(
|
||||
"Blob direct write v1 does not support concurrent memtable writes.");
|
||||
}
|
||||
if (db_options.unordered_write) {
|
||||
return Status::NotSupported(
|
||||
"Blob direct write v1 does not support unordered writes.");
|
||||
}
|
||||
if (db_options.two_write_queues) {
|
||||
return Status::NotSupported(
|
||||
"Blob direct write v1 does not support two write queues.");
|
||||
}
|
||||
if (cf_options.experimental_mempurge_threshold > 0.0) {
|
||||
return Status::NotSupported(
|
||||
"Blob direct write does not support MemPurge.");
|
||||
}
|
||||
if (ucmp->timestamp_size() > 0) {
|
||||
return Status::NotSupported(
|
||||
"Blob direct write does not support user-defined timestamps.");
|
||||
}
|
||||
}
|
||||
if (ucmp->timestamp_size() > 0 &&
|
||||
!cf_options.persist_user_defined_timestamps) {
|
||||
if (db_options.atomic_flush) {
|
||||
@@ -1563,6 +1605,14 @@ Status ColumnFamilyData::ValidateOptions(
|
||||
}
|
||||
}
|
||||
|
||||
if (cf_options.read_triggered_compaction_threshold < 0.0 ||
|
||||
std::isnan(cf_options.read_triggered_compaction_threshold) ||
|
||||
std::isinf(cf_options.read_triggered_compaction_threshold)) {
|
||||
return Status::InvalidArgument(
|
||||
"read_triggered_compaction_threshold must be >= 0.0 and finite. "
|
||||
"Use 0.0 to disable.");
|
||||
}
|
||||
|
||||
if (cf_options.compaction_style == kCompactionStyleFIFO &&
|
||||
db_options.max_open_files != -1 && cf_options.ttl > 0) {
|
||||
return Status::NotSupported(
|
||||
@@ -1759,16 +1809,14 @@ void ColumnFamilyData::RecoverEpochNumbers() {
|
||||
vstorage->RecoverEpochNumbers(this);
|
||||
}
|
||||
|
||||
ColumnFamilySet::ColumnFamilySet(const std::string& dbname,
|
||||
const ImmutableDBOptions* db_options,
|
||||
const FileOptions& file_options,
|
||||
Cache* table_cache,
|
||||
WriteBufferManager* _write_buffer_manager,
|
||||
WriteController* _write_controller,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::string& db_id,
|
||||
const std::string& db_session_id)
|
||||
ColumnFamilySet::ColumnFamilySet(
|
||||
const std::string& dbname, const ImmutableDBOptions* db_options,
|
||||
const FileOptions& file_options, Cache* table_cache,
|
||||
WriteBufferManager* _write_buffer_manager,
|
||||
WriteController* _write_controller,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
|
||||
const std::string& db_session_id, bool fast_sst_open)
|
||||
: max_column_family_(0),
|
||||
file_options_(file_options),
|
||||
dummy_cfd_(new ColumnFamilyData(
|
||||
@@ -1785,7 +1833,8 @@ ColumnFamilySet::ColumnFamilySet(const std::string& dbname,
|
||||
block_cache_tracer_(block_cache_tracer),
|
||||
io_tracer_(io_tracer),
|
||||
db_id_(db_id),
|
||||
db_session_id_(db_session_id) {
|
||||
db_session_id_(db_session_id),
|
||||
fast_sst_open_(fast_sst_open) {
|
||||
// initialize linked list
|
||||
dummy_cfd_->prev_ = dummy_cfd_;
|
||||
dummy_cfd_->next_ = dummy_cfd_;
|
||||
@@ -1852,7 +1901,7 @@ ColumnFamilyData* ColumnFamilySet::CreateColumnFamily(
|
||||
ColumnFamilyData* new_cfd = new ColumnFamilyData(
|
||||
id, name, dummy_versions, table_cache_, write_buffer_manager_, options,
|
||||
*db_options_, &file_options_, this, block_cache_tracer_, io_tracer_,
|
||||
db_id_, db_session_id_, read_only);
|
||||
db_id_, db_session_id_, read_only, fast_sst_open_);
|
||||
column_families_.insert({name, id});
|
||||
column_family_data_.insert({id, new_cfd});
|
||||
auto ucmp = new_cfd->user_comparator();
|
||||
|
||||
+41
-9
@@ -49,6 +49,7 @@ class InstrumentedMutex;
|
||||
class InstrumentedMutexLock;
|
||||
struct SuperVersionContext;
|
||||
class BlobFileCache;
|
||||
class BlobFilePartitionManager;
|
||||
class BlobSource;
|
||||
|
||||
extern const double kIncSlowdownRatio;
|
||||
@@ -415,6 +416,19 @@ class ColumnFamilyData {
|
||||
TableCache* table_cache() const { return table_cache_.get(); }
|
||||
BlobFileCache* blob_file_cache() const { return blob_file_cache_.get(); }
|
||||
BlobSource* blob_source() const { return blob_source_.get(); }
|
||||
// Returns the write-path blob partition manager for this CF, or null if BDW
|
||||
// is disabled.
|
||||
BlobFilePartitionManager* blob_partition_manager() const {
|
||||
return blob_partition_manager_.get();
|
||||
}
|
||||
// Returns a shared ownership handle for cold paths that must keep the
|
||||
// manager alive beyond the lifetime of this ColumnFamilyData.
|
||||
std::shared_ptr<BlobFilePartitionManager> blob_partition_manager_handle()
|
||||
const {
|
||||
return blob_partition_manager_;
|
||||
}
|
||||
// Installs the write-path blob partition manager for this CF.
|
||||
void SetBlobPartitionManager(std::shared_ptr<BlobFilePartitionManager> mgr);
|
||||
|
||||
// See documentation in compaction_picker.h
|
||||
// REQUIRES: DB mutex held
|
||||
@@ -605,16 +619,23 @@ class ColumnFamilyData {
|
||||
return ioptions_.cf_allow_ingest_behind || ioptions_.allow_ingest_behind;
|
||||
}
|
||||
|
||||
// Per-CF reader-writer lock that serializes IngestExternalFiles with range
|
||||
// tombstone conversion.
|
||||
port::RWMutex& GetIngestSstLock() { return ingest_sst_lock_; }
|
||||
|
||||
private:
|
||||
friend class ColumnFamilySet;
|
||||
ColumnFamilyData(
|
||||
uint32_t id, const std::string& name, Version* dummy_versions,
|
||||
Cache* table_cache, WriteBufferManager* write_buffer_manager,
|
||||
const ColumnFamilyOptions& options, const ImmutableDBOptions& db_options,
|
||||
const FileOptions* file_options, ColumnFamilySet* column_family_set,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
|
||||
const std::string& db_session_id, bool read_only);
|
||||
ColumnFamilyData(uint32_t id, const std::string& name,
|
||||
Version* dummy_versions, Cache* table_cache,
|
||||
WriteBufferManager* write_buffer_manager,
|
||||
const ColumnFamilyOptions& options,
|
||||
const ImmutableDBOptions& db_options,
|
||||
const FileOptions* file_options,
|
||||
ColumnFamilySet* column_family_set,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::string& db_id, const std::string& db_session_id,
|
||||
bool read_only, bool fast_sst_open = false);
|
||||
|
||||
std::vector<std::string> GetDbPaths() const;
|
||||
|
||||
@@ -648,6 +669,8 @@ class ColumnFamilyData {
|
||||
std::unique_ptr<TableCache> table_cache_;
|
||||
std::unique_ptr<BlobFileCache> blob_file_cache_;
|
||||
std::unique_ptr<BlobSource> blob_source_;
|
||||
// Per-CF manager for write-path blob direct-write files.
|
||||
std::shared_ptr<BlobFilePartitionManager> blob_partition_manager_;
|
||||
|
||||
std::unique_ptr<InternalStats> internal_stats_;
|
||||
|
||||
@@ -713,6 +736,10 @@ class ColumnFamilyData {
|
||||
bool mempurge_used_;
|
||||
|
||||
std::atomic<uint64_t> next_epoch_number_;
|
||||
|
||||
// Used to synchronize IngestExternalFile with range tombstone conversion. See
|
||||
// also Memtable::ingest_seqno_barrier_.
|
||||
port::RWMutex ingest_sst_lock_;
|
||||
};
|
||||
|
||||
// ColumnFamilySet has interesting thread-safety requirements
|
||||
@@ -758,7 +785,8 @@ class ColumnFamilySet {
|
||||
WriteController* _write_controller,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::string& db_id, const std::string& db_session_id);
|
||||
const std::string& db_id, const std::string& db_session_id,
|
||||
bool fast_sst_open = false);
|
||||
~ColumnFamilySet();
|
||||
|
||||
ColumnFamilyData* GetDefault() const;
|
||||
@@ -794,6 +822,9 @@ class ColumnFamilySet {
|
||||
|
||||
Cache* get_table_cache() { return table_cache_; }
|
||||
|
||||
bool GetFastSstOpen() const { return fast_sst_open_; }
|
||||
void SetFastSstOpen(bool v) { fast_sst_open_ = v; }
|
||||
|
||||
WriteBufferManager* write_buffer_manager() { return write_buffer_manager_; }
|
||||
|
||||
WriteController* write_controller() { return write_controller_; }
|
||||
@@ -842,6 +873,7 @@ class ColumnFamilySet {
|
||||
std::shared_ptr<IOTracer> io_tracer_;
|
||||
const std::string& db_id_;
|
||||
std::string db_session_id_;
|
||||
bool fast_sst_open_;
|
||||
};
|
||||
|
||||
// A wrapper for ColumnFamilySet that supports releasing DB mutex during each
|
||||
|
||||
@@ -734,6 +734,42 @@ TEST_P(ColumnFamilyTest, AddDrop) {
|
||||
std::vector<std::string>({"default", "four", "three"}));
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, EmptyNameRejected) {
|
||||
// Creating a column family with an empty-string name should be rejected with
|
||||
// InvalidArgument. Empty strings are reserved by various RocksDB APIs and
|
||||
// serialization formats to mean "unknown/unspecified column family" (e.g.
|
||||
// TablePropertiesCollectorFactory::Context::kUnknownColumnFamily, table
|
||||
// properties).
|
||||
//
|
||||
// Prior behavior was silently broken: CreateColumnFamily("") would return
|
||||
// OK and a usable handle, but the CF was not persisted in the manifest --
|
||||
// data written to it was lost on DB reopen.
|
||||
Open();
|
||||
ColumnFamilyHandle* handle = nullptr;
|
||||
Status s =
|
||||
db_->CreateColumnFamily(column_family_options_, std::string(), &handle);
|
||||
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
|
||||
ASSERT_EQ(handle, nullptr);
|
||||
|
||||
// Same via the bulk-by-names API. The first valid name should still get
|
||||
// created; the failure should come on the empty one.
|
||||
std::vector<ColumnFamilyHandle*> handles;
|
||||
s = db_->CreateColumnFamilies(column_family_options_,
|
||||
{"ok_name", "", "another"}, &handles);
|
||||
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
|
||||
// Clean up the partially-created handle ("ok_name") before the failure.
|
||||
ASSERT_EQ(handles.size(), 1U);
|
||||
ASSERT_OK(db_->DropColumnFamily(handles[0]));
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(handles[0]));
|
||||
handles.clear();
|
||||
|
||||
// Same via the bulk-by-descriptor API.
|
||||
s = db_->CreateColumnFamilies(
|
||||
{ColumnFamilyDescriptor("", column_family_options_)}, &handles);
|
||||
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
|
||||
ASSERT_TRUE(handles.empty());
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, BulkAddDrop) {
|
||||
constexpr int kNumCF = 1000;
|
||||
ColumnFamilyOptions cf_options;
|
||||
|
||||
@@ -454,6 +454,18 @@ class Compaction {
|
||||
return notify_on_compaction_completion_;
|
||||
}
|
||||
|
||||
// Called by DBImpl::NotifyOnCompactionPreCommit to ensure the
|
||||
// OnCompactionPreCommit listener callback fires at most once per
|
||||
// compaction lifecycle, even though the notify helper is invoked at multiple
|
||||
// potential ReleaseCompactionFiles() sites for safety.
|
||||
void SetNotifyOnCompactionPreCommitCalled() {
|
||||
notify_on_compaction_pre_commit_called_ = true;
|
||||
}
|
||||
|
||||
bool WasNotifyOnCompactionPreCommitCalled() const {
|
||||
return notify_on_compaction_pre_commit_called_;
|
||||
}
|
||||
|
||||
static constexpr int kInvalidLevel = -1;
|
||||
|
||||
// Evaluate proximal output level. If the compaction supports
|
||||
@@ -626,6 +638,12 @@ class Compaction {
|
||||
// begin.
|
||||
bool notify_on_compaction_completion_;
|
||||
|
||||
// Tracks whether OnCompactionPreCommit has already fired for this
|
||||
// compaction. The notify helper is called from multiple potential
|
||||
// ReleaseCompactionFiles() call sites; this flag guarantees that listeners
|
||||
// observe the callback exactly once per compaction.
|
||||
bool notify_on_compaction_pre_commit_called_ = false;
|
||||
|
||||
// Enable/disable GC collection for blobs during compaction.
|
||||
bool enable_blob_garbage_collection_;
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/blob/blob_file_builder.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/blob/prefetch_buffer_collection.h"
|
||||
#include "db/snapshot_checker.h"
|
||||
#include "db/wide/blob_column_resolver_util.h"
|
||||
#include "db/wide/wide_column_serialization.h"
|
||||
#include "db/wide/wide_columns_helper.h"
|
||||
#include "logging/logging.h"
|
||||
@@ -20,8 +22,106 @@
|
||||
#include "rocksdb/listener.h"
|
||||
#include "table/internal_iterator.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/autovector.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// CompactionBlobResolver implementation
|
||||
void CompactionBlobResolver::Init(BlobFetcher* blob_fetcher,
|
||||
PrefetchBufferCollection* prefetch_buffers,
|
||||
CompactionIterationStats* iter_stats) {
|
||||
blob_fetcher_ = blob_fetcher;
|
||||
prefetch_buffers_ = prefetch_buffers;
|
||||
iter_stats_ = iter_stats;
|
||||
}
|
||||
|
||||
void CompactionBlobResolver::Reset(
|
||||
const Slice& user_key, const std::vector<WideColumn>* columns,
|
||||
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns,
|
||||
bool track_resolve_error) {
|
||||
user_key_ = user_key;
|
||||
columns_ = columns;
|
||||
blob_columns_ = blob_columns;
|
||||
track_resolve_error_ = track_resolve_error;
|
||||
resolved_cache_.clear();
|
||||
resolve_error_.reset();
|
||||
}
|
||||
|
||||
Status CompactionBlobResolver::ResolveColumn(size_t column_index,
|
||||
Slice* resolved_value) {
|
||||
Status status = Status::OK();
|
||||
if (columns_ == nullptr || column_index >= columns_->size()) {
|
||||
status = Status::InvalidArgument("Column index out of bounds");
|
||||
} else {
|
||||
const BlobIndex* blob_index_ptr =
|
||||
blob_resolver_util::FindBlobColumn(blob_columns_, column_index);
|
||||
|
||||
if (blob_index_ptr == nullptr) {
|
||||
// Not a blob column - return the inline value directly
|
||||
*resolved_value = (*columns_)[column_index].value();
|
||||
} else {
|
||||
// Check if we've already resolved this column
|
||||
PinnableSlice* cached =
|
||||
blob_resolver_util::FindInCache(resolved_cache_, column_index);
|
||||
if (cached != nullptr) {
|
||||
*resolved_value = Slice(*cached);
|
||||
} else if (blob_fetcher_ == nullptr) {
|
||||
status = Status::NotSupported("Blob fetcher not available");
|
||||
} else {
|
||||
const BlobIndex& blob_index = *blob_index_ptr;
|
||||
|
||||
// Handle inlined blobs (e.g., from legacy stacked BlobDB with TTL)
|
||||
if (blob_index.IsInlined()) {
|
||||
*resolved_value = blob_resolver_util::CacheInlinedBlob(
|
||||
resolved_cache_, column_index, blob_index);
|
||||
} else {
|
||||
FilePrefetchBuffer* prefetch_buffer =
|
||||
prefetch_buffers_ ? prefetch_buffers_->GetOrCreatePrefetchBuffer(
|
||||
blob_index.file_number())
|
||||
: nullptr;
|
||||
|
||||
uint64_t bytes_read = 0;
|
||||
resolved_cache_.emplace_back(column_index,
|
||||
std::make_unique<PinnableSlice>());
|
||||
auto& new_entry = resolved_cache_.back();
|
||||
|
||||
status =
|
||||
blob_fetcher_->FetchBlob(user_key_, blob_index, prefetch_buffer,
|
||||
new_entry.second.get(), &bytes_read);
|
||||
if (!status.ok()) {
|
||||
resolved_cache_.pop_back();
|
||||
} else {
|
||||
if (iter_stats_ != nullptr) {
|
||||
++iter_stats_->num_blobs_read;
|
||||
iter_stats_->total_blob_bytes_read += bytes_read;
|
||||
}
|
||||
|
||||
*resolved_value = Slice(*new_entry.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!status.ok() && track_resolve_error_ && !resolve_error_.has_value()) {
|
||||
resolve_error_.emplace(status);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
bool CompactionBlobResolver::IsBlobColumn(size_t column_index) const {
|
||||
if (columns_ == nullptr || column_index >= columns_->size()) {
|
||||
return false;
|
||||
}
|
||||
return blob_resolver_util::IsBlobColumnIndex(blob_columns_, column_index);
|
||||
}
|
||||
|
||||
size_t CompactionBlobResolver::NumColumns() const {
|
||||
if (columns_ == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
return columns_->size();
|
||||
}
|
||||
|
||||
CompactionIterator::CompactionIterator(
|
||||
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
|
||||
SequenceNumber last_sequence, std::vector<SequenceNumber>* snapshots,
|
||||
@@ -38,7 +138,8 @@ CompactionIterator::CompactionIterator(
|
||||
const std::atomic<bool>* shutting_down,
|
||||
const std::shared_ptr<Logger> info_log,
|
||||
const std::string* full_history_ts_low,
|
||||
std::optional<SequenceNumber> preserve_seqno_min)
|
||||
std::optional<SequenceNumber> preserve_seqno_min,
|
||||
const Version* input_version, Env::IOActivity blob_read_io_activity)
|
||||
: CompactionIterator(
|
||||
input, cmp, merge_helper, last_sequence, snapshots, earliest_snapshot,
|
||||
earliest_write_conflict_snapshot, job_snapshot, snapshot_checker, env,
|
||||
@@ -47,7 +148,8 @@ CompactionIterator::CompactionIterator(
|
||||
manual_compaction_canceled,
|
||||
compaction ? std::make_unique<RealCompaction>(compaction) : nullptr,
|
||||
must_count_input_entries, compaction_filter, shutting_down, info_log,
|
||||
full_history_ts_low, preserve_seqno_min) {}
|
||||
full_history_ts_low, preserve_seqno_min, input_version,
|
||||
blob_read_io_activity) {}
|
||||
|
||||
CompactionIterator::CompactionIterator(
|
||||
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
|
||||
@@ -65,7 +167,8 @@ CompactionIterator::CompactionIterator(
|
||||
const std::atomic<bool>* shutting_down,
|
||||
const std::shared_ptr<Logger> info_log,
|
||||
const std::string* full_history_ts_low,
|
||||
std::optional<SequenceNumber> preserve_seqno_min)
|
||||
std::optional<SequenceNumber> preserve_seqno_min,
|
||||
const Version* input_version, Env::IOActivity blob_read_io_activity)
|
||||
: input_(input, cmp, must_count_input_entries),
|
||||
cmp_(cmp),
|
||||
merge_helper_(merge_helper),
|
||||
@@ -80,6 +183,8 @@ CompactionIterator::CompactionIterator(
|
||||
blob_file_builder_(blob_file_builder),
|
||||
compaction_(std::move(compaction)),
|
||||
compaction_filter_(compaction_filter),
|
||||
filter_supports_v4_(
|
||||
compaction_filter_ ? compaction_filter_->SupportsFilterV4() : false),
|
||||
shutting_down_(shutting_down),
|
||||
manual_compaction_canceled_(manual_compaction_canceled),
|
||||
bottommost_level_(compaction_ && compaction_->bottommost_level() &&
|
||||
@@ -98,7 +203,8 @@ CompactionIterator::CompactionIterator(
|
||||
merge_out_iter_(merge_helper_),
|
||||
blob_garbage_collection_cutoff_file_number_(
|
||||
ComputeBlobGarbageCollectionCutoffFileNumber(compaction_.get())),
|
||||
blob_fetcher_(CreateBlobFetcherIfNeeded(compaction_.get())),
|
||||
blob_fetcher_(CreateBlobFetcherIfNeeded(compaction_.get(), input_version,
|
||||
blob_read_io_activity)),
|
||||
prefetch_buffers_(
|
||||
CreatePrefetchBufferCollectionIfNeeded(compaction_.get())),
|
||||
current_key_committed_(false),
|
||||
@@ -120,6 +226,8 @@ CompactionIterator::CompactionIterator(
|
||||
timestamp_size_ == full_history_ts_low_->size());
|
||||
#endif
|
||||
input_.SetPinnedItersMgr(&pinned_iters_mgr_);
|
||||
blob_resolver_.Init(blob_fetcher_.get(), prefetch_buffers_.get(),
|
||||
&iter_stats_);
|
||||
// The default `merge_until_status_` does not need to be checked since it is
|
||||
// overwritten as soon as `MergeUntil()` is called
|
||||
merge_until_status_.PermitUncheckedError();
|
||||
@@ -131,6 +239,24 @@ CompactionIterator::~CompactionIterator() {
|
||||
input_.SetPinnedItersMgr(nullptr);
|
||||
}
|
||||
|
||||
void CompactionIterator::SetBlobFetcher(const Version* version,
|
||||
BlobFileCache* blob_file_cache,
|
||||
Env::IOActivity io_activity,
|
||||
bool allow_write_path_fallback) {
|
||||
if (blob_fetcher_ != nullptr || version == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.io_activity = io_activity;
|
||||
read_options.fill_cache = false;
|
||||
|
||||
blob_fetcher_ = std::make_unique<BlobFetcher>(
|
||||
version, read_options, blob_file_cache, allow_write_path_fallback);
|
||||
blob_resolver_.Init(blob_fetcher_.get(), prefetch_buffers_.get(),
|
||||
&iter_stats_);
|
||||
}
|
||||
|
||||
void CompactionIterator::ResetRecordCounts() {
|
||||
iter_stats_.num_record_drop_user = 0;
|
||||
iter_stats_.num_record_drop_hidden = 0;
|
||||
@@ -258,9 +384,10 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
compaction_filter_skip_until_.rep());
|
||||
if (decision == CompactionFilter::Decision::kUndetermined &&
|
||||
!compaction_filter_->IsStackedBlobDbInternalCompactionFilter()) {
|
||||
if (!compaction_) {
|
||||
status_ =
|
||||
Status::Corruption("Unexpected blob index outside of compaction");
|
||||
if (!blob_fetcher_) {
|
||||
status_ = Status::NotSupported(
|
||||
"Blob-backed value filtering requires blob read support in the "
|
||||
"current table-file creation context");
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
@@ -287,8 +414,6 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
assert(blob_fetcher_);
|
||||
|
||||
s = blob_fetcher_->FetchBlob(ikey_.user_key, blob_index,
|
||||
prefetch_buffer, &blob_value_,
|
||||
&bytes_read);
|
||||
@@ -309,7 +434,19 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
const Slice* existing_val = nullptr;
|
||||
const WideColumns* existing_col = nullptr;
|
||||
|
||||
WideColumns existing_columns;
|
||||
// Reuse member variable to avoid per-key heap allocation.
|
||||
filter_existing_columns_.clear();
|
||||
WideColumnBlobResolver* blob_resolver_ptr = nullptr;
|
||||
|
||||
// Use member variables for entity column storage to avoid per-key
|
||||
// heap allocations. Cleared at start of each use.
|
||||
entity_columns_.clear();
|
||||
entity_blob_columns_.clear();
|
||||
// Storage for eagerly resolved blob values used by the FilterV3
|
||||
// compatibility path. Keep this in the same scope as the FilterV4()
|
||||
// invocation below because filter_existing_columns_ stores Slices into
|
||||
// these buffers.
|
||||
autovector<PinnableSlice, 4> resolved_blobs_storage;
|
||||
|
||||
if (ikey_.type != kTypeWideColumnEntity) {
|
||||
if (!blob_value_.empty()) {
|
||||
@@ -318,23 +455,125 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
existing_val = &value_;
|
||||
}
|
||||
} else {
|
||||
Slice value_copy = value_;
|
||||
const Status s =
|
||||
WideColumnSerialization::Deserialize(value_copy, existing_columns);
|
||||
// Check if entity has blob columns that need resolution
|
||||
bool has_blob_columns = false;
|
||||
{
|
||||
Status s_hbc =
|
||||
WideColumnSerialization::HasBlobColumns(value_, has_blob_columns);
|
||||
if (!s_hbc.ok()) {
|
||||
status_ = s_hbc;
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (UNLIKELY(has_blob_columns)) {
|
||||
// Entity has blob columns. DeserializeV2() populates
|
||||
// entity_columns_ with the full column set; blob-backed entries still
|
||||
// carry the serialized BlobIndex bytes in value(). The companion
|
||||
// entity_blob_columns_ side list identifies which column indexes are
|
||||
// blob references and provides the decoded BlobIndex objects.
|
||||
Slice input_copy = value_;
|
||||
Status s = WideColumnSerialization::DeserializeV2(
|
||||
input_copy, entity_columns_, entity_blob_columns_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
// Mark as deserialized so PrepareOutput can skip re-deserialization
|
||||
// if the filter returns kKeep.
|
||||
entity_deserialized_ = true;
|
||||
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
// Build filter_existing_columns_ with raw values (blob columns
|
||||
// have blob index as value, not the actual blob data)
|
||||
for (size_t i = 0; i < entity_columns_.size(); ++i) {
|
||||
filter_existing_columns_.emplace_back(entity_columns_[i].name(),
|
||||
entity_columns_[i].value());
|
||||
}
|
||||
|
||||
if (filter_supports_v4_) {
|
||||
// Reset member blob resolver for this entity - filter can call
|
||||
// resolver->ResolveColumn() to fetch blob values on-demand
|
||||
blob_resolver_.Reset(ikey_.user_key, &entity_columns_,
|
||||
&entity_blob_columns_,
|
||||
/*track_resolve_error=*/true);
|
||||
blob_resolver_ptr = &blob_resolver_;
|
||||
} else {
|
||||
// FilterV3 compatibility: eagerly resolve all blob columns so
|
||||
// the filter sees actual values (not raw BlobIndex bytes).
|
||||
// resolved_blobs_storage is in the outer scope so the data
|
||||
// outlives filter_existing_columns_ (which holds Slices into
|
||||
// it).
|
||||
resolved_blobs_storage.resize(entity_blob_columns_.size());
|
||||
for (size_t bi = 0; bi < entity_blob_columns_.size(); ++bi) {
|
||||
const size_t col_idx = entity_blob_columns_[bi].first;
|
||||
const BlobIndex& blob_idx = entity_blob_columns_[bi].second;
|
||||
|
||||
if (blob_idx.IsInlined()) {
|
||||
resolved_blobs_storage[bi].PinSelf(blob_idx.value());
|
||||
} else {
|
||||
FilePrefetchBuffer* prefetch_buffer =
|
||||
prefetch_buffers_
|
||||
? prefetch_buffers_->GetOrCreatePrefetchBuffer(
|
||||
blob_idx.file_number())
|
||||
: nullptr;
|
||||
uint64_t bytes_read = 0;
|
||||
assert(blob_fetcher_);
|
||||
Status s_fetch = blob_fetcher_->FetchBlob(
|
||||
ikey_.user_key, blob_idx, prefetch_buffer,
|
||||
&resolved_blobs_storage[bi], &bytes_read);
|
||||
if (!s_fetch.ok()) {
|
||||
status_ = s_fetch;
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
++iter_stats_.num_blobs_read;
|
||||
iter_stats_.total_blob_bytes_read += bytes_read;
|
||||
}
|
||||
|
||||
// Replace the blob index value in filter_existing_columns_
|
||||
// with the resolved blob value
|
||||
filter_existing_columns_[col_idx] =
|
||||
WideColumn(entity_columns_[col_idx].name(),
|
||||
Slice(resolved_blobs_storage[bi]));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No blob columns, use fast path
|
||||
Slice value_copy = value_;
|
||||
const Status s = WideColumnSerialization::Deserialize(
|
||||
value_copy, filter_existing_columns_);
|
||||
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
existing_col = &filter_existing_columns_;
|
||||
}
|
||||
|
||||
// existing_val / existing_col point into value_, blob_value_,
|
||||
// entity_columns_, and resolved_blobs_storage, all of which stay alive
|
||||
// until this call returns.
|
||||
decision = compaction_filter_->FilterV4(
|
||||
level_, filter_key, value_type, existing_val, existing_col,
|
||||
&compaction_filter_value_, &new_columns,
|
||||
compaction_filter_skip_until_.rep(), blob_resolver_ptr);
|
||||
|
||||
if (blob_resolver_ptr != nullptr) {
|
||||
Status resolve_status = blob_resolver_.resolve_status();
|
||||
if (!resolve_status.ok()) {
|
||||
// Keep lazy FilterV4 failure semantics aligned with the eager
|
||||
// FilterV3 compatibility path: if blob resolution fails while the
|
||||
// filter is inspecting the entry, fail compaction even if the
|
||||
// filter returned kKeep after noticing the error.
|
||||
status_ = std::move(resolve_status);
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
|
||||
existing_col = &existing_columns;
|
||||
}
|
||||
|
||||
decision = compaction_filter_->FilterV3(
|
||||
level_, filter_key, value_type, existing_val, existing_col,
|
||||
&compaction_filter_value_, &new_columns,
|
||||
compaction_filter_skip_until_.rep());
|
||||
}
|
||||
|
||||
iter_stats_.total_filter_time +=
|
||||
@@ -342,10 +581,10 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
}
|
||||
|
||||
if (decision == CompactionFilter::Decision::kUndetermined) {
|
||||
// Should not reach here, since FilterV2/FilterV3 should never return
|
||||
// Should not reach here, since FilterV2/V3/V4 should never return
|
||||
// kUndetermined.
|
||||
status_ = Status::NotSupported(
|
||||
"FilterV2/FilterV3 should never return kUndetermined");
|
||||
"FilterV2/V3/V4 should never return kUndetermined");
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
@@ -354,7 +593,7 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
cmp_->Compare(*compaction_filter_skip_until_.rep(), ikey_.user_key) <=
|
||||
0) {
|
||||
// Can't skip to a key smaller than the current one.
|
||||
// Keep the key as per FilterV2/FilterV3 documentation.
|
||||
// Keep the key as per FilterV2/V3/V4 documentation.
|
||||
decision = CompactionFilter::Decision::kKeep;
|
||||
}
|
||||
|
||||
@@ -442,6 +681,16 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
}
|
||||
|
||||
value_ = compaction_filter_value_;
|
||||
// Value changed, force re-deserialization in PrepareOutput
|
||||
entity_deserialized_ = false;
|
||||
}
|
||||
|
||||
if (UNLIKELY(compaction_filter_value_.size() >
|
||||
std::numeric_limits<uint32_t>::max())) {
|
||||
status_ = Status::Corruption(
|
||||
"CompactionFilter result value size exceeds 4GB limit");
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -450,9 +699,14 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
void CompactionIterator::NextFromInput() {
|
||||
at_next_ = false;
|
||||
validity_info_.Invalidate();
|
||||
entity_deserialized_ = false;
|
||||
|
||||
while (!Valid() && input_.Valid() && !IsPausingManualCompaction() &&
|
||||
!IsShuttingDown()) {
|
||||
// A filtered-out key can advance directly to the next input record without
|
||||
// returning to PrepareOutput(), so clear the per-record deserialization
|
||||
// cache at the start of each loop iteration.
|
||||
entity_deserialized_ = false;
|
||||
key_ = input_.key();
|
||||
value_ = input_.value();
|
||||
blob_value_.Reset();
|
||||
@@ -1266,6 +1520,340 @@ void CompactionIterator::GarbageCollectBlobIfNeeded() {
|
||||
}
|
||||
}
|
||||
|
||||
// Wide-column entity compaction flow after InvokeFilterIfNeeded():
|
||||
// 1. PrepareOutput() is the only caller of the helper block below.
|
||||
// 2. PrepareOutput() ensures entity_columns_/entity_blob_columns_ describe
|
||||
// the current entity, either by reusing the filter-time deserialization or
|
||||
// by deserializing once itself.
|
||||
// 3. PrepareOutput() then picks exactly one follow-up path:
|
||||
// - inline-only entity:
|
||||
// ExtractLargeColumnValuesIfNeeded()
|
||||
// This is the flush/compaction extraction path. It may rewrite large
|
||||
// inline columns into blob references.
|
||||
// - entity already containing blob references:
|
||||
// GarbageCollectEntityBlobsIfNeeded()
|
||||
// This is the blob-GC path. It only fetches / relocates references
|
||||
// that fall below the current GC cutoff.
|
||||
// Both helpers leave value_ unchanged when no rewrite is needed.
|
||||
void CompactionIterator::ExtractLargeColumnValuesIfNeeded() {
|
||||
assert(ikey_.type == kTypeWideColumnEntity);
|
||||
|
||||
// Check if blob extraction is enabled
|
||||
if (!blob_file_builder_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Deserialize using member variables (already populated by PrepareOutput
|
||||
// for the combined deserialization path). If not yet populated, deserialize
|
||||
// here.
|
||||
if (entity_columns_.empty()) {
|
||||
Slice entity_slice = value_;
|
||||
entity_columns_.clear();
|
||||
entity_blob_columns_.clear();
|
||||
Status s = WideColumnSerialization::DeserializeV2(
|
||||
entity_slice, entity_columns_, entity_blob_columns_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// IMPORTANT: If there are already blob columns, we skip extraction entirely.
|
||||
// This means new large inline columns added to such entities (e.g., via
|
||||
// PutEntity after a previous PutEntity that already had blob columns) will
|
||||
// NOT be extracted to blob files until the entity undergoes a full rewrite
|
||||
// (e.g., via compaction that GCs all existing blob refs).
|
||||
//
|
||||
// This is a deliberate simplification: supporting partial extraction
|
||||
// (extracting only new large inline columns while preserving existing
|
||||
// blob refs) would require merging old and new blob column sets during
|
||||
// serialization, and the benefit is marginal since full rewrites happen
|
||||
// naturally during blob GC.
|
||||
if (UNLIKELY(!entity_blob_columns_.empty())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to extract large column values to blob files
|
||||
// Track which columns were extracted and their blob indices
|
||||
std::vector<std::pair<size_t, BlobIndex>> new_blob_columns;
|
||||
std::string temp_blob_index;
|
||||
|
||||
for (size_t i = 0; i < entity_columns_.size(); ++i) {
|
||||
const Slice& col_value = entity_columns_[i].value();
|
||||
|
||||
// Clear the temporary buffer for each column
|
||||
temp_blob_index.clear();
|
||||
|
||||
// Try to add the column value to blob file
|
||||
// blob_file_builder_->Add() will check min_blob_size internally
|
||||
// and only write to blob file if the value is large enough
|
||||
const Status add_status =
|
||||
blob_file_builder_->Add(user_key(), col_value, &temp_blob_index);
|
||||
if (!add_status.ok()) {
|
||||
status_ = add_status;
|
||||
validity_info_.Invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
// If blob_index is not empty, the value was extracted to a blob file
|
||||
if (!temp_blob_index.empty()) {
|
||||
BlobIndex blob_idx;
|
||||
const Status decode_status = blob_idx.DecodeFrom(temp_blob_index);
|
||||
if (!decode_status.ok()) {
|
||||
status_ = decode_status;
|
||||
validity_info_.Invalidate();
|
||||
return;
|
||||
}
|
||||
new_blob_columns.emplace_back(i, blob_idx);
|
||||
}
|
||||
}
|
||||
|
||||
// If no columns were extracted, nothing to do
|
||||
if (new_blob_columns.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-serialize the entity with blob indices using the Slice-based overload
|
||||
// to avoid copying column names and values to strings.
|
||||
entity_wide_columns_.clear();
|
||||
entity_wide_columns_.reserve(entity_columns_.size());
|
||||
for (const auto& col : entity_columns_) {
|
||||
entity_wide_columns_.emplace_back(col.name(), col.value());
|
||||
}
|
||||
|
||||
rewritten_entity_.clear();
|
||||
const Status serialize_status = WideColumnSerialization::SerializeV2(
|
||||
entity_wide_columns_, new_blob_columns, rewritten_entity_);
|
||||
if (!serialize_status.ok()) {
|
||||
status_ = serialize_status;
|
||||
validity_info_.Invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update value_ to point to the rewritten entity
|
||||
value_ = rewritten_entity_;
|
||||
}
|
||||
|
||||
bool CompactionIterator::FetchBlobsNeedingGC(
|
||||
const std::vector<WideColumn>& /*columns*/,
|
||||
const std::vector<std::pair<size_t, BlobIndex>>& blob_columns,
|
||||
std::vector<std::pair<size_t, PinnableSlice>>* fetched_blob_values) {
|
||||
assert(fetched_blob_values != nullptr);
|
||||
|
||||
for (const auto& blob_col : blob_columns) {
|
||||
const size_t col_idx = blob_col.first;
|
||||
const BlobIndex& blob_index = blob_col.second;
|
||||
|
||||
// Skip inlined blobs - they don't need GC
|
||||
if (blob_index.IsInlined()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this blob file needs garbage collection
|
||||
if (blob_index.file_number() >=
|
||||
blob_garbage_collection_cutoff_file_number_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// This blob needs to be relocated - fetch its value
|
||||
FilePrefetchBuffer* prefetch_buffer =
|
||||
prefetch_buffers_ ? prefetch_buffers_->GetOrCreatePrefetchBuffer(
|
||||
blob_index.file_number())
|
||||
: nullptr;
|
||||
|
||||
uint64_t bytes_read = 0;
|
||||
assert(blob_fetcher_);
|
||||
|
||||
PinnableSlice blob_value;
|
||||
Status s = blob_fetcher_->FetchBlob(user_key(), blob_index, prefetch_buffer,
|
||||
&blob_value, &bytes_read);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
|
||||
++iter_stats_.num_blobs_read;
|
||||
iter_stats_.total_blob_bytes_read += bytes_read;
|
||||
|
||||
fetched_blob_values->emplace_back(col_idx, std::move(blob_value));
|
||||
}
|
||||
|
||||
return !fetched_blob_values->empty();
|
||||
}
|
||||
|
||||
std::vector<std::pair<size_t, BlobIndex>>
|
||||
CompactionIterator::RelocateBlobValues(
|
||||
const std::vector<std::pair<size_t, PinnableSlice>>& fetched_blob_values) {
|
||||
std::vector<std::pair<size_t, BlobIndex>> new_blob_columns;
|
||||
|
||||
if (blob_file_builder_) {
|
||||
for (const auto& fv : fetched_blob_values) {
|
||||
const size_t col_idx = fv.first;
|
||||
const Slice col_value(fv.second);
|
||||
|
||||
std::string temp_blob_index;
|
||||
Status status =
|
||||
blob_file_builder_->Add(user_key(), col_value, &temp_blob_index);
|
||||
if (!status.ok()) {
|
||||
status_ = status;
|
||||
validity_info_.Invalidate();
|
||||
new_blob_columns.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
if (!temp_blob_index.empty()) {
|
||||
BlobIndex blob_idx;
|
||||
status = blob_idx.DecodeFrom(temp_blob_index);
|
||||
if (!status.ok()) {
|
||||
status_ = status;
|
||||
validity_info_.Invalidate();
|
||||
new_blob_columns.clear();
|
||||
break;
|
||||
}
|
||||
new_blob_columns.emplace_back(col_idx, blob_idx);
|
||||
|
||||
// Track relocation stats here (after successful relocation)
|
||||
++iter_stats_.num_blobs_relocated;
|
||||
iter_stats_.total_blob_bytes_relocated += col_value.size();
|
||||
}
|
||||
// If temp_blob_index is empty, the value will be inlined
|
||||
}
|
||||
}
|
||||
|
||||
return new_blob_columns;
|
||||
}
|
||||
|
||||
void CompactionIterator::SerializeEntityAfterGC(
|
||||
const std::vector<WideColumn>& columns,
|
||||
const std::vector<std::pair<size_t, BlobIndex>>& original_blob_columns,
|
||||
const std::vector<std::pair<size_t, PinnableSlice>>& fetched_blob_values,
|
||||
const std::vector<std::pair<size_t, BlobIndex>>& new_blob_columns) {
|
||||
// Collect all blob columns for final serialization: include non-GC'd
|
||||
// originals and newly relocated. Use linear scans since these
|
||||
// collections are typically small (<5 elements).
|
||||
std::vector<std::pair<size_t, BlobIndex>> final_blob_columns;
|
||||
for (const auto& blob_col : original_blob_columns) {
|
||||
// Check if this column was fetched (GC'd) via linear scan
|
||||
bool was_fetched = false;
|
||||
for (const auto& fv : fetched_blob_values) {
|
||||
if (fv.first == blob_col.first) {
|
||||
was_fetched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!was_fetched) {
|
||||
// This blob column wasn't GC'd - keep original
|
||||
final_blob_columns.push_back(blob_col);
|
||||
}
|
||||
}
|
||||
// Add newly relocated blobs
|
||||
for (const auto& nb : new_blob_columns) {
|
||||
final_blob_columns.push_back(nb);
|
||||
}
|
||||
|
||||
// Build WideColumns for serialization, replacing fetched blob values
|
||||
// with their resolved inline values.
|
||||
WideColumns wide_columns;
|
||||
wide_columns.reserve(columns.size());
|
||||
|
||||
for (size_t i = 0; i < columns.size(); ++i) {
|
||||
// Linear scan to find if this column was fetched
|
||||
const PinnableSlice* fetched_value = nullptr;
|
||||
for (const auto& fv : fetched_blob_values) {
|
||||
if (fv.first == i) {
|
||||
fetched_value = &fv.second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fetched_value != nullptr) {
|
||||
// This blob column was fetched for GC; use the resolved inline value.
|
||||
wide_columns.emplace_back(columns[i].name(), Slice(*fetched_value));
|
||||
} else {
|
||||
// For inline columns, this is the actual value. For non-fetched blob
|
||||
// columns, this contains the raw serialized blob index, which is fine
|
||||
// because SerializeV2() re-encodes from the BlobIndex
|
||||
// object in final_blob_columns, ignoring this value for blob-type
|
||||
// columns.
|
||||
wide_columns.emplace_back(columns[i].name(), columns[i].value());
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the entity
|
||||
rewritten_entity_.clear();
|
||||
Status s;
|
||||
if (final_blob_columns.empty()) {
|
||||
s = WideColumnSerialization::Serialize(wide_columns, rewritten_entity_);
|
||||
} else {
|
||||
s = WideColumnSerialization::SerializeV2(wide_columns, final_blob_columns,
|
||||
rewritten_entity_);
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
value_ = rewritten_entity_;
|
||||
}
|
||||
|
||||
void CompactionIterator::GarbageCollectEntityBlobsIfNeeded() {
|
||||
assert(ikey_.type == kTypeWideColumnEntity);
|
||||
|
||||
if (!compaction_ || !compaction_->enable_blob_garbage_collection()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This is the second branch in the flow above: the entity already has V2
|
||||
// blob references, so compaction considers only relocating the subset of
|
||||
// referenced blob files that are old enough for blob GC.
|
||||
|
||||
// Use member variables already populated by PrepareOutput's combined
|
||||
// deserialization path. If not yet populated, deserialize here.
|
||||
if (entity_columns_.empty()) {
|
||||
Slice entity_slice = value_;
|
||||
entity_columns_.clear();
|
||||
entity_blob_columns_.clear();
|
||||
Status s = WideColumnSerialization::DeserializeV2(
|
||||
entity_slice, entity_columns_, entity_blob_columns_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The caller (PrepareOutput) already verified blob columns exist.
|
||||
assert(!entity_blob_columns_.empty());
|
||||
if (entity_blob_columns_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch blobs that need GC
|
||||
std::vector<std::pair<size_t, PinnableSlice>> fetched_blob_values;
|
||||
if (!FetchBlobsNeedingGC(entity_columns_, entity_blob_columns_,
|
||||
&fetched_blob_values)) {
|
||||
// Either no blobs needed GC (status_ is OK) or an error occurred
|
||||
// (status_ is already set by FetchBlobsNeedingGC). In both cases,
|
||||
// we return without modifying the entity.
|
||||
return;
|
||||
}
|
||||
|
||||
// Relocate fetched values to new blob files
|
||||
std::vector<std::pair<size_t, BlobIndex>> new_blob_columns =
|
||||
RelocateBlobValues(fetched_blob_values);
|
||||
if (!status_.ok()) {
|
||||
return; // Error occurred during relocation
|
||||
}
|
||||
|
||||
// Serialize the final entity
|
||||
SerializeEntityAfterGC(entity_columns_, entity_blob_columns_,
|
||||
fetched_blob_values, new_blob_columns);
|
||||
}
|
||||
|
||||
void CompactionIterator::PrepareOutput() {
|
||||
if (Valid()) {
|
||||
if (LIKELY(!is_range_del_)) {
|
||||
@@ -1273,6 +1861,37 @@ void CompactionIterator::PrepareOutput() {
|
||||
ExtractLargeValueIfNeeded();
|
||||
} else if (ikey_.type == kTypeBlobIndex) {
|
||||
GarbageCollectBlobIfNeeded();
|
||||
} else if (ikey_.type == kTypeWideColumnEntity) {
|
||||
// If entity was already deserialized (e.g., by InvokeFilterIfNeeded)
|
||||
// and the filter returned kKeep, skip redundant re-deserialization.
|
||||
if (!entity_deserialized_) {
|
||||
TEST_SYNC_POINT(
|
||||
"CompactionIterator::PrepareOutput:DeserializeEntity");
|
||||
// Deserialize entity once into member variables, then decide between
|
||||
// blob GC and extraction based on whether blob columns exist.
|
||||
// This avoids the double parse of HasBlobColumns() + DeserializeV2().
|
||||
entity_columns_.clear();
|
||||
entity_blob_columns_.clear();
|
||||
Slice entity_slice = value_;
|
||||
{
|
||||
Status s_deser = WideColumnSerialization::DeserializeV2(
|
||||
entity_slice, entity_columns_, entity_blob_columns_);
|
||||
if (!s_deser.ok()) {
|
||||
status_ = s_deser;
|
||||
validity_info_.Invalidate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TEST_SYNC_POINT(
|
||||
"CompactionIterator::PrepareOutput:SkipDeserializeEntity");
|
||||
}
|
||||
entity_deserialized_ = false; // Reset for next iteration
|
||||
if (UNLIKELY(!entity_blob_columns_.empty())) {
|
||||
GarbageCollectEntityBlobsIfNeeded();
|
||||
} else {
|
||||
ExtractLargeColumnValuesIfNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1421,21 +2040,32 @@ uint64_t CompactionIterator::ComputeBlobGarbageCollectionCutoffFileNumber(
|
||||
}
|
||||
|
||||
std::unique_ptr<BlobFetcher> CompactionIterator::CreateBlobFetcherIfNeeded(
|
||||
const CompactionProxy* compaction) {
|
||||
if (!compaction) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const Version* const version = compaction->input_version();
|
||||
if (!version) {
|
||||
const CompactionProxy* compaction, const Version* input_version,
|
||||
Env::IOActivity blob_read_io_activity) {
|
||||
const Version* const version =
|
||||
compaction != nullptr ? compaction->input_version() : input_version;
|
||||
if (version == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.io_activity = Env::IOActivity::kCompaction;
|
||||
read_options.io_activity = compaction != nullptr
|
||||
? Env::IOActivity::kCompaction
|
||||
: blob_read_io_activity;
|
||||
read_options.fill_cache = false;
|
||||
|
||||
return std::unique_ptr<BlobFetcher>(new BlobFetcher(version, read_options));
|
||||
BlobFileCache* blob_file_cache = nullptr;
|
||||
bool allow_write_path_fallback = false;
|
||||
if (compaction == nullptr) {
|
||||
allow_write_path_fallback = true;
|
||||
auto* cfd = version->cfd();
|
||||
if (cfd != nullptr) {
|
||||
blob_file_cache = cfd->blob_file_cache();
|
||||
}
|
||||
}
|
||||
|
||||
return std::unique_ptr<BlobFetcher>(new BlobFetcher(
|
||||
version, read_options, blob_file_cache, allow_write_path_fallback));
|
||||
}
|
||||
|
||||
std::unique_ptr<PrefetchBufferCollection>
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/compaction/compaction.h"
|
||||
#include "db/compaction/compaction_iteration_stats.h"
|
||||
#include "db/merge_helper.h"
|
||||
@@ -19,12 +21,69 @@
|
||||
#include "db/snapshot_checker.h"
|
||||
#include "options/cf_options.h"
|
||||
#include "rocksdb/compaction_filter.h"
|
||||
#include "rocksdb/slice.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class BlobFileBuilder;
|
||||
class BlobFileCache;
|
||||
class BlobFetcher;
|
||||
class Version;
|
||||
class PrefetchBufferCollection;
|
||||
class FilePrefetchBuffer;
|
||||
class Version;
|
||||
|
||||
// Internal implementation of WideColumnBlobResolver for compaction.
|
||||
// This class enables lazy loading of blob column values during compaction
|
||||
// filter processing.
|
||||
class CompactionBlobResolver : public WideColumnBlobResolver {
|
||||
public:
|
||||
CompactionBlobResolver()
|
||||
: columns_(nullptr),
|
||||
blob_columns_(nullptr),
|
||||
blob_fetcher_(nullptr),
|
||||
prefetch_buffers_(nullptr),
|
||||
iter_stats_(nullptr) {}
|
||||
|
||||
// Set the fixed context (blob fetcher, prefetch buffers, stats) once.
|
||||
void Init(BlobFetcher* blob_fetcher,
|
||||
PrefetchBufferCollection* prefetch_buffers,
|
||||
CompactionIterationStats* iter_stats);
|
||||
|
||||
Status ResolveColumn(size_t column_index, Slice* resolved_value) override;
|
||||
bool IsBlobColumn(size_t column_index) const override;
|
||||
size_t NumColumns() const override;
|
||||
Status resolve_status() const {
|
||||
if (!resolve_error_.has_value()) {
|
||||
return Status::OK();
|
||||
}
|
||||
return *resolve_error_;
|
||||
}
|
||||
|
||||
// Reset the resolver for a new entity. Clears resolved cache.
|
||||
void Reset(const Slice& user_key, const std::vector<WideColumn>* columns,
|
||||
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns,
|
||||
bool track_resolve_error = false);
|
||||
|
||||
private:
|
||||
Slice user_key_;
|
||||
const std::vector<WideColumn>* columns_;
|
||||
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns_;
|
||||
BlobFetcher* blob_fetcher_;
|
||||
PrefetchBufferCollection* prefetch_buffers_;
|
||||
CompactionIterationStats* iter_stats_;
|
||||
bool track_resolve_error_ = false;
|
||||
|
||||
// Cache for resolved blob values to avoid re-fetching. Uses a vector of
|
||||
// (column_index, PinnableSlice) pairs -- typical entities have few blob
|
||||
// columns (<5), making linear scan cheaper than hash map overhead.
|
||||
std::vector<std::pair<size_t, std::unique_ptr<PinnableSlice>>>
|
||||
resolved_cache_;
|
||||
// Sticky resolver error for the current entity. This is enabled only for the
|
||||
// lazy FilterV4 path so compaction can still fail even if the filter
|
||||
// notices the error and returns kKeep.
|
||||
std::optional<Status> resolve_error_;
|
||||
};
|
||||
|
||||
// A wrapper of internal iterator whose purpose is to count how
|
||||
// many entries there are in the iterator.
|
||||
@@ -213,34 +272,43 @@ class CompactionIterator {
|
||||
const std::atomic<bool>* shutting_down = nullptr,
|
||||
const std::shared_ptr<Logger> info_log = nullptr,
|
||||
const std::string* full_history_ts_low = nullptr,
|
||||
std::optional<SequenceNumber> preserve_seqno_min = {});
|
||||
std::optional<SequenceNumber> preserve_seqno_min = {},
|
||||
const Version* input_version = nullptr,
|
||||
Env::IOActivity blob_read_io_activity = Env::IOActivity::kCompaction);
|
||||
|
||||
// Constructor with custom CompactionProxy, used for tests.
|
||||
CompactionIterator(InternalIterator* input, const Comparator* cmp,
|
||||
MergeHelper* merge_helper, SequenceNumber last_sequence,
|
||||
std::vector<SequenceNumber>* snapshots,
|
||||
SequenceNumber earliest_snapshot,
|
||||
SequenceNumber earliest_write_conflict_snapshot,
|
||||
SequenceNumber job_snapshot,
|
||||
const SnapshotChecker* snapshot_checker, Env* env,
|
||||
bool report_detailed_time,
|
||||
CompactionRangeDelAggregator* range_del_agg,
|
||||
BlobFileBuilder* blob_file_builder,
|
||||
bool allow_data_in_errors,
|
||||
bool enforce_single_del_contracts,
|
||||
const std::atomic<bool>& manual_compaction_canceled,
|
||||
std::unique_ptr<CompactionProxy> compaction,
|
||||
bool must_count_input_entries,
|
||||
const CompactionFilter* compaction_filter = nullptr,
|
||||
const std::atomic<bool>* shutting_down = nullptr,
|
||||
const std::shared_ptr<Logger> info_log = nullptr,
|
||||
const std::string* full_history_ts_low = nullptr,
|
||||
std::optional<SequenceNumber> preserve_seqno_min = {});
|
||||
CompactionIterator(
|
||||
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
|
||||
SequenceNumber last_sequence, std::vector<SequenceNumber>* snapshots,
|
||||
SequenceNumber earliest_snapshot,
|
||||
SequenceNumber earliest_write_conflict_snapshot,
|
||||
SequenceNumber job_snapshot, const SnapshotChecker* snapshot_checker,
|
||||
Env* env, bool report_detailed_time,
|
||||
CompactionRangeDelAggregator* range_del_agg,
|
||||
BlobFileBuilder* blob_file_builder, bool allow_data_in_errors,
|
||||
bool enforce_single_del_contracts,
|
||||
const std::atomic<bool>& manual_compaction_canceled,
|
||||
std::unique_ptr<CompactionProxy> compaction,
|
||||
bool must_count_input_entries,
|
||||
const CompactionFilter* compaction_filter = nullptr,
|
||||
const std::atomic<bool>* shutting_down = nullptr,
|
||||
const std::shared_ptr<Logger> info_log = nullptr,
|
||||
const std::string* full_history_ts_low = nullptr,
|
||||
std::optional<SequenceNumber> preserve_seqno_min = {},
|
||||
const Version* input_version = nullptr,
|
||||
Env::IOActivity blob_read_io_activity = Env::IOActivity::kCompaction);
|
||||
|
||||
~CompactionIterator();
|
||||
|
||||
void ResetRecordCounts();
|
||||
|
||||
// Non-compaction table-file creation (e.g., flush/recovery) can still need
|
||||
// blob resolution when direct-write wide entities already contain blob
|
||||
// references. Plumb a fetcher explicitly in those cases.
|
||||
void SetBlobFetcher(const Version* version, BlobFileCache* blob_file_cache,
|
||||
Env::IOActivity io_activity,
|
||||
bool allow_write_path_fallback);
|
||||
|
||||
// Seek to the beginning of the compaction iterator output.
|
||||
//
|
||||
// REQUIRED: Call only once.
|
||||
@@ -302,6 +370,12 @@ class CompactionIterator {
|
||||
// for regular values (kTypeValue).
|
||||
void ExtractLargeValueIfNeeded();
|
||||
|
||||
// Extracts large column values from wide column entities to blob files.
|
||||
// For each column whose value size >= min_blob_size, the value is written
|
||||
// to a blob file and replaced with a blob index reference. Should only be
|
||||
// called for wide column entities (kTypeWideColumnEntity).
|
||||
void ExtractLargeColumnValuesIfNeeded();
|
||||
|
||||
// Relocates valid blobs residing in the oldest blob files if garbage
|
||||
// collection is enabled. Relocated blobs are written to new blob files or
|
||||
// inlined in the LSM tree depending on the current settings (i.e.
|
||||
@@ -312,6 +386,39 @@ class CompactionIterator {
|
||||
// algorithm is also called from here.
|
||||
void GarbageCollectBlobIfNeeded();
|
||||
|
||||
// Garbage collects blob references within wide column entities. For entities
|
||||
// that contain blob column references, this method checks if any of the
|
||||
// referenced blob files need garbage collection. If so, the blob values are
|
||||
// fetched and either relocated to new blob files or inlined based on
|
||||
// current settings. Should only be called for wide column entities
|
||||
// (kTypeWideColumnEntity) that have blob columns.
|
||||
void GarbageCollectEntityBlobsIfNeeded();
|
||||
|
||||
// Helper for GarbageCollectEntityBlobsIfNeeded: Fetches blob values that need
|
||||
// garbage collection based on the cutoff file number.
|
||||
// Returns true if any blobs were fetched, false otherwise.
|
||||
// On error, sets status_ and returns false.
|
||||
bool FetchBlobsNeedingGC(
|
||||
const std::vector<WideColumn>& columns,
|
||||
const std::vector<std::pair<size_t, BlobIndex>>& blob_columns,
|
||||
std::vector<std::pair<size_t, PinnableSlice>>* fetched_blob_values);
|
||||
|
||||
// Helper for GarbageCollectEntityBlobsIfNeeded: Relocates fetched blob
|
||||
// values to new blob files if blob_file_builder_ is available.
|
||||
// Returns the new blob column indices. On error, sets status_ and returns
|
||||
// empty.
|
||||
std::vector<std::pair<size_t, BlobIndex>> RelocateBlobValues(
|
||||
const std::vector<std::pair<size_t, PinnableSlice>>& fetched_blob_values);
|
||||
|
||||
// Helper for GarbageCollectEntityBlobsIfNeeded: Serializes the entity after
|
||||
// garbage collection, combining original columns with fetched values and
|
||||
// new blob indices. On error, sets status_.
|
||||
void SerializeEntityAfterGC(
|
||||
const std::vector<WideColumn>& columns,
|
||||
const std::vector<std::pair<size_t, BlobIndex>>& original_blob_columns,
|
||||
const std::vector<std::pair<size_t, PinnableSlice>>& fetched_blob_values,
|
||||
const std::vector<std::pair<size_t, BlobIndex>>& new_blob_columns);
|
||||
|
||||
// Invoke compaction filter if needed.
|
||||
// Return true on success, false on failures (e.g.: kIOError).
|
||||
bool InvokeFilterIfNeeded(bool* need_skip, Slice* skip_until);
|
||||
@@ -352,7 +459,8 @@ class CompactionIterator {
|
||||
static uint64_t ComputeBlobGarbageCollectionCutoffFileNumber(
|
||||
const CompactionProxy* compaction);
|
||||
static std::unique_ptr<BlobFetcher> CreateBlobFetcherIfNeeded(
|
||||
const CompactionProxy* compaction);
|
||||
const CompactionProxy* compaction, const Version* input_version,
|
||||
Env::IOActivity blob_read_io_activity);
|
||||
static std::unique_ptr<PrefetchBufferCollection>
|
||||
CreatePrefetchBufferCollectionIfNeeded(const CompactionProxy* compaction);
|
||||
|
||||
@@ -376,6 +484,7 @@ class CompactionIterator {
|
||||
BlobFileBuilder* blob_file_builder_;
|
||||
std::unique_ptr<CompactionProxy> compaction_;
|
||||
const CompactionFilter* compaction_filter_;
|
||||
const bool filter_supports_v4_;
|
||||
const std::atomic<bool>* shutting_down_;
|
||||
const std::atomic<bool>& manual_compaction_canceled_;
|
||||
const bool bottommost_level_;
|
||||
@@ -479,6 +588,32 @@ class CompactionIterator {
|
||||
PinnableSlice blob_value_;
|
||||
std::string compaction_filter_value_;
|
||||
InternalKey compaction_filter_skip_until_;
|
||||
// Buffer for storing rewritten entity with blob references after
|
||||
// extracting large column values to blob files.
|
||||
std::string rewritten_entity_;
|
||||
// Cached deserialized columns for the current wide-column entity, reused
|
||||
// across iterations to avoid per-key heap allocations. This is the full
|
||||
// sorted column set. For blob-backed columns, value() initially contains the
|
||||
// serialized BlobIndex bytes from the entity.
|
||||
std::vector<WideColumn> entity_columns_;
|
||||
// Side list for the blob-backed subset of entity_columns_. Each pair is
|
||||
// (column_index_in_entity_columns_, decoded_blob_index), so callers can tell
|
||||
// which entries in entity_columns_ are blob references without reparsing.
|
||||
std::vector<std::pair<size_t, BlobIndex>> entity_blob_columns_;
|
||||
// Temporary Slice-based view over entity_columns_ used when reserializing an
|
||||
// entity after blob extraction, to avoid copying names/values into strings.
|
||||
WideColumns entity_wide_columns_;
|
||||
// Reusable storage for existing_columns in InvokeFilterIfNeeded,
|
||||
// avoiding per-key heap allocations.
|
||||
WideColumns filter_existing_columns_;
|
||||
// Reusable blob resolver for compaction filter, avoiding per-key heap
|
||||
// allocation. Init() is called once; Reset() is called per entity.
|
||||
CompactionBlobResolver blob_resolver_;
|
||||
// True when entity_columns_/entity_blob_columns_ already describe the
|
||||
// current input record (e.g., because InvokeFilterIfNeeded() deserialized
|
||||
// it) and PrepareOutput() can skip redundant work. Reset for each candidate
|
||||
// input record and whenever the filter rewrites the entity value.
|
||||
bool entity_deserialized_{false};
|
||||
// "level_ptrs" holds indices that remember which file of an associated
|
||||
// level we were last checking during the last call to compaction->
|
||||
// KeyNotExistsBeyondOutputLevel(). This allows future calls to the function
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -58,6 +58,7 @@
|
||||
#include "table/table_builder.h"
|
||||
#include "table/unique_id_impl.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/hash_containers.h"
|
||||
#include "util/stop_watch.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -104,6 +105,8 @@ const char* GetCompactionReasonString(CompactionReason compaction_reason) {
|
||||
return "RoundRobinTtl";
|
||||
case CompactionReason::kRefitLevel:
|
||||
return "RefitLevel";
|
||||
case CompactionReason::kReadTriggered:
|
||||
return "ReadTriggered";
|
||||
case CompactionReason::kNumOfReasons:
|
||||
// fall through
|
||||
default:
|
||||
@@ -896,6 +899,10 @@ Status CompactionJob::VerifyOutputFiles() {
|
||||
}
|
||||
|
||||
auto verify_table = [&](SubcompactionState& subcompaction_state) {
|
||||
// Collect file open metadata during verification when fast_sst_open
|
||||
// is enabled, keyed by file number.
|
||||
UnorderedMap<uint64_t, std::string> file_open_metadata_map;
|
||||
|
||||
for (const auto& output_file : subcompaction_state.GetOutputs()) {
|
||||
// Verify that the table is usable
|
||||
// We set for_compaction to false and don't
|
||||
@@ -913,6 +920,10 @@ Status CompactionJob::VerifyOutputFiles() {
|
||||
TableReader* table_reader_ptr = table_reader_guard.get();
|
||||
verification_read_options.rate_limiter_priority =
|
||||
GetRateLimiterPriority();
|
||||
std::string file_open_metadata;
|
||||
std::string* file_open_metadata_ptr =
|
||||
mutable_db_options_copy_.fast_sst_open ? &file_open_metadata
|
||||
: nullptr;
|
||||
InternalIterator* iter = cfd->table_cache()->NewIterator(
|
||||
verification_read_options, file_options_, cfd->internal_comparator(),
|
||||
output_file.meta,
|
||||
@@ -925,7 +936,10 @@ Status CompactionJob::VerifyOutputFiles() {
|
||||
MaxFileSizeForL0MetaPin(compact_->compaction->mutable_cf_options()),
|
||||
/*smallest_compaction_key=*/nullptr,
|
||||
/*largest_compaction_key=*/nullptr,
|
||||
/*allow_unprepared_value=*/false);
|
||||
/*allow_unprepared_value=*/false,
|
||||
/*range_del_read_seqno=*/nullptr,
|
||||
/*range_del_iter=*/nullptr,
|
||||
/*maybe_pin_table_handle=*/false, file_open_metadata_ptr);
|
||||
auto s = iter->status();
|
||||
if (s.ok()) {
|
||||
// Check for remote/local compaction and verify_output_flags flags
|
||||
@@ -1000,6 +1014,27 @@ Status CompactionJob::VerifyOutputFiles() {
|
||||
subcompaction_state.status = s;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!file_open_metadata.empty()) {
|
||||
file_open_metadata_map[output_file.meta.fd.GetNumber()] =
|
||||
std::move(file_open_metadata);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply collected file open metadata to mutable outputs
|
||||
if (!file_open_metadata_map.empty()) {
|
||||
auto apply_metadata =
|
||||
[&file_open_metadata_map](
|
||||
std::vector<CompactionOutputs::Output>& outputs) {
|
||||
for (auto& output : outputs) {
|
||||
auto it = file_open_metadata_map.find(output.meta.fd.GetNumber());
|
||||
if (it != file_open_metadata_map.end()) {
|
||||
output.meta.file_open_metadata = std::move(it->second);
|
||||
}
|
||||
}
|
||||
};
|
||||
apply_metadata(subcompaction_state.GetMutableCompactionOutputs());
|
||||
apply_metadata(subcompaction_state.GetMutableProximalOutputs());
|
||||
}
|
||||
};
|
||||
for (size_t i = 1; i < compact_->sub_compact_states.size(); i++) {
|
||||
@@ -1305,6 +1340,16 @@ Status CompactionJob::Install(bool* compaction_released) {
|
||||
<< pl_stats.bytes_written_blob;
|
||||
}
|
||||
|
||||
// Propagate Install failure to compact_->status so that
|
||||
// CleanupCompaction() -> SubcompactionState::Cleanup() sees the failure and
|
||||
// calls ReleaseObsolete on output files' table cache entries. Without this,
|
||||
// if Run() succeeds but InstallCompactionResults() fails, Cleanup would see
|
||||
// overall_status = OK and skip ReleaseObsolete, leaking entries for output
|
||||
// files that were never installed into any Version.
|
||||
if (!status.ok() && compact_->status.ok()) {
|
||||
compact_->status = status;
|
||||
}
|
||||
|
||||
CleanupCompaction();
|
||||
return status;
|
||||
}
|
||||
@@ -2020,10 +2065,18 @@ Status CompactionJob::FinishCompactionOutputFile(
|
||||
std::pair<SequenceNumber, SequenceNumber> keep_seqno_range{
|
||||
0, kMaxSequenceNumber};
|
||||
if (sub_compact->compaction->SupportsPerKeyPlacement()) {
|
||||
// Point entries are routed to proximal output only when their seqno is
|
||||
// strictly greater than `proximal_after_seqno_`. Range tombstones use a
|
||||
// [lower, upper) filter, so split them at the next seqno to preserve the
|
||||
// same boundary.
|
||||
SequenceNumber range_del_split_seqno = proximal_after_seqno_;
|
||||
if (range_del_split_seqno < kMaxSequenceNumber) {
|
||||
range_del_split_seqno++;
|
||||
}
|
||||
if (outputs.IsProximalLevel()) {
|
||||
keep_seqno_range.first = proximal_after_seqno_;
|
||||
keep_seqno_range.first = range_del_split_seqno;
|
||||
} else {
|
||||
keep_seqno_range.second = proximal_after_seqno_;
|
||||
keep_seqno_range.second = range_del_split_seqno;
|
||||
}
|
||||
}
|
||||
CompactionIterationStats range_del_out_stats;
|
||||
@@ -2353,11 +2406,16 @@ Status CompactionJob::InstallCompactionResults(bool* compaction_released) {
|
||||
*compaction_released = true;
|
||||
};
|
||||
|
||||
return versions_->LogAndApply(compaction->column_family_data(), read_options,
|
||||
write_options, edit, db_mutex_, db_directory_,
|
||||
/*new_descriptor_log=*/false,
|
||||
/*column_family_options=*/nullptr,
|
||||
manifest_wcb);
|
||||
Status s;
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"CompactionJob::InstallCompactionResults:BeforeLogAndApply", &s);
|
||||
if (s.ok()) {
|
||||
s = versions_->LogAndApply(compaction->column_family_data(), read_options,
|
||||
write_options, edit, db_mutex_, db_directory_,
|
||||
/*new_descriptor_log=*/false,
|
||||
/*column_family_options=*/nullptr, manifest_wcb);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void CompactionJob::RecordCompactionIOStats() {
|
||||
|
||||
@@ -60,6 +60,7 @@ class CompactionOutputs {
|
||||
}
|
||||
|
||||
const std::vector<Output>& GetOutputs() const { return outputs_; }
|
||||
std::vector<Output>& GetMutableOutputs() { return outputs_; }
|
||||
|
||||
// Set new table builder for the current output
|
||||
void NewBuilder(const TableBuilderOptions& tboptions);
|
||||
|
||||
@@ -251,7 +251,7 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
|
||||
// When using blob-aware sizing, use proportional estimation (same
|
||||
// principle as EstimateTotalDataForSST): each SST "owns"
|
||||
// effective_size / num_files of total data. This is an approximation
|
||||
// — individual SSTs may reference different amounts of blob data,
|
||||
// -- individual SSTs may reference different amounts of blob data,
|
||||
// but uniform distribution is a reasonable estimate for FIFO dropping.
|
||||
uint64_t remaining_size = effective_size;
|
||||
const uint64_t num_files = last_level_files.size();
|
||||
@@ -477,12 +477,12 @@ Compaction* FIFOCompactionPicker::PickIntraL0Compaction(
|
||||
if (fifo_opts.max_data_files_size == 0) {
|
||||
ROCKS_LOG_BUFFER(
|
||||
log_buffer,
|
||||
"[%s] FIFO kv-ratio compaction: skipping — "
|
||||
"[%s] FIFO kv-ratio compaction: skipping -- "
|
||||
"max_data_files_size is 0, cannot compute target file size. ",
|
||||
cf_name.c_str());
|
||||
} else if (fifo_opts.max_data_files_size < fifo_opts.max_table_files_size) {
|
||||
ROCKS_LOG_BUFFER(log_buffer,
|
||||
"[%s] FIFO kv-ratio compaction: skipping — "
|
||||
"[%s] FIFO kv-ratio compaction: skipping -- "
|
||||
"max_data_files_size (%" PRIu64
|
||||
") < max_table_files_size "
|
||||
"(%" PRIu64 ").",
|
||||
@@ -547,7 +547,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
|
||||
for (int level = 1; level < vstorage->num_levels(); ++level) {
|
||||
if (!vstorage->LevelFiles(level).empty()) {
|
||||
ROCKS_LOG_BUFFER(log_buffer,
|
||||
"[%s] FIFO kv-ratio compaction: skipping — non-L0 "
|
||||
"[%s] FIFO kv-ratio compaction: skipping -- non-L0 "
|
||||
"level %d still has %" ROCKSDB_PRIszt
|
||||
" files (migration in progress)",
|
||||
cf_name.c_str(), level,
|
||||
@@ -587,7 +587,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
|
||||
// once per flush or compaction completion, so no caching is needed.
|
||||
uint64_t target = 0;
|
||||
if (mutable_cf_options.max_compaction_bytes > 0) {
|
||||
// User explicitly set max_compaction_bytes — use it as target
|
||||
// User explicitly set max_compaction_bytes -- use it as target
|
||||
target = mutable_cf_options.max_compaction_bytes;
|
||||
} else {
|
||||
// Auto-calculate from capacity and observed SST/blob ratio
|
||||
@@ -642,7 +642,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
|
||||
// of linear) write amplification.
|
||||
|
||||
// Build tier boundaries from smallest to largest.
|
||||
// Stop at 10KB minimum — SST files of most workloads are larger than
|
||||
// Stop at 10KB minimum -- SST files of most workloads are larger than
|
||||
// this, so lower boundaries would only waste CPU scanning L0 files.
|
||||
// Files smaller than the lowest boundary simply merge at that boundary.
|
||||
static constexpr uint64_t kMinTierBoundary = 10 * 1024; // 10KB
|
||||
@@ -651,7 +651,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
|
||||
boundaries.push_back(b);
|
||||
}
|
||||
if (boundaries.empty()) {
|
||||
// target itself is below kMinTierBoundary — use target as the
|
||||
// target itself is below kMinTierBoundary -- use target as the
|
||||
// sole boundary so we can still compact at the target size.
|
||||
boundaries.push_back(target);
|
||||
}
|
||||
@@ -668,7 +668,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found a file < boundary — collect contiguous batch
|
||||
// Found a file < boundary -- collect contiguous batch
|
||||
std::vector<FileMetaData*> batch;
|
||||
uint64_t accumulated = 0;
|
||||
size_t pos = scan;
|
||||
@@ -713,7 +713,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
|
||||
return c;
|
||||
}
|
||||
|
||||
// This batch wasn't enough — advance past it
|
||||
// This batch wasn't enough -- advance past it
|
||||
scan = pos;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ bool LevelCompactionPicker::NeedsCompaction(
|
||||
if (!vstorage->FilesMarkedForForcedBlobGC().empty()) {
|
||||
return true;
|
||||
}
|
||||
if (!vstorage->ReadTriggeredCompactionFiles().empty()) {
|
||||
return true;
|
||||
}
|
||||
for (int i = 0; i <= vstorage->MaxInputLevel(); i++) {
|
||||
if (vstorage->CompactionScore(i) >= 1) {
|
||||
return true;
|
||||
@@ -325,6 +328,14 @@ void LevelCompactionBuilder::SetupInitialFiles() {
|
||||
compaction_reason_ = CompactionReason::kForcedBlobGC;
|
||||
return;
|
||||
}
|
||||
|
||||
// Read-triggered compaction
|
||||
PickFileToCompact(vstorage_->ReadTriggeredCompactionFiles(),
|
||||
CompactToNextLevel::kYes);
|
||||
if (!start_level_inputs_.empty()) {
|
||||
compaction_reason_ = CompactionReason::kReadTriggered;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool LevelCompactionBuilder::SetupOtherL0FilesIfNeeded() {
|
||||
|
||||
@@ -984,6 +984,186 @@ TEST_F(CompactionPickerTest, UniversalIncrementalSpace1) {
|
||||
ASSERT_EQ(14U, compaction->input(1, 3)->fd.GetNumber());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, ReadTriggeredCompactionDisabled) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
// threshold=0 means disabled, no files should be marked
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 0.0;
|
||||
Add(0, 1U, "100", "200", 1000U);
|
||||
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(999999);
|
||||
UpdateVersionStorageInfo();
|
||||
ASSERT_TRUE(vstorage_->ReadTriggeredCompactionFiles().empty());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, ReadTriggeredCompactionBelowThreshold) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 1.0;
|
||||
// file_size=1000, reads=500 => reads_per_byte=0.5 < 1.0
|
||||
Add(0, 1U, "100", "200", 1000U);
|
||||
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(500);
|
||||
UpdateVersionStorageInfo();
|
||||
ASSERT_TRUE(vstorage_->ReadTriggeredCompactionFiles().empty());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, ReadTriggeredCompactionAboveThreshold) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 0.5;
|
||||
// file_size=1000, reads=600 => reads_per_byte=0.6 > 0.5
|
||||
Add(0, 1U, "100", "200", 1000U);
|
||||
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(600);
|
||||
// file_size=1000, reads=300 => reads_per_byte=0.3 < 0.5 (not marked)
|
||||
Add(1, 2U, "300", "400", 1000U);
|
||||
file_map_[2U].first->stats.num_collapsible_entry_reads_sampled.store(300);
|
||||
// file_size=1000, reads=800 => reads_per_byte=0.8 > 0.5 (hottest)
|
||||
Add(2, 3U, "500", "600", 1000U);
|
||||
file_map_[3U].first->stats.num_collapsible_entry_reads_sampled.store(800);
|
||||
// Add a file at the bottom so L2 is not the last non-empty level
|
||||
Add(4, 4U, "700", "800", 1000U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
const auto& marked = vstorage_->ReadTriggeredCompactionFiles();
|
||||
ASSERT_EQ(marked.size(), 2);
|
||||
// Sorted by reads_per_byte descending: file 3 (0.8) then file 1 (0.6)
|
||||
ASSERT_EQ(marked[0].second->fd.GetNumber(), 3U);
|
||||
ASSERT_EQ(marked[1].second->fd.GetNumber(), 1U);
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, NeedsCompactionReadTriggered) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 0.1;
|
||||
Add(1, 1U, "100", "200", 1000U);
|
||||
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(500);
|
||||
Add(3, 2U, "300", "400", 1000U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
ASSERT_FALSE(vstorage_->ReadTriggeredCompactionFiles().empty());
|
||||
ASSERT_TRUE(level_compaction_picker.NeedsCompaction(vstorage_.get()));
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, ReadTriggeredPicksFile) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 0.1;
|
||||
Add(1, 1U, "100", "200", 1000U);
|
||||
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(500);
|
||||
Add(3, 2U, "300", "400", 1000U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
std::unique_ptr<Compaction> compaction(level_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_,
|
||||
/*existing_snapshots=*/{}, /*snapshot_checker=*/nullptr, vstorage_.get(),
|
||||
&log_buffer_, /*full_history_ts_low=*/""));
|
||||
ASSERT_TRUE(compaction.get() != nullptr);
|
||||
ASSERT_EQ(compaction->compaction_reason(), CompactionReason::kReadTriggered);
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, UniversalReadTriggeredCompaction) {
|
||||
const uint64_t kFileSize = 100000;
|
||||
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 0.001;
|
||||
// Set trigger high so size amp / sorted run pickers don't fire
|
||||
mutable_cf_options_.level0_file_num_compaction_trigger = 10;
|
||||
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
|
||||
|
||||
NewVersionStorage(5, kCompactionStyleUniversal);
|
||||
|
||||
// Hot file at L2 with data at L4 below it
|
||||
Add(0, 1U, "150", "200", kFileSize, 0, 500, 550);
|
||||
Add(2, 2U, "301", "350", kFileSize, 0, 201, 250);
|
||||
Add(4, 3U, "301", "400", kFileSize, 0, 101, 150);
|
||||
|
||||
// Mark file 2 (L2) as having high reads
|
||||
file_map_[2U].first->stats.num_collapsible_entry_reads_sampled.store(
|
||||
kFileSize);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
ASSERT_TRUE(universal_compaction_picker.NeedsCompaction(vstorage_.get()));
|
||||
|
||||
std::unique_ptr<Compaction> compaction(
|
||||
universal_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_,
|
||||
/*existing_snapshots=*/{}, /*snapshot_checker=*/nullptr,
|
||||
vstorage_.get(), &log_buffer_, /*full_history_ts_low=*/""));
|
||||
|
||||
ASSERT_TRUE(compaction);
|
||||
ASSERT_EQ(compaction->compaction_reason(), CompactionReason::kReadTriggered);
|
||||
ASSERT_EQ(compaction->start_level(), 2);
|
||||
ASSERT_EQ(compaction->output_level(), 4);
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, ReadTriggeredSkipsLastLevel) {
|
||||
const uint64_t kFileSize = 100000;
|
||||
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 0.001;
|
||||
mutable_cf_options_.level0_file_num_compaction_trigger = 10;
|
||||
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
|
||||
|
||||
NewVersionStorage(5, kCompactionStyleUniversal);
|
||||
|
||||
Add(0, 1U, "150", "200", kFileSize, 0, 500, 550);
|
||||
Add(4, 3U, "301", "350", kFileSize, 0, 101, 150);
|
||||
|
||||
// File 3 is at the last non-empty level -- should NOT be marked for
|
||||
// read-triggered compaction. Bottommost file cleanup is handled
|
||||
// separately by ComputeBottommostFilesMarkedForCompaction().
|
||||
file_map_[3U].first->stats.num_collapsible_entry_reads_sampled.store(
|
||||
kFileSize);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
ASSERT_TRUE(vstorage_->ReadTriggeredCompactionFiles().empty());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, UniversalReadTriggeredNoPickWhenNotMarked) {
|
||||
const uint64_t kFileSize = 100000;
|
||||
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 0.001;
|
||||
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
|
||||
|
||||
NewVersionStorage(5, kCompactionStyleUniversal);
|
||||
|
||||
Add(0, 1U, "150", "200", kFileSize, 0, 500, 550);
|
||||
Add(4, 3U, "301", "350", kFileSize, 0, 101, 150);
|
||||
|
||||
// No reads on any file
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
ASSERT_TRUE(vstorage_->ReadTriggeredCompactionFiles().empty());
|
||||
// Not enough sorted runs to trigger compaction either
|
||||
ASSERT_FALSE(universal_compaction_picker.NeedsCompaction(vstorage_.get()));
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, UniversalReadTriggeredIntraL0) {
|
||||
const uint64_t kFileSize = 100000;
|
||||
|
||||
mutable_cf_options_.read_triggered_compaction_threshold = 0.001;
|
||||
mutable_cf_options_.level0_file_num_compaction_trigger = 10;
|
||||
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
|
||||
|
||||
// Single-level universal with overlapping L0 files
|
||||
NewVersionStorage(1, kCompactionStyleUniversal);
|
||||
|
||||
// Two L0 files with overlapping key ranges
|
||||
Add(0, 1U, "100", "300", kFileSize, 0, 500, 550);
|
||||
Add(0, 2U, "200", "400", kFileSize, 0, 400, 450);
|
||||
|
||||
// Mark file 1 as hot
|
||||
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(
|
||||
kFileSize);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
ASSERT_FALSE(vstorage_->ReadTriggeredCompactionFiles().empty());
|
||||
ASSERT_TRUE(universal_compaction_picker.NeedsCompaction(vstorage_.get()));
|
||||
|
||||
std::unique_ptr<Compaction> compaction(
|
||||
universal_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_,
|
||||
/*existing_snapshots=*/{}, /*snapshot_checker=*/nullptr,
|
||||
vstorage_.get(), &log_buffer_, /*full_history_ts_low=*/""));
|
||||
|
||||
ASSERT_TRUE(compaction);
|
||||
ASSERT_EQ(compaction->compaction_reason(), CompactionReason::kReadTriggered);
|
||||
ASSERT_EQ(compaction->start_level(), 0);
|
||||
ASSERT_EQ(compaction->output_level(), 0);
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, UniversalIncrementalSpace2) {
|
||||
const uint64_t kFileSize = 100000;
|
||||
|
||||
|
||||
@@ -275,6 +275,21 @@ class UniversalCompactionBuilder {
|
||||
return c;
|
||||
}
|
||||
|
||||
Compaction* MaybePickReadTriggeredCompaction(
|
||||
Compaction* const prev_picked_c) {
|
||||
if (prev_picked_c != nullptr ||
|
||||
vstorage_->ReadTriggeredCompactionFiles().empty()) {
|
||||
return prev_picked_c;
|
||||
}
|
||||
Compaction* c = PickReadTriggeredCompaction();
|
||||
if (c != nullptr) {
|
||||
ROCKS_LOG_BUFFER(log_buffer_,
|
||||
"[%s] Universal: picked for read triggered compaction\n",
|
||||
cf_name_.c_str());
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
// Pick Universal compaction to limit read amplification
|
||||
Compaction* PickCompactionToReduceSortedRuns(
|
||||
unsigned int ratio, unsigned int max_number_of_files_to_compact);
|
||||
@@ -292,6 +307,14 @@ class UniversalCompactionBuilder {
|
||||
|
||||
Compaction* PickDeleteTriggeredCompaction();
|
||||
|
||||
Compaction* PickReadTriggeredCompaction();
|
||||
|
||||
// Given already-selected start_level_inputs, find the first non-empty output
|
||||
// level, compute overlapping inputs, and create a Compaction. Can produce
|
||||
// intra-L0 compaction if there is only level 0.
|
||||
Compaction* BuildCompactionToNextLevel(
|
||||
CompactionInputFiles& start_level_inputs, CompactionReason reason);
|
||||
|
||||
// Returns true if this given file (that is marked be compaction) should be
|
||||
// skipped from being picked for now. We do this to best use standalone range
|
||||
// tombstone files.
|
||||
@@ -594,6 +617,9 @@ bool UniversalCompactionPicker::NeedsCompaction(
|
||||
if (!vstorage->FilesMarkedForCompaction().empty()) {
|
||||
return true;
|
||||
}
|
||||
if (!vstorage->ReadTriggeredCompactionFiles().empty()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -759,6 +785,7 @@ Compaction* UniversalCompactionBuilder::PickCompaction() {
|
||||
if (sorted_runs_.size() == 0 ||
|
||||
(vstorage_->FilesMarkedForPeriodicCompaction().empty() &&
|
||||
vstorage_->FilesMarkedForCompaction().empty() &&
|
||||
vstorage_->ReadTriggeredCompactionFiles().empty() &&
|
||||
sorted_runs_.size() < (unsigned int)file_num_compaction_trigger)) {
|
||||
ROCKS_LOG_BUFFER(log_buffer_, "[%s] Universal: nothing to do\n",
|
||||
cf_name_.c_str());
|
||||
@@ -782,6 +809,7 @@ Compaction* UniversalCompactionBuilder::PickCompaction() {
|
||||
c = MaybePickCompactionToReduceSortedRuns(c, file_num_compaction_trigger,
|
||||
ratio);
|
||||
c = MaybePickDeleteTriggeredCompaction(c);
|
||||
c = MaybePickReadTriggeredCompaction(c);
|
||||
|
||||
if (c == nullptr) {
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
@@ -1461,9 +1489,6 @@ Compaction* UniversalCompactionBuilder::PickIncrementalForReduceSizeAmp(
|
||||
// CompactOnDeleteCollector due to the presence of tombstones.
|
||||
Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
|
||||
CompactionInputFiles start_level_inputs;
|
||||
int output_level;
|
||||
std::vector<CompactionInputFiles> inputs;
|
||||
std::vector<FileMetaData*> grandparents;
|
||||
|
||||
if (vstorage_->num_levels() == 1) {
|
||||
// This is single level universal. Since we're basically trying to reclaim
|
||||
@@ -1474,7 +1499,6 @@ Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
|
||||
|
||||
start_level_inputs.level = 0;
|
||||
start_level_inputs.files.clear();
|
||||
output_level = 0;
|
||||
// Find the first file marked for compaction. Ignore the last file
|
||||
for (size_t loop = 0; loop + 1 < sorted_runs_.size(); loop++) {
|
||||
SortedRun* sr = &sorted_runs_[loop];
|
||||
@@ -1503,13 +1527,9 @@ Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
|
||||
FileMetaData* f = vstorage_->LevelFiles(0)[loop];
|
||||
start_level_inputs.files.push_back(f);
|
||||
}
|
||||
if (start_level_inputs.size() <= 1) {
|
||||
// If only the last file in L0 is marked for compaction, ignore it
|
||||
return nullptr;
|
||||
}
|
||||
inputs.push_back(start_level_inputs);
|
||||
} else {
|
||||
int start_level;
|
||||
int output_level;
|
||||
|
||||
// For multi-level universal, the strategy is to make this look more like
|
||||
// leveled. We pick one of the files marked for compaction and compact with
|
||||
@@ -1522,100 +1542,10 @@ Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
|
||||
if (start_level_inputs.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int max_output_level = vstorage_->MaxOutputLevel(allow_ingest_behind_);
|
||||
// Pick the first non-empty level after the start_level
|
||||
for (output_level = start_level + 1; output_level <= max_output_level;
|
||||
output_level++) {
|
||||
if (vstorage_->NumLevelFiles(output_level) != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If all higher levels are empty, pick the highest level as output level
|
||||
if (output_level > max_output_level) {
|
||||
if (start_level == 0) {
|
||||
output_level = max_output_level;
|
||||
} else {
|
||||
// If start level is non-zero and all higher levels are empty, this
|
||||
// compaction will translate into a trivial move. Since the idea is
|
||||
// to reclaim space and trivial move doesn't help with that, we
|
||||
// skip compaction in this case and return nullptr
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
assert(output_level <= max_output_level);
|
||||
|
||||
if (!MeetsOutputLevelRequirements(output_level)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (output_level != 0) {
|
||||
// For standalone range deletion, we don't want to compact it with newer
|
||||
// L0 files that it doesn't cover.
|
||||
const FileMetaData* starting_l0_file =
|
||||
(start_level == 0 && start_level_inputs.size() == 1 &&
|
||||
start_level_inputs.files[0]->FileIsStandAloneRangeTombstone())
|
||||
? start_level_inputs.files[0]
|
||||
: nullptr;
|
||||
|
||||
if (start_level == 0) {
|
||||
if (!picker_->GetOverlappingL0Files(vstorage_, &start_level_inputs,
|
||||
output_level, nullptr,
|
||||
starting_l0_file)) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
CompactionInputFiles output_level_inputs;
|
||||
int parent_index = -1;
|
||||
|
||||
output_level_inputs.level = output_level;
|
||||
if (!picker_->SetupOtherInputs(cf_name_, mutable_cf_options_, vstorage_,
|
||||
&start_level_inputs, &output_level_inputs,
|
||||
&parent_index, -1, false,
|
||||
starting_l0_file)) {
|
||||
return nullptr;
|
||||
}
|
||||
inputs.push_back(start_level_inputs);
|
||||
if (!output_level_inputs.empty()) {
|
||||
inputs.push_back(output_level_inputs);
|
||||
}
|
||||
if (picker_->FilesRangeOverlapWithCompaction(
|
||||
inputs, output_level,
|
||||
Compaction::EvaluateProximalLevel(vstorage_, mutable_cf_options_,
|
||||
ioptions_, start_level,
|
||||
output_level))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
picker_->GetGrandparents(vstorage_, start_level_inputs,
|
||||
output_level_inputs, &grandparents);
|
||||
} else {
|
||||
inputs.push_back(start_level_inputs);
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t estimated_total_size = 0;
|
||||
// Use size of the output level as estimated file size
|
||||
for (FileMetaData* f : vstorage_->LevelFiles(output_level)) {
|
||||
estimated_total_size += f->fd.GetFileSize();
|
||||
}
|
||||
uint32_t path_id =
|
||||
GetPathId(ioptions_, mutable_cf_options_, estimated_total_size);
|
||||
return new Compaction(
|
||||
vstorage_, ioptions_, mutable_cf_options_, mutable_db_options_,
|
||||
std::move(inputs), output_level,
|
||||
MaxFileSizeForLevel(mutable_cf_options_, output_level,
|
||||
kCompactionStyleUniversal),
|
||||
/* max_grandparent_overlap_bytes */ GetMaxOverlappingBytes(), path_id,
|
||||
GetCompressionType(vstorage_, mutable_cf_options_, output_level, 1),
|
||||
GetCompressionOptions(mutable_cf_options_, vstorage_, output_level),
|
||||
Temperature::kUnknown,
|
||||
/* max_subcompactions */ 0, grandparents, earliest_snapshot_,
|
||||
snapshot_checker_, CompactionReason::kFilesMarkedForCompaction,
|
||||
/* trim_ts */ "", score_,
|
||||
/* l0_files_might_overlap */ true);
|
||||
return BuildCompactionToNextLevel(
|
||||
start_level_inputs, CompactionReason::kFilesMarkedForCompaction);
|
||||
}
|
||||
|
||||
Compaction* UniversalCompactionBuilder::PickCompactionToOldest(
|
||||
@@ -1782,6 +1712,153 @@ Compaction* UniversalCompactionBuilder::PickPeriodicCompaction() {
|
||||
return c;
|
||||
}
|
||||
|
||||
Compaction* UniversalCompactionBuilder::PickReadTriggeredCompaction() {
|
||||
ROCKS_LOG_BUFFER(log_buffer_, "[%s] Universal: Read Triggered Compaction",
|
||||
cf_name_.c_str());
|
||||
|
||||
if (vstorage_->ReadTriggeredCompactionFiles().empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CompactionInputFiles start_level_inputs;
|
||||
int start_level = -1;
|
||||
|
||||
for (const auto& level_file : vstorage_->ReadTriggeredCompactionFiles()) {
|
||||
assert(!level_file.second->being_compacted);
|
||||
start_level = level_file.first;
|
||||
|
||||
if (start_level == 0 &&
|
||||
!picker_->level0_compactions_in_progress()->empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
start_level_inputs.files = {level_file.second};
|
||||
start_level_inputs.level = start_level;
|
||||
if (picker_->ExpandInputsToCleanCut(cf_name_, vstorage_,
|
||||
&start_level_inputs)) {
|
||||
break;
|
||||
}
|
||||
start_level_inputs.files.clear();
|
||||
}
|
||||
|
||||
if (start_level_inputs.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// For intra L0 compactions, pick up all overlapping files
|
||||
if (vstorage_->num_levels() == 1 && start_level_inputs.level == 0) {
|
||||
if (!picker_->GetOverlappingL0Files(vstorage_, &start_level_inputs,
|
||||
/*output_level=*/0, nullptr, nullptr)) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
return BuildCompactionToNextLevel(start_level_inputs,
|
||||
CompactionReason::kReadTriggered);
|
||||
}
|
||||
|
||||
Compaction* UniversalCompactionBuilder::BuildCompactionToNextLevel(
|
||||
CompactionInputFiles& start_level_inputs, CompactionReason reason) {
|
||||
int start_level = start_level_inputs.level;
|
||||
int max_output_level = vstorage_->MaxOutputLevel(allow_ingest_behind_);
|
||||
|
||||
// Pick the first non-empty level after start_level as output.
|
||||
int output_level;
|
||||
for (output_level = start_level + 1; output_level <= max_output_level;
|
||||
output_level++) {
|
||||
if (vstorage_->NumLevelFiles(output_level) != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If all higher levels are empty, pick the highest level as output level
|
||||
// for L0 start. For non-L0 start, a trivial move doesn't help reclaim
|
||||
// space, so skip.
|
||||
if (output_level > max_output_level) {
|
||||
if (start_level == 0) {
|
||||
output_level = max_output_level;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (start_level_inputs.size() <= 1 && output_level == 0) {
|
||||
// If only the last file in L0 is marked for compaction, ignore it
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<CompactionInputFiles> inputs;
|
||||
std::vector<FileMetaData*> grandparents;
|
||||
|
||||
if (output_level != 0) {
|
||||
if (!MeetsOutputLevelRequirements(output_level)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// For standalone range deletion, we don't want to compact it with newer
|
||||
// L0 files that it doesn't cover.
|
||||
const FileMetaData* starting_l0_file =
|
||||
(start_level == 0 && start_level_inputs.size() == 1 &&
|
||||
start_level_inputs.files[0]->FileIsStandAloneRangeTombstone())
|
||||
? start_level_inputs.files[0]
|
||||
: nullptr;
|
||||
|
||||
if (start_level == 0) {
|
||||
if (!picker_->GetOverlappingL0Files(vstorage_, &start_level_inputs,
|
||||
output_level, nullptr,
|
||||
starting_l0_file)) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
CompactionInputFiles output_level_inputs;
|
||||
int parent_index = -1;
|
||||
|
||||
output_level_inputs.level = output_level;
|
||||
if (!picker_->SetupOtherInputs(
|
||||
cf_name_, mutable_cf_options_, vstorage_, &start_level_inputs,
|
||||
&output_level_inputs, &parent_index, -1, false, starting_l0_file)) {
|
||||
return nullptr;
|
||||
}
|
||||
inputs.push_back(start_level_inputs);
|
||||
if (!output_level_inputs.empty()) {
|
||||
inputs.push_back(output_level_inputs);
|
||||
}
|
||||
if (picker_->FilesRangeOverlapWithCompaction(
|
||||
inputs, output_level,
|
||||
Compaction::EvaluateProximalLevel(vstorage_, mutable_cf_options_,
|
||||
ioptions_, start_level,
|
||||
output_level))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
picker_->GetGrandparents(vstorage_, start_level_inputs, output_level_inputs,
|
||||
&grandparents);
|
||||
} else {
|
||||
inputs.push_back(start_level_inputs);
|
||||
}
|
||||
|
||||
uint64_t estimated_total_size = 0;
|
||||
for (FileMetaData* f : vstorage_->LevelFiles(output_level)) {
|
||||
estimated_total_size += f->fd.GetFileSize();
|
||||
}
|
||||
uint32_t path_id =
|
||||
GetPathId(ioptions_, mutable_cf_options_, estimated_total_size);
|
||||
return new Compaction(
|
||||
vstorage_, ioptions_, mutable_cf_options_, mutable_db_options_,
|
||||
std::move(inputs), output_level,
|
||||
MaxFileSizeForLevel(mutable_cf_options_, output_level,
|
||||
kCompactionStyleUniversal),
|
||||
/* max_grandparent_overlap_bytes */ GetMaxOverlappingBytes(), path_id,
|
||||
GetCompressionType(vstorage_, mutable_cf_options_, output_level, 1),
|
||||
GetCompressionOptions(mutable_cf_options_, vstorage_, output_level),
|
||||
Temperature::kUnknown,
|
||||
/* max_subcompactions */ 0, grandparents, earliest_snapshot_,
|
||||
snapshot_checker_, reason,
|
||||
/* trim_ts */ "", score_,
|
||||
/* l0_files_might_overlap */ true);
|
||||
}
|
||||
|
||||
uint64_t UniversalCompactionBuilder::GetMaxOverlappingBytes() const {
|
||||
if (!mutable_cf_options_.compaction_options_universal.incremental) {
|
||||
return std::numeric_limits<uint64_t>::max();
|
||||
|
||||
@@ -189,14 +189,24 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
|
||||
return compaction_status;
|
||||
}
|
||||
|
||||
// CompactionServiceJobStatus::kSuccess was returned, but somehow we failed to
|
||||
// read the result. Consider this as an installation failure
|
||||
if (!s.ok()) {
|
||||
sub_compact->status = s;
|
||||
// Wait() returned kSuccess, but the primary host could not deserialize the
|
||||
// remote result before importing any output files into this DB. The remote
|
||||
// output is still isolated in the service-managed staging directory, so it
|
||||
// is safe to fall back to local compaction for the same job and notify the
|
||||
// service via OnInstallation(kUseLocal).
|
||||
assert(sub_compact->status.ok());
|
||||
assert(sub_compact->GetMutableCompactionOutputs().empty());
|
||||
assert(sub_compact->GetMutableProximalOutputs().empty());
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"[%s] [JOB %d] Failed to parse remote compaction result "
|
||||
"(%s), falling back to local compaction",
|
||||
compaction->column_family_data()->GetName().c_str(), job_id_,
|
||||
s.ToString().c_str());
|
||||
compaction_result.status.PermitUncheckedError();
|
||||
db_options_.compaction_service->OnInstallation(
|
||||
response.scheduled_job_id, CompactionServiceJobStatus::kFailure);
|
||||
return CompactionServiceJobStatus::kFailure;
|
||||
response.scheduled_job_id, CompactionServiceJobStatus::kUseLocal);
|
||||
return CompactionServiceJobStatus::kUseLocal;
|
||||
}
|
||||
sub_compact->status = compaction_result.status;
|
||||
|
||||
@@ -641,6 +651,10 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
||||
{"cpu_micros",
|
||||
{offsetof(struct CompactionJobStats, cpu_micros), OptionType::kUInt64T,
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone}},
|
||||
{"has_accurate_num_input_records",
|
||||
{offsetof(struct CompactionJobStats, has_accurate_num_input_records),
|
||||
OptionType::kBoolean, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"num_input_records",
|
||||
{offsetof(struct CompactionJobStats, num_input_records),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
@@ -846,11 +860,16 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
||||
{offsetof(struct InternalStats::CompactionStats, count),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"counts", OptionTypeInfo::Array<
|
||||
int, static_cast<int>(CompactionReason::kNumOfReasons)>(
|
||||
offsetof(struct InternalStats::CompactionStats, counts),
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
|
||||
{0, OptionType::kInt})},
|
||||
{"counts",
|
||||
OptionTypeInfo::Array<
|
||||
/* In release 11.2, a new compaction reason was added. This broken
|
||||
* the reader and writer. To unblock release 11.1, we temporarily
|
||||
* reduce the count array size to the old one. TODO add a proper
|
||||
* serialization and deserialization method. */
|
||||
int, static_cast<int>(CompactionReason::kNumOfReasons) - 1>(
|
||||
offsetof(struct InternalStats::CompactionStats, counts),
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
|
||||
{0, OptionType::kInt})},
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo>
|
||||
|
||||
@@ -127,6 +127,7 @@ class MyTestCompactionService : public CompactionService {
|
||||
|
||||
void OnInstallation(const std::string& /*scheduled_job_id*/,
|
||||
CompactionServiceJobStatus status) override {
|
||||
installation_callback_count_.fetch_add(1);
|
||||
final_updated_status_ = status;
|
||||
}
|
||||
|
||||
@@ -167,6 +168,8 @@ class MyTestCompactionService : public CompactionService {
|
||||
return final_updated_status_.load();
|
||||
}
|
||||
|
||||
int GetOnInstallationCount() { return installation_callback_count_.load(); }
|
||||
|
||||
protected:
|
||||
InstrumentedMutex mutex_;
|
||||
const std::string db_path_;
|
||||
@@ -195,6 +198,7 @@ class MyTestCompactionService : public CompactionService {
|
||||
std::vector<std::shared_ptr<EventListener>> listeners_;
|
||||
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>
|
||||
table_properties_collector_factories_;
|
||||
std::atomic_int installation_callback_count_{0};
|
||||
std::atomic<CompactionServiceJobStatus> final_updated_status_{
|
||||
CompactionServiceJobStatus::kUseLocal};
|
||||
|
||||
@@ -939,6 +943,37 @@ TEST_F(CompactionServiceTest, VerifyInputRecordCount) {
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, InaccurateInputRecordCount) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
ReopenWithCompactionService(&options);
|
||||
GenerateTestData();
|
||||
|
||||
auto my_cs = GetCompactionService();
|
||||
|
||||
std::string start_str = Key(15);
|
||||
std::string end_str = Key(45);
|
||||
Slice start(start_str);
|
||||
Slice end(end_str);
|
||||
uint64_t comp_num = my_cs->GetCompactionNum();
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionServiceCompactionJob::Run:0", [&](void* arg) {
|
||||
CompactionServiceResult* compaction_result =
|
||||
*(static_cast<CompactionServiceResult**>(arg));
|
||||
ASSERT_TRUE(compaction_result != nullptr);
|
||||
compaction_result->stats.has_accurate_num_input_records = false;
|
||||
compaction_result->stats.num_input_records = 0;
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &start, &end));
|
||||
ASSERT_GE(my_cs->GetCompactionNum(), comp_num + 1);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, EmptyResult) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
@@ -1281,6 +1316,10 @@ TEST_F(CompactionServiceTest, TruncatedOutput) {
|
||||
|
||||
TEST_F(CompactionServiceTest, CustomFileChecksum) {
|
||||
Options options = CurrentOptions();
|
||||
// Pin compression so the auto-compacted LSM shape (and thus whether the
|
||||
// manual CompactRange below schedules a remote compaction) doesn't depend on
|
||||
// the default compression type. kNoCompression is always available.
|
||||
options.compression = kNoCompression;
|
||||
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
|
||||
ReopenWithCompactionService(&options);
|
||||
GenerateTestData();
|
||||
@@ -1448,7 +1487,7 @@ TEST_F(CompactionServiceTest, FailedToStart) {
|
||||
ASSERT_TRUE(s.IsIncomplete());
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, InvalidResult) {
|
||||
TEST_F(CompactionServiceTest, InvalidResultFallsBackToLocal) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
ReopenWithCompactionService(&options);
|
||||
@@ -1456,15 +1495,22 @@ TEST_F(CompactionServiceTest, InvalidResult) {
|
||||
GenerateTestData();
|
||||
|
||||
auto my_cs = GetCompactionService();
|
||||
ASSERT_EQ(0, my_cs->GetOnInstallationCount());
|
||||
my_cs->OverrideWaitResult("Invalid Str");
|
||||
int compaction_num_before = my_cs->GetCompactionNum();
|
||||
|
||||
std::string start_str = Key(15);
|
||||
std::string end_str = Key(45);
|
||||
Slice start(start_str);
|
||||
Slice end(end_str);
|
||||
// The remote result is malformed before any installation starts, so the
|
||||
// primary should recover by rerunning the compaction locally for this job.
|
||||
Status s = db_->CompactRange(CompactRangeOptions(), &start, &end);
|
||||
ASSERT_FALSE(s.ok());
|
||||
ASSERT_EQ(CompactionServiceJobStatus::kFailure,
|
||||
ASSERT_OK(s);
|
||||
ASSERT_GE(my_cs->GetCompactionNum(), compaction_num_before + 1);
|
||||
VerifyTestData();
|
||||
ASSERT_EQ(1, my_cs->GetOnInstallationCount());
|
||||
ASSERT_EQ(CompactionServiceJobStatus::kUseLocal,
|
||||
my_cs->GetFinalCompactionServiceJobStatus());
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,15 @@ class SubcompactionState {
|
||||
// it returns both the last level outputs and proximal level outputs.
|
||||
OutputIterator GetOutputs() const;
|
||||
|
||||
// Get mutable access to outputs for post-processing (e.g., retrieving
|
||||
// file open metadata).
|
||||
std::vector<CompactionOutputs::Output>& GetMutableCompactionOutputs() {
|
||||
return compaction_outputs_.GetMutableOutputs();
|
||||
}
|
||||
std::vector<CompactionOutputs::Output>& GetMutableProximalOutputs() {
|
||||
return proximal_level_outputs_.GetMutableOutputs();
|
||||
}
|
||||
|
||||
// Assign range dels aggregator. The various tombstones will potentially
|
||||
// be filtered to different outputs.
|
||||
void AssignRangeDelAggregator(
|
||||
|
||||
@@ -2897,6 +2897,61 @@ TEST_P(PrecludeLastLevelTest, RangeDelsCauseFileEndpointsToOverlap) {
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(PrecludeLastLevelTestBase,
|
||||
RangeDelAtProximalSeqnoBoundaryStaysInLastLevel) {
|
||||
constexpr int kNumLevels = 7;
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.env = mock_env_.get();
|
||||
options.num_levels = kNumLevels;
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
Defer close_db([&] { Close(); });
|
||||
|
||||
ASSERT_OK(Put(Key(2), "old"));
|
||||
ASSERT_OK(Put(Key(12), "old"));
|
||||
ManagedSnapshot snapshot(db_.get());
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(2),
|
||||
Key(12)));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(6);
|
||||
|
||||
const int l5_file_keys[][2] = {{0, 4}, {5, 9}};
|
||||
for (const auto& l5_file : l5_file_keys) {
|
||||
ASSERT_OK(Put(Key(l5_file[0]), "hot"));
|
||||
ASSERT_OK(Put(Key(l5_file[1]), "hot"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(5);
|
||||
}
|
||||
ASSERT_EQ("0,0,0,0,0,2,1", FilesPerLevel());
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::PrepareTimes():preclude_last_level_min_seqno",
|
||||
[](void* arg) { *static_cast<SequenceNumber*>(arg) = 0; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(db_->SetOptions({{"preclude_last_level_data_seconds", "10000"}}));
|
||||
|
||||
auto l5_files = GetLevelFileMetadatas(5);
|
||||
auto l6_files = GetLevelFileMetadatas(6);
|
||||
ASSERT_EQ(2, l5_files.size());
|
||||
ASSERT_EQ(1, l6_files.size());
|
||||
|
||||
ASSERT_OK(db_->CompactFiles(CompactionOptions(),
|
||||
{MakeTableFileName(l5_files[1]->fd.GetNumber()),
|
||||
MakeTableFileName(l6_files[0]->fd.GetNumber())},
|
||||
6));
|
||||
|
||||
uint64_t l6_range_deletions = 0;
|
||||
for (auto* f : GetLevelFileMetadatas(5)) {
|
||||
ASSERT_EQ(0, f->num_range_deletions);
|
||||
}
|
||||
for (auto* f : GetLevelFileMetadatas(6)) {
|
||||
l6_range_deletions += f->num_range_deletions;
|
||||
}
|
||||
ASSERT_GT(l6_range_deletions, 0);
|
||||
}
|
||||
|
||||
// Tests DBIter::GetProperty("rocksdb.iterator.write-time") return a data's
|
||||
// approximate write unix time.
|
||||
class IteratorWriteTimeTest
|
||||
|
||||
@@ -102,6 +102,862 @@ TEST_F(DBBasicTest, OpenWhenOpen) {
|
||||
ASSERT_TRUE(strstr(s.getState(), "lock ") != nullptr);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Helper that captures per-branch SkippedNoopEdit counts for tests.
|
||||
struct RecoveryOptimizationCounters {
|
||||
std::atomic<int> setup_dbid{0};
|
||||
std::atomic<int> per_cf{0};
|
||||
std::atomic<int> wal_deletion{0};
|
||||
std::atomic<int> next_file_number{0};
|
||||
|
||||
void Install() {
|
||||
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
|
||||
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:SetupDBId",
|
||||
[this](void*) { setup_dbid.fetch_add(1); });
|
||||
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:PerCF",
|
||||
[this](void*) { per_cf.fetch_add(1); });
|
||||
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:WalDeletion",
|
||||
[this](void*) { wal_deletion.fetch_add(1); });
|
||||
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber",
|
||||
[this](void*) { next_file_number.fetch_add(1); });
|
||||
sp->EnableProcessing();
|
||||
}
|
||||
void Uninstall() {
|
||||
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
|
||||
sp->DisableProcessing();
|
||||
sp->ClearAllCallBacks();
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
// optimize_manifest_for_recovery=true: a clean reopen of a flushed DB must
|
||||
// append fewer individual records to the MANIFEST than the default-off path.
|
||||
// Verified by counting AddRecord calls (one per VersionEdit written to the
|
||||
// MANIFEST log).
|
||||
TEST_F(DBBasicTest,
|
||||
OptimizeManifestForRecoveryReducesManifestWritesOnCleanReopen) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
|
||||
|
||||
// Measure MANIFEST records with optimize_manifest_for_recovery OFF (default).
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k1", "v1"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
std::atomic<int> records_off{0};
|
||||
std::atomic<int> next_file_skips_off{0};
|
||||
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
|
||||
[&](void*) { records_off.fetch_add(1); });
|
||||
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber",
|
||||
[&](void*) { next_file_skips_off.fetch_add(1); });
|
||||
sp->EnableProcessing();
|
||||
Reopen(options);
|
||||
sp->DisableProcessing();
|
||||
sp->ClearAllCallBacks();
|
||||
ASSERT_EQ("v1", Get("k1"));
|
||||
int off_count = records_off.load();
|
||||
ASSERT_GT(off_count, 0);
|
||||
ASSERT_EQ(0, next_file_skips_off.load());
|
||||
Close();
|
||||
|
||||
// Measure MANIFEST records with optimize_manifest_for_recovery ON.
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k2", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
std::atomic<int> records_on{0};
|
||||
std::atomic<int> next_file_skips_on{0};
|
||||
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
|
||||
[&](void*) { records_on.fetch_add(1); });
|
||||
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber",
|
||||
[&](void*) { next_file_skips_on.fetch_add(1); });
|
||||
sp->EnableProcessing();
|
||||
Reopen(options);
|
||||
sp->DisableProcessing();
|
||||
sp->ClearAllCallBacks();
|
||||
ASSERT_EQ("v2", Get("k2"));
|
||||
int on_count = records_on.load();
|
||||
ASSERT_GT(next_file_skips_on.load(), 0);
|
||||
ASSERT_LT(on_count, off_count);
|
||||
}
|
||||
|
||||
// When the per-CF log_number actually advances, the per-CF skip must NOT
|
||||
// fire -- the edit carries real information and must be emitted.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryEmitsPerCFWhenLogAdvances) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
// Write data and close WITHOUT flushing -- the WAL has un-replayed
|
||||
// records, so on reopen recovery flushes the memtable and the per-CF
|
||||
// edit's log_number must advance past the prior WAL.
|
||||
Close();
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_EQ(0, counters.per_cf.load());
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// With track_and_verify_wals_in_manifest=true, the wal_deletion edit
|
||||
// must STILL be emitted (the DeleteWalsBefore record is required).
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryPreservesWalTracking) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.track_and_verify_wals_in_manifest = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_EQ(0, counters.wal_deletion.load());
|
||||
}
|
||||
|
||||
// best_efforts_recovery requires a fresh MANIFEST + CURRENT to be
|
||||
// produced on every open (the salvage contract). Even with the option
|
||||
// on, none of the SkippedNoopEdit branches must fire under
|
||||
// best_efforts_recovery -- otherwise CURRENT can be left missing or
|
||||
// stale (regression caught by DBBasicTest.RecoverWithNoCurrentFile and
|
||||
// DBTest2.BestEffortsRecoveryWithSstUniqueIdVerification under an
|
||||
// option-on default).
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryDisabledByBestEffortsRecovery) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.best_efforts_recovery = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_EQ(0, counters.setup_dbid.load());
|
||||
ASSERT_EQ(0, counters.per_cf.load());
|
||||
ASSERT_EQ(0, counters.wal_deletion.load());
|
||||
ASSERT_EQ(0, counters.next_file_number.load());
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// Multi-column-family coverage: with one CF flushed and another not,
|
||||
// the un-flushed CF's edit must still be emitted.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryMultiCF) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
CreateAndReopenWithCF({"pikachu", "raichu"}, options);
|
||||
ASSERT_OK(Put(0, "k", "v0"));
|
||||
ASSERT_OK(Put(1, "k", "v1"));
|
||||
ASSERT_OK(Put(2, "k", "v2"));
|
||||
ASSERT_OK(Flush(0));
|
||||
// CF 1 and 2 keep dirty memtables -- recovery must emit their per-CF
|
||||
// edits (log_number advance from WAL replay).
|
||||
Close();
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
ReopenWithColumnFamilies({"default", "pikachu", "raichu"}, options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_EQ("v0", Get(0, "k"));
|
||||
ASSERT_EQ("v1", Get(1, "k"));
|
||||
ASSERT_EQ("v2", Get(2, "k"));
|
||||
}
|
||||
|
||||
// MaybeUpdateNextFileNumber's seed value (next_file_number_) makes the
|
||||
// post-loop comparison trivially true on every clean recovery, causing a
|
||||
// no-op SetNextFile edit to be appended to the MANIFEST. With
|
||||
// optimize_manifest_for_recovery=true, that emission is gated and
|
||||
// the SkippedNoopEdit:NextFileNumber sync point fires in its place.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoverySkipsNextFileNumber) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_EQ(1, counters.next_file_number.load());
|
||||
}
|
||||
|
||||
// With option=true and a synthetic on-disk file whose number is at or
|
||||
// above next_file_number_, MaybeUpdateNextFileNumber MUST emit the
|
||||
// SetNextFile edit and advance the counter past the synthetic number.
|
||||
TEST_F(DBBasicTest,
|
||||
OptimizeManifestForRecoveryEmitsNextFileNumberWhenJustified) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
const uint64_t next_before_close =
|
||||
dbfull()->GetVersionSet()->current_next_file_number();
|
||||
Close();
|
||||
|
||||
// Drop an empty .sst with a number well above next_file_number into
|
||||
// the DB dir. MaybeUpdateNextFileNumber must observe it and advance.
|
||||
const uint64_t synthetic_number = next_before_close + 100;
|
||||
const std::string synthetic_sst =
|
||||
dbname_ + "/" + MakeTableFileName("", synthetic_number);
|
||||
ASSERT_OK(WriteStringToFile(env_, "" /*data*/, synthetic_sst,
|
||||
/*should_sync=*/true));
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_EQ(0, counters.next_file_number.load());
|
||||
ASSERT_GT(dbfull()->GetVersionSet()->current_next_file_number(),
|
||||
synthetic_number);
|
||||
}
|
||||
|
||||
// optimize_manifest_for_recovery=true: after a clean Put + Flush + Close, the
|
||||
// next Open's min_log_number_to_keep equals (max_wal_before_close + 1),
|
||||
// proving the close-time MANIFEST write took effect.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryAdvancesWalMarkersOnClose) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
uint64_t max_wal_before_close = 0;
|
||||
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
|
||||
sp->SetCallBack("DBImpl::CloseHelper:CapturedMaxWal", [&](void* arg) {
|
||||
max_wal_before_close = *static_cast<uint64_t*>(arg);
|
||||
});
|
||||
sp->EnableProcessing();
|
||||
Close();
|
||||
sp->DisableProcessing();
|
||||
sp->ClearAllCallBacks();
|
||||
ASSERT_GT(max_wal_before_close, 0u);
|
||||
|
||||
Reopen(options);
|
||||
ASSERT_EQ(max_wal_before_close + 1,
|
||||
dbfull()->GetVersionSet()->min_log_number_to_keep());
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// Shared-option matrix: default-off and disable-before-close should both fall
|
||||
// back to recovery-time MANIFEST work, while enable-through-close should avoid
|
||||
// MANIFEST appends on a clean reopen.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryCleanReopenMatrix) {
|
||||
struct TestCase {
|
||||
const char* name;
|
||||
bool enable_on_open;
|
||||
bool disable_before_close;
|
||||
bool expect_close_write;
|
||||
};
|
||||
const TestCase test_cases[] = {
|
||||
{"default_off", false, false, false},
|
||||
{"enabled", true, false, true},
|
||||
{"disabled_before_close", true, true, false},
|
||||
};
|
||||
|
||||
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
|
||||
for (const auto& test_case : test_cases) {
|
||||
SCOPED_TRACE(test_case.name);
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = test_case.enable_on_open;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", test_case.name));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
if (test_case.disable_before_close) {
|
||||
ASSERT_OK(dbfull()->SetDBOptions(
|
||||
{{"optimize_manifest_for_recovery", "false"}}));
|
||||
}
|
||||
|
||||
std::atomic<int> entered{0};
|
||||
sp->SetCallBack("DBImpl::CloseHelper:WriteWalMarkersOnCloseEntered",
|
||||
[&](void*) { entered.fetch_add(1); });
|
||||
sp->EnableProcessing();
|
||||
Close();
|
||||
sp->DisableProcessing();
|
||||
sp->ClearAllCallBacks();
|
||||
|
||||
std::atomic<int> records{0};
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
|
||||
[&](void*) { records.fetch_add(1); });
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
if (test_case.expect_close_write) {
|
||||
ASSERT_EQ(1, entered.load());
|
||||
ASSERT_EQ(0, records.load());
|
||||
ASSERT_GT(counters.per_cf.load(), 0);
|
||||
} else {
|
||||
ASSERT_EQ(0, entered.load());
|
||||
ASSERT_GT(records.load(), 0);
|
||||
}
|
||||
ASSERT_EQ(test_case.name, Get("k"));
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
// allow_2pc=true: even with the option on, the close-time write must NOT
|
||||
// advance MinLogNumberToKeep / DeleteWalsBefore (2pc requires the WAL to
|
||||
// remain replayable for uncommitted prepared transactions).
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryRespectsTwoPC) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.allow_2pc = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
const uint64_t min_log_before_close =
|
||||
dbfull()->GetVersionSet()->min_log_number_to_keep();
|
||||
Close();
|
||||
Reopen(options);
|
||||
ASSERT_EQ(min_log_before_close,
|
||||
dbfull()->GetVersionSet()->min_log_number_to_keep());
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// Mixed-CF emptiness: on reopen, recovery should still have MANIFEST work to
|
||||
// do for the dirty CF while skipping per-CF recovery edits for the empty CFs
|
||||
// whose markers were persisted at close.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoverySkipsNonEmptyCF) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
CreateAndReopenWithCF({"empty_cf", "dirty_cf"}, options);
|
||||
ASSERT_OK(Put(1, "k", "v1"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(Put(2, "k", "v2"));
|
||||
Close();
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
std::atomic<int> records{0};
|
||||
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
|
||||
counters.Install();
|
||||
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
|
||||
[&](void*) { records.fetch_add(1); });
|
||||
ReopenWithColumnFamilies({"default", "empty_cf", "dirty_cf"}, options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_GT(counters.per_cf.load(), 0);
|
||||
ASSERT_GT(records.load(), 0);
|
||||
ASSERT_EQ("v1", Get(1, "k"));
|
||||
ASSERT_EQ("v2", Get(2, "k"));
|
||||
}
|
||||
|
||||
// track_and_verify_wals_in_manifest=true: the close-time write must leave
|
||||
// recovery with only the mandatory WAL-tracking MANIFEST work. The per-CF
|
||||
// recovery edits should still skip.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryEmitsWalDeletionWhenTracking) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.track_and_verify_wals_in_manifest = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
std::atomic<int> records{0};
|
||||
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
|
||||
counters.Install();
|
||||
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
|
||||
[&](void*) { records.fetch_add(1); });
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_GT(counters.per_cf.load(), 0);
|
||||
ASSERT_EQ(1, records.load());
|
||||
ASSERT_GT(dbfull()->GetVersionSet()->GetWalSet().GetMinWalNumberToKeep(), 0u);
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// Regression for the close-time marker path: if a new WAL is created while an
|
||||
// otherwise-empty user CF stays empty, Close() must reserve the next file
|
||||
// number before persisting SetLogNumber(cur_wal + 1) for that CF.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryImmediateCloseAfterWarmReopen) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
CreateAndReopenWithCF({"empty_cf"}, options);
|
||||
|
||||
ASSERT_OK(Put(0, "k", "v"));
|
||||
const uint64_t wal_before_switch = dbfull()->TEST_LogfileNumber();
|
||||
ASSERT_OK(dbfull()->TEST_SwitchMemtable());
|
||||
ASSERT_GT(dbfull()->TEST_LogfileNumber(), wal_before_switch);
|
||||
|
||||
const uint64_t expected_new_log_num = dbfull()->TEST_LogfileNumber() + 1;
|
||||
ASSERT_EQ(expected_new_log_num,
|
||||
dbfull()->GetVersionSet()->current_next_file_number());
|
||||
|
||||
Close();
|
||||
ReopenWithColumnFamilies({"default", "empty_cf"}, options);
|
||||
|
||||
ASSERT_GT(dbfull()->GetVersionSet()->current_next_file_number(),
|
||||
expected_new_log_num);
|
||||
ASSERT_EQ("v", Get(0, "k"));
|
||||
}
|
||||
|
||||
// Dropped-CF safety: dropped column families must NOT have markers written for
|
||||
// them at close time (the !IsDropped() guard protects against attaching the
|
||||
// global edit to a dropped CF, which would later trip MANIFEST replay
|
||||
// assertions).
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoverySkipsDroppedCF) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
CreateAndReopenWithCF({"to_drop", "keeper"}, options);
|
||||
ASSERT_OK(Put(1, "k", "v1"));
|
||||
ASSERT_OK(Put(2, "k", "v2"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(Flush(2));
|
||||
ASSERT_OK(db_->DropColumnFamily(handles_[1]));
|
||||
Close();
|
||||
|
||||
ReopenWithColumnFamilies({"default", "keeper"}, options);
|
||||
ASSERT_EQ("v2", Get(1, "k"));
|
||||
}
|
||||
|
||||
// reuse_manifest_on_open=true: the next LogAndApply after Recover must
|
||||
// append to the existing MANIFEST file instead of allocating a fresh
|
||||
// one. Force a write after reopen and verify the MANIFEST file number
|
||||
// stays unchanged.
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenAppendsToExistingFile) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
Reopen(options);
|
||||
const uint64_t manifest_after_reopen =
|
||||
dbfull()->TEST_Current_Manifest_FileNo();
|
||||
ASSERT_OK(Put("k2", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
const uint64_t manifest_after_flush =
|
||||
dbfull()->TEST_Current_Manifest_FileNo();
|
||||
|
||||
EXPECT_EQ(manifest_after_reopen, manifest_after_flush);
|
||||
EXPECT_EQ("v", Get("k"));
|
||||
EXPECT_EQ("v2", Get("k2"));
|
||||
}
|
||||
|
||||
// Default off: ReopenManifestForAppend must NOT be invoked.
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenDefaultOffSkipsReopen) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
// reuse_manifest_on_open defaults to false
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
std::atomic<int> reopened{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ReopenManifestForAppend:Reopened",
|
||||
[&](void* /*arg*/) { reopened.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Reopen(options);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_EQ(0, reopened.load());
|
||||
}
|
||||
|
||||
// Regression for the WritableFileWriter::filesize_=0 bug fixed via
|
||||
// constructor-time initial_file_size: after ReopenManifestForAppend binds a
|
||||
// writer to the existing MANIFEST, GetFileSize() must return the on-disk
|
||||
// size, not 0.
|
||||
// (If it returned 0, the size-limit check in ProcessManifestWrites would
|
||||
// compare against 0 and Close-time Truncate could shrink the file.)
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenAdoptsOnDiskSize) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
// Capture the on-disk MANIFEST size before reopen.
|
||||
Reopen(options);
|
||||
const uint64_t manifest_no = dbfull()->TEST_Current_Manifest_FileNo();
|
||||
const std::string manifest_path = DescriptorFileName(dbname_, manifest_no);
|
||||
uint64_t on_disk_size = 0;
|
||||
ASSERT_OK(env_->GetFileSize(manifest_path, &on_disk_size));
|
||||
ASSERT_GT(on_disk_size, 0u);
|
||||
|
||||
// Force a write to flush the writer's buffer; verify Close doesn't
|
||||
// shrink the file (which would happen if Truncate(filesize_=0) ran).
|
||||
ASSERT_OK(Put("k2", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
uint64_t after_close_size = 0;
|
||||
ASSERT_OK(env_->GetFileSize(manifest_path, &after_close_size));
|
||||
EXPECT_GE(after_close_size, on_disk_size);
|
||||
}
|
||||
|
||||
// MANIFEST rotation continues to work under reuse: when the file grows
|
||||
// past tuned_max_manifest_file_size_, ProcessManifestWrites must rotate
|
||||
// to a fresh MANIFEST (same as legacy). Use the rotation SyncPoint to
|
||||
// observe rotation directly, since tuned_max_manifest_file_size_ is
|
||||
// auto-derived and not directly = max_manifest_file_size.
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenStillRotatesOnSizeCap) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
options.max_manifest_file_size = 1;
|
||||
options.max_manifest_space_amp_pct = 0; // disable amp-based tuning
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
std::atomic<int> rotations{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ProcessManifestWrites:BeforeNewManifest",
|
||||
[&](void* /*arg*/) { rotations.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Reopen(options);
|
||||
ASSERT_OK(Put("k2", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// The Flush after Reopen must have triggered a rotation given the
|
||||
// tiny size cap -- proves the size-driven rotation path still runs
|
||||
// when descriptor_log_ is bound by reuse.
|
||||
EXPECT_GT(rotations.load(), 0);
|
||||
EXPECT_EQ("v", Get("k"));
|
||||
EXPECT_EQ("v2", Get("k2"));
|
||||
}
|
||||
|
||||
// Multi-CF reuse: append-mode MANIFEST must correctly handle edits
|
||||
// from multiple CFs after reopen.
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenMultiCF) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
CreateAndReopenWithCF({"alpha", "beta"}, options);
|
||||
ASSERT_OK(Put(0, "k", "v0"));
|
||||
ASSERT_OK(Put(1, "k", "v1"));
|
||||
ASSERT_OK(Put(2, "k", "v2"));
|
||||
ASSERT_OK(Flush(0));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(Flush(2));
|
||||
Close();
|
||||
|
||||
ReopenWithColumnFamilies({"default", "alpha", "beta"}, options);
|
||||
// Trigger more writes post-reopen on each CF -- these get appended to
|
||||
// the reused MANIFEST.
|
||||
ASSERT_OK(Put(0, "k2", "v0b"));
|
||||
ASSERT_OK(Put(1, "k2", "v1b"));
|
||||
ASSERT_OK(Put(2, "k2", "v2b"));
|
||||
ASSERT_OK(Flush(0));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(Flush(2));
|
||||
|
||||
EXPECT_EQ("v0", Get(0, "k"));
|
||||
EXPECT_EQ("v1", Get(1, "k"));
|
||||
EXPECT_EQ("v2", Get(2, "k"));
|
||||
EXPECT_EQ("v0b", Get(0, "k2"));
|
||||
EXPECT_EQ("v1b", Get(1, "k2"));
|
||||
EXPECT_EQ("v2b", Get(2, "k2"));
|
||||
}
|
||||
|
||||
// Disabled under best_efforts_recovery: that mode rebuilds CURRENT and
|
||||
// MANIFEST as the side-effect of LogAndApplyForRecovery emitting an
|
||||
// edit; reusing the prior MANIFEST contradicts the salvage contract.
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenDisabledByBestEffortsRecovery) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
options.best_efforts_recovery = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
std::atomic<int> reopened{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ReopenManifestForAppend:Reopened",
|
||||
[&](void* /*arg*/) { reopened.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Reopen(options);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_EQ(0, reopened.load());
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// Full-stack composition: optimize_manifest_for_recovery plus
|
||||
// reuse_manifest_on_open. Verifies that Close writes recovery markers,
|
||||
// Reopen skips clean-recovery MANIFEST edits, and the MANIFEST is reused
|
||||
// instead of recreated for the next metadata update.
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenFullStackComposition) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
// Capture the MANIFEST file number before reopen.
|
||||
std::atomic<int> reopened{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ReopenManifestForAppend:Reopened",
|
||||
[&](void* /*arg*/) { reopened.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Reopen(options);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// All three composing: reuse fired, data is intact, no fresh MANIFEST.
|
||||
EXPECT_EQ(1, reopened.load());
|
||||
EXPECT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// Regression test for WAL recovery while publishing a fresh MANIFEST. The test
|
||||
// stores SSTs in a separate DB path, injects failure after CURRENT points at
|
||||
// the new MANIFEST, and simulates crash cleanup; the recovered SST must survive
|
||||
// because the synced MANIFEST references it.
|
||||
TEST_F(DBBasicTest, RecoverySstDirSyncedBeforeFreshManifestPublish) {
|
||||
auto fault_fs = std::make_shared<FaultInjectionTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
options.env = fault_env.get();
|
||||
options.reuse_manifest_on_open = false;
|
||||
options.db_paths.emplace_back(dbname_ + "_2", 1ULL << 30);
|
||||
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("base", "value"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(Put("recovered", "value"));
|
||||
ASSERT_OK(db_->FlushWAL(true));
|
||||
Close();
|
||||
|
||||
fault_fs->ResetState();
|
||||
|
||||
std::atomic<bool> fail_after_current_publish{true};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ProcessManifestWrites:AfterSetCurrentFile", [&](void* arg) {
|
||||
if (fail_after_current_publish.exchange(false)) {
|
||||
ASSERT_NE(nullptr, arg);
|
||||
IOStatus* io_s = static_cast<IOStatus*>(arg);
|
||||
ASSERT_OK(*io_s);
|
||||
*io_s = IOStatus::IOError("injected current publish aftermath");
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status s = TryReopen(options);
|
||||
ASSERT_TRUE(s.IsIOError()) << s.ToString();
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_OK(fault_fs->DeleteFilesCreatedAfterLastDirSync(IOOptions(), nullptr));
|
||||
|
||||
s = TryReopen(options);
|
||||
ASSERT_OK(s);
|
||||
ASSERT_EQ("value", Get("base"));
|
||||
ASSERT_EQ("value", Get("recovered"));
|
||||
Close();
|
||||
}
|
||||
|
||||
// Regression test for WAL recovery while appending to a reused MANIFEST. The
|
||||
// first reopen forces recovery to create an SST and then fail after MANIFEST
|
||||
// sync. The simulated crash cleanup deletes files without a prior directory
|
||||
// sync; the recovered SST must survive because the synced MANIFEST references
|
||||
// it.
|
||||
TEST_F(DBBasicTest,
|
||||
ReuseManifestOnOpenSyncsRecoverySstDirBeforeManifestAppend) {
|
||||
auto fault_fs = std::make_shared<FaultInjectionTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
options.env = fault_env.get();
|
||||
options.reuse_manifest_on_open = true;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("base", "value"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(Put("recovered", "value"));
|
||||
ASSERT_OK(db_->FlushWAL(true));
|
||||
Close();
|
||||
|
||||
fault_fs->ResetState();
|
||||
|
||||
std::atomic<bool> fail_after_manifest_sync{true};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ProcessManifestWrites:AfterSyncManifest", [&](void* arg) {
|
||||
if (fail_after_manifest_sync.exchange(false)) {
|
||||
ASSERT_NE(nullptr, arg);
|
||||
IOStatus* io_s = static_cast<IOStatus*>(arg);
|
||||
ASSERT_OK(*io_s);
|
||||
*io_s = IOStatus::IOError("injected manifest sync aftermath");
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status s = TryReopen(options);
|
||||
ASSERT_TRUE(s.IsIOError()) << s.ToString();
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_OK(fault_fs->DeleteFilesCreatedAfterLastDirSync(IOOptions(), nullptr));
|
||||
|
||||
s = TryReopen(options);
|
||||
ASSERT_OK(s);
|
||||
ASSERT_EQ("value", Get("base"));
|
||||
ASSERT_EQ("value", Get("recovered"));
|
||||
Close();
|
||||
}
|
||||
|
||||
// Direct unit test for WritableFileWriter's initial_file_size parameter:
|
||||
// verifies the visible size accessors report the existing on-disk size
|
||||
// immediately, rather than the constructor's zero default.
|
||||
TEST_F(DBBasicTest, WritableFileWriterInitialFileSizeAdoptsExistingSize) {
|
||||
Env* env = Env::Default();
|
||||
std::string fname = test::PerThreadDBPath("set_file_size_test");
|
||||
ASSERT_OK(env->CreateDirIfMissing(test::TmpDir(env)));
|
||||
|
||||
// Create a file with some bytes, close it.
|
||||
{
|
||||
std::unique_ptr<WritableFile> raw;
|
||||
ASSERT_OK(env->NewWritableFile(fname, &raw, EnvOptions()));
|
||||
ASSERT_OK(raw->Append("hello world"));
|
||||
ASSERT_OK(raw->Close());
|
||||
}
|
||||
|
||||
// Reopen and wrap in a WritableFileWriter without initial_file_size --
|
||||
// GetFileSize() should be 0 (the constructor's default).
|
||||
std::unique_ptr<FSWritableFile> fs_file;
|
||||
ASSERT_OK(env->GetFileSystem()->ReopenWritableFile(
|
||||
fname, FileOptions(), &fs_file, /*dbg=*/nullptr));
|
||||
std::unique_ptr<WritableFileWriter> writer(
|
||||
new WritableFileWriter(std::move(fs_file), fname, FileOptions()));
|
||||
EXPECT_EQ(0u, writer->GetFileSize());
|
||||
|
||||
// Reopen again and seed the writer's size accounting from the existing
|
||||
// bytes. GetFileSize and GetFlushedSize must reflect it immediately.
|
||||
ASSERT_OK(env->GetFileSystem()->ReopenWritableFile(
|
||||
fname, FileOptions(), &fs_file, /*dbg=*/nullptr));
|
||||
writer.reset(new WritableFileWriter(
|
||||
std::move(fs_file), fname, FileOptions(),
|
||||
/*clock=*/nullptr, /*io_tracer=*/nullptr, /*stats=*/nullptr,
|
||||
Histograms::HISTOGRAM_ENUM_MAX, /*listeners=*/{},
|
||||
/*file_checksum_gen_factory=*/nullptr,
|
||||
/*perform_data_verification=*/false,
|
||||
/*buffered_data_with_checksum=*/false,
|
||||
/*initial_file_size=*/11));
|
||||
EXPECT_EQ(11u, writer->GetFileSize());
|
||||
EXPECT_EQ(11u, writer->GetFlushedSize());
|
||||
|
||||
ASSERT_OK(env->DeleteFile(fname));
|
||||
}
|
||||
|
||||
// Tail corruption: appending garbage bytes to the MANIFEST after a
|
||||
// clean close must prevent reuse -- the physical size exceeds the
|
||||
// last valid record end.
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
// Find the MANIFEST file and append garbage to it.
|
||||
std::string manifest_path;
|
||||
{
|
||||
std::vector<std::string> files;
|
||||
ASSERT_OK(env_->GetChildren(dbname_, &files));
|
||||
for (const auto& f : files) {
|
||||
uint64_t number;
|
||||
FileType type;
|
||||
if (ParseFileName(f, &number, &type) && type == kDescriptorFile) {
|
||||
manifest_path = dbname_ + "/" + f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_FALSE(manifest_path.empty());
|
||||
{
|
||||
std::string contents;
|
||||
ASSERT_OK(ReadFileToString(env_, manifest_path, &contents));
|
||||
contents.append("garbage!");
|
||||
ASSERT_OK(WriteStringToFile(env_, contents, manifest_path));
|
||||
}
|
||||
|
||||
std::atomic<int> reopened{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ReopenManifestForAppend:Reopened",
|
||||
[&](void* /*arg*/) { reopened.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Reopen(options);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_EQ(0, reopened.load());
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, EnableDirectIOWithZeroBuf) {
|
||||
if (!IsDirectIOSupported()) {
|
||||
ROCKSDB_GTEST_BYPASS("Direct IO not supported");
|
||||
|
||||
@@ -542,7 +542,7 @@ TEST_F(DBBlockCacheTest, WarmCacheWithDataBlocksDuringCompaction) {
|
||||
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.
|
||||
// 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"));
|
||||
|
||||
@@ -2022,7 +2022,11 @@ TEST_F(DBBloomFilterTest, MutableFilterPolicy) {
|
||||
double expected_bpk = 10.0;
|
||||
// Other configs to try
|
||||
std::vector<std::pair<std::string, double>> configs = {
|
||||
{"ribbonfilter:10:-1", 7.0}, {"bloomfilter:5", 5.0}, {"nullptr", 0.0}};
|
||||
{"ribbonfilter:10:-1", 7.0},
|
||||
{"bloomfilter:5", 5.0},
|
||||
{"nullptr", 0.0},
|
||||
// As serialized in OPTIONS file
|
||||
{"{id=ribbonfilter:12:-1;bloom_before_level=-1;}", 8.4}};
|
||||
|
||||
table_options.cache_index_and_filter_blocks = true;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
|
||||
@@ -427,6 +427,38 @@ TEST_F(DBCompactionAbortTest, AbortAutomaticCompaction) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortScheduledAutomaticCompactionBeforePick) {
|
||||
// The goal is to cover an automatic compaction that has been scheduled, but
|
||||
// aborts before the worker picks and removes a column family from the
|
||||
// compaction queue. This pins the scheduler bookkeeping invariant that
|
||||
// ResumeAllCompactions() must be able to schedule that still-queued work.
|
||||
constexpr int kCompactionTrigger = 4;
|
||||
constexpr int kNumL0Files = kCompactionTrigger + 1;
|
||||
Options options = GetOptionsWithStats();
|
||||
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
||||
options.max_background_compactions = 1;
|
||||
options.max_subcompactions = 1;
|
||||
options.disable_auto_compactions = false;
|
||||
Reopen(options);
|
||||
|
||||
SyncPointAbortHelper helper("BackgroundCallCompaction:0");
|
||||
helper.Setup(dbfull());
|
||||
|
||||
PopulateData(/*num_files=*/kNumL0Files, /*keys_per_file=*/100,
|
||||
/*value_size=*/1000);
|
||||
helper.CleanupAndWait();
|
||||
|
||||
const uint64_t compact_write_bytes_before_resume =
|
||||
stats_->getTickerCount(COMPACT_WRITE_BYTES);
|
||||
dbfull()->ResumeAllCompactions();
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
ASSERT_GT(stats_->getTickerCount(COMPACT_WRITE_BYTES),
|
||||
compact_write_bytes_before_resume);
|
||||
ASSERT_LT(NumTableFilesAtLevel(0), kNumL0Files);
|
||||
VerifyDataIntegrity(/*num_keys=*/100);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionAbortTest, AbortAndVerifyNoOutputFiles) {
|
||||
Options options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
|
||||
+355
-4
@@ -119,6 +119,17 @@ class DBCompactionTest : public DBTestBase {
|
||||
DBCompactionTest()
|
||||
: DBTestBase("db_compaction_test", /*env_do_fsync=*/false) {}
|
||||
|
||||
void TearDown() override {
|
||||
// Reset Env::Default() thread pool state that tests may have modified.
|
||||
// Under sharded execution multiple tests share a process, so leaked
|
||||
// thread-pool sizes (especially BOTTOM) cause later tests to see
|
||||
// unexpected compaction scheduling (e.g. ForwardToBottomPriPool).
|
||||
Env::Default()->SetBackgroundThreads(0, Env::Priority::BOTTOM);
|
||||
Env::Default()->SetBackgroundThreads(1, Env::Priority::LOW);
|
||||
Env::Default()->SetBackgroundThreads(1, Env::Priority::HIGH);
|
||||
DBTestBase::TearDown();
|
||||
}
|
||||
|
||||
protected:
|
||||
/*
|
||||
* Verifies compaction stats of cfd are valid.
|
||||
@@ -11682,6 +11693,76 @@ TEST_F(DBCompactionTest, PeriodicTask) {
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, ReadTriggeredCompaction) {
|
||||
Options options = CurrentOptions();
|
||||
options.num_levels = 3;
|
||||
options.level0_file_num_compaction_trigger = 10;
|
||||
options.read_triggered_compaction_threshold = 0.001;
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Write data at L2 first so L1 is not the bottommost level
|
||||
Random rnd(301);
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
ASSERT_OK(Put(Key(i), rnd.RandomString(1024)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(2);
|
||||
ASSERT_EQ("0,0,1", FilesPerLevel());
|
||||
|
||||
// Write more data and move to L1 (the hot level)
|
||||
for (int i = 5; i < 10; ++i) {
|
||||
ASSERT_OK(Put(Key(i), rnd.RandomString(1024)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ("0,1,1", FilesPerLevel());
|
||||
|
||||
ColumnFamilyMetaData cf_meta;
|
||||
dbfull()->GetColumnFamilyMetaData(dbfull()->DefaultColumnFamily(), &cf_meta);
|
||||
ASSERT_EQ(cf_meta.levels[1].files.size(), 1);
|
||||
uint64_t file_size = cf_meta.levels[1].files[0].size;
|
||||
// Set reads high enough to exceed threshold (0.001 * file_size)
|
||||
uint64_t reads_needed =
|
||||
static_cast<uint64_t>(0.002 * static_cast<double>(file_size));
|
||||
|
||||
{
|
||||
auto* cfd = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
dbfull()->DefaultColumnFamily())
|
||||
->cfd();
|
||||
auto* vstorage = cfd->current()->storage_info();
|
||||
for (auto* f : vstorage->LevelFiles(1)) {
|
||||
f->stats.num_collapsible_entry_reads_sampled.store(reads_needed);
|
||||
}
|
||||
}
|
||||
|
||||
std::atomic<int> read_triggered_compactions{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"LevelCompactionPicker::PickCompaction:Return", [&](void* arg) {
|
||||
Compaction* compaction = static_cast<Compaction*>(arg);
|
||||
if (compaction->compaction_reason() ==
|
||||
CompactionReason::kReadTriggered) {
|
||||
read_triggered_compactions.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Enable auto compactions to trigger
|
||||
ASSERT_OK(dbfull()->SetOptions({{"disable_auto_compactions", "false"}}));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
ASSERT_GE(read_triggered_compactions, 1);
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
|
||||
// Verify the L1 file was compacted down (L1 should be empty now)
|
||||
ASSERT_EQ(0, NumTableFilesAtLevel(1));
|
||||
|
||||
// Verify data integrity: all keys are still readable
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
ASSERT_NE(Get(Key(i)), "NOT_FOUND");
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test for a bug in SetupOtherFilesWithRoundRobinExpansion where
|
||||
// duplicate files are added to the compaction input, corrupting
|
||||
// ExpandInputsToCleanCut and violating the clean-cut invariant. The bug
|
||||
@@ -11752,7 +11833,7 @@ TEST_F(DBCompactionTest, RoundRobinCleanCutWithSharedBoundary) {
|
||||
// 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.
|
||||
// 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
|
||||
@@ -11783,7 +11864,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
|
||||
// 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.
|
||||
// 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) {
|
||||
@@ -11795,7 +11876,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
|
||||
// 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 —
|
||||
// --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*) {
|
||||
@@ -11808,7 +11889,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Trigger compaction — fails after VerifyOutputFiles.
|
||||
// Trigger compaction -- fails after VerifyOutputFiles.
|
||||
Status s = dbfull()->TEST_CompactRange(0, nullptr, nullptr);
|
||||
ASSERT_NOK(s);
|
||||
|
||||
@@ -11830,6 +11911,226 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
|
||||
db_ = nullptr;
|
||||
}
|
||||
|
||||
// Regression test for table cache leak when InstallCompactionResults fails.
|
||||
// Similar to LeakedTableCacheEntryOnCompactionFailure but the failure occurs
|
||||
// during Install() (MANIFEST write) rather than during Run().
|
||||
// 1. Compaction Run() succeeds: all subcompactions OK, VerifyOutputFiles
|
||||
// inserts output files into the table cache, compact_->status = OK.
|
||||
// 2. Install() calls InstallCompactionResults() -> LogAndApply(), which
|
||||
// fails (injected MANIFEST write error).
|
||||
// 3. Install()'s local status captures the error, but compact_->status
|
||||
// was never updated (THE BUG). CleanupCompaction passes compact_->status
|
||||
// (OK) to Cleanup(), which skips ReleaseObsolete.
|
||||
// 4. FaultInjection prevents FindObsoleteFiles backstop from working.
|
||||
// 5. Close()'s TEST_VerifyNoObsoleteFilesCached finds the leaked entry.
|
||||
TEST_F(DBCompactionTest, LeakedTableCacheEntryOnInstallFailure) {
|
||||
auto fault_fs = std::make_shared<FaultInjectionTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.env = fault_env.get();
|
||||
options.paranoid_file_checks = true;
|
||||
options.level0_file_num_compaction_trigger = 2;
|
||||
options.disable_auto_compactions = true;
|
||||
options.num_levels = 3;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Write overlapping data to force a real (non-trivial) compaction.
|
||||
ASSERT_OK(Put("a", std::string(1024, 'x')));
|
||||
ASSERT_OK(Put("z", std::string(1024, 'x')));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("a", std::string(1024, 'y')));
|
||||
ASSERT_OK(Put("z", std::string(1024, 'y')));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ(NumTableFilesAtLevel(0), 2);
|
||||
|
||||
// After VerifyOutputFiles succeeds (cache entries created) and Run()
|
||||
// completes successfully, inject an error into InstallCompactionResults()
|
||||
// so that Install() fails while compact_->status remains OK.
|
||||
// Also enable metadata read faults to prevent the FindObsoleteFiles
|
||||
// backstop from evicting the leaked cache entries.
|
||||
std::atomic<bool> inject_error{true};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::InstallCompactionResults:BeforeLogAndApply",
|
||||
[&](void* arg) {
|
||||
if (inject_error.exchange(false)) {
|
||||
*static_cast<Status*>(arg) = Status::IOError("injected");
|
||||
}
|
||||
});
|
||||
|
||||
// After the compaction job finishes (including Install), enable metadata
|
||||
// read faults so FindObsoleteFiles backstop cannot scan the directory.
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"BackgroundCallCompaction:1", [&](void*) {
|
||||
fault_fs->SetThreadLocalErrorContext(
|
||||
FaultInjectionIOType::kMetadataRead, /*seed=*/0, /*one_in=*/1,
|
||||
/*retryable=*/false, /*has_data_loss=*/false);
|
||||
fault_fs->EnableThreadLocalErrorInjection(
|
||||
FaultInjectionIOType::kMetadataRead);
|
||||
});
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Trigger compaction -- Run() succeeds but Install() fails.
|
||||
Status s = dbfull()->TEST_CompactRange(0, nullptr, nullptr);
|
||||
ASSERT_NOK(s);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// Enable metadata read fault injection on the main thread too, so
|
||||
// Close()'s FindObsoleteFiles also fails to find the orphan file.
|
||||
fault_fs->SetThreadLocalErrorContext(
|
||||
FaultInjectionIOType::kMetadataRead, /*seed=*/0, /*one_in=*/1,
|
||||
/*retryable=*/false, /*has_data_loss=*/false);
|
||||
fault_fs->EnableThreadLocalErrorInjection(
|
||||
FaultInjectionIOType::kMetadataRead);
|
||||
|
||||
// TEST_VerifyNoObsoleteFilesCached asserted within Close on ASAN builds
|
||||
s = db_->Close();
|
||||
ASSERT_OK(s);
|
||||
// Release DB before fault_env goes out of scope to avoid use-after-free.
|
||||
db_ = nullptr;
|
||||
}
|
||||
|
||||
// Regression test for ReleaseObsolete failing to erase table cache entries
|
||||
// when concurrent readers hold references.
|
||||
// 1. Flush creates an L0 SST file with an entry in the table cache.
|
||||
// 2. A concurrent reader acquires a cache reference on the file (simulated
|
||||
// with a direct cache Lookup).
|
||||
// 3. ReleaseObsolete is called (simulating what PurgeObsoleteFiles does when
|
||||
// the file becomes obsolete). With the old code, this calls
|
||||
// ReleaseAndEraseIfLastRef which fails because of the concurrent ref.
|
||||
// 4. The concurrent reader releases its reference.
|
||||
// 5. Without the fix, the entry remains in the cache (leak). With the fix,
|
||||
// ReleaseObsolete's Erase() marked the entry Invisible, so it was cleaned
|
||||
// up when the concurrent ref was released.
|
||||
TEST_F(DBCompactionTest, ObsoleteFileTableCacheEntryWithConcurrentRef) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Create an L0 file.
|
||||
ASSERT_OK(Put("a", std::string(1024, 'x')));
|
||||
ASSERT_OK(Put("z", std::string(1024, 'x')));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ(NumTableFilesAtLevel(0), 1);
|
||||
|
||||
// Get the file number of the L0 file.
|
||||
std::vector<LiveFileMetaData> files;
|
||||
dbfull()->GetLiveFilesMetaData(&files);
|
||||
ASSERT_EQ(files.size(), 1);
|
||||
uint64_t target_file_number = files[0].file_number;
|
||||
|
||||
// Ensure the file is in the table cache by reading from it.
|
||||
ASSERT_EQ(Get("a"), std::string(1024, 'x'));
|
||||
|
||||
Cache* table_cache = dbfull()->TEST_table_cache();
|
||||
|
||||
// Simulate a concurrent reader acquiring a cache reference.
|
||||
Cache::Handle* concurrent_handle =
|
||||
TableCache::Lookup(table_cache, target_file_number);
|
||||
ASSERT_NE(concurrent_handle, nullptr);
|
||||
|
||||
// Call ReleaseObsolete directly -- this is what PurgeObsoleteFiles calls
|
||||
// when a file becomes obsolete. Pass nullptr for the handle to make
|
||||
// ReleaseObsolete do its own Lookup internally.
|
||||
TableCache::ReleaseObsolete(table_cache, target_file_number,
|
||||
/*handle=*/nullptr,
|
||||
/*uncache_aggressiveness=*/0);
|
||||
|
||||
// The concurrent reader releases its reference.
|
||||
table_cache->Release(concurrent_handle);
|
||||
|
||||
// With the fix, the entry should be gone: ReleaseObsolete called Erase()
|
||||
// which marked it Invisible, so it was freed when the concurrent ref was
|
||||
// released. Without the fix, the entry leaks here.
|
||||
Cache::Handle* leaked = TableCache::Lookup(table_cache, target_file_number);
|
||||
ASSERT_EQ(leaked, nullptr);
|
||||
}
|
||||
|
||||
// Regression test for the atomic flush path leaking table cache entries when
|
||||
// InstallMemtableAtomicFlushResults (MANIFEST write) fails.
|
||||
// 1. atomic_flush is enabled with two column families.
|
||||
// 2. Both CFs have data, a flush is triggered.
|
||||
// 3. FlushJob::Run() succeeds for both (files are added to table cache by
|
||||
// BuildTable), but write_manifest=false so no install happens in Run().
|
||||
// 4. The combined MANIFEST write (InstallMemtableAtomicFlushResults) fails
|
||||
// due to injected I/O error.
|
||||
// 5. Without the fix, the files remain in the table cache but are not in
|
||||
// any Version, causing TEST_VerifyNoObsoleteFilesCached to fire during
|
||||
// Close() (especially when metadata_read_fault_one_in disables the
|
||||
// FindObsoleteFiles backstop).
|
||||
TEST_F(DBCompactionTest, LeakedTableCacheEntryOnAtomicFlushInstallFailure) {
|
||||
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.atomic_flush = true;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
// Enough buffer so auto-flush doesn't trigger prematurely.
|
||||
options.write_buffer_size = 64 << 20;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Create a second column family.
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
ASSERT_EQ(2, handles_.size());
|
||||
|
||||
// Write data to both CFs.
|
||||
ASSERT_OK(Put(0, "key0", std::string(1024, 'a')));
|
||||
ASSERT_OK(Put(1, "key1", std::string(1024, 'b')));
|
||||
|
||||
// Inject a write error during the combined MANIFEST write that happens
|
||||
// inside InstallMemtableAtomicFlushResults (via LogAndApply).
|
||||
std::atomic<bool> inject_error{true};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::LogAndApply:WriteManifest", [&](void*) {
|
||||
if (inject_error.exchange(false)) {
|
||||
fault_fs->SetThreadLocalErrorContext(
|
||||
FaultInjectionIOType::kWrite, /*seed=*/0, /*one_in=*/1,
|
||||
/*retryable=*/false, /*has_data_loss=*/false);
|
||||
fault_fs->EnableThreadLocalErrorInjection(
|
||||
FaultInjectionIOType::kWrite);
|
||||
}
|
||||
});
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Trigger atomic flush - Run() succeeds but MANIFEST write fails.
|
||||
FlushOptions flush_opts;
|
||||
flush_opts.wait = true;
|
||||
Status s = db_->Flush(flush_opts, handles_);
|
||||
ASSERT_NOK(s);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// Disable the write fault so Close() can proceed.
|
||||
fault_fs->DisableThreadLocalErrorInjection(FaultInjectionIOType::kWrite);
|
||||
|
||||
// Enable metadata read fault injection on the main thread so that
|
||||
// Close()'s FindObsoleteFiles full-scan backstop cannot find the orphan
|
||||
// files (simulating metadata_read_fault_one_in from the stress test).
|
||||
fault_fs->SetThreadLocalErrorContext(
|
||||
FaultInjectionIOType::kMetadataRead, /*seed=*/0, /*one_in=*/1,
|
||||
/*retryable=*/false, /*has_data_loss=*/false);
|
||||
fault_fs->EnableThreadLocalErrorInjection(
|
||||
FaultInjectionIOType::kMetadataRead);
|
||||
|
||||
// Destroy column family handles before closing.
|
||||
for (auto h : handles_) {
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(h));
|
||||
}
|
||||
handles_.clear();
|
||||
|
||||
// Close the DB. Without the fix, TEST_VerifyNoObsoleteFilesCached fires.
|
||||
s = db_->Close();
|
||||
ASSERT_OK(s);
|
||||
db_ = nullptr;
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, VerifyFileChecksumOnCompactionOutput) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
@@ -11958,6 +12259,56 @@ TEST_F(DBCompactionTest, VerifyAllOutputFlagsWithoutParanoidFileChecks) {
|
||||
}
|
||||
}
|
||||
|
||||
// Requires ~8GB+ RAM because the compaction filter internally creates a ~4GB
|
||||
// value via std::string::assign().
|
||||
TEST_F(DBCompactionTest, CompactionFilterLargeValueRejected) {
|
||||
if (!test::HasBigMem()) {
|
||||
ROCKSDB_GTEST_BYPASS("insufficient memory for reliable continuous testing");
|
||||
return;
|
||||
}
|
||||
|
||||
// A compaction filter that inflates every value to a configurable size
|
||||
class InflatingFilter : public CompactionFilter {
|
||||
public:
|
||||
std::atomic<size_t> target_size{0};
|
||||
Decision FilterV2(int /*level*/, const Slice& /*key*/,
|
||||
ValueType /*value_type*/, const Slice& /*existing_value*/,
|
||||
std::string* new_value,
|
||||
std::string* /*skip_until*/) const override {
|
||||
new_value->assign(target_size.load(), 'X');
|
||||
return Decision::kChangeValue;
|
||||
}
|
||||
const char* Name() const override { return "InflatingFilter"; }
|
||||
};
|
||||
|
||||
InflatingFilter inflating_filter;
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_filter = &inflating_filter;
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Control: value at exactly 4GB - 1 should be accepted
|
||||
inflating_filter.target_size = size_t{std::numeric_limits<uint32_t>::max()};
|
||||
ASSERT_OK(Put("key", "small_value"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("key", "small_value2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
// Now test rejection: value at 4GB should be rejected
|
||||
inflating_filter.target_size =
|
||||
size_t{std::numeric_limits<uint32_t>::max()} + 1;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("key", "small_value"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("key", "small_value2"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
Status s = db_->CompactRange(CompactRangeOptions(), nullptr, nullptr);
|
||||
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
|
||||
ASSERT_TRUE(s.ToString().find("4GB") != std::string::npos) << s.ToString();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class DBTest2 : public DBTestBase {
|
||||
public:
|
||||
DBTest2() : DBTestBase("db_test2", /*env_do_fsync=*/true) {}
|
||||
DBTest2() : DBTestBase("db_etc2_test", /*env_do_fsync=*/true) {}
|
||||
};
|
||||
|
||||
TEST_F(DBTest2, OpenForReadOnly) {
|
||||
@@ -107,7 +107,7 @@ TEST_F(DBTest2, ReadOnlyDBWalInDbPathInitialized) {
|
||||
ASSERT_OK(Put("key2", "value2"));
|
||||
Close();
|
||||
|
||||
// Reopen as read-only — wal_in_db_path_ must be properly initialized.
|
||||
// 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;
|
||||
@@ -115,7 +115,7 @@ TEST_F(DBTest2, ReadOnlyDBWalInDbPathInitialized) {
|
||||
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
|
||||
// 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();
|
||||
|
||||
@@ -7510,6 +7510,65 @@ TEST_F(DBTest2, GetLatestSeqAndTsForKey) {
|
||||
ASSERT_EQ(0, options.statistics->getTickerCount(GET_HIT_L0));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2,
|
||||
GetLatestSequenceForKeyFromHistoryWithBlobBackedWideColumnEntity) {
|
||||
// Goal: exercise the memtable history lookup in GetLatestSequenceForKey()
|
||||
// after blob direct write stores a blob-backed V2 entity in a flushed
|
||||
// memtable. Using cache_only=true ensures the lookup succeeds only if the
|
||||
// history path can resolve the wide-column base value under the newer merge.
|
||||
Destroy(last_options_);
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.enable_blob_files = true;
|
||||
options.enable_blob_direct_write = true;
|
||||
options.min_blob_size = 50;
|
||||
options.allow_concurrent_memtable_write = false;
|
||||
options.blob_direct_write_partitions = 1;
|
||||
options.max_write_buffer_size_to_maintain = 64 << 10;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
|
||||
|
||||
Reopen(options);
|
||||
|
||||
const std::string key = "history_blob_entity";
|
||||
const std::string default_value(100, 'd');
|
||||
const std::string merge_operand = "suffix";
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"meta", "inline"}};
|
||||
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
|
||||
merge_operand));
|
||||
const SequenceNumber expected_seq = dbfull()->GetLatestSequenceNumber();
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_FALSE(GetBlobFileNumbers().empty());
|
||||
|
||||
uint64_t num_immutable_memtables = 0;
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumImmutableMemTable,
|
||||
&num_immutable_memtables));
|
||||
ASSERT_EQ(num_immutable_memtables, 0);
|
||||
|
||||
auto* cfhi = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
dbfull()->DefaultColumnFamily());
|
||||
assert(cfhi);
|
||||
assert(cfhi->cfd());
|
||||
SuperVersion* sv = cfhi->cfd()->GetSuperVersion();
|
||||
|
||||
SequenceNumber seq = kMaxSequenceNumber;
|
||||
bool found_record_for_key = false;
|
||||
bool is_blob_index = false;
|
||||
const Status s = dbfull()->GetLatestSequenceForKey(
|
||||
sv, key, /*cache_only=*/true, /*lower_bound_seq=*/0, &seq,
|
||||
/*timestamp=*/nullptr, &found_record_for_key, &is_blob_index);
|
||||
|
||||
ASSERT_OK(s);
|
||||
ASSERT_TRUE(found_record_for_key);
|
||||
ASSERT_EQ(expected_seq, seq);
|
||||
ASSERT_FALSE(is_blob_index);
|
||||
}
|
||||
|
||||
#if defined(ZSTD)
|
||||
TEST_F(DBTest2, ZSTDChecksum) {
|
||||
// Verify that corruption during decompression is caught.
|
||||
@@ -7638,6 +7697,558 @@ TEST_F(DBTest2, GetFileChecksumsFromCurrentManifest_CRC32) {
|
||||
}
|
||||
}
|
||||
|
||||
// Parameterized by (allow_concurrent_memtable_write,
|
||||
// min_tombstones_for_range_conversion).
|
||||
class DBTestConcurrentRangeTombstoneConversions
|
||||
: public DBTestBase,
|
||||
public testing::WithParamInterface<std::tuple<bool, uint32_t>> {
|
||||
public:
|
||||
DBTestConcurrentRangeTombstoneConversions()
|
||||
: DBTestBase("db_test_concurrent_memtable_ops", /*env_do_fsync=*/true) {}
|
||||
};
|
||||
|
||||
TEST_P(DBTestConcurrentRangeTombstoneConversions,
|
||||
MixedWritesWithConcurrentReaders) {
|
||||
auto [allow_concurrent, range_conversion_threshold] = GetParam();
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.allow_concurrent_memtable_write = allow_concurrent;
|
||||
options.min_tombstones_for_range_conversion = range_conversion_threshold;
|
||||
options.statistics = CreateDBStatistics();
|
||||
// Larger memtable to avoid flushes during the test.
|
||||
options.write_buffer_size = 64 << 20;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Seed 100 keys.
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ASSERT_OK(Put(Key(i), "seed_" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Thread 1: Put 10 keys then SingleDelete them.
|
||||
port::Thread writer_thread([&] {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ASSERT_OK(Put(Key(i), "new_" + std::to_string(i)));
|
||||
ASSERT_OK(db_->SingleDelete(WriteOptions(), Key(i)));
|
||||
}
|
||||
});
|
||||
|
||||
// Thread 2: Delete keys 20-29 (10 contiguous point tombstones).
|
||||
port::Thread deleter_thread([&] {
|
||||
for (int i = 20; i < 30; i++) {
|
||||
ASSERT_OK(Delete(Key(i)));
|
||||
}
|
||||
});
|
||||
|
||||
// Thread 3: DeleteRange 3 ranges.
|
||||
port::Thread range_deleter_thread([&] {
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(),
|
||||
Key(40), Key(50)));
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(),
|
||||
Key(60), Key(70)));
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(),
|
||||
Key(80), Key(90)));
|
||||
});
|
||||
|
||||
// Wait for deletions to land before reading.
|
||||
deleter_thread.join();
|
||||
|
||||
// 4 forward + 4 reverse read threads scan keys 20-30 to hit the contiguous
|
||||
// point tombstones.
|
||||
std::vector<port::Thread> reader_threads;
|
||||
reader_threads.reserve(8);
|
||||
for (int t = 0; t < 4; t++) {
|
||||
// Forward readers
|
||||
reader_threads.emplace_back([&] {
|
||||
ReadOptions ro;
|
||||
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ro));
|
||||
iter->Seek(Key(20));
|
||||
while (iter->Valid() && iter->key().compare(Key(30)) < 0) {
|
||||
iter->Next();
|
||||
}
|
||||
ASSERT_OK(iter->status());
|
||||
});
|
||||
// Reverse readers
|
||||
reader_threads.emplace_back([&] {
|
||||
ReadOptions ro;
|
||||
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ro));
|
||||
iter->SeekForPrev(Key(30));
|
||||
while (iter->Valid() && iter->key().compare(Key(20)) >= 0) {
|
||||
iter->Prev();
|
||||
}
|
||||
ASSERT_OK(iter->status());
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t : reader_threads) {
|
||||
t.join();
|
||||
}
|
||||
writer_thread.join();
|
||||
range_deleter_thread.join();
|
||||
|
||||
if (range_conversion_threshold > 0) {
|
||||
uint64_t inserted =
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED);
|
||||
uint64_t discarded = options.statistics->getTickerCount(
|
||||
READ_PATH_RANGE_TOMBSTONES_DISCARDED);
|
||||
ASSERT_GT(inserted + discarded, 0);
|
||||
}
|
||||
|
||||
// Build expected keys: 10-19, 30-39, 50-59, 70-79, 90-99 are alive.
|
||||
// Keys 0-9 removed by Put+SingleDelete which also covers the SST entry.
|
||||
// Keys 20-29 deleted, 40-49/60-69/80-89 range-deleted.
|
||||
std::vector<std::string> expected_keys;
|
||||
for (int i = 0; i < 100; i++) {
|
||||
if ((i >= 0 && i < 10) || (i >= 20 && i < 30) || (i >= 40 && i < 50) ||
|
||||
(i >= 60 && i < 70) || (i >= 80 && i < 90)) {
|
||||
continue;
|
||||
}
|
||||
expected_keys.push_back(Key(i));
|
||||
}
|
||||
|
||||
// Verify forward iteration matches expected keys and values.
|
||||
{
|
||||
ReadOptions ro;
|
||||
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ro));
|
||||
int idx = 0;
|
||||
for (iter->SeekToFirst(); iter->Valid(); iter->Next(), idx++) {
|
||||
ASSERT_LT(idx, static_cast<int>(expected_keys.size()));
|
||||
ASSERT_EQ(iter->key().ToString(), expected_keys[idx]);
|
||||
}
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_EQ(idx, static_cast<int>(expected_keys.size()));
|
||||
}
|
||||
|
||||
// Verify reverse iteration matches expected keys in reverse order.
|
||||
{
|
||||
ReadOptions ro;
|
||||
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ro));
|
||||
int idx = static_cast<int>(expected_keys.size()) - 1;
|
||||
for (iter->SeekToLast(); iter->Valid(); iter->Prev(), idx--) {
|
||||
ASSERT_GE(idx, 0);
|
||||
ASSERT_EQ(iter->key().ToString(), expected_keys[idx]);
|
||||
}
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_EQ(idx, -1);
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
DBTestConcurrentRangeTombstoneConversions,
|
||||
DBTestConcurrentRangeTombstoneConversions,
|
||||
::testing::Combine(
|
||||
/*allow_concurrent_memtable_write=*/::testing::Bool(),
|
||||
/*min_tombstones_for_range_conversion=*/::testing::Values(0, 4)));
|
||||
|
||||
// Test file system that supports GetFileOpenMetadata for fast_sst_open testing
|
||||
class FastOpenTestRandomAccessFile : public FSRandomAccessFileWrapper {
|
||||
public:
|
||||
explicit FastOpenTestRandomAccessFile(
|
||||
std::unique_ptr<FSRandomAccessFile>&& file, const std::string& fname,
|
||||
std::atomic<int>* metadata_retrieved_count)
|
||||
: FSRandomAccessFileWrapper(file.get()),
|
||||
file_(std::move(file)),
|
||||
fname_(fname),
|
||||
metadata_retrieved_count_(metadata_retrieved_count) {}
|
||||
|
||||
IOStatus GetFileOpenMetadata(std::string* metadata) override {
|
||||
// Return the file name as opaque metadata for testing
|
||||
*metadata = "fast_open_metadata:" + fname_;
|
||||
metadata_retrieved_count_->fetch_add(1, std::memory_order_relaxed);
|
||||
return IOStatus::OK();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<FSRandomAccessFile> file_;
|
||||
std::string fname_;
|
||||
std::atomic<int>* metadata_retrieved_count_;
|
||||
};
|
||||
|
||||
class FastOpenTestFS : public FileSystemWrapper {
|
||||
public:
|
||||
explicit FastOpenTestFS(const std::shared_ptr<FileSystem>& base)
|
||||
: FileSystemWrapper(base) {}
|
||||
|
||||
const char* Name() const override { return "FastOpenTestFS"; }
|
||||
|
||||
IOStatus NewRandomAccessFile(const std::string& fname,
|
||||
const FileOptions& file_opts,
|
||||
std::unique_ptr<FSRandomAccessFile>* result,
|
||||
IODebugContext* dbg) override {
|
||||
if (file_opts.file_metadata != nullptr) {
|
||||
metadata_passed_on_open_.fetch_add(1, std::memory_order_relaxed);
|
||||
last_metadata_received_ = *file_opts.file_metadata;
|
||||
}
|
||||
IOStatus s = target()->NewRandomAccessFile(fname, file_opts, result, dbg);
|
||||
if (s.ok()) {
|
||||
result->reset(new FastOpenTestRandomAccessFile(
|
||||
std::move(*result), fname, &metadata_retrieved_count_));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
int GetMetadataRetrievedCount() const {
|
||||
return metadata_retrieved_count_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
int GetMetadataPassedOnOpenCount() const {
|
||||
return metadata_passed_on_open_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
std::string GetLastMetadataReceived() const {
|
||||
return last_metadata_received_;
|
||||
}
|
||||
|
||||
void ResetCounters() {
|
||||
metadata_retrieved_count_.store(0, std::memory_order_relaxed);
|
||||
metadata_passed_on_open_.store(0, std::memory_order_relaxed);
|
||||
last_metadata_received_.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<int> metadata_retrieved_count_{0};
|
||||
std::atomic<int> metadata_passed_on_open_{0};
|
||||
std::string last_metadata_received_;
|
||||
};
|
||||
|
||||
TEST_F(DBTest2, FastSstOpenDefaultFSReturnsNotSupported) {
|
||||
// Verify that the default FS returns NotSupported for GetFileOpenMetadata.
|
||||
std::string fname = dbname_ + "/000001.sst";
|
||||
// Create a small file so we can open it
|
||||
{
|
||||
std::unique_ptr<FSWritableFile> wf;
|
||||
ASSERT_OK(env_->GetFileSystem()->NewWritableFile(fname, FileOptions(), &wf,
|
||||
nullptr));
|
||||
ASSERT_OK(wf->Append("test", IOOptions(), nullptr));
|
||||
ASSERT_OK(wf->Close(IOOptions(), nullptr));
|
||||
}
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
ASSERT_OK(env_->GetFileSystem()->NewRandomAccessFile(fname, FileOptions(),
|
||||
&file, nullptr));
|
||||
std::string metadata;
|
||||
IOStatus s = file->GetFileOpenMetadata(&metadata);
|
||||
ASSERT_TRUE(s.IsNotSupported());
|
||||
ASSERT_TRUE(metadata.empty());
|
||||
// Clean up
|
||||
ASSERT_OK(env_->GetFileSystem()->DeleteFile(fname, IOOptions(), nullptr));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, FastSstOpenFlushAndReopen) {
|
||||
// Test with default FS (GetFileOpenMetadata returns NotSupported).
|
||||
// Verifies the code path works without crashing.
|
||||
Options options = CurrentOptions();
|
||||
options.fast_sst_open = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("key1", "value1"));
|
||||
ASSERT_OK(Put("key2", "value2"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
Close();
|
||||
Reopen(options);
|
||||
|
||||
ASSERT_EQ("value1", Get("key1"));
|
||||
ASSERT_EQ("value2", Get("key2"));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, FastSstOpenCompactionAndReopen) {
|
||||
Options options = CurrentOptions();
|
||||
options.fast_sst_open = true;
|
||||
options.level0_file_num_compaction_trigger = 2;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("key1", "value1"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("key2", "value2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
Close();
|
||||
Reopen(options);
|
||||
|
||||
ASSERT_EQ("value1", Get("key1"));
|
||||
ASSERT_EQ("value2", Get("key2"));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, FastSstOpenWithTestFS) {
|
||||
// Full fast_sst_open flow: flush produces 1 SST, verify exact counts.
|
||||
auto test_fs = std::make_shared<FastOpenTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> test_env(new CompositeEnvWrapper(env_, test_fs));
|
||||
|
||||
Options options;
|
||||
options.env = test_env.get();
|
||||
options.fast_sst_open = true;
|
||||
options.create_if_missing = true;
|
||||
|
||||
std::string test_dbname = test::PerThreadDBPath("fast_open_test");
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
test_fs->ResetCounters();
|
||||
|
||||
// One flush produces one SST file -> exactly 1 GetFileOpenMetadata call
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key1", "value1"));
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key2", "value2"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
|
||||
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
|
||||
ASSERT_EQ(0, test_fs->GetMetadataPassedOnOpenCount());
|
||||
|
||||
test_fs->ResetCounters();
|
||||
db.reset();
|
||||
|
||||
// Reopen: the SST file should be opened with metadata
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
|
||||
std::string value;
|
||||
ASSERT_OK(db->Get(ReadOptions(), "key1", &value));
|
||||
ASSERT_EQ("value1", value);
|
||||
ASSERT_OK(db->Get(ReadOptions(), "key2", &value));
|
||||
ASSERT_EQ("value2", value);
|
||||
|
||||
// Exactly 1 SST file was opened with metadata passed
|
||||
ASSERT_EQ(1, test_fs->GetMetadataPassedOnOpenCount());
|
||||
ASSERT_TRUE(test_fs->GetLastMetadataReceived().find("fast_open_metadata:") ==
|
||||
0);
|
||||
|
||||
db.reset();
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, FastSstOpenCompactionWithTestFS) {
|
||||
// Compaction: 2 flushes trigger compaction, producing 1 output SST.
|
||||
auto test_fs = std::make_shared<FastOpenTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> test_env(new CompositeEnvWrapper(env_, test_fs));
|
||||
|
||||
Options options;
|
||||
options.env = test_env.get();
|
||||
options.fast_sst_open = true;
|
||||
options.create_if_missing = true;
|
||||
options.level0_file_num_compaction_trigger = 2;
|
||||
|
||||
std::string test_dbname = test::PerThreadDBPath("fast_open_compact");
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
test_fs->ResetCounters();
|
||||
|
||||
// 2 flushes = 2 SST files -> 2 GetFileOpenMetadata calls from flushes
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key1", "value1"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key2", "value2"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
ASSERT_OK(static_cast<DBImpl*>(db.get())->TEST_WaitForCompact());
|
||||
|
||||
// 2 flush SSTs retrieved metadata. Compaction output may or may not
|
||||
// trigger an additional retrieval depending on table cache state.
|
||||
ASSERT_GE(test_fs->GetMetadataRetrievedCount(), 2);
|
||||
|
||||
test_fs->ResetCounters();
|
||||
db.reset();
|
||||
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
std::string value;
|
||||
ASSERT_OK(db->Get(ReadOptions(), "key1", &value));
|
||||
ASSERT_EQ("value1", value);
|
||||
ASSERT_OK(db->Get(ReadOptions(), "key2", &value));
|
||||
ASSERT_EQ("value2", value);
|
||||
|
||||
// At least 1 SST opened with metadata on reopen
|
||||
ASSERT_GE(test_fs->GetMetadataPassedOnOpenCount(), 1);
|
||||
|
||||
db.reset();
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, FastSstOpenDisabledNoMetadata) {
|
||||
// When fast_sst_open is disabled, no metadata should be retrieved or passed.
|
||||
auto test_fs = std::make_shared<FastOpenTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> test_env(new CompositeEnvWrapper(env_, test_fs));
|
||||
|
||||
Options options;
|
||||
options.env = test_env.get();
|
||||
options.fast_sst_open = false;
|
||||
options.create_if_missing = true;
|
||||
|
||||
std::string test_dbname = test::PerThreadDBPath("fast_open_disabled");
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
test_fs->ResetCounters();
|
||||
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key1", "value1"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
|
||||
// No metadata retrieved when disabled
|
||||
ASSERT_EQ(0, test_fs->GetMetadataRetrievedCount());
|
||||
|
||||
test_fs->ResetCounters();
|
||||
db.reset();
|
||||
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
|
||||
std::string value;
|
||||
ASSERT_OK(db->Get(ReadOptions(), "key1", &value));
|
||||
ASSERT_EQ("value1", value);
|
||||
|
||||
// No metadata passed on open since none was saved
|
||||
ASSERT_EQ(0, test_fs->GetMetadataPassedOnOpenCount());
|
||||
|
||||
db.reset();
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, FastSstOpenToggleOption) {
|
||||
// Toggle: disabled for first flush, enabled for second.
|
||||
// Only the second file should have metadata on reopen.
|
||||
auto test_fs = std::make_shared<FastOpenTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> test_env(new CompositeEnvWrapper(env_, test_fs));
|
||||
|
||||
Options options;
|
||||
options.env = test_env.get();
|
||||
options.fast_sst_open = false;
|
||||
options.create_if_missing = true;
|
||||
// Prevent compaction so we have 2 separate L0 files
|
||||
options.level0_file_num_compaction_trigger = 100;
|
||||
|
||||
std::string test_dbname = test::PerThreadDBPath("fast_open_toggle");
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key1", "value1"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
ASSERT_EQ(0, test_fs->GetMetadataRetrievedCount());
|
||||
db.reset();
|
||||
|
||||
// Enable fast_sst_open for second session
|
||||
options.fast_sst_open = true;
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
test_fs->ResetCounters();
|
||||
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key2", "value2"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
// Exactly 1 metadata retrieved (for the new flush)
|
||||
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
|
||||
|
||||
test_fs->ResetCounters();
|
||||
db.reset();
|
||||
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
|
||||
std::string value;
|
||||
ASSERT_OK(db->Get(ReadOptions(), "key1", &value));
|
||||
ASSERT_EQ("value1", value);
|
||||
ASSERT_OK(db->Get(ReadOptions(), "key2", &value));
|
||||
ASSERT_EQ("value2", value);
|
||||
|
||||
// 2 SST files total, but only 1 has metadata (the one from the enabled
|
||||
// session). The other file opens without metadata.
|
||||
ASSERT_EQ(1, test_fs->GetMetadataPassedOnOpenCount());
|
||||
|
||||
db.reset();
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, FastSstOpenIngestion) {
|
||||
// Test that file ingestion retrieves and persists metadata.
|
||||
auto test_fs = std::make_shared<FastOpenTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> test_env(new CompositeEnvWrapper(env_, test_fs));
|
||||
|
||||
Options options;
|
||||
options.env = test_env.get();
|
||||
options.fast_sst_open = true;
|
||||
options.create_if_missing = true;
|
||||
|
||||
std::string test_dbname = test::PerThreadDBPath("fast_open_ingest");
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
|
||||
// Create an SST file externally
|
||||
std::string sst_file = test_dbname + "/external.sst";
|
||||
SstFileWriter sst_writer(EnvOptions(), options);
|
||||
ASSERT_OK(sst_writer.Open(sst_file));
|
||||
ASSERT_OK(sst_writer.Put("ikey1", "ival1"));
|
||||
ASSERT_OK(sst_writer.Put("ikey2", "ival2"));
|
||||
ASSERT_OK(sst_writer.Finish());
|
||||
|
||||
test_fs->ResetCounters();
|
||||
|
||||
// Ingest the external SST file
|
||||
IngestExternalFileOptions ingest_opts;
|
||||
ingest_opts.move_files = false;
|
||||
ASSERT_OK(db->IngestExternalFile({sst_file}, ingest_opts));
|
||||
|
||||
// Ingestion should retrieve metadata for the ingested file (1 call from
|
||||
// the separate open in the ingestion path)
|
||||
ASSERT_GE(test_fs->GetMetadataRetrievedCount(), 1);
|
||||
|
||||
test_fs->ResetCounters();
|
||||
db.reset();
|
||||
|
||||
// Reopen and verify metadata is passed
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
|
||||
std::string value;
|
||||
ASSERT_OK(db->Get(ReadOptions(), "ikey1", &value));
|
||||
ASSERT_EQ("ival1", value);
|
||||
ASSERT_OK(db->Get(ReadOptions(), "ikey2", &value));
|
||||
ASSERT_EQ("ival2", value);
|
||||
|
||||
// Metadata should be passed when opening the ingested SST
|
||||
ASSERT_GE(test_fs->GetMetadataPassedOnOpenCount(), 1);
|
||||
|
||||
db.reset();
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, FastSstOpenDisableAfterMetadataPersisted) {
|
||||
// Verify that disabling fast_sst_open prevents previously-persisted
|
||||
// metadata from being passed to NewRandomAccessFile. This is critical
|
||||
// for cases where stale metadata (e.g. expired credentials) would
|
||||
// cause file open failures.
|
||||
auto test_fs = std::make_shared<FastOpenTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> test_env(new CompositeEnvWrapper(env_, test_fs));
|
||||
|
||||
Options options;
|
||||
options.env = test_env.get();
|
||||
options.fast_sst_open = true;
|
||||
options.create_if_missing = true;
|
||||
|
||||
std::string test_dbname = test::PerThreadDBPath("fast_open_disable_after");
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
|
||||
// Open with fast_sst_open enabled, write and flush to persist metadata
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
ASSERT_OK(db->Put(WriteOptions(), "key1", "value1"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
|
||||
db.reset();
|
||||
|
||||
// Reopen with fast_sst_open DISABLED -- metadata is in the MANIFEST
|
||||
// but should NOT be passed to the filesystem
|
||||
options.fast_sst_open = false;
|
||||
test_fs->ResetCounters();
|
||||
ASSERT_OK(DB::Open(options, test_dbname, &db));
|
||||
|
||||
std::string value;
|
||||
ASSERT_OK(db->Get(ReadOptions(), "key1", &value));
|
||||
ASSERT_EQ("value1", value);
|
||||
|
||||
// No metadata should have been passed despite being in the MANIFEST
|
||||
ASSERT_EQ(0, test_fs->GetMetadataPassedOnOpenCount());
|
||||
|
||||
db.reset();
|
||||
ASSERT_OK(DestroyDB(test_dbname, options));
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
+253
-34
@@ -1,9 +1,12 @@
|
||||
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
|
||||
// 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/db_test_util.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
#include "rocksdb/metadata.h"
|
||||
#include "rocksdb/sst_file_writer.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -43,50 +46,98 @@ TEST_F(DBEtc3Test, AutoTuneManifestSize) {
|
||||
ASSERT_EQ(DBOptions{}.max_manifest_space_amp_pct, 500);
|
||||
|
||||
Options options = CurrentOptions();
|
||||
ASSERT_OK(db_->SetOptions({{"level0_file_num_compaction_trigger", "20"}}));
|
||||
// Test setup: create many flushed files. Keep level compaction semantics so
|
||||
// DeleteFilesInRanges can remove non-L0 files, but prevent automatic
|
||||
// compactions and write stalls from adding unrelated behavior.
|
||||
options.disable_auto_compactions = true;
|
||||
options.level0_slowdown_writes_trigger = 100;
|
||||
options.level0_stop_writes_trigger = 200;
|
||||
|
||||
// Use large column family names to essentially control the amount of payload
|
||||
// data needed for the manifest file. Drop manifest entries don't include the
|
||||
// CF name so are small.
|
||||
// Test strategy: use large column family names to control the rough amount
|
||||
// of payload added to the MANIFEST. Drop manifest entries do not include the
|
||||
// CF name, so they are small.
|
||||
//
|
||||
// Most CF helper calls piggy-back a background manifest write so the main
|
||||
// auto-tuning phases continue to test the unrelaxed background threshold
|
||||
// even though CF manipulation itself is foreground. Phase-specific foreground
|
||||
// checks disable that piggy-backed background write.
|
||||
uint64_t prev_manifest_num = 0, cur_manifest_num = 0;
|
||||
std::deque<ColumnFamilyHandle*> handles;
|
||||
int counter = 5;
|
||||
auto AddCfFn = [&]() {
|
||||
auto UpdateManifestNumsFrom = [&](uint64_t before_manifest_num) {
|
||||
prev_manifest_num = before_manifest_num;
|
||||
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
|
||||
};
|
||||
auto BackgroundManifestWriteFn = [&]() {
|
||||
uint64_t before_manifest_num = cur_manifest_num;
|
||||
ASSERT_OK(Put("x", std::to_string(counter++)));
|
||||
ASSERT_OK(Flush());
|
||||
UpdateManifestNumsFrom(before_manifest_num);
|
||||
};
|
||||
auto AddCfFn = [&](bool include_background_manifest_write = true) {
|
||||
uint64_t before_manifest_num = cur_manifest_num;
|
||||
std::string name = "cf" + std::to_string(counter++);
|
||||
name.resize(1000, 'a');
|
||||
ASSERT_OK(db_->CreateColumnFamily(options, name, &handles.emplace_back()));
|
||||
prev_manifest_num = cur_manifest_num;
|
||||
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
|
||||
if (include_background_manifest_write) {
|
||||
BackgroundManifestWriteFn();
|
||||
}
|
||||
UpdateManifestNumsFrom(before_manifest_num);
|
||||
};
|
||||
auto DropCfFn = [&]() {
|
||||
auto DropCfFn = [&](bool include_background_manifest_write = true) {
|
||||
uint64_t before_manifest_num = cur_manifest_num;
|
||||
ASSERT_OK(db_->DropColumnFamily(handles.front()));
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(handles.front()));
|
||||
handles.pop_front();
|
||||
prev_manifest_num = cur_manifest_num;
|
||||
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
|
||||
};
|
||||
auto TrivialManifestWriteFn = [&]() {
|
||||
ASSERT_OK(Put("x", std::to_string(counter++)));
|
||||
ASSERT_OK(Flush());
|
||||
prev_manifest_num = cur_manifest_num;
|
||||
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
|
||||
if (include_background_manifest_write) {
|
||||
BackgroundManifestWriteFn();
|
||||
}
|
||||
UpdateManifestNumsFrom(before_manifest_num);
|
||||
};
|
||||
|
||||
// ---- Phase 1: foreground threshold relaxation is bounded ----
|
||||
//
|
||||
// Foreground operations should only get about 25% extra headroom, not an
|
||||
// unbounded threshold. With 1000-byte CF names and a 3000-byte normal limit,
|
||||
// the relaxed limit should allow the first four foreground-only CF additions
|
||||
// but require rotation on the fifth.
|
||||
options.max_manifest_file_size = 3000;
|
||||
options.max_manifest_space_amp_pct = 0;
|
||||
DestroyAndReopen(options);
|
||||
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
|
||||
prev_manifest_num = cur_manifest_num;
|
||||
for (int i = 1; i <= 4; ++i) {
|
||||
AddCfFn(/*include_background_manifest_write=*/false);
|
||||
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
|
||||
}
|
||||
AddCfFn(/*include_background_manifest_write=*/false);
|
||||
ASSERT_LT(prev_manifest_num, cur_manifest_num);
|
||||
while (!handles.empty()) {
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(handles.front()));
|
||||
handles.pop_front();
|
||||
}
|
||||
|
||||
// ---- Phase 2: no auto-tuning means frequent rotation ----
|
||||
//
|
||||
options.max_manifest_file_size = 1000000;
|
||||
options.max_manifest_space_amp_pct = 0; // no auto-tuning yet
|
||||
DestroyAndReopen(options);
|
||||
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
|
||||
prev_manifest_num = cur_manifest_num;
|
||||
|
||||
// With the generous (minimum) maximum manifest size, should not be rotated
|
||||
// With the generous minimum manifest size, should not be rotated.
|
||||
AddCfFn();
|
||||
AddCfFn();
|
||||
AddCfFn();
|
||||
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
|
||||
|
||||
// Change options for small max and (still) no auto-tuning
|
||||
// Lower the minimum while still disabling auto-tuning.
|
||||
ASSERT_OK(db_->SetDBOptions({{"max_manifest_file_size", "3000"}}));
|
||||
|
||||
// Takes effect on the next manifest write
|
||||
TrivialManifestWriteFn();
|
||||
BackgroundManifestWriteFn();
|
||||
ASSERT_LT(prev_manifest_num, cur_manifest_num);
|
||||
|
||||
// Now we have to rewrite the whole manifest on each write because the
|
||||
@@ -97,28 +148,31 @@ TEST_F(DBEtc3Test, AutoTuneManifestSize) {
|
||||
ASSERT_LT(prev_manifest_num, cur_manifest_num);
|
||||
AddCfFn();
|
||||
ASSERT_LT(prev_manifest_num, cur_manifest_num);
|
||||
TrivialManifestWriteFn();
|
||||
BackgroundManifestWriteFn();
|
||||
ASSERT_LT(prev_manifest_num, cur_manifest_num);
|
||||
|
||||
// ---- Phase 3: auto-tuning raises the background threshold ----
|
||||
//
|
||||
// Enabling auto-tuning should fix this, immediately for next manifest writes.
|
||||
// This will allow up to double-ish the size of the compacted manifest,
|
||||
// which last should have been 4000 + some bytes.
|
||||
// This will allow up to roughly double the size of the compacted manifest,
|
||||
// which now includes CF entries plus the piggy-backed background writes.
|
||||
ASSERT_EQ(handles.size(), 4U);
|
||||
ASSERT_OK(db_->SetDBOptions({{"max_manifest_space_amp_pct", "105"}}));
|
||||
ASSERT_OK(db_->SetDBOptions({{"max_manifest_space_amp_pct", "95"}}));
|
||||
|
||||
// After 9 CF names should be enough to rotate the manifest
|
||||
for (int i = 1; i <= 5; ++i) {
|
||||
// Auto-tuning lets several more CF names accumulate in the MANIFEST before
|
||||
// the piggy-backed background write crosses the threshold.
|
||||
for (int i = 1; i <= 3; ++i) {
|
||||
if ((i % 2) == 1) {
|
||||
DropCfFn();
|
||||
}
|
||||
AddCfFn();
|
||||
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
|
||||
}
|
||||
TrivialManifestWriteFn();
|
||||
AddCfFn();
|
||||
ASSERT_LT(prev_manifest_num, cur_manifest_num);
|
||||
|
||||
// We now have a different last compacted manifest size, should be
|
||||
// able to go beyond 9 CFs named in manifest this time.
|
||||
// We now have a different last compacted manifest size, so the next
|
||||
// threshold should be based on the newly compacted MANIFEST.
|
||||
ASSERT_EQ(handles.size(), 6U);
|
||||
|
||||
DropCfFn();
|
||||
@@ -128,11 +182,10 @@ TEST_F(DBEtc3Test, AutoTuneManifestSize) {
|
||||
AddCfFn();
|
||||
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
|
||||
}
|
||||
// We've written 10 named CFs to the manifest. We should be able to
|
||||
// dynamically change the auto-tuning still based on the last "compacted"
|
||||
// manifest size of 7000 + some bytes.
|
||||
// We should be able to dynamically change the auto-tuning still based on
|
||||
// the last "compacted" manifest size.
|
||||
ASSERT_OK(db_->SetDBOptions({{"max_manifest_space_amp_pct", "51"}}));
|
||||
TrivialManifestWriteFn();
|
||||
BackgroundManifestWriteFn();
|
||||
ASSERT_LT(prev_manifest_num, cur_manifest_num);
|
||||
// And the "compacted" manifest size has reset again, so should be changed
|
||||
// again sooner.
|
||||
@@ -141,13 +194,179 @@ TEST_F(DBEtc3Test, AutoTuneManifestSize) {
|
||||
AddCfFn();
|
||||
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
|
||||
}
|
||||
// Enough for manifest change
|
||||
AddCfFn();
|
||||
|
||||
// ---- Phase 4: foreground operations use relaxed threshold ----
|
||||
//
|
||||
// The current MANIFEST is now large enough for the next background manifest
|
||||
// write to rotate it, but still small enough for foreground operations to use
|
||||
// their 25% extra headroom. Assert that each foreground operation stays on
|
||||
// the same MANIFEST, then verify the next background write rotates.
|
||||
const std::string external_files_dir = dbname_ + "/external_files";
|
||||
ASSERT_OK(env_->CreateDirIfMissing(external_files_dir));
|
||||
auto WriteExternalSstFile = [&](const std::string& file_name,
|
||||
const std::string& key,
|
||||
const std::string& value) {
|
||||
const std::string file_path = external_files_dir + "/" + file_name;
|
||||
Status ignored = env_->DeleteFile(file_path);
|
||||
ignored.PermitUncheckedError();
|
||||
|
||||
SstFileWriter sst_file_writer(EnvOptions(), options);
|
||||
ASSERT_OK(sst_file_writer.Open(file_path));
|
||||
ASSERT_OK(sst_file_writer.Put(key, value));
|
||||
ASSERT_OK(sst_file_writer.Finish());
|
||||
};
|
||||
auto IngestExternalFileForegroundManifestWriteFn =
|
||||
[&](std::string* ingested_key) {
|
||||
uint64_t before_manifest_num = cur_manifest_num;
|
||||
const std::string key = "z" + std::to_string(counter++);
|
||||
const std::string value = "v" + std::to_string(counter++);
|
||||
const std::string file_name =
|
||||
"ingest_" + std::to_string(counter++) + ".sst";
|
||||
const std::string file_path = external_files_dir + "/" + file_name;
|
||||
WriteExternalSstFile(file_name, key, value);
|
||||
|
||||
ASSERT_OK(
|
||||
db_->IngestExternalFile({file_path}, IngestExternalFileOptions()));
|
||||
ASSERT_EQ(value, Get(key));
|
||||
*ingested_key = key;
|
||||
Status ignored = env_->DeleteFile(file_path);
|
||||
ignored.PermitUncheckedError();
|
||||
UpdateManifestNumsFrom(before_manifest_num);
|
||||
};
|
||||
auto CreateColumnFamilyWithImportForegroundManifestWriteFn = [&]() {
|
||||
uint64_t before_manifest_num = cur_manifest_num;
|
||||
const std::string cf_name = "import_cf" + std::to_string(counter++);
|
||||
const std::string key = "import" + std::to_string(counter++);
|
||||
const std::string value = "v" + std::to_string(counter++);
|
||||
const std::string file_name =
|
||||
"import_" + std::to_string(counter++) + ".sst";
|
||||
const std::string file_path = external_files_dir + "/" + file_name;
|
||||
WriteExternalSstFile(file_name, key, value);
|
||||
|
||||
LiveFileMetaData file_metadata;
|
||||
file_metadata.name = file_name;
|
||||
file_metadata.db_path = external_files_dir;
|
||||
file_metadata.smallest_seqno = 0;
|
||||
file_metadata.largest_seqno = 0;
|
||||
file_metadata.level = 0;
|
||||
ExportImportFilesMetaData metadata;
|
||||
metadata.files.push_back(file_metadata);
|
||||
metadata.db_comparator_name = options.comparator->Name();
|
||||
|
||||
ColumnFamilyHandle* import_handle = nullptr;
|
||||
ASSERT_OK(db_->CreateColumnFamilyWithImport(options, cf_name,
|
||||
ImportColumnFamilyOptions(),
|
||||
metadata, &import_handle));
|
||||
ASSERT_NE(import_handle, nullptr);
|
||||
handles.push_back(import_handle);
|
||||
|
||||
std::string result;
|
||||
ASSERT_OK(db_->Get(ReadOptions(), import_handle, key, &result));
|
||||
ASSERT_EQ(value, result);
|
||||
Status ignored = env_->DeleteFile(file_path);
|
||||
ignored.PermitUncheckedError();
|
||||
UpdateManifestNumsFrom(before_manifest_num);
|
||||
};
|
||||
auto DeleteFilesInRangesForegroundManifestWriteFn =
|
||||
[&](const std::string& key) {
|
||||
uint64_t before_manifest_num = cur_manifest_num;
|
||||
const std::string limit = key + "\xff";
|
||||
std::vector<RangeOpt> ranges;
|
||||
ranges.emplace_back(key, limit);
|
||||
ASSERT_OK(DeleteFilesInRanges(db_.get(), db_->DefaultColumnFamily(),
|
||||
ranges.data(), ranges.size(),
|
||||
/*include_end=*/false));
|
||||
|
||||
std::string result;
|
||||
ASSERT_TRUE(db_->Get(ReadOptions(), key, &result).IsNotFound());
|
||||
UpdateManifestNumsFrom(before_manifest_num);
|
||||
};
|
||||
|
||||
// Column family manipulation.
|
||||
AddCfFn(/*include_background_manifest_write=*/false);
|
||||
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
|
||||
|
||||
// SetOptions should not write to the MANIFEST. If that regresses, the write
|
||||
// should be treated as a background write and rotate here.
|
||||
{
|
||||
ASSERT_OK(db_->SetOptions({{"level0_slowdown_writes_trigger", "101"}}));
|
||||
UpdateManifestNumsFrom(cur_manifest_num);
|
||||
}
|
||||
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
|
||||
|
||||
// External file ingestion.
|
||||
std::string ingested_key;
|
||||
IngestExternalFileForegroundManifestWriteFn(&ingested_key);
|
||||
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
|
||||
|
||||
// Imported column family creation.
|
||||
CreateColumnFamilyWithImportForegroundManifestWriteFn();
|
||||
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
|
||||
|
||||
// Physical file deletion by range.
|
||||
DeleteFilesInRangesForegroundManifestWriteFn(ingested_key);
|
||||
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
|
||||
|
||||
// Background flush.
|
||||
BackgroundManifestWriteFn();
|
||||
ASSERT_LT(prev_manifest_num, cur_manifest_num);
|
||||
|
||||
// ---- Phase 5: persisted compacted manifest size survives close/reopen ----
|
||||
// Close with CFs still live. Reopen with reuse_manifest_on_open so the
|
||||
// manifest is NOT rewritten from scratch. The persisted compacted size
|
||||
// should be loaded and used for auto-tuning.
|
||||
// At this point we have 8 CF handles plus default.
|
||||
ASSERT_EQ(handles.size(), 8U);
|
||||
|
||||
// Collect CF names for reopen, then release handles (Close needs this)
|
||||
std::vector<std::string> cf_names = {"default"};
|
||||
for (auto* h : handles) {
|
||||
cf_names.push_back(h->GetName());
|
||||
}
|
||||
for (auto* h : handles) {
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(h));
|
||||
}
|
||||
handles.clear();
|
||||
|
||||
Close();
|
||||
// Use a max_manifest_file_size below the reused manifest size. Auto-tuning
|
||||
// with the persisted compacted size at 200% amp keeps the tuned threshold
|
||||
// high enough. Without persistence, the threshold would be
|
||||
// max(max_manifest_file_size, 0 * anything) = max_manifest_file_size.
|
||||
//
|
||||
// With max_manifest_file_size set to 3000, missing persisted compacted size
|
||||
// would keep tuned = 3000 and the first AddCf would rotate because the
|
||||
// reused manifest is already larger. With persisted compacted size loaded,
|
||||
// the tuned threshold is based on the compacted size, so no rotation until
|
||||
// the manifest grows well beyond the minimum.
|
||||
options.max_manifest_file_size = 3000;
|
||||
options.max_manifest_space_amp_pct = 200;
|
||||
options.reuse_manifest_on_open = true;
|
||||
uint64_t manifest_num_before_reopen = cur_manifest_num;
|
||||
ReopenWithColumnFamilies(cf_names, options);
|
||||
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
|
||||
|
||||
// With persistence: the manifest file number should NOT have changed
|
||||
// during reopen, because the persisted compacted size keeps the tuned
|
||||
// threshold high enough. Without persistence, last_compacted = 0, so
|
||||
// tuned = max_manifest_file_size = 3000, and the first LogAndApply
|
||||
// during Open rotates the manifest because it is already larger than 3000.
|
||||
ASSERT_EQ(manifest_num_before_reopen, cur_manifest_num);
|
||||
|
||||
// Adding CFs should still not trigger rotation because the tuned threshold
|
||||
// from the persisted compacted size exceeds the current manifest size.
|
||||
for (int i = 1; i <= 4; ++i) {
|
||||
AddCfFn();
|
||||
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
|
||||
}
|
||||
for (int i = 1; i <= 4; ++i) {
|
||||
AddCfFn();
|
||||
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
|
||||
}
|
||||
|
||||
// Wrap up
|
||||
while (!handles.empty()) {
|
||||
DropCfFn();
|
||||
DropCfFn(/*include_background_manifest_write=*/false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+30
-18
@@ -205,7 +205,6 @@ Status DBImpl::GetLiveFilesStorageInfo(
|
||||
|
||||
// NOTE: This implementation was largely migrated from Checkpoint.
|
||||
|
||||
Status s;
|
||||
VectorWalPtr live_wal_files;
|
||||
bool flush_memtable = true;
|
||||
if (!immutable_db_options_.allow_2pc) {
|
||||
@@ -217,12 +216,12 @@ Status DBImpl::GetLiveFilesStorageInfo(
|
||||
// Don't take archived log size into account when calculating wal
|
||||
// size for flush, and don't need to verify consistency with manifest
|
||||
// here & now.
|
||||
s = wal_manager_.GetSortedWalFiles(live_wal_files,
|
||||
/* need_seqnos */ false,
|
||||
/*include_archived*/ false);
|
||||
Status wal_s = wal_manager_.GetSortedWalFiles(live_wal_files,
|
||||
/* need_seqnos */ false,
|
||||
/*include_archived*/ false);
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
if (!wal_s.ok()) {
|
||||
return wal_s;
|
||||
}
|
||||
|
||||
// Don't flush column families if total log size is smaller than
|
||||
@@ -243,9 +242,24 @@ Status DBImpl::GetLiveFilesStorageInfo(
|
||||
// This is a modified version of GetLiveFiles, to get access to more
|
||||
// metadata.
|
||||
mutex_.Lock();
|
||||
bool wal_locked = false;
|
||||
const bool needs_blob_direct_write_flush =
|
||||
HasInFlightBlobDirectWriteFilesWithLockHeld();
|
||||
if (needs_blob_direct_write_flush && !flush_memtable) {
|
||||
mutex_.Unlock();
|
||||
return Status::NotSupported(
|
||||
"Blob direct write requires flushing active blob files before "
|
||||
"capturing live files. Retry with flush enabled.");
|
||||
}
|
||||
if (flush_memtable) {
|
||||
bool wal_locked = lock_wal_count_ > 0;
|
||||
wal_locked = lock_wal_count_ > 0;
|
||||
if (wal_locked) {
|
||||
if (needs_blob_direct_write_flush) {
|
||||
mutex_.Unlock();
|
||||
return Status::NotSupported(
|
||||
"Blob direct write requires flushing active blob files before "
|
||||
"capturing live files. Retry with WAL unlocked.");
|
||||
}
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"Can't FlushForGetLiveFiles while WAL is locked");
|
||||
} else {
|
||||
@@ -393,17 +407,15 @@ Status DBImpl::GetLiveFilesStorageInfo(
|
||||
TEST_SYNC_POINT("CheckpointImpl::CreateCheckpoint:SavedLiveFiles1");
|
||||
TEST_SYNC_POINT("CheckpointImpl::CreateCheckpoint:SavedLiveFiles2");
|
||||
|
||||
if (s.ok()) {
|
||||
// FlushWAL is required to ensure we can physically copy everything
|
||||
// logically written to the WAL. (Sync not strictly required for
|
||||
// active WAL to be copied rather than hard linked, even when
|
||||
// Checkpoint guarantees that the copied-to file is sync-ed. Plus we can't
|
||||
// help track_and_verify_wals_in_manifest after manifest_size is
|
||||
// already determined.)
|
||||
s = FlushWAL(/*sync=*/false);
|
||||
if (s.IsNotSupported()) { // read-only DB or similar
|
||||
s = Status::OK();
|
||||
}
|
||||
// FlushWAL is required to ensure we can physically copy everything
|
||||
// logically written to the WAL. (Sync not strictly required for
|
||||
// active WAL to be copied rather than hard linked, even when
|
||||
// Checkpoint guarantees that the copied-to file is sync-ed. Plus we can't
|
||||
// help track_and_verify_wals_in_manifest after manifest_size is
|
||||
// already determined.)
|
||||
Status s = FlushWAL(/*sync=*/false);
|
||||
if (s.IsNotSupported()) { // read-only DB or similar
|
||||
s = Status::OK();
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT("CheckpointImpl::CreateCustomCheckpoint:AfterGetLive1");
|
||||
|
||||
@@ -1852,6 +1852,8 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_OK(WaitForFlushCallbacks());
|
||||
|
||||
// Check that there was at least one mempurge
|
||||
uint32_t expected_min_mempurge_count = 1;
|
||||
// Check that there was no SST files created during flush.
|
||||
@@ -1872,6 +1874,8 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
|
||||
ASSERT_EQ(Get(key), value);
|
||||
}
|
||||
|
||||
ASSERT_OK(WaitForFlushCallbacks());
|
||||
|
||||
// Check that there was at least one SST files created during flush.
|
||||
expected_sst_count = 1;
|
||||
EXPECT_GE(sst_count.load(), expected_sst_count);
|
||||
@@ -1891,6 +1895,9 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
|
||||
// Extra check of database consistency.
|
||||
ASSERT_EQ(Get(key), value);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
@@ -3849,6 +3856,125 @@ TEST_F(DBFlushTest, BuilderWriteFaultPropagationDuringFlush) {
|
||||
|
||||
Close();
|
||||
}
|
||||
// Regression test for table cache leak when flush install fails.
|
||||
// BuildTable caches the flush output file (for user reads) but if the
|
||||
// subsequent TryInstallMemtableFlushResults (LogAndApply) fails, the
|
||||
// cache entry was never evicted. The FindObsoleteFiles backstop normally
|
||||
// catches this, but fails under metadata read fault injection
|
||||
// (open_metadata_read_fault_one_in), causing
|
||||
// TEST_VerifyNoObsoleteFilesCached to fire during Close().
|
||||
TEST_F(DBFlushTest, LeakedTableCacheEntryOnFlushInstallFailure) {
|
||||
auto fault_fs = std::make_shared<FaultInjectionTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.env = fault_env.get();
|
||||
options.paranoid_file_checks = true;
|
||||
// Disable auto-recovery so it doesn't clean up the orphan on a separate
|
||||
// thread where metadata faults aren't enabled.
|
||||
options.max_bgerror_resume_count = 0;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Write data so flush produces a non-empty file.
|
||||
ASSERT_OK(Put("a", std::string(1024, 'x')));
|
||||
|
||||
// After BuildTable succeeds (caching the output file), inject a failure
|
||||
// before the install-or-rollback decision. This simulates scenarios like
|
||||
// CF dropped or shutdown after BuildTable. Also deactivate the filesystem
|
||||
// so the post-flush FindObsoleteFiles backstop cannot scan the directory.
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"FlushJob::Run:PostBuildTable", [&](void* arg) {
|
||||
*static_cast<Status*>(arg) = Status::IOError("injected");
|
||||
fault_fs->SetFilesystemActive(false);
|
||||
});
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Trigger flush -- BuildTable succeeds but LogAndApply fails.
|
||||
Status s = Flush();
|
||||
ASSERT_NOK(s);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// With the fix, the leaked cache entry was already evicted by
|
||||
// ReleaseObsolete in FlushJob::Run(). Without the fix, it persists.
|
||||
// We can verify by counting non-live cache entries. The quarantine
|
||||
// includes the file (BG error is active), but that's only a workaround --
|
||||
// the cache entry should not exist at all with the fix.
|
||||
//
|
||||
// Check: with the fix, the table cache should be empty (the flush output
|
||||
// was never installed and the fix evicted it). Without the fix, there's
|
||||
// a leaked entry.
|
||||
int cache_entries = 0;
|
||||
auto count_fn = [&cache_entries](const Slice&, Cache::ObjectPtr, size_t,
|
||||
const Cache::CacheItemHelper*) {
|
||||
cache_entries++;
|
||||
};
|
||||
dbfull()->TEST_table_cache()->ApplyToAllEntries(count_fn, {});
|
||||
ASSERT_EQ(cache_entries, 0)
|
||||
<< "Leaked table cache entry for flush output that was never installed";
|
||||
|
||||
// Reactivate filesystem for clean shutdown.
|
||||
fault_fs->SetFilesystemActive(true);
|
||||
db_ = nullptr; // destructor handles Close
|
||||
}
|
||||
|
||||
// Verify that ConstructFragmentedRangeTombstones runs after MarkImmutable
|
||||
// in SwitchMemtable. A range tombstone inserted between construction and
|
||||
// immutability would cause a flush entry count mismatch.
|
||||
TEST_F(DBFlushTest, FlushAfterReadPathRangeTombstoneInsertion) {
|
||||
Options options = CurrentOptions();
|
||||
options.min_tombstones_for_range_conversion = 2;
|
||||
options.flush_verify_memtable_count = true;
|
||||
options.statistics = CreateDBStatistics();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Write base data and flush to L0.
|
||||
for (char c = 'a'; c <= 'h'; c++) {
|
||||
ASSERT_OK(Put(std::string(1, c), std::string("v") + c));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Delete contiguous keys in the memtable.
|
||||
for (const auto& key : {"b", "c", "d", "e"}) {
|
||||
ASSERT_OK(Delete(key));
|
||||
}
|
||||
|
||||
// When SwitchMemtable hits the sync point after
|
||||
// ConstructFragmentedRangeTombstones, directly call
|
||||
// AddLogicallyRedundantRangeTombstone on the memtable being switched.
|
||||
// We can't create an iterator here (would deadlock on the DB mutex).
|
||||
auto* cfh = static_cast<ColumnFamilyHandleImpl*>(db_->DefaultColumnFamily());
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::SwitchMemtable:AfterConstructFragmentedRangeTombstones",
|
||||
[&](void*) {
|
||||
// Try to insert a range tombstone. With the fix (Construct after
|
||||
// MarkImmutable), this returns false because the memtable is
|
||||
// already immutable. Without the fix, this succeeds and creates
|
||||
// an entry count mismatch.
|
||||
// Try to insert a range tombstone. With the fix (Construct after
|
||||
// MarkImmutable), this returns false because the memtable is
|
||||
// already immutable. Without the fix, this succeeds and creates
|
||||
// an entry count mismatch.
|
||||
cfh->cfd()->mem()->AddLogicallyRedundantRangeTombstone(
|
||||
1 /* seq */, "b", "f", cfh->cfd()->GetIngestSstLock());
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status s = Flush();
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_OK(s);
|
||||
|
||||
ASSERT_EQ(Get("a"), "va");
|
||||
ASSERT_EQ(Get("b"), "NOT_FOUND");
|
||||
ASSERT_EQ(Get("d"), "NOT_FOUND");
|
||||
ASSERT_EQ(Get("f"), "vf");
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "db/db_impl/compacted_db_impl.h"
|
||||
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/version_set.h"
|
||||
#include "logging/logging.h"
|
||||
@@ -75,13 +76,16 @@ Status CompactedDBImpl::Get(const ReadOptions& _read_options,
|
||||
}
|
||||
|
||||
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
|
||||
BlobFetcher blob_fetcher(version_, read_options);
|
||||
std::string* ts =
|
||||
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr;
|
||||
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
|
||||
GetContext get_context(user_comparator_, nullptr, nullptr, nullptr,
|
||||
GetContext::kNotFound, lkey.user_key(), value,
|
||||
/*columns=*/nullptr, ts, nullptr, nullptr, true,
|
||||
nullptr, nullptr, nullptr, nullptr, &read_cb);
|
||||
nullptr, nullptr, nullptr, nullptr, &read_cb,
|
||||
nullptr /* is_blob_index */, 0 /* tracing_get_id */,
|
||||
&blob_fetcher);
|
||||
|
||||
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
|
||||
if (user_comparator_->CompareWithoutTimestamp(
|
||||
@@ -165,6 +169,7 @@ void CompactedDBImpl::MultiGet(const ReadOptions& _read_options,
|
||||
}
|
||||
|
||||
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
|
||||
BlobFetcher blob_fetcher(version_, read_options);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
const Slice& key = keys[i];
|
||||
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
|
||||
@@ -177,13 +182,15 @@ void CompactedDBImpl::MultiGet(const ReadOptions& _read_options,
|
||||
statuses[i] = Status::NotFound();
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
nullptr, true, nullptr, nullptr, nullptr, nullptr, &read_cb,
|
||||
nullptr /* is_blob_index */, 0 /* tracing_get_id */, &blob_fetcher);
|
||||
TableReader* t = nullptr;
|
||||
TableCache::TypedHandle* handle = nullptr;
|
||||
Status status = cfd_->table_cache()->FindTable(
|
||||
@@ -201,12 +208,10 @@ void CompactedDBImpl::MultiGet(const ReadOptions& _read_options,
|
||||
}
|
||||
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[i] = Status::OK();
|
||||
} else {
|
||||
statuses[i] = Status::NotFound();
|
||||
}
|
||||
statuses[i] = Status::NotFound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+887
-53
File diff suppressed because it is too large
Load Diff
+346
-16
@@ -15,6 +15,8 @@
|
||||
#include <limits>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -260,6 +262,10 @@ class DBImpl : public DB {
|
||||
const WriteOptions& options,
|
||||
std::shared_ptr<WriteBatchWithIndex> wbwi) override;
|
||||
|
||||
// Returns true if any live column family currently has blob direct write
|
||||
// enabled.
|
||||
bool HasAnyBlobDirectWriteColumnFamily();
|
||||
|
||||
using DB::Get;
|
||||
Status Get(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
@@ -1092,6 +1098,7 @@ class DBImpl : public DB {
|
||||
superversions_to_free_queue_.push_back(sv);
|
||||
}
|
||||
|
||||
void EnableTrackPublishedSeqInSnapshotContext();
|
||||
void SetSnapshotChecker(SnapshotChecker* snapshot_checker);
|
||||
|
||||
// Fill JobContext with snapshot information needed by flush and compaction.
|
||||
@@ -1448,6 +1455,13 @@ class DBImpl : public DB {
|
||||
std::atomic<int> next_job_id_ = 1;
|
||||
|
||||
std::atomic<bool> shutting_down_ = false;
|
||||
// Protected by mutex_. This is separate from shutting_down_ because
|
||||
// OnDBShutdownBegin must fire before shutting_down_ is published, and the
|
||||
// notification releases mutex_ while invoking listeners. Marking that the
|
||||
// notification has started prevents a concurrent or reentrant
|
||||
// CancelAllBackgroundWork() call from firing it again during that unlocked
|
||||
// callback window.
|
||||
bool shutdown_notification_sent_ = false;
|
||||
|
||||
// No new background jobs can be queued if true. This is used to prevent new
|
||||
// background jobs from being queued after WaitForCompact() completes waiting
|
||||
@@ -1522,6 +1536,16 @@ class DBImpl : public DB {
|
||||
const Status& st,
|
||||
const CompactionJobStats& job_stats,
|
||||
int job_id);
|
||||
// Fires the OnCompactionPreCommit listener callback. Mirrors
|
||||
// NotifyOnCompactionCompleted but is invoked before ReleaseCompactionFiles()
|
||||
// (i.e. while input files still have being_compacted == true). Idempotent:
|
||||
// safe to call from multiple potential release sites; only fires once per
|
||||
// compaction, and only if NotifyOnCompactionBegin previously fired.
|
||||
void NotifyOnCompactionPreCommit(ColumnFamilyData* cfd, Compaction* c,
|
||||
const Status& st,
|
||||
const CompactionJobStats& job_stats,
|
||||
int job_id);
|
||||
void NotifyOnDBShutdownBegin();
|
||||
void NotifyOnMemTableSealed(ColumnFamilyData* cfd,
|
||||
const MemTableInfo& mem_table_info);
|
||||
|
||||
@@ -1551,10 +1575,17 @@ class DBImpl : public DB {
|
||||
// @param memtable_updated Whether the same write that ingests wbwi has
|
||||
// updated memtable. This is useful for determining whether to set bg
|
||||
// error when IngestWBWIAsMemtable fails.
|
||||
// @param ingest_wbwi_for_commit Whether wbwi ingestion is publishing the
|
||||
// committed data of a prepared transaction. This means a failure can leave
|
||||
// committed data durable in WAL but not published in memtables.
|
||||
// @param ignore_missing_cf If true, skip column families not found in the DB
|
||||
// instead of returning an error.
|
||||
Status IngestWBWIAsMemtable(std::shared_ptr<WriteBatchWithIndex> wbwi,
|
||||
const WBWIMemTable::SeqnoRange& assigned_seqno,
|
||||
uint64_t min_prep_log, SequenceNumber last_seqno,
|
||||
bool memtable_updated, bool ignore_missing_cf);
|
||||
bool memtable_updated,
|
||||
bool ingest_wbwi_for_commit,
|
||||
bool ignore_missing_cf);
|
||||
|
||||
// If disable_memtable is set the application logic must guarantee that the
|
||||
// batch will still be skipped from memtable during the recovery. An excption
|
||||
@@ -1577,21 +1608,131 @@ class DBImpl : public DB {
|
||||
// See more in comment above PreReleaseCallback::Callback().
|
||||
// post_memtable_callback is called after memtable write but before publishing
|
||||
// the sequence number to readers.
|
||||
// `trace_batch_override`, when non-null, supplies the logical batch to trace
|
||||
// instead of `updates`. Fast paths may rewrite or rebuild `updates` before
|
||||
// apply, but tracing should still record the original user-visible batch.
|
||||
// `skip_blob_direct_write_transform` indicates the caller has already
|
||||
// performed any needed batch-level blob direct-write preprocessing.
|
||||
// `deferred_put_entities`, when non-null, carries structured `PutEntity()`
|
||||
// ops that should be materialized into `updates` after `PreprocessWrite()`
|
||||
// has selected the target memtable/blob generation.
|
||||
//
|
||||
// The main write queue. This is the only write queue that updates
|
||||
// LastSequence. When using one write queue, the same sequence also indicates
|
||||
// the last published sequence.
|
||||
Status WriteImpl(const WriteOptions& options, WriteBatch* updates,
|
||||
WriteCallback* callback = nullptr,
|
||||
UserWriteCallback* user_write_cb = nullptr,
|
||||
uint64_t* wal_used = nullptr, uint64_t log_ref = 0,
|
||||
bool disable_memtable = false, uint64_t* seq_used = nullptr,
|
||||
size_t batch_cnt = 0,
|
||||
PreReleaseCallback* pre_release_callback = nullptr,
|
||||
PostMemTableCallback* post_memtable_callback = nullptr,
|
||||
std::shared_ptr<WriteBatchWithIndex> wbwi = nullptr);
|
||||
struct DeferredPutEntityBatch;
|
||||
Status WriteImpl(
|
||||
const WriteOptions& options, WriteBatch* updates,
|
||||
WriteCallback* callback = nullptr,
|
||||
UserWriteCallback* user_write_cb = nullptr, uint64_t* wal_used = nullptr,
|
||||
uint64_t log_ref = 0, bool disable_memtable = false,
|
||||
uint64_t* seq_used = nullptr, size_t batch_cnt = 0,
|
||||
PreReleaseCallback* pre_release_callback = nullptr,
|
||||
PostMemTableCallback* post_memtable_callback = nullptr,
|
||||
std::shared_ptr<WriteBatchWithIndex> wbwi = nullptr,
|
||||
WriteBatch* trace_batch_override = nullptr,
|
||||
bool skip_blob_direct_write_transform = false,
|
||||
const DeferredPutEntityBatch* deferred_put_entities = nullptr);
|
||||
|
||||
// Per-WriteImpl state that keeps BDW column families pinned through
|
||||
// referenced SuperVersions until the transformed write either commits or
|
||||
// rolls back.
|
||||
struct BlobDirectWriteContext;
|
||||
// High-level `PutEntity()` fast-path flow:
|
||||
//
|
||||
// non-BDW target CF:
|
||||
// PutEntityFastPath()
|
||||
// -> AppendPreprocessedPutEntityToBatch()
|
||||
// -> WritePreprocessedPutEntityBatch()
|
||||
// -> WriteImpl(skip_blob_direct_write_transform = true)
|
||||
//
|
||||
// BDW-enabled target CF:
|
||||
// PutEntityFastPath()
|
||||
// -> sort columns and stash DeferredPutEntityBatch::Op
|
||||
// -> WritePreprocessedPutEntityBatch()
|
||||
// -> WriteImpl(..., deferred_put_entities)
|
||||
// -> after PreprocessWrite() picks the memtable/blob generation:
|
||||
// AppendSortedPutEntityToBatch()
|
||||
//
|
||||
// The BDW branch defers final serialization until `WriteImpl()` knows the
|
||||
// target memtable/blob generation, avoiding the old
|
||||
// serialize -> deserialize -> serialize cycle on the write hot path.
|
||||
// Staging buffer for the `PutEntity()` fast path when blob direct write may
|
||||
// need to rewrite wide-column values. `PutEntityFastPath()` records the
|
||||
// logical operations here instead of serializing them into a `WriteBatch`
|
||||
// immediately, then passes this struct to `WriteImpl()`. Once
|
||||
// `PreprocessWrite()` has selected the memtable/blob-file generation,
|
||||
// `WriteImpl()` materializes each op directly into the final batch with blob
|
||||
// direct-write preprocessing applied. This avoids the old
|
||||
// serialize-deserialize-serialize cycle on the write hot path.
|
||||
struct DeferredPutEntityBatch {
|
||||
// The fast path sorts columns up front so `WriteImpl()` can append the
|
||||
// final serialized entity without redoing that work.
|
||||
struct Op {
|
||||
// Target column family for the logical `PutEntity()` op.
|
||||
uint32_t column_family_id = 0;
|
||||
// Owned key bytes kept alive until `WriteImpl()` rebuilds the batch.
|
||||
std::string key;
|
||||
// Pre-sorted columns ready for final serialization and blob
|
||||
// direct-write preprocessing.
|
||||
WideColumns sorted_columns;
|
||||
};
|
||||
|
||||
// Logical `PutEntity()` ops collected by the fast path and replayed into
|
||||
// the final `WriteBatch` inside `WriteImpl()`.
|
||||
std::vector<Op> ops;
|
||||
};
|
||||
// Rewrites a write batch for blob direct write when the current DB and batch
|
||||
// shape are compatible, recording touched managers and rollback metadata in
|
||||
// `blob_direct_write_ctx`.
|
||||
Status MaybeTransformBatchForBlobDirectWrite(
|
||||
const WriteOptions& write_options, WriteBatch** batch,
|
||||
bool should_write_to_memtable, bool lookup_from_write_thread,
|
||||
WriteBatch* transformed_storage,
|
||||
BlobDirectWriteContext* blob_direct_write_ctx);
|
||||
// Sorts and preprocesses one `PutEntity()` op, then appends the final
|
||||
// serialized form to `batch`.
|
||||
Status AppendPreprocessedPutEntityToBatch(
|
||||
const WriteOptions& write_options, WriteBatch* batch,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
const WideColumns& columns,
|
||||
BlobDirectWriteContext* blob_direct_write_ctx);
|
||||
// Appends a `PutEntity()` op whose columns are already sorted, allowing the
|
||||
// fast path to avoid re-sorting before final serialization.
|
||||
Status AppendSortedPutEntityToBatch(
|
||||
const WriteOptions& write_options, WriteBatch* batch,
|
||||
uint32_t column_family_id, const Slice& key,
|
||||
const WideColumns& sorted_columns,
|
||||
BlobDirectWriteContext* blob_direct_write_ctx);
|
||||
// Specialized `PutEntity()` write path for a single column family. This
|
||||
// avoids building an intermediate serialized entity when blob direct write
|
||||
// needs to rewrite the final batch inside `WriteImpl()`.
|
||||
Status PutEntityFastPath(const WriteOptions& write_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
const WideColumns& columns);
|
||||
// Specialized `PutEntity(AttributeGroups)` path that defers final batch
|
||||
// materialization until `WriteImpl()` when any target CF may use blob direct
|
||||
// write.
|
||||
Status PutEntityFastPath(const WriteOptions& write_options, const Slice& key,
|
||||
const AttributeGroups& attribute_groups);
|
||||
// Writes a batch prepared by the `PutEntity()` fast path. When
|
||||
// `deferred_put_entities` is present, `WriteImpl()` rebuilds `batch`
|
||||
// internally after `PreprocessWrite()` and disables normal write batching to
|
||||
// keep the staging state local to this write. `trace_batch` stays separate
|
||||
// so tracing can still emit the logical `PutEntity()` batch even when the
|
||||
// applied batch is materialized later inside `WriteImpl()`.
|
||||
Status WritePreprocessedPutEntityBatch(
|
||||
const WriteOptions& write_options, WriteBatch* batch,
|
||||
WriteBatch* trace_batch,
|
||||
const DeferredPutEntityBatch* deferred_put_entities = nullptr);
|
||||
// Flushes or syncs all blob direct-write managers touched by the current
|
||||
// transformed write before the write can proceed.
|
||||
Status SyncBlobDirectWriteManagers(
|
||||
const WriteOptions& write_options,
|
||||
const BlobDirectWriteContext& blob_direct_write_ctx);
|
||||
|
||||
Status PipelinedWriteImpl(const WriteOptions& options, WriteBatch* updates,
|
||||
WriteBatch* trace_batch,
|
||||
WriteCallback* callback = nullptr,
|
||||
UserWriteCallback* user_write_cb = nullptr,
|
||||
uint64_t* wal_used = nullptr, uint64_t log_ref = 0,
|
||||
@@ -1620,7 +1761,7 @@ class DBImpl : public DB {
|
||||
// marks start of a new sub-batch.
|
||||
Status WriteImplWALOnly(
|
||||
WriteThread* write_thread, const WriteOptions& options,
|
||||
WriteBatch* updates, WriteCallback* callback,
|
||||
WriteBatch* updates, WriteBatch* trace_batch, WriteCallback* callback,
|
||||
UserWriteCallback* user_write_cb, uint64_t* wal_used,
|
||||
const uint64_t log_ref, uint64_t* seq_used, const size_t sub_batch_cnt,
|
||||
PreReleaseCallback* pre_release_callback, const AssignOrder assign_order,
|
||||
@@ -1700,6 +1841,9 @@ class DBImpl : public DB {
|
||||
Status FailIfCfHasTs(const ColumnFamilyHandle* column_family) const;
|
||||
Status FailIfTsMismatchCf(ColumnFamilyHandle* column_family,
|
||||
const Slice& ts) const;
|
||||
Status FailIfTableFilterWithRangeConversion(
|
||||
const ReadOptions& read_options,
|
||||
const MutableCFOptions& mutable_cf_options) const;
|
||||
|
||||
// Check that the read timestamp `ts` is at or above the `full_history_ts_low`
|
||||
// timestamp in a `SuperVersion`. It's necessary to do this check after
|
||||
@@ -1730,6 +1874,11 @@ class DBImpl : public DB {
|
||||
// Background work function for async file opening.
|
||||
static void BGWorkAsyncFileOpen(void* arg);
|
||||
|
||||
// Block the until any in-flight async file open work has
|
||||
// completed. No-op when open_files_async is false. Returns early if
|
||||
// shutdown begins.
|
||||
void WaitForAsyncFileOpen();
|
||||
|
||||
void InvokeWalFilterIfNeededOnColumnFamilyToWalNumberMap();
|
||||
|
||||
// Return true to proceed with current WAL record whose content is stored in
|
||||
@@ -1745,6 +1894,7 @@ class DBImpl : public DB {
|
||||
|
||||
private:
|
||||
friend class DB;
|
||||
friend class DBImplReadOnly;
|
||||
friend class DBImplSecondary;
|
||||
friend class ErrorHandler;
|
||||
friend class InternalStats;
|
||||
@@ -1778,6 +1928,21 @@ class DBImpl : public DB {
|
||||
friend class CompactionServiceTest_PreservedOptionsRemoteCompaction_Test;
|
||||
#endif
|
||||
|
||||
// Same as HasAnyBlobDirectWriteColumnFamily(), but requires `mutex_` held.
|
||||
bool HasAnyBlobDirectWriteColumnFamilyWithLockHeld();
|
||||
// Returns true if any BDW column family still owns blob files that have not
|
||||
// yet been made visible through MANIFEST. Requires `mutex_` held.
|
||||
bool HasInFlightBlobDirectWriteFilesWithLockHeld();
|
||||
// Creates and attaches the per-CF blob direct-write partition manager when
|
||||
// the column family is opened with the feature enabled.
|
||||
void MaybeInitBlobDirectWriteColumnFamily(
|
||||
ColumnFamilyData* cfd, const ColumnFamilyOptions& cf_options,
|
||||
const std::string& column_family_name);
|
||||
// Increments the DB-level count of live blob direct-write column families.
|
||||
void RegisterBlobDirectWriteColumnFamily();
|
||||
// Decrements the DB-level count of live blob direct-write column families.
|
||||
void UnregisterBlobDirectWriteColumnFamily();
|
||||
|
||||
struct CompactionState;
|
||||
struct PrepickedCompaction;
|
||||
struct PurgeFileInfo;
|
||||
@@ -2053,6 +2218,10 @@ class DBImpl : public DB {
|
||||
void DeleteObsoleteFileImpl(int job_id, const std::string& fname,
|
||||
const std::string& path_to_sync, FileType type,
|
||||
uint64_t number);
|
||||
// Returns true when a blob file must be preserved because it is still
|
||||
// tracked by blob direct write or is still footer-less on disk.
|
||||
bool ShouldKeepBlobFileDuringPurge(uint64_t number,
|
||||
const std::string& blob_file_path);
|
||||
|
||||
// Background process needs to call
|
||||
// auto x = CaptureCurrentFileNumberInPendingOutputs()
|
||||
@@ -2424,10 +2593,15 @@ class DBImpl : public DB {
|
||||
// Helper function to perform trivial move by updating manifest metadata
|
||||
// without rewriting data files. This is called when IsTrivialMove() is true.
|
||||
// REQUIRES: mutex held
|
||||
// Returns: Status of the trivial move operation
|
||||
Status PerformTrivialMove(Compaction& c, LogBuffer* log_buffer,
|
||||
bool& compaction_released, size_t& moved_files,
|
||||
size_t& moved_bytes);
|
||||
// Populates the Compaction's edit with DeleteFile/AddFile entries for
|
||||
// a trivial move (no data rewriting). Does not commit the edit.
|
||||
void PrepareTrivialMoveEdit(Compaction& c, LogBuffer* log_buffer,
|
||||
size_t& moved_files, size_t& moved_bytes);
|
||||
|
||||
// REQUIRES: mutex held, PrepareTrivialMoveEdit already called
|
||||
// Commits the trivial move by calling LogAndApply on the prepared edit.
|
||||
// Returns: Status of the manifest write
|
||||
Status CommitTrivialMove(Compaction& c, bool& compaction_released);
|
||||
|
||||
// REQUIRES: mutex unlocked
|
||||
void TrackOrUntrackFiles(const std::vector<std::string>& existing_data_files,
|
||||
@@ -2600,6 +2774,8 @@ class DBImpl : public DB {
|
||||
|
||||
Status MaybeReleaseTimestampedSnapshotsAndCheck();
|
||||
|
||||
Status MaybeWriteWalMarkersToManifestOnClose();
|
||||
|
||||
Status CloseHelper();
|
||||
|
||||
void WaitForBackgroundWork();
|
||||
@@ -2658,7 +2834,68 @@ class DBImpl : public DB {
|
||||
size_t GetWalPreallocateBlockSize(uint64_t write_buffer_size) const;
|
||||
Env::WriteLifeTimeHint CalculateWALWriteHint() { return Env::WLTH_SHORT; }
|
||||
|
||||
IOStatus CreateWAL(const WriteOptions& write_options, uint64_t log_file_num,
|
||||
// Returns true when async WAL precreation is enabled and compatible with the
|
||||
// active WAL strategy. WAL recycling already avoids file creation latency, so
|
||||
// precreation is disabled when recycle_log_file_num is non-zero.
|
||||
bool AsyncWALPrecreateEnabled() const;
|
||||
|
||||
// A WAL file that has a reserved file number and may have an opened writer,
|
||||
// but has not been added to DBImpl's in-memory logical WAL tracking lists
|
||||
// (logs_ and alive_wal_files_).
|
||||
struct UnpublishedWAL {
|
||||
uint64_t log_number = 0;
|
||||
std::unique_ptr<log::Writer> writer;
|
||||
|
||||
UnpublishedWAL() = default;
|
||||
UnpublishedWAL(const UnpublishedWAL&) = delete;
|
||||
UnpublishedWAL& operator=(const UnpublishedWAL&) = delete;
|
||||
|
||||
UnpublishedWAL(UnpublishedWAL&& other) noexcept {
|
||||
*this = std::move(other);
|
||||
}
|
||||
UnpublishedWAL& operator=(UnpublishedWAL&& other) noexcept {
|
||||
if (this != &other) {
|
||||
log_number = other.log_number;
|
||||
writer = std::move(other.writer);
|
||||
other.Reset();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
log_number = 0;
|
||||
writer.reset();
|
||||
}
|
||||
};
|
||||
|
||||
// Reserves the next WAL file number and schedules a HIGH-priority background
|
||||
// task to precreate that WAL file. A precreated WAL is not a logical WAL
|
||||
// until a foreground WAL rotation consumes it.
|
||||
void MaybeScheduleAsyncWALPrecreate(size_t preallocate_block_size);
|
||||
|
||||
// Background task for opening the reserved future WAL and publishing the
|
||||
// result under mutex_.
|
||||
static void BGWorkAsyncWALPrecreate(void* arg);
|
||||
|
||||
// Waits for an in-flight async WAL precreation and returns a prepared WAL if
|
||||
// one is available. If precreation failed, returns an empty WAL and lets the
|
||||
// foreground rotation create the WAL synchronously. Caller must hold mutex_.
|
||||
UnpublishedWAL WaitForAsyncWALPrecreate();
|
||||
|
||||
// Opens and preallocates a WAL writer without writing logical WAL records.
|
||||
// Used by async WAL precreation and by synchronous WAL creation.
|
||||
IOStatus CreateWALWriter(const DBOptions& db_options, uint64_t log_file_num,
|
||||
uint64_t recycle_log_number,
|
||||
size_t preallocate_block_size,
|
||||
UnpublishedWAL* new_wal);
|
||||
|
||||
// Starts an opened WAL file by writing the initial records required before it
|
||||
// can be installed as the current WAL for foreground writes.
|
||||
IOStatus StartWALFile(const WriteOptions& write_options,
|
||||
const PredecessorWALInfo& predecessor_wal_info,
|
||||
log::Writer* new_log);
|
||||
IOStatus CreateWAL(const DBOptions& db_options,
|
||||
const WriteOptions& write_options, uint64_t log_file_num,
|
||||
uint64_t recycle_log_number, size_t preallocate_block_size,
|
||||
const PredecessorWALInfo& predecessor_wal_info,
|
||||
log::Writer** new_log);
|
||||
@@ -2777,6 +3014,53 @@ class DBImpl : public DB {
|
||||
std::string ts_low);
|
||||
|
||||
bool ShouldReferenceSuperVersion(const MergeContext& merge_context);
|
||||
// Resolves a plain value whose current payload is an encoded direct-write
|
||||
// blob index, either through `value` directly or through the default-column
|
||||
// view layered on `columns`.
|
||||
static Status ResolveDirectWritePlainValue(const ReadOptions& read_options,
|
||||
const Slice& key,
|
||||
const Version* current,
|
||||
ColumnFamilyData* cfd,
|
||||
PinnableSlice* value,
|
||||
PinnableWideColumns* columns);
|
||||
// Resolves each unresolved direct-write blob-valued column in `columns` and
|
||||
// rebuilds the serialized wide-column entity in place.
|
||||
static Status ResolveDirectWriteWideColumns(const ReadOptions& read_options,
|
||||
const Slice& key,
|
||||
const Version* current,
|
||||
ColumnFamilyData* cfd,
|
||||
PinnableWideColumns* columns);
|
||||
// Dispatches between plain-value and wide-column direct-write resolution for
|
||||
// read results observed before flush makes the corresponding blob file
|
||||
// visible through normal Version metadata.
|
||||
static bool MaybeResolveDirectWriteValue(
|
||||
const ReadOptions& read_options, const Slice& key,
|
||||
bool resolve_direct_write_value, const Version* current,
|
||||
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index, bool* value_found = nullptr);
|
||||
// Resolves memtable read results that still carry blob references through
|
||||
// either a raw blob-index payload in `value` or unresolved blob columns in
|
||||
// `columns`. Unlike the direct-write helper above, this path only depends on
|
||||
// a BlobFetcher and therefore works for read-only/secondary DBs.
|
||||
static bool MaybeResolveMemtableBlobValue(const Slice& key,
|
||||
const BlobFetcher* blob_fetcher,
|
||||
PinnableSlice* value,
|
||||
PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index,
|
||||
bool* value_found = nullptr);
|
||||
// Completes read-only/secondary memtable Get()/GetEntity() hits by resolving
|
||||
// blob-backed payloads when `resolve_blob_backed_memtable_value` is true,
|
||||
// pinning plain values on success, and clearing outputs on error. When the
|
||||
// caller explicitly requested raw blob indices via
|
||||
// `GetImplOptions::is_blob_index`, this helper leaves that payload
|
||||
// untouched. `memtable_blob_fetcher` may be null when blob support is
|
||||
// disabled for the column family.
|
||||
static void PostprocessMemtableValueRead(
|
||||
const Slice& key, const std::string* timestamp,
|
||||
bool resolve_blob_backed_memtable_value,
|
||||
const BlobFetcher* memtable_blob_fetcher, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
|
||||
bool* value_found = nullptr);
|
||||
|
||||
template <typename IterType, typename ImplType,
|
||||
typename ErrorIteratorFuncType>
|
||||
@@ -3107,6 +3391,28 @@ class DBImpl : public DB {
|
||||
AsyncFileOpenState bg_async_file_open_state_ =
|
||||
AsyncFileOpenState::kNotScheduled;
|
||||
|
||||
// State machine for the single async WAL precreation slot protected by
|
||||
// mutex_. Background precreation failure returns to kNotScheduled; foreground
|
||||
// rotation handles it the same as no prepared WAL and creates one
|
||||
// synchronously. kScheduled owns a reserved file number; kReady owns an
|
||||
// opened writer that has not been started or added to logical WAL tracking.
|
||||
enum class AsyncWALPrecreateState : uint8_t {
|
||||
kNotScheduled = 0, // No WAL precreate work is in-flight or ready.
|
||||
kScheduled, // Background task owns creation of the reserved WAL.
|
||||
kReady, // Reserved WAL writer is open but not logically live.
|
||||
};
|
||||
|
||||
// Protected by mutex_. Tracks at most one background precreated WAL. A
|
||||
// precreated WAL is only reserved empty storage until SwitchMemtable()
|
||||
// consumes it and installs it in DBImpl's in-memory logical WAL tracking
|
||||
// lists (logs_ and alive_wal_files_).
|
||||
AsyncWALPrecreateState async_wal_precreate_state_ =
|
||||
AsyncWALPrecreateState::kNotScheduled;
|
||||
|
||||
// Reserved in-flight/ready precreated WAL. The writer is populated only while
|
||||
// state is kReady.
|
||||
UnpublishedWAL async_wal_precreate_wal_;
|
||||
|
||||
std::deque<ManualCompactionState*> manual_compaction_dequeue_;
|
||||
|
||||
// shall we disable deletion of obsolete files
|
||||
@@ -3138,6 +3444,11 @@ class DBImpl : public DB {
|
||||
// Used when disableWAL is true.
|
||||
std::atomic<bool> has_unpersisted_data_{false};
|
||||
|
||||
// Number of live column families with an active blob direct write partition
|
||||
// manager. This keeps the read/write fast paths at O(1) when the feature is
|
||||
// completely disabled for the DB.
|
||||
std::atomic<uint32_t> blob_direct_write_cf_count_{0};
|
||||
|
||||
// if an attempt was made to flush all column families that
|
||||
// the oldest log depends on but uncommitted data in the oldest
|
||||
// log prevents the log from being released.
|
||||
@@ -3176,6 +3487,13 @@ class DBImpl : public DB {
|
||||
// Callback for compaction to check if a key is visible to a snapshot.
|
||||
// REQUIRES: mutex held
|
||||
std::unique_ptr<SnapshotChecker> snapshot_checker_;
|
||||
// When set, InitSnapshotContext() appends GetLastPublishedSequence() to the
|
||||
// job's snapshot_seqs (if not already present) so that flush/compaction
|
||||
// preserves the published-sequence boundary even when no explicit user
|
||||
// snapshot exists there. Unlike taking a real ManagedSnapshot, this just
|
||||
// pins the seqno into the snapshot list: no allocation, no snapshot list
|
||||
// mutation, and no SnapshotChecker.
|
||||
bool track_published_seq_in_snapshot_context_ = false;
|
||||
|
||||
// Callback for when the cached_recoverable_state_ is written to memtable
|
||||
// Only to be set during initialization
|
||||
@@ -3366,6 +3684,18 @@ inline Status DBImpl::FailIfTsMismatchCf(ColumnFamilyHandle* column_family,
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
inline Status DBImpl::FailIfTableFilterWithRangeConversion(
|
||||
const ReadOptions& read_options,
|
||||
const MutableCFOptions& mutable_cf_options) const {
|
||||
if (read_options.table_filter &&
|
||||
mutable_cf_options.min_tombstones_for_range_conversion > 0) {
|
||||
return Status::InvalidArgument(
|
||||
"ReadOptions::table_filter is not supported when "
|
||||
"min_tombstones_for_range_conversion > 0");
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
inline Status DBImpl::FailIfReadCollapsedHistory(const ColumnFamilyData* cfd,
|
||||
const SuperVersion* sv,
|
||||
const Slice& ts) const {
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
#include <cinttypes>
|
||||
#include <deque>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "db/blob/blob_file_partition_manager.h"
|
||||
#include "db/builder.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/error_handler.h"
|
||||
@@ -215,7 +217,8 @@ Status DBImpl::FlushMemTableToOutputFile(
|
||||
&event_logger_, mutable_cf_options.report_bg_io_stats,
|
||||
true /* sync_output_directory */, true /* write_manifest */, thread_pri,
|
||||
io_tracer_, cfd->GetSuperVersion()->ShareSeqnoToTimeMapping(), db_id_,
|
||||
db_session_id_, cfd->GetFullHistoryTsLow(), &blob_callback_);
|
||||
db_session_id_, cfd->GetFullHistoryTsLow(), &blob_callback_,
|
||||
mutable_db_options_.fast_sst_open);
|
||||
FileMetaData file_meta;
|
||||
|
||||
Status s;
|
||||
@@ -279,12 +282,49 @@ Status DBImpl::FlushMemTableToOutputFile(
|
||||
flush_reason);
|
||||
|
||||
bool switched_to_mempurge = false;
|
||||
size_t prepared_blob_generations = 0;
|
||||
std::vector<uint64_t> sealed_blob_numbers;
|
||||
// Within flush_job.Run, rocksdb may call event listener to notify
|
||||
// file creation and deletion.
|
||||
//
|
||||
// Note that flush_job.Run will unlock and lock the db_mutex,
|
||||
// and EventListener callback will be called when the db_mutex
|
||||
// is unlocked by the current thread.
|
||||
if (s.ok()) {
|
||||
auto mgr_handle = cfd->blob_partition_manager_handle();
|
||||
auto* mgr = mgr_handle.get();
|
||||
if (mgr != nullptr) {
|
||||
prepared_blob_generations = flush_job.GetMemTables().size();
|
||||
std::vector<BlobFileAddition> write_path_additions;
|
||||
std::vector<BlobFileGarbage> write_path_garbages;
|
||||
std::vector<std::vector<uint64_t>> generation_blob_file_numbers;
|
||||
mutex_.Unlock();
|
||||
s = mgr->PrepareFlushAdditions(
|
||||
write_options, prepared_blob_generations, &write_path_additions,
|
||||
&write_path_garbages, &generation_blob_file_numbers);
|
||||
mutex_.Lock();
|
||||
if (!s.ok()) {
|
||||
prepared_blob_generations = 0;
|
||||
} else if (!write_path_additions.empty()) {
|
||||
const auto& memtables = flush_job.GetMemTables();
|
||||
assert(generation_blob_file_numbers.size() == memtables.size());
|
||||
for (size_t i = 0; i < generation_blob_file_numbers.size(); ++i) {
|
||||
// Old SuperVersions can keep these memtables alive after the flush
|
||||
// commits, so keep their sealed direct-write blob files protected
|
||||
// from obsolete-file purge until the memtable is finally released.
|
||||
memtables[i]->ProtectSealedBlobFiles(mgr_handle,
|
||||
generation_blob_file_numbers[i]);
|
||||
}
|
||||
for (const auto& addition : write_path_additions) {
|
||||
sealed_blob_numbers.push_back(addition.GetBlobFileNumber());
|
||||
}
|
||||
flush_job.AddExternalBlobFileAdditions(std::move(write_path_additions));
|
||||
flush_job.AddExternalBlobFileGarbages(std::move(write_path_garbages));
|
||||
} else if (!write_path_garbages.empty()) {
|
||||
flush_job.AddExternalBlobFileGarbages(std::move(write_path_garbages));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
s = flush_job.Run(&logs_with_prep_tracker_, &file_meta,
|
||||
&switched_to_mempurge, &skip_set_bg_error,
|
||||
@@ -292,10 +332,33 @@ Status DBImpl::FlushMemTableToOutputFile(
|
||||
need_cancel = false;
|
||||
}
|
||||
|
||||
if (cfd->blob_partition_manager() != nullptr &&
|
||||
prepared_blob_generations > 0) {
|
||||
auto unconsumed_additions = flush_job.TakeExternalBlobFileAdditions();
|
||||
auto unconsumed_garbages = flush_job.TakeExternalBlobFileGarbages();
|
||||
if (!s.ok() || !unconsumed_additions.empty() ||
|
||||
!unconsumed_garbages.empty()) {
|
||||
// Skip committing this attempt, but keep the prepared generations queued
|
||||
// in pending_generations_. The same immutable memtables still reference
|
||||
// these sealed blob files, so a later flush retry must publish the exact
|
||||
// same additions rather than abandoning them here.
|
||||
prepared_blob_generations = 0;
|
||||
sealed_blob_numbers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.ok() && need_cancel) {
|
||||
flush_job.Cancel();
|
||||
}
|
||||
|
||||
if (s.ok() && prepared_blob_generations > 0) {
|
||||
auto* mgr = cfd->blob_partition_manager();
|
||||
mgr->CommitPreparedGenerations(prepared_blob_generations);
|
||||
if (!sealed_blob_numbers.empty()) {
|
||||
mgr->RemoveFilePartitionMappings(sealed_blob_numbers);
|
||||
}
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
InstallSuperVersionAndScheduleWork(cfd, superversion_context);
|
||||
if (made_progress) {
|
||||
@@ -486,7 +549,8 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
|
||||
false /* sync_output_directory */, false /* write_manifest */,
|
||||
thread_pri, io_tracer_,
|
||||
cfd->GetSuperVersion()->ShareSeqnoToTimeMapping(), db_id_,
|
||||
db_session_id_, cfd->GetFullHistoryTsLow(), &blob_callback_));
|
||||
db_session_id_, cfd->GetFullHistoryTsLow(), &blob_callback_,
|
||||
mutable_db_options_.fast_sst_open));
|
||||
}
|
||||
|
||||
std::vector<FileMetaData> file_meta(num_cfs);
|
||||
@@ -535,11 +599,13 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
|
||||
// <bool /* executed */, Status /* status code */>
|
||||
autovector<std::pair<bool, Status>> exec_status;
|
||||
std::vector<bool> pick_status;
|
||||
std::vector<size_t> prepared_blob_generations(num_cfs, 0);
|
||||
for (int i = 0; i != num_cfs; ++i) {
|
||||
// Initially all jobs are not executed, with status OK.
|
||||
exec_status.emplace_back(false, Status::OK());
|
||||
pick_status.push_back(false);
|
||||
}
|
||||
std::unordered_map<int, std::vector<uint64_t>> sealed_blob_numbers_by_cf;
|
||||
|
||||
bool flush_for_recovery =
|
||||
bg_flush_args[0].flush_reason_ == FlushReason::kErrorRecovery ||
|
||||
@@ -563,6 +629,46 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
|
||||
}
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
for (int i = 0; i != num_cfs; ++i) {
|
||||
auto mgr_handle = cfds[i]->blob_partition_manager_handle();
|
||||
auto* mgr = mgr_handle.get();
|
||||
if (mgr == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
prepared_blob_generations[i] = jobs[i]->GetMemTables().size();
|
||||
std::vector<BlobFileAddition> write_path_additions;
|
||||
std::vector<BlobFileGarbage> write_path_garbages;
|
||||
std::vector<std::vector<uint64_t>> generation_blob_file_numbers;
|
||||
mutex_.Unlock();
|
||||
s = mgr->PrepareFlushAdditions(
|
||||
write_options, prepared_blob_generations[i], &write_path_additions,
|
||||
&write_path_garbages, &generation_blob_file_numbers);
|
||||
mutex_.Lock();
|
||||
if (!s.ok()) {
|
||||
prepared_blob_generations[i] = 0;
|
||||
break;
|
||||
}
|
||||
if (!write_path_additions.empty()) {
|
||||
const auto& memtables = jobs[i]->GetMemTables();
|
||||
assert(generation_blob_file_numbers.size() == memtables.size());
|
||||
for (size_t j = 0; j < generation_blob_file_numbers.size(); ++j) {
|
||||
memtables[j]->ProtectSealedBlobFiles(mgr_handle,
|
||||
generation_blob_file_numbers[j]);
|
||||
}
|
||||
auto& sealed_blob_numbers = sealed_blob_numbers_by_cf[i];
|
||||
for (const auto& addition : write_path_additions) {
|
||||
sealed_blob_numbers.push_back(addition.GetBlobFileNumber());
|
||||
}
|
||||
jobs[i]->AddExternalBlobFileAdditions(std::move(write_path_additions));
|
||||
}
|
||||
if (!write_path_garbages.empty()) {
|
||||
jobs[i]->AddExternalBlobFileGarbages(std::move(write_path_garbages));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
assert(switched_to_mempurge.size() ==
|
||||
static_cast<long unsigned int>(num_cfs));
|
||||
@@ -768,6 +874,48 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
|
||||
directories_.GetDbDir(), log_buffer);
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
// If the atomic flush's combined MANIFEST write failed, the output files
|
||||
// are cached in the table cache (added by BuildTable during FlushJob::Run)
|
||||
// but never installed into any Version. Evict them now so that
|
||||
// TEST_VerifyNoObsoleteFilesCached does not fire during Close() when the
|
||||
// FindObsoleteFiles full-scan backstop is disabled by
|
||||
// metadata_read_fault_one_in.
|
||||
for (int i = 0; i != num_cfs; ++i) {
|
||||
if (exec_status[i].first && exec_status[i].second.ok() &&
|
||||
!switched_to_mempurge[i] && file_meta[i].fd.GetFileSize() > 0) {
|
||||
TableCache::ReleaseObsolete(
|
||||
cfds[i]->table_cache()->get_cache().get(),
|
||||
file_meta[i].fd.GetNumber(), nullptr /*handle*/,
|
||||
all_mutable_cf_options[i].uncache_aggressiveness);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i != num_cfs; ++i) {
|
||||
auto* mgr = cfds[i]->blob_partition_manager();
|
||||
if (mgr == nullptr || prepared_blob_generations[i] == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto unconsumed_additions = jobs[i]->TakeExternalBlobFileAdditions();
|
||||
auto unconsumed_garbages = jobs[i]->TakeExternalBlobFileGarbages();
|
||||
if (!s.ok() || !unconsumed_additions.empty() ||
|
||||
!unconsumed_garbages.empty()) {
|
||||
// Same retry semantics as the single-CF flush path above: do not
|
||||
// consume these prepared generations yet, because the rolled-back
|
||||
// memtables still need to reuse the same sealed blob files on retry.
|
||||
prepared_blob_generations[i] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
mgr->CommitPreparedGenerations(prepared_blob_generations[i]);
|
||||
auto it = sealed_blob_numbers_by_cf.find(i);
|
||||
if (it != sealed_blob_numbers_by_cf.end() && !it->second.empty()) {
|
||||
mgr->RemoveFilePartitionMappings(it->second);
|
||||
}
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
assert(num_cfs ==
|
||||
static_cast<int>(job_context->superversion_contexts.size()));
|
||||
@@ -1433,9 +1581,8 @@ Status DBImpl::CompactFiles(const CompactionOptions& compact_options,
|
||||
return s;
|
||||
}
|
||||
|
||||
Status DBImpl::PerformTrivialMove(Compaction& c, LogBuffer* log_buffer,
|
||||
bool& compaction_released,
|
||||
size_t& moved_files, size_t& moved_bytes) {
|
||||
void DBImpl::PrepareTrivialMoveEdit(Compaction& c, LogBuffer* log_buffer,
|
||||
size_t& moved_files, size_t& moved_bytes) {
|
||||
mutex_.AssertHeld();
|
||||
|
||||
ROCKS_LOG_BUFFER(log_buffer, "[%s] Moving %d files to level-%d\n",
|
||||
@@ -1468,6 +1615,10 @@ Status DBImpl::PerformTrivialMove(Compaction& c, LogBuffer* log_buffer,
|
||||
}
|
||||
moved_files += c.num_input_files(l);
|
||||
}
|
||||
}
|
||||
|
||||
Status DBImpl::CommitTrivialMove(Compaction& c, bool& compaction_released) {
|
||||
mutex_.AssertHeld();
|
||||
|
||||
// Install the new version
|
||||
const ReadOptions read_options(Env::IOActivity::kCompaction);
|
||||
@@ -1592,8 +1743,8 @@ Status DBImpl::CompactFilesImpl(
|
||||
bool compaction_released = false;
|
||||
size_t moved_files = 0;
|
||||
size_t moved_bytes = 0;
|
||||
Status status = PerformTrivialMove(
|
||||
*c.get(), log_buffer, compaction_released, moved_files, moved_bytes);
|
||||
PrepareTrivialMoveEdit(*c.get(), log_buffer, moved_files, moved_bytes);
|
||||
Status status = CommitTrivialMove(*c.get(), compaction_released);
|
||||
|
||||
if (status.ok()) {
|
||||
InstallSuperVersionAndScheduleWork(
|
||||
@@ -1861,6 +2012,46 @@ void DBImpl::NotifyOnCompactionCompleted(
|
||||
// flush process.
|
||||
}
|
||||
|
||||
void DBImpl::NotifyOnCompactionPreCommit(
|
||||
ColumnFamilyData* cfd, Compaction* c, const Status& st,
|
||||
const CompactionJobStats& compaction_job_stats, const int job_id) {
|
||||
if (immutable_db_options_.listeners.size() == 0U) {
|
||||
return;
|
||||
}
|
||||
mutex_.AssertHeld();
|
||||
if (shutting_down_.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only fire if OnCompactionBegin has fired for this compaction.
|
||||
if (c->ShouldNotifyOnCompactionCompleted() == false) {
|
||||
return;
|
||||
}
|
||||
// Idempotency: ensure listeners observe at most one
|
||||
// OnCompactionPreCommit per compaction lifecycle, even though the
|
||||
// notify helper is invoked at multiple potential ReleaseCompactionFiles()
|
||||
// sites for safety.
|
||||
if (c->WasNotifyOnCompactionPreCommitCalled()) {
|
||||
return;
|
||||
}
|
||||
c->SetNotifyOnCompactionPreCommitCalled();
|
||||
|
||||
int num_l0_files = cfd->current()->storage_info()->NumLevelFiles(0);
|
||||
// release lock while notifying events
|
||||
mutex_.Unlock();
|
||||
TEST_SYNC_POINT("DBImpl::NotifyOnCompactionPreCommit::UnlockMutex");
|
||||
{
|
||||
CompactionJobInfo info{};
|
||||
info.num_l0_files = num_l0_files;
|
||||
BuildCompactionJobInfo(cfd, c, st, compaction_job_stats, job_id, &info);
|
||||
for (const auto& listener : immutable_db_options_.listeners) {
|
||||
listener->OnCompactionPreCommit(this, info);
|
||||
}
|
||||
info.status.PermitUncheckedError();
|
||||
}
|
||||
mutex_.Lock();
|
||||
}
|
||||
|
||||
// REQUIREMENT: block all background work by calling PauseBackgroundWork()
|
||||
// before calling this function
|
||||
// TODO (hx235): Replace Status::NotSupported() with Status::Aborted() for
|
||||
@@ -3876,6 +4067,13 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
status = Status::ShutdownInProgress();
|
||||
} else if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
|
||||
status = Status::Incomplete(Status::SubCode::kCompactionAborted);
|
||||
if (!is_prepicked) {
|
||||
// This automatic compaction was scheduled from compaction_queue_, but
|
||||
// abort happened before PickCompactionFromQueue() could remove the
|
||||
// queued CF. Restore the unscheduled count so ResumeAllCompactions()
|
||||
// can schedule the still-queued work.
|
||||
unscheduled_compactions_++;
|
||||
}
|
||||
} else if (is_manual &&
|
||||
manual_compaction->canceled.load(std::memory_order_acquire)) {
|
||||
status = Status::Incomplete(Status::SubCode::kManualCompactionPaused);
|
||||
@@ -3883,10 +4081,14 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
} else {
|
||||
status = error_handler_.GetBGError();
|
||||
// If we get here, it means a hard error happened after this compaction
|
||||
// was scheduled by MaybeScheduleFlushOrCompaction(), but before it got
|
||||
// a chance to execute. Since we didn't pop a cfd from the compaction
|
||||
// queue, increment unscheduled_compactions_
|
||||
unscheduled_compactions_++;
|
||||
// was scheduled by MaybeScheduleFlushOrCompaction(), but before it got a
|
||||
// chance to execute. Since non-prepicked work consumed an unscheduled
|
||||
// compaction credit without popping a cfd from the compaction queue,
|
||||
// restore the credit here. Prepicked work was scheduled directly and did
|
||||
// not consume unscheduled_compactions_.
|
||||
if (!is_prepicked) {
|
||||
unscheduled_compactions_++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!status.ok()) {
|
||||
@@ -4107,6 +4309,8 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
for (const auto& f : *c->inputs(0)) {
|
||||
c->edit()->DeleteFile(c->level(), f->fd.GetNumber());
|
||||
}
|
||||
NotifyOnCompactionPreCommit(c->column_family_data(), c.get(), status,
|
||||
compaction_job_stats, job_context->job_id);
|
||||
status = versions_->LogAndApply(
|
||||
c->column_family_data(), read_options, write_options, c->edit(),
|
||||
&mutex_, directories_.GetDbDir(),
|
||||
@@ -4325,6 +4529,8 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
++out_file_metadata_it;
|
||||
}
|
||||
|
||||
NotifyOnCompactionPreCommit(c->column_family_data(), c.get(), status,
|
||||
compaction_job_stats, job_context->job_id);
|
||||
status = versions_->LogAndApply(
|
||||
c->column_family_data(), read_options, write_options, c->edit(),
|
||||
&mutex_, directories_.GetDbDir(),
|
||||
@@ -4401,8 +4607,12 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
// Perform the trivial move
|
||||
size_t moved_files = 0;
|
||||
size_t moved_bytes = 0;
|
||||
status = PerformTrivialMove(*c.get(), log_buffer, compaction_released,
|
||||
moved_files, moved_bytes);
|
||||
// Populate the edit first so that CompactionJobInfo has output file
|
||||
// info when OnCompactionPreCommit fires.
|
||||
PrepareTrivialMoveEdit(*c.get(), log_buffer, moved_files, moved_bytes);
|
||||
NotifyOnCompactionPreCommit(c->column_family_data(), c.get(), status,
|
||||
compaction_job_stats, job_context->job_id);
|
||||
status = CommitTrivialMove(*c.get(), compaction_released);
|
||||
io_s = versions_->io_status();
|
||||
InstallSuperVersionAndScheduleWork(
|
||||
c->column_family_data(), job_context->superversion_contexts.data());
|
||||
@@ -4525,6 +4735,10 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
ReleaseOptionsFileNumber(min_options_file_number_elem);
|
||||
}
|
||||
|
||||
// Fire OnCompactionPreCommit before compaction_job.Install runs
|
||||
// ReleaseCompactionFiles in its manifest write callback.
|
||||
NotifyOnCompactionPreCommit(c->column_family_data(), c.get(), status,
|
||||
compaction_job_stats, job_context->job_id);
|
||||
status = compaction_job.Install(&compaction_released);
|
||||
io_s = compaction_job.io_status();
|
||||
if (status.ok()) {
|
||||
@@ -4544,6 +4758,14 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
|
||||
if (c != nullptr) {
|
||||
if (!compaction_released) {
|
||||
// Safety net: if any of the paths above did not fire the
|
||||
// OnCompactionPreCommit notification (e.g. status was not OK and
|
||||
// an early-out skipped the LogAndApply / Install), still fire the
|
||||
// notify here while files are about to be released. The notify is
|
||||
// idempotent and is a no-op if it has already fired (or if Begin never
|
||||
// fired).
|
||||
NotifyOnCompactionPreCommit(c->column_family_data(), c.get(), status,
|
||||
compaction_job_stats, job_context->job_id);
|
||||
c->ReleaseCompactionFiles(status);
|
||||
} else {
|
||||
#ifndef NDEBUG
|
||||
@@ -4984,6 +5206,11 @@ void DBImpl::MarkAsGrabbedForPurge(uint64_t file_number) {
|
||||
files_grabbed_for_purge_.insert(file_number);
|
||||
}
|
||||
|
||||
void DBImpl::EnableTrackPublishedSeqInSnapshotContext() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
track_published_seq_in_snapshot_context_ = true;
|
||||
}
|
||||
|
||||
void DBImpl::SetSnapshotChecker(SnapshotChecker* snapshot_checker) {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
// snapshot_checker_ should only set once. If we need to set it multiple
|
||||
@@ -5017,6 +5244,19 @@ void DBImpl::InitSnapshotContext(JobContext* job_context) {
|
||||
SequenceNumber earliest_write_conflict_snapshot = kMaxSequenceNumber;
|
||||
std::vector<SequenceNumber> snapshot_seqs =
|
||||
snapshots_.GetAll(&earliest_write_conflict_snapshot);
|
||||
|
||||
// The SnapshotChecker path above already adds a managed snapshot to
|
||||
// snapshot_seqs.
|
||||
if (track_published_seq_in_snapshot_context_ && !snapshot_checker) {
|
||||
// Pin the published-sequence boundary into snapshot_seqs without taking
|
||||
// a real snapshot. This prevents flush/compaction from collapsing a version
|
||||
// visible at the published boundary into a newer unpublished seqno.
|
||||
const SequenceNumber published_seq = GetLastPublishedSequence();
|
||||
if (snapshot_seqs.empty() || snapshot_seqs.back() < published_seq) {
|
||||
snapshot_seqs.push_back(published_seq);
|
||||
}
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::InitSnapshotContext:BeforeInit");
|
||||
job_context->InitSnapshotContext(
|
||||
snapshot_checker, std::move(managed_snapshot),
|
||||
earliest_write_conflict_snapshot, std::move(snapshot_seqs));
|
||||
@@ -5025,6 +5265,10 @@ void DBImpl::InitSnapshotContext(JobContext* job_context) {
|
||||
Status DBImpl::WaitForCompact(
|
||||
const WaitForCompactOptions& wait_for_compact_options) {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
// `close_db=true` needs the same direct-write cleanup guarantee as DB close:
|
||||
// register live blob files before relying on the DB being reopenable.
|
||||
const bool flush_for_blob_direct_write =
|
||||
HasAnyBlobDirectWriteColumnFamilyWithLockHeld();
|
||||
if (wait_for_compact_options.flush) {
|
||||
Status s = DBImpl::FlushAllColumnFamilies(FlushOptions(),
|
||||
FlushReason::kManualFlush);
|
||||
@@ -5032,8 +5276,9 @@ Status DBImpl::WaitForCompact(
|
||||
return s;
|
||||
}
|
||||
} else if (wait_for_compact_options.close_db &&
|
||||
has_unpersisted_data_.load(std::memory_order_relaxed) &&
|
||||
!mutable_db_options_.avoid_flush_during_shutdown) {
|
||||
(flush_for_blob_direct_write ||
|
||||
(has_unpersisted_data_.load(std::memory_order_relaxed) &&
|
||||
!mutable_db_options_.avoid_flush_during_shutdown))) {
|
||||
Status s =
|
||||
DBImpl::FlushAllColumnFamilies(FlushOptions(), FlushReason::kShutDown);
|
||||
if (!s.ok()) {
|
||||
|
||||
@@ -8,14 +8,17 @@
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
#ifndef NDEBUG
|
||||
#include <iostream>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "db/blob/blob_file_cache.h"
|
||||
#include "db/blob/blob_file_partition_manager.h"
|
||||
#include "db/column_family.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/error_handler.h"
|
||||
#include "db/periodic_task_scheduler.h"
|
||||
#include "monitoring/thread_status_updater.h"
|
||||
#include "util/cast_util.h"
|
||||
#include "util/hash_containers.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
uint64_t DBImpl::TEST_GetLevel0TotalSize() {
|
||||
@@ -357,11 +360,15 @@ void DBImpl::TEST_VerifyNoObsoleteFilesCached(
|
||||
}
|
||||
|
||||
// Live and "quarantined" files are allowed to be open in table cache
|
||||
std::set<uint64_t> live_and_quar_files;
|
||||
UnorderedSet<uint64_t> live_and_quar_files;
|
||||
for (auto cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (cfd->IsDropped()) {
|
||||
continue;
|
||||
}
|
||||
if (auto* mgr = cfd->blob_partition_manager(); mgr != nullptr) {
|
||||
mgr->GetActiveBlobFileNumbers(&live_and_quar_files);
|
||||
mgr->GetProtectedBlobFileNumbers(&live_and_quar_files);
|
||||
}
|
||||
// Iterate over live versions
|
||||
Version* current = cfd->current();
|
||||
Version* ver = current;
|
||||
|
||||
+164
-14
@@ -6,10 +6,13 @@
|
||||
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
#include <set>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "db/blob/blob_file_partition_manager.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/event_helpers.h"
|
||||
#include "db/memtable_list.h"
|
||||
@@ -17,6 +20,7 @@
|
||||
#include "file/filename.h"
|
||||
#include "file/sst_file_manager_impl.h"
|
||||
#include "logging/logging.h"
|
||||
#include "monitoring/thread_status_util.h"
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "util/autovector.h"
|
||||
@@ -24,6 +28,91 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace {
|
||||
|
||||
Env::IOActivity GetCurrentThreadIOActivityForMetadataRead() {
|
||||
switch (ThreadStatusUtil::GetThreadOperation()) {
|
||||
case ThreadStatus::OperationType::OP_FLUSH:
|
||||
return Env::IOActivity::kFlush;
|
||||
case ThreadStatus::OperationType::OP_COMPACTION:
|
||||
return Env::IOActivity::kCompaction;
|
||||
case ThreadStatus::OperationType::OP_DBOPEN:
|
||||
return Env::IOActivity::kDBOpen;
|
||||
case ThreadStatus::OperationType::OP_GET:
|
||||
return Env::IOActivity::kGet;
|
||||
case ThreadStatus::OperationType::OP_MULTIGET:
|
||||
return Env::IOActivity::kMultiGet;
|
||||
case ThreadStatus::OperationType::OP_DBITERATOR:
|
||||
return Env::IOActivity::kDBIterator;
|
||||
case ThreadStatus::OperationType::OP_VERIFY_DB_CHECKSUM:
|
||||
return Env::IOActivity::kVerifyDBChecksum;
|
||||
case ThreadStatus::OperationType::OP_VERIFY_FILE_CHECKSUMS:
|
||||
return Env::IOActivity::kVerifyFileChecksums;
|
||||
case ThreadStatus::OperationType::OP_GETENTITY:
|
||||
return Env::IOActivity::kGetEntity;
|
||||
case ThreadStatus::OperationType::OP_MULTIGETENTITY:
|
||||
return Env::IOActivity::kMultiGetEntity;
|
||||
case ThreadStatus::OperationType::
|
||||
OP_GET_FILE_CHECKSUMS_FROM_CURRENT_MANIFEST:
|
||||
return Env::IOActivity::kGetFileChecksumsFromCurrentManifest;
|
||||
default:
|
||||
return Env::IOActivity::kUnknown;
|
||||
}
|
||||
}
|
||||
|
||||
// A full-scan obsolete-file purge can observe a newly created direct-write
|
||||
// blob file before it is added to the manager's active-file set. Those
|
||||
// in-flight files are still missing their footer, so conservatively keep any
|
||||
// footer-less blob file here rather than risk deleting a live write-path file.
|
||||
bool ShouldKeepFooterlessBlobFile(FileSystem* fs,
|
||||
const FileOptions& file_options,
|
||||
const std::string& blob_file_path) {
|
||||
assert(fs != nullptr);
|
||||
|
||||
constexpr IODebugContext* dbg = nullptr;
|
||||
// This purge path can run from DB open, flush/compaction cleanup, or
|
||||
// iterator-triggered obsolete-file cleanup. Tag the probe with the current
|
||||
// thread activity so db_stress keeps validating the read against the active
|
||||
// operation instead of seeing an unexpected kUnknown metadata read.
|
||||
IOOptions io_options;
|
||||
io_options.io_activity = GetCurrentThreadIOActivityForMetadataRead();
|
||||
uint64_t file_size = 0;
|
||||
IOStatus io_s = fs->GetFileSize(blob_file_path, io_options, &file_size, dbg);
|
||||
if (!io_s.ok()) {
|
||||
return !io_s.IsPathNotFound();
|
||||
}
|
||||
|
||||
if (file_size < BlobLogHeader::kSize + BlobLogFooter::kSize) {
|
||||
return true;
|
||||
}
|
||||
|
||||
FileOptions read_file_options = file_options;
|
||||
read_file_options.use_direct_reads = false;
|
||||
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
io_s = fs->NewRandomAccessFile(blob_file_path, read_file_options, &file, dbg);
|
||||
if (!io_s.ok()) {
|
||||
return !io_s.IsPathNotFound();
|
||||
}
|
||||
|
||||
std::array<char, BlobLogFooter::kSize> scratch{};
|
||||
Slice footer_slice;
|
||||
io_s = file->Read(file_size - BlobLogFooter::kSize, BlobLogFooter::kSize,
|
||||
io_options, &footer_slice, scratch.data(), dbg);
|
||||
if (!io_s.ok()) {
|
||||
return !io_s.IsPathNotFound();
|
||||
}
|
||||
|
||||
if (footer_slice.size() != BlobLogFooter::kSize) {
|
||||
return true;
|
||||
}
|
||||
|
||||
BlobLogFooter footer;
|
||||
return !footer.DecodeFrom(footer_slice).ok();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint64_t DBImpl::MinLogNumberToKeep() {
|
||||
return versions_->min_log_number_to_keep();
|
||||
}
|
||||
@@ -156,6 +245,17 @@ void DBImpl::FindObsoleteFiles(JobContext* job_context, bool force,
|
||||
job_context->min_pending_output = MinObsoleteSstNumberToKeep();
|
||||
job_context->files_to_quarantine = error_handler_.GetFilesToQuarantine();
|
||||
job_context->min_options_file_number = MinOptionsFileNumberToKeep();
|
||||
job_context->min_blob_file_number_to_keep =
|
||||
versions_->current_next_file_number();
|
||||
for (auto* cfd : *versions_->GetColumnFamilySet()) {
|
||||
auto* mgr = cfd->blob_partition_manager();
|
||||
if (mgr != nullptr) {
|
||||
mgr->GetActiveBlobFileNumbers(
|
||||
&job_context->active_blob_direct_write_files);
|
||||
mgr->GetProtectedBlobFileNumbers(
|
||||
&job_context->active_blob_direct_write_files);
|
||||
}
|
||||
}
|
||||
|
||||
// Get obsolete files. This function will also update the list of
|
||||
// pending files in VersionSet().
|
||||
@@ -587,13 +687,24 @@ void DBImpl::PurgeObsoleteFiles(JobContext& state, bool schedule_only) {
|
||||
files_to_del.insert(number);
|
||||
}
|
||||
break;
|
||||
case kBlobFile:
|
||||
case kBlobFile: {
|
||||
keep = number >= state.min_pending_output ||
|
||||
(blob_live_set.find(number) != blob_live_set.end());
|
||||
number >= state.min_blob_file_number_to_keep ||
|
||||
(blob_live_set.find(number) != blob_live_set.end()) ||
|
||||
(state.active_blob_direct_write_files.find(number) !=
|
||||
state.active_blob_direct_write_files.end());
|
||||
if (!keep) {
|
||||
const std::string blob_file_path =
|
||||
BlobFileName(candidate_file.file_path, number);
|
||||
if (ShouldKeepBlobFileDuringPurge(number, blob_file_path)) {
|
||||
keep = true;
|
||||
break;
|
||||
}
|
||||
TableCache::Evict(table_cache_.get(), number);
|
||||
files_to_del.insert(number);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kTempFile:
|
||||
// Any temp files that are currently being written to must
|
||||
// be recorded in pending_outputs_, which is inserted into "live".
|
||||
@@ -750,6 +861,21 @@ void DBImpl::DeleteObsoleteFiles() {
|
||||
mutex_.Lock();
|
||||
}
|
||||
|
||||
bool DBImpl::ShouldKeepBlobFileDuringPurge(uint64_t number,
|
||||
const std::string& blob_file_path) {
|
||||
{
|
||||
InstrumentedMutexLock lock(&mutex_);
|
||||
for (auto* cfd : *versions_->GetColumnFamilySet()) {
|
||||
auto* mgr = cfd->blob_partition_manager();
|
||||
if (mgr != nullptr && mgr->IsTrackedBlobFileNumber(number)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ShouldKeepFooterlessBlobFile(fs_.get(), file_options_, blob_file_path);
|
||||
}
|
||||
|
||||
VersionEdit GetDBRecoveryEditForObsoletingMemTables(
|
||||
VersionSet* vset, const ColumnFamilyData& cfd,
|
||||
const autovector<VersionEdit*>& edit_list,
|
||||
@@ -1060,8 +1186,9 @@ std::set<std::string> DBImpl::CollectAllDBPaths() {
|
||||
|
||||
Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
mutex_.AssertHeld();
|
||||
uint64_t next_file_number = versions_->current_next_file_number();
|
||||
uint64_t largest_file_number = next_file_number;
|
||||
const uint64_t prev_next_file_number = versions_->current_next_file_number();
|
||||
uint64_t largest_file_number = prev_next_file_number;
|
||||
bool on_disk_file_advanced_counter = false;
|
||||
Status s;
|
||||
for (const auto& path : CollectAllDBPaths()) {
|
||||
std::vector<std::string> files;
|
||||
@@ -1075,7 +1202,15 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
if (!ParseFileName(fname, &number, &type)) {
|
||||
continue;
|
||||
}
|
||||
const std::string normalized_fpath = path + kFilePathSeparator + fname;
|
||||
std::string normalized_fpath = path;
|
||||
normalized_fpath += kFilePathSeparator;
|
||||
normalized_fpath.append(fname);
|
||||
// Use >= so a crashed-mid-allocation file at exactly
|
||||
// prev_next_file_number triggers the advance -- otherwise the next
|
||||
// NewFileNumber() would collide with it.
|
||||
if (number >= prev_next_file_number) {
|
||||
on_disk_file_advanced_counter = true;
|
||||
}
|
||||
largest_file_number = std::max(largest_file_number, number);
|
||||
if ((type == kTableFile || type == kBlobFile)) {
|
||||
recovery_ctx->existing_data_files_.push_back(normalized_fpath);
|
||||
@@ -1086,16 +1221,31 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (largest_file_number >= next_file_number) {
|
||||
versions_->next_file_number_.store(largest_file_number + 1);
|
||||
}
|
||||
// Legacy bug: initializing largest_file_number from prev_next_file_number
|
||||
// made the post-loop check trivially true and emitted a noop SetNextFile
|
||||
// on every recovery.
|
||||
// Preserved verbatim by default (some tests pin file numbers); skipped
|
||||
// only when the option is on AND we're not in salvage mode (which
|
||||
// requires the MANIFEST rewrite this emit triggers).
|
||||
const bool optimize_manifest_for_recovery =
|
||||
mutable_db_options_.optimize_manifest_for_recovery &&
|
||||
!immutable_db_options_.best_efforts_recovery;
|
||||
|
||||
VersionEdit edit;
|
||||
edit.SetNextFile(versions_->next_file_number_.load());
|
||||
assert(versions_->GetColumnFamilySet());
|
||||
ColumnFamilyData* default_cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
assert(default_cfd);
|
||||
recovery_ctx->UpdateVersionEdits(default_cfd, edit);
|
||||
if (!optimize_manifest_for_recovery || on_disk_file_advanced_counter) {
|
||||
if (largest_file_number >= prev_next_file_number) {
|
||||
versions_->next_file_number_.store(largest_file_number + 1);
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::MaybeUpdateNextFileNumber:EmitEdit");
|
||||
VersionEdit edit;
|
||||
edit.SetNextFile(versions_->next_file_number_.load());
|
||||
assert(versions_->GetColumnFamilySet());
|
||||
ColumnFamilyData* default_cfd =
|
||||
versions_->GetColumnFamilySet()->GetDefault();
|
||||
assert(default_cfd);
|
||||
recovery_ctx->UpdateVersionEdits(default_cfd, edit);
|
||||
} else {
|
||||
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber");
|
||||
}
|
||||
return s;
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
@@ -295,7 +295,8 @@ Status DB::OpenAsFollower(
|
||||
impl->versions_.reset(new ReactiveVersionSet(
|
||||
dbname, &impl->immutable_db_options_, impl->mutable_db_options_,
|
||||
impl->file_options_, impl->table_cache_.get(),
|
||||
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_));
|
||||
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_,
|
||||
impl->db_id_, impl->db_session_id_));
|
||||
impl->column_family_memtables_.reset(
|
||||
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
|
||||
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
|
||||
|
||||
+259
-33
@@ -8,6 +8,7 @@
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
#include <cinttypes>
|
||||
|
||||
#include "db/blob/blob_file_partition_manager.h"
|
||||
#include "db/builder.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/error_handler.h"
|
||||
@@ -20,6 +21,7 @@
|
||||
#include "file/writable_file_writer.h"
|
||||
#include "logging/logging.h"
|
||||
#include "monitoring/persistent_stats_history.h"
|
||||
#include "monitoring/statistics_impl.h"
|
||||
#include "monitoring/thread_status_util.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "rocksdb/options.h"
|
||||
@@ -121,6 +123,16 @@ DBOptions SanitizeOptions(const std::string& dbname, const DBOptions& src,
|
||||
result.recycle_log_file_num = 0;
|
||||
}
|
||||
|
||||
if (result.async_wal_precreate && result.recycle_log_file_num != 0) {
|
||||
// Async WAL precreation reserves a future log number, while WAL recycling
|
||||
// chooses from old WAL files. Keep the recycling behavior and disable only
|
||||
// the async optimization.
|
||||
result.async_wal_precreate = false;
|
||||
ROCKS_LOG_WARN(result.info_log,
|
||||
"async_wal_precreate is disabled since "
|
||||
"recycle_log_file_num is non-zero");
|
||||
}
|
||||
|
||||
if (result.db_paths.size() == 0) {
|
||||
result.db_paths.emplace_back(dbname, std::numeric_limits<uint64_t>::max());
|
||||
} else if (result.wal_dir.empty()) {
|
||||
@@ -679,8 +691,18 @@ Status DBImpl::Recover(
|
||||
} else if (immutable_db_options_.write_dbid_to_manifest && recovery_ctx) {
|
||||
VersionEdit edit;
|
||||
s = SetupDBId(write_options, read_only, is_new_db, is_retry, &edit);
|
||||
recovery_ctx->UpdateVersionEdits(
|
||||
versions_->GetColumnFamilySet()->GetDefault(), edit);
|
||||
// best_efforts_recovery rebuilds CURRENT/MANIFEST as the side-effect
|
||||
// of LogAndApplyForRecovery emitting an edit, so the optimization is
|
||||
// disabled there (see optimize_manifest_for_recovery doc).
|
||||
const bool optimize_manifest_for_recovery =
|
||||
mutable_db_options_.optimize_manifest_for_recovery &&
|
||||
!immutable_db_options_.best_efforts_recovery;
|
||||
if (!optimize_manifest_for_recovery || edit.HasDbId()) {
|
||||
recovery_ctx->UpdateVersionEdits(
|
||||
versions_->GetColumnFamilySet()->GetDefault(), edit);
|
||||
} else {
|
||||
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:SetupDBId");
|
||||
}
|
||||
} else {
|
||||
s = SetupDBId(write_options, read_only, is_new_db, is_retry, nullptr);
|
||||
}
|
||||
@@ -769,6 +791,10 @@ Status DBImpl::Recover(
|
||||
// otherwise, in the future, if WAL tracking is enabled again,
|
||||
// since the WALs deleted when WAL tracking is disabled are not persisted
|
||||
// into MANIFEST, WAL check may fail.
|
||||
//
|
||||
// Intentionally NOT gated by optimize_manifest_for_recovery:
|
||||
// this edit is safety-critical for a future re-enable of WAL tracking,
|
||||
// even though it can look noop-ish under the current session.
|
||||
VersionEdit edit;
|
||||
WalNumber max_wal_number =
|
||||
versions_->GetWalSet().GetWals().rbegin()->first;
|
||||
@@ -784,9 +810,19 @@ Status DBImpl::Recover(
|
||||
|
||||
if (!wal_files.empty()) {
|
||||
if (error_if_wal_file_exists) {
|
||||
return Status::Corruption(
|
||||
"The db was opened in readonly mode with error_if_wal_file_exists"
|
||||
"flag but a WAL file already exists");
|
||||
for (const auto& wal_file : wal_files) {
|
||||
uint64_t bytes;
|
||||
s = env_->GetFileSize(wal_file.second, &bytes);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (bytes > 0) {
|
||||
return Status::Corruption(
|
||||
"The db was opened in readonly mode with "
|
||||
"error_if_wal_file_exists flag but a non-empty WAL file "
|
||||
"already exists");
|
||||
}
|
||||
}
|
||||
} else if (error_if_data_exists_in_wals) {
|
||||
for (auto& wal_file : wal_files) {
|
||||
uint64_t bytes;
|
||||
@@ -982,7 +1018,11 @@ Status DBImpl::InitPersistStatsColumnFamily() {
|
||||
|
||||
Status DBImpl::LogAndApplyForRecovery(const RecoveryContext& recovery_ctx) {
|
||||
mutex_.AssertHeld();
|
||||
assert(versions_->descriptor_log_ == nullptr);
|
||||
// descriptor_log_ is normally null after Recover, but when
|
||||
// reuse_manifest_on_open is set VersionSet::Recover may have already
|
||||
// bound a log::Writer to the existing MANIFEST for append.
|
||||
assert(versions_->descriptor_log_ == nullptr ||
|
||||
immutable_db_options_.reuse_manifest_on_open);
|
||||
const ReadOptions read_options(Env::IOActivity::kDBOpen);
|
||||
const WriteOptions write_options(Env::IOActivity::kDBOpen);
|
||||
|
||||
@@ -1869,25 +1909,47 @@ Status DBImpl::MaybeFlushFinalMemtableOrRestoreActiveLogFiles(
|
||||
versions_->MarkFileNumberUsed(max_wal_number + 1);
|
||||
assert(recovery_ctx != nullptr);
|
||||
|
||||
const bool optimize_manifest_for_recovery =
|
||||
mutable_db_options_.optimize_manifest_for_recovery &&
|
||||
!immutable_db_options_.best_efforts_recovery;
|
||||
for (auto* cfd : *versions_->GetColumnFamilySet()) {
|
||||
auto iter = version_edits->find(cfd->GetID());
|
||||
assert(iter != version_edits->end());
|
||||
recovery_ctx->UpdateVersionEdits(cfd, iter->second);
|
||||
const VersionEdit& cf_edit = iter->second;
|
||||
if (!optimize_manifest_for_recovery ||
|
||||
cf_edit.ShouldEmitPerColumnFamilyRecoveryEdit(
|
||||
cfd->GetLogNumber())) {
|
||||
recovery_ctx->UpdateVersionEdits(cfd, cf_edit);
|
||||
} else {
|
||||
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:PerCF");
|
||||
}
|
||||
}
|
||||
|
||||
if (flushed || !data_seen) {
|
||||
const uint64_t new_min_log = max_wal_number + 1;
|
||||
VersionEdit wal_deletion;
|
||||
bool emit_wal_deletion = false;
|
||||
if (immutable_db_options_.track_and_verify_wals_in_manifest) {
|
||||
wal_deletion.DeleteWalsBefore(max_wal_number + 1);
|
||||
// Determining whether DeleteWalsBefore actually shrinks WalSet
|
||||
// membership requires WalSet state outside this site, so emit
|
||||
// unconditionally (pre-existing behavior).
|
||||
wal_deletion.DeleteWalsBefore(new_min_log);
|
||||
emit_wal_deletion = true;
|
||||
}
|
||||
if (!allow_2pc()) {
|
||||
// In non-2pc mode, flushing the memtables of the column families
|
||||
// means we can advance min_log_number_to_keep.
|
||||
wal_deletion.SetMinLogNumberToKeep(max_wal_number + 1);
|
||||
const bool min_log_advances =
|
||||
new_min_log > versions_->min_log_number_to_keep();
|
||||
if (!allow_2pc() &&
|
||||
(!optimize_manifest_for_recovery || min_log_advances)) {
|
||||
wal_deletion.SetMinLogNumberToKeep(new_min_log);
|
||||
emit_wal_deletion = true;
|
||||
}
|
||||
assert(versions_->GetColumnFamilySet() != nullptr);
|
||||
recovery_ctx->UpdateVersionEdits(
|
||||
versions_->GetColumnFamilySet()->GetDefault(), wal_deletion);
|
||||
if (!optimize_manifest_for_recovery || emit_wal_deletion) {
|
||||
recovery_ctx->UpdateVersionEdits(
|
||||
versions_->GetColumnFamilySet()->GetDefault(), wal_deletion);
|
||||
} else {
|
||||
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:WalDeletion");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2172,6 +2234,16 @@ Status DBImpl::WriteLevel0TableForRecovery(int job_id, ColumnFamilyData* cfd,
|
||||
|
||||
constexpr int level = 0;
|
||||
|
||||
if (s.ok() && has_output) {
|
||||
// Before publishing any recovery edit, make the new SST's directory entry
|
||||
// durable. Reused MANIFESTs have no CURRENT update, and fresh MANIFEST
|
||||
// publication should not be relied on to order the recovered SST entry.
|
||||
s = GetDataDir(cfd, meta.fd.GetPathId())
|
||||
->FsyncWithDirOptions(
|
||||
IOOptions(), nullptr,
|
||||
DirFsyncOptions(DirFsyncOptions::FsyncReason::kNewFileSynced));
|
||||
}
|
||||
|
||||
if (s.ok() && has_output) {
|
||||
edit->AddFile(level, meta.fd.GetNumber(), meta.fd.GetPathId(),
|
||||
meta.fd.GetFileSize(), meta.smallest, meta.largest,
|
||||
@@ -2336,16 +2408,17 @@ Status DB::OpenAndTrimHistory(
|
||||
return s;
|
||||
}
|
||||
|
||||
IOStatus DBImpl::CreateWAL(const WriteOptions& write_options,
|
||||
uint64_t log_file_num, uint64_t recycle_log_number,
|
||||
size_t preallocate_block_size,
|
||||
const PredecessorWALInfo& predecessor_wal_info,
|
||||
log::Writer** new_log) {
|
||||
IOStatus DBImpl::CreateWALWriter(const DBOptions& db_options,
|
||||
uint64_t log_file_num,
|
||||
uint64_t recycle_log_number,
|
||||
size_t preallocate_block_size,
|
||||
UnpublishedWAL* new_wal) {
|
||||
assert(new_wal);
|
||||
assert(new_wal->log_number == 0);
|
||||
assert(new_wal->writer == nullptr);
|
||||
IOStatus io_s;
|
||||
std::unique_ptr<FSWritableFile> lfile;
|
||||
|
||||
DBOptions db_options =
|
||||
BuildDBOptions(immutable_db_options_, mutable_db_options_);
|
||||
FileOptions opt_file_options =
|
||||
fs_->OptimizeForLogWrite(file_options_, db_options);
|
||||
opt_file_options.write_hint = CalculateWALWriteHint();
|
||||
@@ -2383,21 +2456,162 @@ IOStatus DBImpl::CreateWAL(const WriteOptions& write_options,
|
||||
Histograms::HISTOGRAM_ENUM_MAX /* hist_type */, listeners, nullptr,
|
||||
tmp_set.Contains(FileType::kWalFile),
|
||||
tmp_set.Contains(FileType::kWalFile)));
|
||||
*new_log = new log::Writer(std::move(file_writer), log_file_num,
|
||||
immutable_db_options_.recycle_log_file_num > 0,
|
||||
immutable_db_options_.manual_wal_flush,
|
||||
immutable_db_options_.wal_compression,
|
||||
immutable_db_options_.track_and_verify_wals);
|
||||
io_s = (*new_log)->AddCompressionTypeRecord(write_options);
|
||||
if (io_s.ok()) {
|
||||
io_s = (*new_log)->MaybeAddPredecessorWALInfo(write_options,
|
||||
predecessor_wal_info);
|
||||
}
|
||||
new_wal->log_number = log_file_num;
|
||||
new_wal->writer = std::make_unique<log::Writer>(
|
||||
std::move(file_writer), log_file_num,
|
||||
immutable_db_options_.recycle_log_file_num > 0,
|
||||
immutable_db_options_.manual_wal_flush,
|
||||
immutable_db_options_.wal_compression,
|
||||
immutable_db_options_.track_and_verify_wals);
|
||||
}
|
||||
|
||||
return io_s;
|
||||
}
|
||||
|
||||
IOStatus DBImpl::StartWALFile(const WriteOptions& write_options,
|
||||
const PredecessorWALInfo& predecessor_wal_info,
|
||||
log::Writer* new_log) {
|
||||
assert(new_log);
|
||||
IOStatus io_s = new_log->AddCompressionTypeRecord(write_options);
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::StartWALFile:AfterCompressionTypeRecord",
|
||||
&io_s);
|
||||
if (io_s.ok()) {
|
||||
io_s = new_log->MaybeAddPredecessorWALInfo(write_options,
|
||||
predecessor_wal_info);
|
||||
}
|
||||
return io_s;
|
||||
}
|
||||
|
||||
IOStatus DBImpl::CreateWAL(const DBOptions& db_options,
|
||||
const WriteOptions& write_options,
|
||||
uint64_t log_file_num, uint64_t recycle_log_number,
|
||||
size_t preallocate_block_size,
|
||||
const PredecessorWALInfo& predecessor_wal_info,
|
||||
log::Writer** new_log) {
|
||||
assert(new_log);
|
||||
assert(*new_log == nullptr);
|
||||
UnpublishedWAL wal;
|
||||
IOStatus io_s = CreateWALWriter(db_options, log_file_num, recycle_log_number,
|
||||
preallocate_block_size, &wal);
|
||||
if (io_s.ok()) {
|
||||
io_s = StartWALFile(write_options, predecessor_wal_info, wal.writer.get());
|
||||
}
|
||||
if (io_s.ok()) {
|
||||
*new_log = wal.writer.release();
|
||||
}
|
||||
return io_s;
|
||||
}
|
||||
|
||||
bool DBImpl::AsyncWALPrecreateEnabled() const {
|
||||
return immutable_db_options_.async_wal_precreate &&
|
||||
immutable_db_options_.recycle_log_file_num == 0;
|
||||
}
|
||||
|
||||
struct AsyncWALPrecreateContext {
|
||||
DBImpl* db = nullptr;
|
||||
DBOptions db_options;
|
||||
uint64_t log_number = 0;
|
||||
size_t preallocate_block_size = 0;
|
||||
};
|
||||
|
||||
void DBImpl::MaybeScheduleAsyncWALPrecreate(size_t preallocate_block_size) {
|
||||
mutex_.AssertHeld();
|
||||
|
||||
if (!AsyncWALPrecreateEnabled() ||
|
||||
shutting_down_.load(std::memory_order_acquire) ||
|
||||
error_handler_.IsBGWorkStopped() ||
|
||||
async_wal_precreate_state_ != AsyncWALPrecreateState::kNotScheduled) {
|
||||
return;
|
||||
}
|
||||
|
||||
async_wal_precreate_wal_.Reset();
|
||||
async_wal_precreate_wal_.log_number = versions_->NewFileNumber();
|
||||
async_wal_precreate_state_ = AsyncWALPrecreateState::kScheduled;
|
||||
|
||||
auto* ctx = new AsyncWALPrecreateContext();
|
||||
ctx->db = this;
|
||||
ctx->db_options = BuildDBOptions(immutable_db_options_, mutable_db_options_);
|
||||
ctx->log_number = async_wal_precreate_wal_.log_number;
|
||||
ctx->preallocate_block_size = preallocate_block_size;
|
||||
env_->Schedule(&DBImpl::BGWorkAsyncWALPrecreate, ctx, Env::Priority::HIGH,
|
||||
nullptr);
|
||||
}
|
||||
|
||||
DBImpl::UnpublishedWAL DBImpl::WaitForAsyncWALPrecreate() {
|
||||
mutex_.AssertHeld();
|
||||
UnpublishedWAL result;
|
||||
|
||||
if (!AsyncWALPrecreateEnabled()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT("DBImpl::WaitForAsyncWALPrecreate:Begin");
|
||||
bool waited = false;
|
||||
uint64_t wait_start_micros = 0;
|
||||
while (async_wal_precreate_state_ == AsyncWALPrecreateState::kScheduled) {
|
||||
TEST_SYNC_POINT("DBImpl::WaitForAsyncWALPrecreate:BeforeWait");
|
||||
if (!waited) {
|
||||
waited = true;
|
||||
wait_start_micros = immutable_db_options_.clock->NowMicros();
|
||||
RecordTick(stats_, WAL_PRECREATE_WAITED);
|
||||
}
|
||||
bg_cv_.Wait();
|
||||
}
|
||||
if (waited) {
|
||||
RecordTick(stats_, WAL_PRECREATE_WAIT_MICROS,
|
||||
immutable_db_options_.clock->NowMicros() - wait_start_micros);
|
||||
}
|
||||
|
||||
if (async_wal_precreate_state_ == AsyncWALPrecreateState::kReady) {
|
||||
result = std::move(async_wal_precreate_wal_);
|
||||
async_wal_precreate_state_ = AsyncWALPrecreateState::kNotScheduled;
|
||||
RecordTick(stats_, WAL_PRECREATE_HIT);
|
||||
return result;
|
||||
}
|
||||
|
||||
assert(async_wal_precreate_state_ == AsyncWALPrecreateState::kNotScheduled);
|
||||
RecordTick(stats_, WAL_PRECREATE_MISS);
|
||||
return result;
|
||||
}
|
||||
|
||||
void DBImpl::BGWorkAsyncWALPrecreate(void* arg) {
|
||||
TEST_SYNC_POINT("DBImpl::BGWorkAsyncWALPrecreate:Start");
|
||||
std::unique_ptr<AsyncWALPrecreateContext> ctx(
|
||||
static_cast<AsyncWALPrecreateContext*>(arg));
|
||||
DBImpl* db = ctx->db;
|
||||
|
||||
UnpublishedWAL new_wal;
|
||||
IOStatus io_s = db->CreateWALWriter(ctx->db_options, ctx->log_number,
|
||||
0 /* recycle_log_number */,
|
||||
ctx->preallocate_block_size, &new_wal);
|
||||
if (!io_s.ok()) {
|
||||
RecordTick(db->stats_, WAL_PRECREATE_FAILED);
|
||||
ROCKS_LOG_WARN(db->immutable_db_options_.info_log,
|
||||
"Async WAL precreate failed for #%" PRIu64 ": %s",
|
||||
ctx->log_number, io_s.ToString().c_str());
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT("DBImpl::BGWorkAsyncWALPrecreate:BeforePublish");
|
||||
TEST_SYNC_POINT("DBImpl::BGWorkAsyncWALPrecreate:Publish");
|
||||
{
|
||||
InstrumentedMutexLock l(&db->mutex_);
|
||||
if (db->async_wal_precreate_state_ == AsyncWALPrecreateState::kScheduled &&
|
||||
db->async_wal_precreate_wal_.log_number == ctx->log_number) {
|
||||
if (io_s.ok()) {
|
||||
db->async_wal_precreate_wal_ = std::move(new_wal);
|
||||
db->async_wal_precreate_state_ = AsyncWALPrecreateState::kReady;
|
||||
} else {
|
||||
// Async WAL precreation is best-effort. Clear the slot so foreground
|
||||
// rotation falls back to normal synchronous WAL creation.
|
||||
db->async_wal_precreate_wal_.Reset();
|
||||
db->async_wal_precreate_state_ = AsyncWALPrecreateState::kNotScheduled;
|
||||
}
|
||||
}
|
||||
db->bg_cv_.SignalAll();
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::BGWorkAsyncWALPrecreate:Done");
|
||||
}
|
||||
|
||||
void DBImpl::TrackExistingDataFiles(
|
||||
const std::vector<std::string>& existing_data_files) {
|
||||
TrackOrUntrackFiles(existing_data_files, /*track=*/true);
|
||||
@@ -2493,8 +2707,10 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
// created during DB open with predecessor WALs from previous DB session due
|
||||
// to `avoid_flush_during_recovery == true`. This can protect the last WAL
|
||||
// recovered.
|
||||
s = impl->CreateWAL(write_options, new_log_number, 0 /*recycle_log_number*/,
|
||||
preallocate_block_size,
|
||||
const DBOptions db_options_snapshot =
|
||||
BuildDBOptions(impl->immutable_db_options_, impl->mutable_db_options_);
|
||||
s = impl->CreateWAL(db_options_snapshot, write_options, new_log_number,
|
||||
0 /*recycle_log_number*/, preallocate_block_size,
|
||||
PredecessorWALInfo() /* predecessor_wal_info */,
|
||||
&new_log);
|
||||
if (s.ok()) {
|
||||
@@ -2606,6 +2822,14 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
}
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
for (size_t i = 0; i < column_families.size(); ++i) {
|
||||
const auto& cf = column_families[i];
|
||||
auto* cfd = static_cast<ColumnFamilyHandleImpl*>((*handles)[i])->cfd();
|
||||
impl->MaybeInitBlobDirectWriteColumnFamily(cfd, cf.options, cf.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (s.ok() && impl->immutable_db_options_.persist_stats_to_disk) {
|
||||
// Install SuperVersion for hidden column family
|
||||
assert(impl->persist_stats_cf_handle_);
|
||||
@@ -2691,6 +2915,8 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
if (impl->immutable_db_options_.open_files_async) {
|
||||
impl->ScheduleAsyncFileOpening();
|
||||
}
|
||||
impl->MaybeScheduleAsyncWALPrecreate(
|
||||
impl->GetWalPreallocateBlockSize(max_write_buffer_size));
|
||||
impl->mutex_.Unlock();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
|
||||
#include "db/db_impl/db_impl_readonly.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "db/arena_wrapped_db_iter.h"
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/db_impl/compacted_db_impl.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/manifest_ops.h"
|
||||
@@ -63,13 +66,25 @@ Status DBImplReadOnly::GetImpl(const ReadOptions& read_options,
|
||||
|
||||
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
std::string* ts =
|
||||
ucmp->timestamp_size() > 0 ? get_impl_options.timestamp : nullptr;
|
||||
SequenceNumber snapshot = versions_->LastSequence();
|
||||
GetWithTimestampReadCallback read_cb(snapshot);
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
get_impl_options.column_family);
|
||||
auto cfd = cfh->cfd();
|
||||
bool is_blob_index = false;
|
||||
bool* is_blob_ptr = get_impl_options.is_blob_index;
|
||||
std::string timestamp_storage;
|
||||
std::string* ts = nullptr;
|
||||
if (ucmp->timestamp_size() > 0) {
|
||||
ts = get_impl_options.timestamp != nullptr
|
||||
? get_impl_options.timestamp
|
||||
: (get_impl_options.get_value ? ×tamp_storage : nullptr);
|
||||
}
|
||||
if (!is_blob_ptr && get_impl_options.get_value) {
|
||||
is_blob_ptr = &is_blob_index;
|
||||
}
|
||||
const bool resolve_blob_backed_memtable_value =
|
||||
get_impl_options.get_value && (is_blob_ptr == &is_blob_index);
|
||||
if (tracer_) {
|
||||
InstrumentedMutexLock lock(&trace_mutex_);
|
||||
if (tracer_) {
|
||||
@@ -95,6 +110,18 @@ Status DBImplReadOnly::GetImpl(const ReadOptions& read_options,
|
||||
SequenceNumber max_covering_tombstone_seq = 0;
|
||||
LookupKey lkey(key, snapshot, read_options.timestamp);
|
||||
PERF_TIMER_STOP(get_snapshot_time);
|
||||
std::optional<BlobFetcher> memtable_blob_fetcher;
|
||||
if (cfd->ioptions().enable_blob_direct_write ||
|
||||
cfd->GetLatestMutableCFOptions().enable_blob_files) {
|
||||
// Recovered memtables can still contain older blob references after
|
||||
// mutable blob-file settings change, so keep blob resolution available
|
||||
// whenever either blob knob indicates it may be needed.
|
||||
memtable_blob_fetcher.emplace(super_version->current, read_options,
|
||||
cfd->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/true);
|
||||
}
|
||||
const BlobFetcher* memtable_blob_fetcher_ptr =
|
||||
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
|
||||
|
||||
// Look up starts here
|
||||
if (super_version->mem->Get(
|
||||
@@ -102,11 +129,12 @@ Status DBImplReadOnly::GetImpl(const ReadOptions& read_options,
|
||||
get_impl_options.value ? get_impl_options.value->GetSelf() : nullptr,
|
||||
get_impl_options.columns, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */, &read_cb,
|
||||
/*is_blob_index=*/nullptr, /*do_merge=*/get_impl_options.get_value)) {
|
||||
if (get_impl_options.value) {
|
||||
get_impl_options.value->PinSelf();
|
||||
}
|
||||
false /* immutable_memtable */, &read_cb, is_blob_ptr,
|
||||
/*do_merge=*/get_impl_options.get_value, memtable_blob_fetcher_ptr)) {
|
||||
DBImpl::PostprocessMemtableValueRead(
|
||||
key, ts, resolve_blob_backed_memtable_value, memtable_blob_fetcher_ptr,
|
||||
get_impl_options.value, get_impl_options.columns, &s, &is_blob_index,
|
||||
get_impl_options.value_found);
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else {
|
||||
PERF_TIMER_GUARD(get_from_output_files_time);
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
#include "db/db_impl/db_impl_secondary.h"
|
||||
|
||||
#include <cinttypes>
|
||||
#include <optional>
|
||||
|
||||
#include "db/arena_wrapped_db_iter.h"
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/log_reader.h"
|
||||
#include "db/log_writer.h"
|
||||
#include "db/merge_context.h"
|
||||
@@ -103,10 +105,22 @@ Status DBImplSecondary::FindNewLogNumbers(std::vector<uint64_t>* logs) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// if log_readers_ is non-empty, it means we have applied all logs with log
|
||||
// numbers smaller than the smallest log in log_readers_, so there is no
|
||||
// need to pass these logs to RecoverLogFiles
|
||||
uint64_t log_number_min = 0;
|
||||
// Drop readers only after MANIFEST replay says the WAL is obsolete. The
|
||||
// primary can create a higher-number empty WAL before it stops appending to a
|
||||
// lower-number current WAL, so seeing a higher-number WAL is not sufficient.
|
||||
const uint64_t min_log_number_to_keep = versions_->min_log_number_to_keep();
|
||||
for (auto iter = log_readers_.begin(); iter != log_readers_.end();) {
|
||||
if (iter->first < min_log_number_to_keep) {
|
||||
iter = log_readers_.erase(iter);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If log_readers_ is non-empty, it means we have applied all logs with log
|
||||
// numbers smaller than the smallest retained log reader. Otherwise, manifest
|
||||
// recovery tells us which older logs can be skipped.
|
||||
uint64_t log_number_min = min_log_number_to_keep;
|
||||
if (!log_readers_.empty()) {
|
||||
log_number_min = log_readers_.begin()->first;
|
||||
}
|
||||
@@ -319,12 +333,6 @@ Status DBImplSecondary::RecoverLogFiles(
|
||||
return status;
|
||||
}
|
||||
}
|
||||
// remove logreaders from map after successfully recovering the WAL
|
||||
if (log_readers_.size() > 1) {
|
||||
auto erase_iter = log_readers_.begin();
|
||||
std::advance(erase_iter, log_readers_.size() - 1);
|
||||
log_readers_.erase(log_readers_.begin(), erase_iter);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -363,13 +371,25 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
||||
|
||||
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
std::string* ts =
|
||||
ucmp->timestamp_size() > 0 ? get_impl_options.timestamp : nullptr;
|
||||
SequenceNumber snapshot = versions_->LastSequence();
|
||||
GetWithTimestampReadCallback read_cb(snapshot);
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
get_impl_options.column_family);
|
||||
auto cfd = cfh->cfd();
|
||||
bool is_blob_index = false;
|
||||
bool* is_blob_ptr = get_impl_options.is_blob_index;
|
||||
std::string timestamp_storage;
|
||||
std::string* ts = nullptr;
|
||||
if (ucmp->timestamp_size() > 0) {
|
||||
ts = get_impl_options.timestamp != nullptr
|
||||
? get_impl_options.timestamp
|
||||
: (get_impl_options.get_value ? ×tamp_storage : nullptr);
|
||||
}
|
||||
if (!is_blob_ptr && get_impl_options.get_value) {
|
||||
is_blob_ptr = &is_blob_index;
|
||||
}
|
||||
const bool resolve_blob_backed_memtable_value =
|
||||
get_impl_options.get_value && (is_blob_ptr == &is_blob_index);
|
||||
if (tracer_) {
|
||||
InstrumentedMutexLock lock(&trace_mutex_);
|
||||
if (tracer_) {
|
||||
@@ -395,6 +415,18 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
||||
LookupKey lkey(key, snapshot, read_options.timestamp);
|
||||
PERF_TIMER_STOP(get_snapshot_time);
|
||||
bool done = false;
|
||||
std::optional<BlobFetcher> memtable_blob_fetcher;
|
||||
if (cfd->ioptions().enable_blob_direct_write ||
|
||||
cfd->GetLatestMutableCFOptions().enable_blob_files) {
|
||||
// Catch-up can rebuild older blob references into memtables after mutable
|
||||
// blob-file settings change, so keep blob resolution available whenever
|
||||
// either blob knob indicates it may be needed.
|
||||
memtable_blob_fetcher.emplace(super_version->current, read_options,
|
||||
cfd->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/true);
|
||||
}
|
||||
const BlobFetcher* memtable_blob_fetcher_ptr =
|
||||
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
|
||||
|
||||
// Look up starts here
|
||||
if (get_impl_options.get_value) {
|
||||
@@ -404,12 +436,14 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
||||
: nullptr,
|
||||
get_impl_options.columns, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */, &read_cb,
|
||||
/*is_blob_index=*/nullptr, /*do_merge=*/true)) {
|
||||
false /* immutable_memtable */, &read_cb, is_blob_ptr,
|
||||
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
if (get_impl_options.value) {
|
||||
get_impl_options.value->PinSelf();
|
||||
}
|
||||
DBImpl::PostprocessMemtableValueRead(
|
||||
key, ts, resolve_blob_backed_memtable_value,
|
||||
memtable_blob_fetcher_ptr, get_impl_options.value,
|
||||
get_impl_options.columns, &s, &is_blob_index,
|
||||
get_impl_options.value_found);
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
super_version->imm->Get(
|
||||
@@ -417,11 +451,14 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
||||
get_impl_options.value ? get_impl_options.value->GetSelf()
|
||||
: nullptr,
|
||||
get_impl_options.columns, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options, &read_cb)) {
|
||||
&max_covering_tombstone_seq, read_options, &read_cb,
|
||||
is_blob_ptr, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
if (get_impl_options.value) {
|
||||
get_impl_options.value->PinSelf();
|
||||
}
|
||||
DBImpl::PostprocessMemtableValueRead(
|
||||
key, ts, resolve_blob_backed_memtable_value,
|
||||
memtable_blob_fetcher_ptr, get_impl_options.value,
|
||||
get_impl_options.columns, &s, &is_blob_index,
|
||||
get_impl_options.value_found);
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
}
|
||||
} else {
|
||||
@@ -433,13 +470,14 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
||||
get_impl_options.columns, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */, &read_cb,
|
||||
/*is_blob_index=*/nullptr, /*do_merge=*/false)) {
|
||||
/*is_blob_index=*/nullptr, /*do_merge=*/false,
|
||||
memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
super_version->imm->GetMergeOperands(lkey, &s, &merge_context,
|
||||
&max_covering_tombstone_seq,
|
||||
read_options)) {
|
||||
super_version->imm->GetMergeOperands(
|
||||
lkey, &s, &merge_context, &max_covering_tombstone_seq,
|
||||
read_options, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
}
|
||||
@@ -457,7 +495,7 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
||||
ts, &s, &merge_context, &max_covering_tombstone_seq, &pinned_iters_mgr,
|
||||
/*value_found*/ nullptr,
|
||||
/*key_exists*/ nullptr, /*seq*/ nullptr, &read_cb, /*is_blob*/ nullptr,
|
||||
/*do_merge*/ true);
|
||||
/*do_merge=*/get_impl_options.get_value);
|
||||
RecordTick(stats_, MEMTABLE_MISS);
|
||||
}
|
||||
{
|
||||
@@ -777,7 +815,8 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
|
||||
impl->versions_.reset(new ReactiveVersionSet(
|
||||
dbname, &impl->immutable_db_options_, impl->mutable_db_options_,
|
||||
impl->file_options_, impl->table_cache_.get(),
|
||||
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_));
|
||||
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_,
|
||||
impl->db_id_, impl->db_session_id_));
|
||||
impl->column_family_memtables_.reset(
|
||||
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
|
||||
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
|
||||
@@ -1553,7 +1592,7 @@ Status DB::OpenAndCompact(
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Open db As Secondary (skip WAL recovery — remote compaction only
|
||||
// 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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user