mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-07 03:22:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10dfc6acb8 | ||
|
|
d46d4b7f82 | ||
|
|
a127d1533b | ||
|
|
928ebd5b8b | ||
|
|
afcf52a996 | ||
|
|
e688e7b235 | ||
|
|
e292aa95b3 | ||
|
|
123030ffee | ||
|
|
5f66923d1e | ||
|
|
7785264e5a | ||
|
|
ad43a5fbe8 | ||
|
|
f7677f0b6b | ||
|
|
c9252b9ac0 | ||
|
|
4d30cfd1fc | ||
|
|
61c3e93a74 | ||
|
|
521cd8b31d | ||
|
|
77d9ed7f63 | ||
|
|
6d4a8144e0 | ||
|
|
4026c3cc08 | ||
|
|
e18d41e08c | ||
|
|
4975341aef | ||
|
|
8d33fe7c70 | ||
|
|
01084e7e8e | ||
|
|
7affaee1c4 | ||
|
|
828f6d189e | ||
|
|
1ede57e5b0 | ||
|
|
82085868e2 | ||
|
|
0493154a73 | ||
|
|
a214d3f1f8 | ||
|
|
2b9e8dc925 | ||
|
|
8c3bd2c3e9 | ||
|
|
76b8cce2f7 | ||
|
|
10e4f4745e | ||
|
|
2d68f30425 | ||
|
|
b5922eab11 | ||
|
|
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 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, isPartial }
|
||||
// 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 {
|
||||
@@ -80,36 +94,25 @@ module.exports = function parseClaude({executionFile, conclusion, meta}) {
|
||||
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.'
|
||||
);
|
||||
@@ -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,20 +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) plus Write
|
||||
# (only for incremental findings file, not repo modifications)
|
||||
# - 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
|
||||
|
||||
@@ -23,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]
|
||||
|
||||
@@ -33,560 +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 {
|
||||
// PR may not be registered yet if push and PR creation raced.
|
||||
// Retry up to 5 times with 10s delay.
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
if (attempt > 0) {
|
||||
core.info(`PR not found for SHA ${headSha}, retrying in 10s (attempt ${attempt + 1}/5)...`);
|
||||
await new Promise(r => setTimeout(r, 10000));
|
||||
}
|
||||
const { data: pulls } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
sort: 'updated',
|
||||
direction: 'desc',
|
||||
per_page: 100
|
||||
});
|
||||
const match = pulls.find(p => p.head.sha === headSha);
|
||||
if (match) {
|
||||
prNumber = match.number;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!prNumber) {
|
||||
core.warning(
|
||||
`Could not find PR for SHA ${headSha} after retries. ` +
|
||||
`This can happen when the PR head advanced before this ` +
|
||||
`workflow_run-triggered job started. Skipping stale review run.`
|
||||
);
|
||||
core.setOutput('skip', 'true');
|
||||
core.setOutput('skip_reason', `stale_sha:${headSha}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
core.setOutput('skip', 'false');
|
||||
core.setOutput('pr_number', prNumber.toString());
|
||||
core.setOutput('head_repo', pr.data.head.repo.clone_url);
|
||||
core.setOutput('head_ref', pr.data.head.ref);
|
||||
core.setOutput('head_sha', pr.data.head.sha);
|
||||
core.setOutput('base_sha', pr.data.base.sha);
|
||||
core.setOutput('base_ref', pr.data.base.ref);
|
||||
core.setOutput('pr_title', pr.data.title);
|
||||
core.setOutput('pr_body', pr.data.body || '');
|
||||
core.setOutput('pr_author', pr.data.user.login);
|
||||
|
||||
- name: Skip stale run
|
||||
if: steps.pr_info.outputs.skip == 'true'
|
||||
run: |-
|
||||
echo "Skipping stale auto-review run: ${{ steps.pr_info.outputs.skip_reason }}"
|
||||
|
||||
- name: Check for existing review
|
||||
if: steps.pr_info.outputs.skip != 'true'
|
||||
id: check_existing
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.pr_info.outputs.pr_number }}
|
||||
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
|
||||
with:
|
||||
script: |
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: parseInt(process.env.PR_NUMBER),
|
||||
per_page: 100
|
||||
});
|
||||
const shortSha = process.env.HEAD_SHA.substring(0, 7);
|
||||
const existing = comments.data.find(c =>
|
||||
c.body.includes('<!-- claude-review -->') &&
|
||||
c.body.includes(shortSha)
|
||||
);
|
||||
core.setOutput('skip', existing ? 'true' : 'false');
|
||||
|
||||
- name: Checkout base repository
|
||||
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.pr_info.outputs.base_ref }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Generate diff and prompt
|
||||
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
|
||||
env:
|
||||
HEAD_REPO: ${{ steps.pr_info.outputs.head_repo }}
|
||||
HEAD_REF: ${{ steps.pr_info.outputs.head_ref }}
|
||||
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
|
||||
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
|
||||
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
|
||||
PR_BODY: ${{ steps.pr_info.outputs.pr_body }}
|
||||
PR_AUTHOR: ${{ steps.pr_info.outputs.pr_author }}
|
||||
run: |
|
||||
git remote add fork "${HEAD_REPO}" || true
|
||||
git fetch fork "${HEAD_REF}" --depth=100
|
||||
|
||||
git diff "${BASE_SHA}...${HEAD_SHA}" > /tmp/pr.diff
|
||||
DIFF_STATS=$(git diff --stat "${BASE_SHA}...${HEAD_SHA}" | tail -1)
|
||||
|
||||
DIFF_SIZE=$(wc -c < /tmp/pr.diff)
|
||||
IS_TRUNCATED=false
|
||||
if [ "$DIFF_SIZE" -gt 100000 ]; then
|
||||
head -c 100000 /tmp/pr.diff > /tmp/pr_truncated.diff
|
||||
mv /tmp/pr_truncated.diff /tmp/pr.diff
|
||||
IS_TRUNCATED=true
|
||||
fi
|
||||
|
||||
cat claude_md/ci_review_prompt.md > /tmp/prompt.txt
|
||||
|
||||
cat >> /tmp/prompt.txt << PROMPT_EOF
|
||||
|
||||
## Pull Request Information
|
||||
- **Title:** ${PR_TITLE}
|
||||
- **Author:** ${PR_AUTHOR}
|
||||
- **Changes:** ${DIFF_STATS}
|
||||
PROMPT_EOF
|
||||
|
||||
if [ "${IS_TRUNCATED}" = "true" ]; then
|
||||
echo "- **Note:** Diff truncated due to size." >> /tmp/prompt.txt
|
||||
fi
|
||||
|
||||
printf '\n## PR Description\n' >> /tmp/prompt.txt
|
||||
printf '%s\n' "${PR_BODY}" >> /tmp/prompt.txt
|
||||
printf '\n---\n\n## Diff to Review\n\n' >> /tmp/prompt.txt
|
||||
cat /tmp/pr.diff >> /tmp/prompt.txt
|
||||
|
||||
- name: Run Claude
|
||||
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
|
||||
id: claude
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
with:
|
||||
prompt_file: /tmp/prompt.txt
|
||||
model: claude-opus-4-6
|
||||
max_turns: "300"
|
||||
timeout_minutes: "60"
|
||||
allowed_tools: "View,GlobTool,GrepTool,Write,Task"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
- name: Recover partial review on turn limit
|
||||
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
|
||||
id: recover
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const log = JSON.parse(fs.readFileSync(process.env.EXECUTION_FILE, 'utf8'));
|
||||
const result = log.find(m => m.type === 'result');
|
||||
const hitLimit = result && result.subtype === 'error_max_turns';
|
||||
const hasFindings = fs.existsSync('review-findings.md');
|
||||
core.setOutput('hit_limit', hitLimit ? 'true' : 'false');
|
||||
core.setOutput('has_findings', hasFindings ? 'true' : 'false');
|
||||
core.setOutput('needs_recovery', (hitLimit && hasFindings) ? 'true' : 'false');
|
||||
|
||||
- name: Format partial findings
|
||||
if: >-
|
||||
steps.pr_info.outputs.skip != 'true' &&
|
||||
steps.check_existing.outputs.skip != 'true' &&
|
||||
steps.recover.outputs.needs_recovery == 'true'
|
||||
id: format_recovery
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
with:
|
||||
prompt_file: claude_md/ci_recovery_prompt.md
|
||||
model: claude-sonnet-4-20250514
|
||||
max_turns: "10"
|
||||
allowed_tools: "View,GlobTool,GrepTool"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
- name: Build review comment
|
||||
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
|
||||
RECOVERY_FILE: ${{ steps.format_recovery.outputs.execution_file }}
|
||||
CONCLUSION: ${{ steps.claude.outputs.conclusion }}
|
||||
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
|
||||
NEEDS_RECOVERY: ${{ steps.recover.outputs.needs_recovery }}
|
||||
with:
|
||||
script: |
|
||||
const parse = require('./.github/scripts/parse-claude-review.js');
|
||||
const fs = require('fs');
|
||||
let execFile = process.env.EXECUTION_FILE;
|
||||
if (process.env.NEEDS_RECOVERY === 'true') {
|
||||
const rf = process.env.RECOVERY_FILE;
|
||||
if (rf && fs.existsSync(rf)) {
|
||||
execFile = rf;
|
||||
}
|
||||
}
|
||||
const body = parse({
|
||||
executionFile: execFile,
|
||||
conclusion: process.env.CONCLUSION,
|
||||
meta: {
|
||||
trigger: 'auto',
|
||||
headSha: process.env.HEAD_SHA,
|
||||
reviewer: '',
|
||||
isQuery: false,
|
||||
isPartial: process.env.NEEDS_RECOVERY === 'true',
|
||||
}
|
||||
});
|
||||
fs.writeFileSync('claude-review-comment.md', body);
|
||||
|
||||
- name: Save PR number
|
||||
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
|
||||
run: echo "${{ steps.pr_info.outputs.pr_number }}" > pr_number.txt
|
||||
|
||||
- name: Save trigger metadata
|
||||
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
|
||||
run: echo "auto" > trigger_type.txt
|
||||
|
||||
- name: Upload review artifact
|
||||
if: always() && steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: claude-review-result
|
||||
path: |
|
||||
claude-review-comment.md
|
||||
pr_number.txt
|
||||
trigger_type.txt
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload execution log
|
||||
if: always() && steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: claude-review-execution-log
|
||||
path: ${{ steps.claude.outputs.execution_file }}
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload recovery execution log
|
||||
if: always() && steps.pr_info.outputs.skip != 'true' && steps.recover.outputs.needs_recovery == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: claude-review-recovery-log
|
||||
path: ${{ steps.format_recovery.outputs.execution_file }}
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
|
||||
# ======================== Manual Review ======================== #
|
||||
manual-review:
|
||||
name: Manual Claude Review
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
(contains(github.event.comment.body, '/claude-review') || contains(github.event.comment.body, '/claude-query')) &&
|
||||
contains(fromJSON('["pdillinger", "jaykorean", "hx235", "dannyhchen", "joshkang97", "zaidoon1", "omkarhgawde", "xingbowang", "anand1976", "virajthakur", "nmk70", "archang19", "mszeszko-meta", "yoshinorim", "cbi42"]'), github.event.comment.user.login)
|
||||
)
|
||||
|
||||
steps:
|
||||
- name: Detect command type
|
||||
id: command
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
if (context.eventName === 'workflow_dispatch') {
|
||||
core.setOutput('type', 'review');
|
||||
core.setOutput('query', '');
|
||||
return;
|
||||
}
|
||||
const body = context.payload.comment.body;
|
||||
if (body.includes('/claude-review')) {
|
||||
core.setOutput('type', 'review');
|
||||
const match = body.match(/\/claude-review\s+([\s\S]*)/);
|
||||
core.setOutput('query', match ? match[1].trim() : '');
|
||||
} else if (body.includes('/claude-query')) {
|
||||
const match = body.match(/\/claude-query\s+([\s\S]*)/);
|
||||
const query = match ? match[1].trim() : '';
|
||||
if (!query) {
|
||||
core.setFailed('No query provided. Usage: /claude-query <your question>');
|
||||
return;
|
||||
}
|
||||
core.setOutput('type', 'query');
|
||||
core.setOutput('query', query);
|
||||
} else {
|
||||
core.setFailed('Unknown command');
|
||||
}
|
||||
|
||||
- name: Get PR details
|
||||
id: pr_info
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
|
||||
with:
|
||||
script: |
|
||||
const prNumber = context.eventName === 'workflow_dispatch'
|
||||
? parseInt(process.env.INPUT_PR_NUMBER, 10)
|
||||
: context.issue.number;
|
||||
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
core.setOutput('pr_number', prNumber.toString());
|
||||
core.setOutput('head_repo', pr.data.head.repo.clone_url);
|
||||
core.setOutput('head_ref', pr.data.head.ref);
|
||||
core.setOutput('head_sha', pr.data.head.sha);
|
||||
core.setOutput('base_sha', pr.data.base.sha);
|
||||
core.setOutput('base_ref', pr.data.base.ref);
|
||||
core.setOutput('pr_title', pr.data.title);
|
||||
core.setOutput('pr_body', pr.data.body || '');
|
||||
core.setOutput('pr_author', pr.data.user.login);
|
||||
|
||||
- name: Checkout base repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.pr_info.outputs.base_ref }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Generate diff and prompt
|
||||
env:
|
||||
HEAD_REPO: ${{ steps.pr_info.outputs.head_repo }}
|
||||
HEAD_REF: ${{ steps.pr_info.outputs.head_ref }}
|
||||
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
|
||||
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
|
||||
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
|
||||
PR_BODY: ${{ steps.pr_info.outputs.pr_body }}
|
||||
PR_AUTHOR: ${{ steps.pr_info.outputs.pr_author }}
|
||||
DIFF_STATS: ""
|
||||
COMMAND_TYPE: ${{ steps.command.outputs.type }}
|
||||
ADDITIONAL_CONTEXT: ${{ steps.command.outputs.query }}
|
||||
USER_QUERY: ${{ steps.command.outputs.query }}
|
||||
run: |
|
||||
git remote add fork "${HEAD_REPO}" || true
|
||||
git fetch fork "${HEAD_REF}" --depth=100
|
||||
|
||||
git diff "${BASE_SHA}...${HEAD_SHA}" > /tmp/pr.diff
|
||||
DIFF_STATS=$(git diff --stat "${BASE_SHA}...${HEAD_SHA}" | tail -1)
|
||||
|
||||
DIFF_SIZE=$(wc -c < /tmp/pr.diff)
|
||||
IS_TRUNCATED=false
|
||||
if [ "$DIFF_SIZE" -gt 100000 ]; then
|
||||
head -c 100000 /tmp/pr.diff > /tmp/pr_truncated.diff
|
||||
mv /tmp/pr_truncated.diff /tmp/pr.diff
|
||||
IS_TRUNCATED=true
|
||||
fi
|
||||
|
||||
if [ "${COMMAND_TYPE}" = "query" ]; then
|
||||
# Query prompt
|
||||
cat claude_md/ci_query_prompt.md > /tmp/prompt.txt
|
||||
printf '\n---\n\n' >> /tmp/prompt.txt
|
||||
|
||||
cat >> /tmp/prompt.txt << PROMPT_EOF
|
||||
## Pull Request Context
|
||||
- **Title:** ${PR_TITLE}
|
||||
- **Author:** ${PR_AUTHOR}
|
||||
- **Changes:** ${DIFF_STATS}
|
||||
|
||||
## PR Description
|
||||
PROMPT_EOF
|
||||
printf '%s\n' "${PR_BODY}" >> /tmp/prompt.txt
|
||||
printf '\n## Question\n' >> /tmp/prompt.txt
|
||||
printf '%s\n' "${USER_QUERY}" >> /tmp/prompt.txt
|
||||
printf '\n---\n## PR Diff (for reference)\n' >> /tmp/prompt.txt
|
||||
cat /tmp/pr.diff >> /tmp/prompt.txt
|
||||
else
|
||||
# Review prompt
|
||||
cat claude_md/ci_review_prompt.md > /tmp/prompt.txt
|
||||
|
||||
cat >> /tmp/prompt.txt << PROMPT_EOF
|
||||
|
||||
## Pull Request Information
|
||||
- **Title:** ${PR_TITLE}
|
||||
- **Author:** ${PR_AUTHOR}
|
||||
- **Changes:** ${DIFF_STATS}
|
||||
PROMPT_EOF
|
||||
|
||||
if [ "${IS_TRUNCATED}" = "true" ]; then
|
||||
echo "- **Note:** Diff truncated due to size." >> /tmp/prompt.txt
|
||||
fi
|
||||
|
||||
printf '\n## PR Description\n' >> /tmp/prompt.txt
|
||||
printf '%s\n' "${PR_BODY}" >> /tmp/prompt.txt
|
||||
|
||||
if [ -n "${ADDITIONAL_CONTEXT}" ]; then
|
||||
printf '\n## Additional Instructions from Reviewer\n' >> /tmp/prompt.txt
|
||||
printf '%s\n' "${ADDITIONAL_CONTEXT}" >> /tmp/prompt.txt
|
||||
fi
|
||||
|
||||
printf '\n---\n\n## Diff to Review\n\n' >> /tmp/prompt.txt
|
||||
cat /tmp/pr.diff >> /tmp/prompt.txt
|
||||
fi
|
||||
|
||||
- name: Run Claude
|
||||
id: claude
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
with:
|
||||
prompt_file: /tmp/prompt.txt
|
||||
model: ${{ github.event_name == 'workflow_dispatch' && inputs.model || 'claude-opus-4-6' }}
|
||||
max_turns: "300"
|
||||
timeout_minutes: "60"
|
||||
allowed_tools: "View,GlobTool,GrepTool,Write,Task"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
- name: Recover partial review on turn limit
|
||||
id: recover
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
|
||||
COMMAND_TYPE: ${{ steps.command.outputs.type }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
// Skip recovery for queries — only reviews use incremental findings
|
||||
if (process.env.COMMAND_TYPE === 'query') {
|
||||
core.setOutput('needs_recovery', 'false');
|
||||
return;
|
||||
}
|
||||
const log = JSON.parse(fs.readFileSync(process.env.EXECUTION_FILE, 'utf8'));
|
||||
const result = log.find(m => m.type === 'result');
|
||||
const hitLimit = result && result.subtype === 'error_max_turns';
|
||||
const hasFindings = fs.existsSync('review-findings.md');
|
||||
core.setOutput('hit_limit', hitLimit ? 'true' : 'false');
|
||||
core.setOutput('has_findings', hasFindings ? 'true' : 'false');
|
||||
core.setOutput('needs_recovery', (hitLimit && hasFindings) ? 'true' : 'false');
|
||||
|
||||
- name: Format partial findings
|
||||
if: steps.recover.outputs.needs_recovery == 'true'
|
||||
id: format_recovery
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
with:
|
||||
prompt_file: claude_md/ci_recovery_prompt.md
|
||||
model: claude-sonnet-4-20250514
|
||||
max_turns: "10"
|
||||
allowed_tools: "View,GlobTool,GrepTool"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
- name: Build review comment
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
|
||||
RECOVERY_FILE: ${{ steps.format_recovery.outputs.execution_file }}
|
||||
CONCLUSION: ${{ steps.claude.outputs.conclusion }}
|
||||
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
|
||||
REVIEWER: ${{ github.event_name == 'workflow_dispatch' && github.actor || github.event.comment.user.login }}
|
||||
COMMAND_TYPE: ${{ steps.command.outputs.type }}
|
||||
NEEDS_RECOVERY: ${{ steps.recover.outputs.needs_recovery }}
|
||||
with:
|
||||
script: |
|
||||
const parse = require('./.github/scripts/parse-claude-review.js');
|
||||
const fs = require('fs');
|
||||
const isReview = process.env.COMMAND_TYPE === 'review';
|
||||
let execFile = process.env.EXECUTION_FILE;
|
||||
if (process.env.NEEDS_RECOVERY === 'true') {
|
||||
const rf = process.env.RECOVERY_FILE;
|
||||
if (rf && fs.existsSync(rf)) {
|
||||
execFile = rf;
|
||||
}
|
||||
}
|
||||
const body = parse({
|
||||
executionFile: execFile,
|
||||
conclusion: process.env.CONCLUSION,
|
||||
meta: {
|
||||
trigger: 'manual',
|
||||
headSha: process.env.HEAD_SHA,
|
||||
reviewer: process.env.REVIEWER,
|
||||
isQuery: !isReview,
|
||||
isPartial: process.env.NEEDS_RECOVERY === 'true',
|
||||
}
|
||||
});
|
||||
fs.writeFileSync('claude-review-comment.md', body);
|
||||
|
||||
- name: Save PR number
|
||||
run: echo "${{ steps.pr_info.outputs.pr_number }}" > pr_number.txt
|
||||
|
||||
- name: Save trigger metadata
|
||||
env:
|
||||
REVIEWER: ${{ github.event_name == 'workflow_dispatch' && github.actor || github.event.comment.user.login }}
|
||||
COMMENT_ID: ${{ github.event.comment.id }}
|
||||
run: |
|
||||
echo "manual" > trigger_type.txt
|
||||
echo "${REVIEWER}" > reviewer.txt
|
||||
echo "${COMMENT_ID}" > comment_id.txt
|
||||
|
||||
- name: Upload review artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: claude-review-result
|
||||
path: |
|
||||
claude-review-comment.md
|
||||
pr_number.txt
|
||||
trigger_type.txt
|
||||
reviewer.txt
|
||||
comment_id.txt
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload execution log
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: claude-review-execution-log
|
||||
path: ${{ steps.claude.outputs.execution_file }}
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload recovery execution log
|
||||
if: always() && steps.recover.outputs.needs_recovery == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: claude-review-recovery-log
|
||||
path: ${{ steps.format_recovery.outputs.execution_file }}
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
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
|
||||
@@ -373,14 +373,17 @@ 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"
|
||||
@@ -534,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: ""
|
||||
|
||||
+1
-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
|
||||
|
||||
@@ -4868,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"],
|
||||
@@ -5024,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"],
|
||||
|
||||
@@ -149,6 +149,26 @@ Cache management is critical for RocksDB's performance.
|
||||
|
||||
When reviewing RocksDB code (or preparing code for review), use this checklist:
|
||||
|
||||
### Contract Boundaries
|
||||
- [ ] Is each behavior owned by the right layer? High-level policy (for example,
|
||||
"compaction wants this I/O mode") should live at the caller/policy layer, while
|
||||
lower layers should expose generic mechanisms (for example, "open a fresh
|
||||
reader", "skip shared cache insertion", or "use these FileOptions").
|
||||
- [ ] Do comments and names describe local contracts rather than leaking a
|
||||
specific caller's rationale into reusable APIs? Generic code should not need to
|
||||
know about one current use case unless the API itself is intentionally
|
||||
use-case-specific.
|
||||
- [ ] Does each flag or parameter control one coherent behavior? If one boolean
|
||||
starts implying ownership, cache policy, I/O mode, prefetching, and caller
|
||||
identity, split it into explicit flags or an options struct.
|
||||
- [ ] Could a future caller use this lower-level API without accidentally
|
||||
inheriting assumptions from compaction, backup, user reads, or a particular
|
||||
table format? If not, tighten the contract with assertions, clearer names, or
|
||||
a narrower API.
|
||||
- [ ] Are implementation details not being used as policy signals? Prefer an
|
||||
explicit contract over inferring behavior from incidental fields such as file
|
||||
options, cache handles, or current table-reader state.
|
||||
|
||||
### Correctness
|
||||
- [ ] Does the change preserve database semantics (e.g., snapshot isolation, key ordering)?
|
||||
- [ ] Are all error cases handled appropriately?
|
||||
@@ -204,19 +224,84 @@ The following patterns emerged as frequent sources of review feedback:
|
||||
|
||||
10. **Platform Compatibility:** Ensure changes work correctly on all supported platforms (Linux, Windows, macOS) and with all supported compilers (GCC, Clang, MSVC).
|
||||
|
||||
11. **Contract Boundary Leaks:** When a change plumbs a new option or use-case
|
||||
specific behavior through multiple subsystems, review the call chain for
|
||||
contract leaks. Caller-specific rationale belongs at the call site or public API
|
||||
documentation; reusable layers should expose precise, layer-local capabilities.
|
||||
Watch especially for comments mentioning one caller in generic code, booleans
|
||||
that silently bundle several behaviors, and downstream code inferring policy
|
||||
from an implementation detail instead of an explicit option.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
### Cross-platform / portability
|
||||
Local `make` only exercises Linux with GCC/Clang, but CI
|
||||
(`.github/workflows/pr-jobs.yml` and `nightly.yml`) gates on a much wider
|
||||
matrix, so portability breaks are invisible locally until CI fails. Code must
|
||||
build (and where noted, run tests) across:
|
||||
|
||||
| Axis | Must support |
|
||||
|------|--------------|
|
||||
| OS | Linux (x86_64 + ARM), macOS, Windows |
|
||||
| Compiler | GCC, Clang (libstdc++ **and** libc++), AppleClang, **MSVC (VS2022)**, MinGW (Linux cross-compile, build-only, no gflags) |
|
||||
| Build system | Make, CMake, and BUCK (internal) -- keep all in sync (see "Build system" above) |
|
||||
| Config | release (`-fno-rtti`), `ASSERT_STATUS_CHECKED`, ASAN/UBSAN/TSAN, folly, unity build, JNI/Java |
|
||||
|
||||
Treat these as constraints to satisfy and infer the specifics from them before
|
||||
adding any system header, libc call, or compiler-specific construct. The most
|
||||
common trap: anything that compiles under GCC/Clang on Linux but not under
|
||||
**MSVC/MinGW** -- e.g. unguarded POSIX-only headers/functions (`<unistd.h>`,
|
||||
`<sys/*.h>`, `getpid`, `_exit`, ...) or GCC/Clang extensions
|
||||
(`__attribute__`, `__builtin_*`, VLAs, `alloca`). Prefer the `port::`/`Env`
|
||||
abstractions; otherwise guard with `#ifdef OS_WIN` (POSIX `<unistd.h>` ->
|
||||
Windows `<process.h>`). Because libc++ is also tested, include what you use
|
||||
rather than relying on libstdc++ transitive includes.
|
||||
|
||||
### 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
|
||||
@@ -232,6 +317,9 @@ The following patterns emerged as frequent sources of review feedback:
|
||||
COERCE_CONTEXT_SWITCH=1 make {test_binary}
|
||||
./{test_binary} --gtest_filter="*YourTestName*" --gtest_repeat=5
|
||||
```
|
||||
* For CI-style flaky tests that do not reproduce with `gtest_parallel.py`,
|
||||
`--gtest_repeat`, or normal coerce-mode runs, inspect
|
||||
`tools/gtest_parallel_repro.py --help`.
|
||||
|
||||
### Unit test dedup guidelines
|
||||
* Extract helper functions for repeated patterns such as object
|
||||
|
||||
+39
-1
@@ -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)
|
||||
@@ -1453,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
|
||||
@@ -1476,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
|
||||
|
||||
+73
-6
@@ -1,19 +1,85 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 11.2.4 (06/24/2026)
|
||||
## 11.5.2 (06/25/2026)
|
||||
### Bug Fixes
|
||||
* Reverted PR14831 that made range_lock_manager aware of reverse-order CF
|
||||
|
||||
## 11.5.1 (06/24/2026)
|
||||
### Bug Fixes
|
||||
* Fixed a bug where closing a read-only DB instance could delete live SST files created by a concurrent read-write DB sharing the same directory.
|
||||
|
||||
## 11.2.3 (05/07/2026)
|
||||
## 11.5.0 (06/16/2026)
|
||||
### New Features
|
||||
* Add experimental mutable DBOption `optimize_manifest_for_recovery` (default false). When enabled, RocksDB can reduce recovery work after a clean shutdown, which may lower DB::Open latency on warm reopens.
|
||||
* Add experimental `reuse_manifest_on_open` DBOption (default false). When enabled, DB::Open reuses the existing MANIFEST file for append instead of creating a fresh one, avoiding the cost of serializing the entire database state into a new MANIFEST on the first post-open write.
|
||||
* External table readers that open files through `ExternalTableOptions::fs` now update RocksDB SST/file-read statistics and file IO listener callbacks, making external file IO activity visible in existing read metrics.
|
||||
* Added `DB::PrepareFileIngestion()`, a two-phase form of `IngestExternalFile`/`IngestExternalFiles`. `PrepareFileIngestion()` performs all of the work that does not require the DB mutex (validating the arguments, reading each external file's metadata, and linking/copying the files into the DB) and returns an opaque `FileIngestionHandle`. `DB::CommitFileIngestionHandle()` (or `DB::CommitFileIngestionHandles()` for several handles at once) then makes the prepared files visible; multiple handles are committed atomically in a single MANIFEST write, or none are. `FileIngestionHandle::Abort()` (or simply destroying the handle) cancels a prepared ingestion and rolls it back. This gives flexibility to the application to Prepare a file separately from when its committed, and may shorten an applications critical path on `IngestExternalFile`.
|
||||
* Added two stats histograms reporting the per-call latency (in microseconds) of each successful `IngestExternalFile`/`IngestExternalFiles` call, split by phase: `rocksdb.ingest.external.file.prepare.micros` (argument validation and reading/validating/linking the external files, which does not block live writes) and `rocksdb.ingest.external.file.run.micros` (the ingestion performed under the DB mutex while live writes are blocked). Failed ingestions are not recorded.
|
||||
* Added `DBOptions::use_direct_io_for_compaction_reads` (default false). When enabled, compaction-input SST reads use `O_DIRECT` while user reads remain buffered, avoiding page-cache eviction of hot user-read data by sequential compaction scans. Pair with `use_direct_io_for_flush_and_compaction = true` on write-heavy workloads for direct I/O on both compaction inputs and outputs. Rejected at Open when combined with `allow_mmap_reads = true`. No-op when `use_direct_reads = true` is already set (since all reads are already direct).
|
||||
|
||||
### Public API Changes
|
||||
* `DB::GetPreparedFileInfoForExternalSstIngestion()` can now prepare metadata for a live DB-generated SST file so it can be passed to `IngestExternalFileArg::file_infos`. The input path must exactly match a live table file owned by the source DB, and the returned metadata handle must outlive the ingestion.
|
||||
* `ExternalSstFileInfo` gained a `prepared_file_info` member produced by `SstFileWriter::Finish`, and `IngestExternalFileArg` gained a `file_infos` field: a vector of borrowed `const PreparedFileInfo*` pointers, parallel to `external_files`. The owning `ExternalSstFileInfo::prepared_file_info` handle must outlive the ingestion. When set, `IngestExternalFiles()` reuses the supplied per-file metadata instead of re-opening and scanning each file to recompute it, avoiding that extra I/O.
|
||||
* Corrected the public C++ option spelling from `memtable_veirfy_per_key_checksum_on_seek` to `memtable_verify_per_key_checksum_on_seek`. RocksDB continues to accept the old misspelled OPTIONS-file key for compatibility, while newly serialized OPTIONS files use the corrected key.
|
||||
* Added experimental read-scoped block buffer provider API, configured through `ReadOptions::read_scoped_block_buffer_provider`, for supported block-based table iterator scans and MultiScan reads to use caller-provided read-scoped storage for final data-block contents. When configured, supported provider-backed scan data-block reads bypass the data-block cache. The provider is ignored when mmap reads are enabled. RocksDB may still use ordinary temporary scratch for serialized block bytes, such as when a block may be compressed. Get and MultiGet do not currently provide API guarantees for this provider.
|
||||
* Added `FileSystem::SyncFile()` and `Env::SyncFile()` public APIs for syncing or fsyncing a file by name without requiring callers to reopen it as writable.
|
||||
|
||||
### Behavior Changes
|
||||
* The default `compression` for column families is now `kLZ4Compression` rather than `kSnappyCompression`. This affects only column families that do not explicitly set `compression`, and only newly-written SST files. The change is fully compatible: existing data remains readable with no migration needed, as RocksDB selects the decompressor per block. LZ4 offers slightly better compression ratios and decompression CPU efficiency vs. Snappy and similar compression CPU, tested across various server CPUs. When support is not compiled in, the fallback path for default compression is LZ4 -> Snappy -> NoCompression.
|
||||
* Disable parallel compression (ignore `CompressionOptions::parallel_threads`) for fast built-in compressors: Snappy, LZ4 (accelerated, not LZ4HC), and accelerated levels of ZSTD (level < 0). These do not generally benefit from parallel compression. This behavior can be overridden with a custom Compressor from a custom CompressionManager.
|
||||
* `PosixFileSystem::OptimizeForCompactionTableRead` now delegates to the base `FileSystem::OptimizeForCompactionTableRead` implementation before applying its Linux-only compaction-readahead clamp (added for #12038). The returned `FileOptions::use_direct_reads` therefore reflects `DBOptions::use_direct_reads`, consistent with the base class and with other `FileSystem` implementations. Previously the override ignored the base implementation and keyed both the returned options and the Linux readahead clamp off the incoming `FileOptions` rather than `DBOptions`. At the in-tree call site the incoming `FileOptions::use_direct_reads` already matches `DBOptions::use_direct_reads`, so this is effectively a no-op for existing configurations; it removes a latent inconsistency where a caller passing `FileOptions` whose `use_direct_reads` disagreed with `DBOptions` would have had the global flag silently ignored for compaction-input reads on Linux. (Note: the `#12038` readahead clamp still keys off the global `use_direct_reads`; it is not skipped for the compaction-only `use_direct_io_for_compaction_reads` path, since that flag enables O_DIRECT after this hook runs.)
|
||||
* Brought more unity to `kLZ4Compression` and `kLZ4HCCompression` by giving each access to the other's compression levels. Negative (fast) values previously only available to LZ4 and positive (slow) values previously only available to LZ4HC are now available to both, so configuring a non-default `compression_opts.level` now selects the LZ4 compressor variant. This is a behavior change for previously tolerated but dubious configurations such as positive compression level with `kLZ4Compression`. `kLZ4Compression` and `kLZ4HCCompression` keep their effective default levels, equivalent to -1 and 9 respectively.
|
||||
* Removed a discontinuity in `kZSTD` compression levels at `compression_opts.level == 0` by mapping that setting to `level = -1` rather than to `level = 3` as the ZSTD library does internally. This improves the compression auto-tuning landscape.
|
||||
|
||||
## 11.2.2 (05/07/2026)
|
||||
## 11.2.1 (04/29/2026)
|
||||
### Bug Fixes
|
||||
* Fixed a bug where the range lock manager would crash with an assertion failure when using a reverse comparator column family.
|
||||
|
||||
### Performance Improvements
|
||||
* Reduced commit latency when ingesting many external files by allowing `IngestExternalFileOptions::file_opening_threads` to open table readers for committed ingested files using multiple threads.
|
||||
* Reduced commit latency for large external file ingestions into the last level by adding `IngestExternalFileOptions::prefetch_lmax_index_and_filter_blocks`, which can skip commit-time index and filter block prefetching for cache-backed table metadata.
|
||||
|
||||
## 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
|
||||
@@ -35,6 +101,7 @@
|
||||
* 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -883,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 \
|
||||
@@ -925,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.
|
||||
@@ -986,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)' \
|
||||
@@ -1008,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)' \
|
||||
@@ -1226,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
|
||||
@@ -1243,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 .
|
||||
@@ -1268,6 +1331,8 @@ 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 \
|
||||
@@ -1283,6 +1348,7 @@ install-hooks:
|
||||
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 \
|
||||
@@ -1518,7 +1584,7 @@ db_open_with_config_test: $(OBJ_DIR)/db/db_open_with_config_test.o $(TEST_LIBRAR
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,13 @@ 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
|
||||
|
||||
|
||||
@@ -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()
|
||||
+26
-26
@@ -46,7 +46,7 @@ This document provides guidance on how to add new options to RocksDB's public AP
|
||||
|
||||
## Pattern 1: Adding a Standard Column Family Option
|
||||
|
||||
Example reference: commit `94e65a2e0b4f817aa4bfa4c96cdf867e7980d7bc` (memtable_veirfy_per_key_checksum_on_seek)
|
||||
Example reference: commit `94e65a2e0b4f817aa4bfa4c96cdf867e7980d7bc` (memtable_verify_per_key_checksum_on_seek)
|
||||
|
||||
### Step 1: Define the Option in Public Header
|
||||
|
||||
@@ -63,7 +63,7 @@ Add the option with documentation in `AdvancedColumnFamilyOptions` struct:
|
||||
// operation.
|
||||
// This option depends on memtable_protection_bytes_per_key to be non zero.
|
||||
// If memtable_protection_bytes_per_key is zero, no validation is performed.
|
||||
bool memtable_veirfy_per_key_checksum_on_seek = false;
|
||||
bool memtable_verify_per_key_checksum_on_seek = false;
|
||||
```
|
||||
|
||||
### Step 2: Add to Internal Options Structs
|
||||
@@ -74,14 +74,14 @@ Add to `MutableCFOptions` struct (or `ImmutableCFOptions` for immutable options)
|
||||
|
||||
```cpp
|
||||
// In MutableCFOptions constructor from Options:
|
||||
memtable_veirfy_per_key_checksum_on_seek(
|
||||
options.memtable_veirfy_per_key_checksum_on_seek),
|
||||
memtable_verify_per_key_checksum_on_seek(
|
||||
options.memtable_verify_per_key_checksum_on_seek),
|
||||
|
||||
// In MutableCFOptions default constructor:
|
||||
memtable_veirfy_per_key_checksum_on_seek(false),
|
||||
memtable_verify_per_key_checksum_on_seek(false),
|
||||
|
||||
// In MutableCFOptions struct member declarations:
|
||||
bool memtable_veirfy_per_key_checksum_on_seek;
|
||||
bool memtable_verify_per_key_checksum_on_seek;
|
||||
```
|
||||
|
||||
### Step 3: Register for Serialization/Deserialization
|
||||
@@ -91,9 +91,9 @@ bool memtable_veirfy_per_key_checksum_on_seek;
|
||||
Add to the options type info map for serialization:
|
||||
|
||||
```cpp
|
||||
{"memtable_veirfy_per_key_checksum_on_seek",
|
||||
{"memtable_verify_per_key_checksum_on_seek",
|
||||
{offsetof(struct MutableCFOptions,
|
||||
memtable_veirfy_per_key_checksum_on_seek),
|
||||
memtable_verify_per_key_checksum_on_seek),
|
||||
OptionType::kBoolean, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kMutable}},
|
||||
```
|
||||
@@ -101,8 +101,8 @@ Add to the options type info map for serialization:
|
||||
Add logging in `MutableCFOptions::Dump()`:
|
||||
|
||||
```cpp
|
||||
ROCKS_LOG_INFO(log, "memtable_veirfy_per_key_checksum_on_seek: %d",
|
||||
memtable_veirfy_per_key_checksum_on_seek);
|
||||
ROCKS_LOG_INFO(log, "memtable_verify_per_key_checksum_on_seek: %d",
|
||||
memtable_verify_per_key_checksum_on_seek);
|
||||
```
|
||||
|
||||
### Step 4: Update Options Helper
|
||||
@@ -112,8 +112,8 @@ ROCKS_LOG_INFO(log, "memtable_veirfy_per_key_checksum_on_seek: %d",
|
||||
Add to `UpdateColumnFamilyOptions()`:
|
||||
|
||||
```cpp
|
||||
cf_opts->memtable_veirfy_per_key_checksum_on_seek =
|
||||
moptions.memtable_veirfy_per_key_checksum_on_seek;
|
||||
cf_opts->memtable_verify_per_key_checksum_on_seek =
|
||||
moptions.memtable_verify_per_key_checksum_on_seek;
|
||||
```
|
||||
|
||||
### Step 5: Add to Options Settable Test
|
||||
@@ -123,7 +123,7 @@ cf_opts->memtable_veirfy_per_key_checksum_on_seek =
|
||||
Add to the test string in `ColumnFamilyOptionsAllFieldsSettable`:
|
||||
|
||||
```cpp
|
||||
"memtable_veirfy_per_key_checksum_on_seek=1;"
|
||||
"memtable_verify_per_key_checksum_on_seek=1;"
|
||||
```
|
||||
|
||||
### Step 6: Add db_stress Support
|
||||
@@ -131,23 +131,23 @@ Add to the test string in `ColumnFamilyOptionsAllFieldsSettable`:
|
||||
**File: `db_stress_tool/db_stress_common.h`**
|
||||
|
||||
```cpp
|
||||
DECLARE_bool(memtable_veirfy_per_key_checksum_on_seek);
|
||||
DECLARE_bool(memtable_verify_per_key_checksum_on_seek);
|
||||
```
|
||||
|
||||
**File: `db_stress_tool/db_stress_gflags.cc`**
|
||||
|
||||
```cpp
|
||||
DEFINE_bool(
|
||||
memtable_veirfy_per_key_checksum_on_seek,
|
||||
ROCKSDB_NAMESPACE::Options().memtable_veirfy_per_key_checksum_on_seek,
|
||||
"Sets CF option memtable_veirfy_per_key_checksum_on_seek.");
|
||||
memtable_verify_per_key_checksum_on_seek,
|
||||
ROCKSDB_NAMESPACE::Options().memtable_verify_per_key_checksum_on_seek,
|
||||
"Sets CF option memtable_verify_per_key_checksum_on_seek.");
|
||||
```
|
||||
|
||||
**File: `db_stress_tool/db_stress_test_base.cc`**
|
||||
|
||||
```cpp
|
||||
options.memtable_veirfy_per_key_checksum_on_seek =
|
||||
FLAGS_memtable_veirfy_per_key_checksum_on_seek;
|
||||
options.memtable_verify_per_key_checksum_on_seek =
|
||||
FLAGS_memtable_verify_per_key_checksum_on_seek;
|
||||
```
|
||||
|
||||
### Step 7: Add db_bench Support
|
||||
@@ -156,12 +156,12 @@ options.memtable_veirfy_per_key_checksum_on_seek =
|
||||
|
||||
```cpp
|
||||
// Flag definition (near related flags):
|
||||
DEFINE_bool(memtable_veirfy_per_key_checksum_on_seek, false,
|
||||
"Sets CF option memtable_veirfy_per_key_checksum_on_seek");
|
||||
DEFINE_bool(memtable_verify_per_key_checksum_on_seek, false,
|
||||
"Sets CF option memtable_verify_per_key_checksum_on_seek");
|
||||
|
||||
// Apply flag to options (in InitializeOptionsFromFlags or similar):
|
||||
options.memtable_veirfy_per_key_checksum_on_seek =
|
||||
FLAGS_memtable_veirfy_per_key_checksum_on_seek;
|
||||
options.memtable_verify_per_key_checksum_on_seek =
|
||||
FLAGS_memtable_verify_per_key_checksum_on_seek;
|
||||
```
|
||||
|
||||
### Step 8: Add Crash Test Support
|
||||
@@ -169,7 +169,7 @@ options.memtable_veirfy_per_key_checksum_on_seek =
|
||||
**File: `tools/db_crashtest.py`**
|
||||
|
||||
```python
|
||||
"memtable_veirfy_per_key_checksum_on_seek": lambda: random.choice([0] * 7 + [1]),
|
||||
"memtable_verify_per_key_checksum_on_seek": lambda: random.choice([0] * 7 + [1]),
|
||||
```
|
||||
|
||||
Also add constraint handling in `finalize_and_sanitize()` if needed:
|
||||
@@ -177,7 +177,7 @@ Also add constraint handling in `finalize_and_sanitize()` if needed:
|
||||
```python
|
||||
# only skip list memtable representation supports paranoid memory checks
|
||||
if dest_params.get("memtablerep") != "skip_list":
|
||||
dest_params["memtable_veirfy_per_key_checksum_on_seek"] = 0
|
||||
dest_params["memtable_verify_per_key_checksum_on_seek"] = 0
|
||||
```
|
||||
|
||||
### Step 9: Add Release Note
|
||||
@@ -185,7 +185,7 @@ if dest_params.get("memtablerep") != "skip_list":
|
||||
**File: `unreleased_history/new_features/<descriptive_name>.md`**
|
||||
|
||||
```markdown
|
||||
A new flag memtable_veirfy_per_key_checksum_on_seek is added to AdvancedColumnFamilyOptions. When it is enabled, it will validate key checksum along the binary search path on skiplist based memtable during seek operation.
|
||||
A new flag memtable_verify_per_key_checksum_on_seek is added to AdvancedColumnFamilyOptions. When it is enabled, it will validate key checksum along the binary search path on skiplist based memtable during seek operation.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -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.
|
||||
@@ -1,14 +1,54 @@
|
||||
Read review-findings.md. This file contains partial code
|
||||
review findings from a review session that ran out of turns before
|
||||
finishing. Format them into a clean final review comment using the
|
||||
output format defined in claude_md/code_review.md (Summary, Issues
|
||||
Found, Cross-Component Analysis, Positive Observations).
|
||||
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.
|
||||
|
||||
At the top of the review, add this notice:
|
||||
> **Partial review** -- the analysis was interrupted before all
|
||||
> perspectives were completed. The findings below cover the
|
||||
> perspectives that were finished. You can request a full re-review
|
||||
> with `/claude-review`.
|
||||
## Required Output Structure
|
||||
|
||||
Write ONLY the formatted review as your final response -- no
|
||||
commentary or explanation outside the review itself.
|
||||
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.
|
||||
@@ -481,6 +481,61 @@ Team lead synthesizes the debate into a consensus document:
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+132
-26
@@ -30,13 +30,78 @@ inline static SequenceNumber GetSeqNum(const DBImpl* db, const Snapshot* s) {
|
||||
Status ArenaWrappedDBIter::GetProperty(std::string prop_name,
|
||||
std::string* prop) {
|
||||
if (prop_name == "rocksdb.iterator.super-version-number") {
|
||||
if (!internal_iter_initialized_) {
|
||||
*prop = std::to_string(sv_number_);
|
||||
return Status::OK();
|
||||
}
|
||||
// First try to pass the value returned from inner iterator.
|
||||
if (!db_iter_->GetProperty(prop_name, prop).ok()) {
|
||||
*prop = std::to_string(sv_number_);
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
return db_iter_->GetProperty(prop_name, prop);
|
||||
if (!EnsureInternalIteratorInitialized(nullptr).ok()) {
|
||||
return db_iter_->status();
|
||||
}
|
||||
return db_iter_->GetProperty(std::move(prop_name), prop);
|
||||
}
|
||||
|
||||
void ArenaWrappedDBIter::CleanupDeferredSuperVersion() {
|
||||
if (deferred_sv_ != nullptr) {
|
||||
assert(db_impl_ != nullptr);
|
||||
db_impl_->CleanupIteratorSuperVersion(
|
||||
deferred_sv_, read_options_.background_purge_on_iterator_cleanup);
|
||||
deferred_sv_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ArenaWrappedDBIter::DestroyDBIter() {
|
||||
db_iter_->~DBIter();
|
||||
CleanupDeferredSuperVersion();
|
||||
}
|
||||
|
||||
void ArenaWrappedDBIter::ColumnFamilyDataUnrefDeleter::operator()(
|
||||
ColumnFamilyData* cfd) const {
|
||||
if (cfd == nullptr) {
|
||||
return;
|
||||
}
|
||||
assert(db_impl != nullptr);
|
||||
|
||||
InstrumentedMutexLock lock(db_impl->mutex());
|
||||
cfd->UnrefAndTryDelete();
|
||||
}
|
||||
|
||||
void ArenaWrappedDBIter::DestroyDBIterAndArena() {
|
||||
DestroyDBIter();
|
||||
arena_.~Arena();
|
||||
}
|
||||
|
||||
Status ArenaWrappedDBIter::EnsureInternalIteratorInitialized(
|
||||
const MultiScanArgs* scan_opts) {
|
||||
if (internal_iter_initialized_) {
|
||||
return Status::OK();
|
||||
}
|
||||
if (db_impl_ == nullptr || deferred_cfd_ == nullptr ||
|
||||
deferred_sv_ == nullptr) {
|
||||
Status s = Status::InvalidArgument(
|
||||
"Internal iterator cannot be initialized without deferred DB state");
|
||||
db_iter_->set_status(s);
|
||||
db_iter_->set_valid(false);
|
||||
return s;
|
||||
}
|
||||
|
||||
const MultiScanArgs* pruning_scan_opts =
|
||||
scan_opts != nullptr && scan_opts->HasBoundedScanRanges() ? scan_opts
|
||||
: nullptr;
|
||||
child_read_options_ = read_options_;
|
||||
child_read_options_.snapshot = nullptr;
|
||||
InternalIterator* internal_iter = db_impl_->NewInternalIterator(
|
||||
child_read_options_, deferred_cfd_, deferred_sv_, &arena_, sequence_,
|
||||
/*allow_unprepared_value=*/true, this, pruning_scan_opts);
|
||||
deferred_cfd_ = nullptr;
|
||||
deferred_sv_ = nullptr;
|
||||
SetIterUnderDBIterImpl(internal_iter);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void ArenaWrappedDBIter::Init(
|
||||
@@ -44,18 +109,26 @@ void ArenaWrappedDBIter::Init(
|
||||
const MutableCFOptions& mutable_cf_options, const Version* version,
|
||||
const SequenceNumber& sequence, uint64_t version_number,
|
||||
ReadCallback* read_callback, ColumnFamilyHandleImpl* cfh,
|
||||
bool expose_blob_index, bool allow_refresh, ReadOnlyMemTable* active_mem) {
|
||||
bool expose_blob_index, bool allow_refresh, ReadOnlyMemTable* active_mem,
|
||||
DBImpl* db_impl, ColumnFamilyData* cfd) {
|
||||
read_options_ = read_options;
|
||||
child_read_options_ = read_options;
|
||||
if (!CheckFSFeatureSupport(env->GetFileSystem().get(),
|
||||
FSSupportedOps::kAsyncIO)) {
|
||||
read_options_.async_io = false;
|
||||
}
|
||||
read_options_.total_order_seek |= ioptions.prefix_seek_opt_in_only;
|
||||
|
||||
db_iter_ = DBIter::NewIter(
|
||||
env, read_options_, ioptions, mutable_cf_options,
|
||||
ioptions.user_comparator, /*internal_iter=*/nullptr, version, sequence,
|
||||
read_callback, active_mem, cfh, expose_blob_index, &arena_);
|
||||
if (cfh != nullptr) {
|
||||
db_impl = cfh->db();
|
||||
cfd = cfh->cfd();
|
||||
}
|
||||
|
||||
db_iter_ = DBIter::NewIter(env, read_options_, ioptions, mutable_cf_options,
|
||||
ioptions.user_comparator,
|
||||
/*internal_iter=*/nullptr, version, sequence,
|
||||
read_callback, active_mem, /*cfh=*/nullptr,
|
||||
expose_blob_index, &arena_, db_impl, cfd);
|
||||
|
||||
sv_number_ = version_number;
|
||||
allow_refresh_ = allow_refresh;
|
||||
@@ -65,13 +138,13 @@ void ArenaWrappedDBIter::Init(
|
||||
|
||||
void ArenaWrappedDBIter::MaybeAutoRefresh(bool is_seek,
|
||||
DBIter::Direction direction) {
|
||||
if (cfh_ != nullptr && read_options_.snapshot != nullptr && allow_refresh_ &&
|
||||
read_options_.auto_refresh_iterator_with_snapshot) {
|
||||
if (cfd_ref_ != nullptr && read_options_.snapshot != nullptr &&
|
||||
allow_refresh_ && read_options_.auto_refresh_iterator_with_snapshot) {
|
||||
// The intent here is to capture the superversion number change
|
||||
// reasonably soon from the time it actually happened. As such,
|
||||
// we're fine with weaker synchronization / ordering guarantees
|
||||
// provided by relaxed atomic (in favor of less CPU / mem overhead).
|
||||
uint64_t cur_sv_number = cfh_->cfd()->GetSuperVersionNumberRelaxed();
|
||||
uint64_t cur_sv_number = cfd_ref_->GetSuperVersionNumberRelaxed();
|
||||
if ((sv_number_ != cur_sv_number) && status().ok()) {
|
||||
// Changing iterators' direction is pretty heavy-weight operation and
|
||||
// could have unintended consequences when it comes to prefix seek.
|
||||
@@ -150,13 +223,13 @@ void ArenaWrappedDBIter::DoRefresh(const Snapshot* snapshot,
|
||||
// present in the error log, but won't be reflected in the iterator status.
|
||||
// This is by design as we expect compaction to clean up those obsolete files
|
||||
// eventually.
|
||||
db_iter_->~DBIter();
|
||||
|
||||
arena_.~Arena();
|
||||
DestroyDBIterAndArena();
|
||||
new (&arena_) Arena();
|
||||
|
||||
auto cfd = cfh_->cfd();
|
||||
auto db_impl = cfh_->db();
|
||||
auto cfd = cfd_ref_.get();
|
||||
auto db_impl = db_impl_;
|
||||
assert(cfd != nullptr);
|
||||
assert(db_impl != nullptr);
|
||||
|
||||
SuperVersion* sv = cfd->GetReferencedSuperVersion(db_impl);
|
||||
assert(sv->version_number >= sv_number);
|
||||
@@ -164,23 +237,27 @@ void ArenaWrappedDBIter::DoRefresh(const Snapshot* snapshot,
|
||||
if (read_callback_) {
|
||||
read_callback_->Refresh(read_seq);
|
||||
}
|
||||
// TODO: Preserve Prepare() scan options across Refresh() so a refreshed
|
||||
// MultiScan iterator can rebuild the same pruned tree.
|
||||
Init(env, read_options_, cfd->ioptions(), sv->mutable_cf_options, sv->current,
|
||||
read_seq, sv->version_number, read_callback_, cfh_, expose_blob_index_,
|
||||
allow_refresh_, allow_mark_memtable_for_flush_ ? sv->mem : nullptr);
|
||||
read_seq, sv->version_number, read_callback_, nullptr,
|
||||
expose_blob_index_, allow_refresh_,
|
||||
allow_mark_memtable_for_flush_ ? sv->mem : nullptr, db_impl, cfd);
|
||||
|
||||
InternalIterator* internal_iter = db_impl->NewInternalIterator(
|
||||
read_options_, cfd, sv, &arena_, read_seq,
|
||||
/* allow_unprepared_value */ true, /* db_iter */ this);
|
||||
SetIterUnderDBIter(internal_iter);
|
||||
internal_iter_initialized_ = true;
|
||||
}
|
||||
|
||||
Status ArenaWrappedDBIter::Refresh(const Snapshot* snapshot) {
|
||||
if (cfh_ == nullptr || !allow_refresh_) {
|
||||
if (cfd_ref_ == nullptr || db_impl_ == nullptr || !allow_refresh_) {
|
||||
return Status::NotSupported("Creating renew iterator is not allowed.");
|
||||
}
|
||||
assert(db_iter_ != nullptr);
|
||||
auto cfd = cfh_->cfd();
|
||||
auto db_impl = cfh_->db();
|
||||
auto cfd = cfd_ref_.get();
|
||||
auto db_impl = db_impl_;
|
||||
|
||||
// TODO(yiwu): For last_seq_same_as_publish_seq_==false, this is not the
|
||||
// correct behavior. Will be corrected automatically when we take a snapshot
|
||||
@@ -193,6 +270,13 @@ Status ArenaWrappedDBIter::Refresh(const Snapshot* snapshot) {
|
||||
TEST_SYNC_POINT("ArenaWrappedDBIter::Refresh:1");
|
||||
TEST_SYNC_POINT("ArenaWrappedDBIter::Refresh:2");
|
||||
|
||||
if (!internal_iter_initialized_) {
|
||||
Status s = EnsureInternalIteratorInitialized(nullptr);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (sv_number_ != cur_sv_number) {
|
||||
DoRefresh(snapshot, cur_sv_number);
|
||||
@@ -250,25 +334,47 @@ Status ArenaWrappedDBIter::Refresh(const Snapshot* snapshot) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void ArenaWrappedDBIter::Prepare(const MultiScanArgs& scan_opts) {
|
||||
if (prepare_called_) {
|
||||
db_iter_->set_status(Status::InvalidArgument(
|
||||
"Prepare called more than once on the same iterator"));
|
||||
db_iter_->set_valid(false);
|
||||
return;
|
||||
}
|
||||
prepare_called_ = true;
|
||||
|
||||
Status s = db_iter_->SetScanOptionsForPrepare(scan_opts);
|
||||
if (!s.ok()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const MultiScanArgs* pruning_scan_opts =
|
||||
scan_opts.HasBoundedScanRanges() ? &scan_opts : nullptr;
|
||||
s = EnsureInternalIteratorInitialized(pruning_scan_opts);
|
||||
if (!s.ok()) {
|
||||
return;
|
||||
}
|
||||
|
||||
db_iter_->PrepareInternalChildren();
|
||||
}
|
||||
|
||||
ArenaWrappedDBIter* NewArenaWrappedDbIterator(
|
||||
Env* env, const ReadOptions& read_options, ColumnFamilyHandleImpl* cfh,
|
||||
SuperVersion* sv, const SequenceNumber& sequence,
|
||||
ReadCallback* read_callback, DBImpl* db_impl, bool expose_blob_index,
|
||||
bool allow_refresh, bool allow_mark_memtable_for_flush) {
|
||||
ArenaWrappedDBIter* db_iter = new ArenaWrappedDBIter();
|
||||
ColumnFamilyData* cfd = cfh->cfd();
|
||||
db_iter->Init(env, read_options, cfh->cfd()->ioptions(),
|
||||
sv->mutable_cf_options, sv->current, sequence,
|
||||
sv->version_number, read_callback, cfh, expose_blob_index,
|
||||
allow_refresh,
|
||||
allow_mark_memtable_for_flush ? sv->mem : nullptr);
|
||||
if (cfh != nullptr && allow_refresh) {
|
||||
db_iter->StoreRefreshInfo(cfh, read_callback, expose_blob_index);
|
||||
if (allow_refresh && cfd != nullptr) {
|
||||
db_iter->StoreRefreshInfo(read_callback, expose_blob_index);
|
||||
}
|
||||
|
||||
InternalIterator* internal_iter = db_impl->NewInternalIterator(
|
||||
db_iter->GetReadOptions(), cfh->cfd(), sv, db_iter->GetArena(), sequence,
|
||||
/*allow_unprepared_value=*/true, db_iter);
|
||||
db_iter->SetIterUnderDBIter(internal_iter);
|
||||
db_iter->StoreDeferredInitInfo(db_impl, cfd, sv, sequence,
|
||||
allow_mark_memtable_for_flush);
|
||||
|
||||
return db_iter;
|
||||
}
|
||||
|
||||
+74
-17
@@ -10,6 +10,7 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "db/db_impl/db_impl.h"
|
||||
@@ -33,10 +34,17 @@ class Version;
|
||||
// When using the class's Iterator interface, the behavior is exactly
|
||||
// the same as the inner DBIter.
|
||||
class ArenaWrappedDBIter : public Iterator {
|
||||
struct ColumnFamilyDataUnrefDeleter {
|
||||
DBImpl* db_impl = nullptr;
|
||||
void operator()(ColumnFamilyData* cfd) const;
|
||||
};
|
||||
using ColumnFamilyDataRef =
|
||||
std::unique_ptr<ColumnFamilyData, ColumnFamilyDataUnrefDeleter>;
|
||||
|
||||
public:
|
||||
~ArenaWrappedDBIter() override {
|
||||
if (db_iter_ != nullptr) {
|
||||
db_iter_->~DBIter();
|
||||
DestroyDBIter();
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
@@ -51,7 +59,7 @@ class ArenaWrappedDBIter : public Iterator {
|
||||
// Set the internal iterator wrapped inside the DB Iterator. Usually it is
|
||||
// a merging iterator.
|
||||
virtual void SetIterUnderDBIter(InternalIterator* iter) {
|
||||
db_iter_->SetIter(iter);
|
||||
SetIterUnderDBIterImpl(iter);
|
||||
}
|
||||
|
||||
void SetMemtableRangetombstoneIter(
|
||||
@@ -60,26 +68,48 @@ class ArenaWrappedDBIter : public Iterator {
|
||||
}
|
||||
|
||||
bool Valid() const override { return db_iter_->Valid(); }
|
||||
void SeekToFirst() override { db_iter_->SeekToFirst(); }
|
||||
void SeekToLast() override { db_iter_->SeekToLast(); }
|
||||
void SeekToFirst() override {
|
||||
if (!EnsureInternalIteratorInitialized(nullptr).ok()) {
|
||||
return;
|
||||
}
|
||||
db_iter_->SeekToFirst();
|
||||
}
|
||||
void SeekToLast() override {
|
||||
if (!EnsureInternalIteratorInitialized(nullptr).ok()) {
|
||||
return;
|
||||
}
|
||||
db_iter_->SeekToLast();
|
||||
}
|
||||
// 'target' does not contain timestamp, even if user timestamp feature is
|
||||
// enabled.
|
||||
void Seek(const Slice& target) override {
|
||||
if (!EnsureInternalIteratorInitialized(nullptr).ok()) {
|
||||
return;
|
||||
}
|
||||
MaybeAutoRefresh(true /* is_seek */, DBIter::kForward);
|
||||
db_iter_->Seek(target);
|
||||
}
|
||||
|
||||
void SeekForPrev(const Slice& target) override {
|
||||
if (!EnsureInternalIteratorInitialized(nullptr).ok()) {
|
||||
return;
|
||||
}
|
||||
MaybeAutoRefresh(true /* is_seek */, DBIter::kReverse);
|
||||
db_iter_->SeekForPrev(target);
|
||||
}
|
||||
|
||||
void Next() override {
|
||||
if (!EnsureInternalIteratorInitialized(nullptr).ok()) {
|
||||
return;
|
||||
}
|
||||
db_iter_->Next();
|
||||
MaybeAutoRefresh(false /* is_seek */, DBIter::kForward);
|
||||
}
|
||||
|
||||
void Prev() override {
|
||||
if (!EnsureInternalIteratorInitialized(nullptr).ok()) {
|
||||
return;
|
||||
}
|
||||
db_iter_->Prev();
|
||||
MaybeAutoRefresh(false /* is_seek */, DBIter::kReverse);
|
||||
}
|
||||
@@ -96,12 +126,13 @@ class ArenaWrappedDBIter : public Iterator {
|
||||
Status Refresh() override;
|
||||
Status Refresh(const Snapshot*) override;
|
||||
|
||||
bool PrepareValue() override { return db_iter_->PrepareValue(); }
|
||||
|
||||
void Prepare(const MultiScanArgs& scan_opts) override {
|
||||
db_iter_->Prepare(scan_opts);
|
||||
bool PrepareValue() override {
|
||||
return EnsureInternalIteratorInitialized(nullptr).ok() &&
|
||||
db_iter_->PrepareValue();
|
||||
}
|
||||
|
||||
void Prepare(const MultiScanArgs& scan_opts) override;
|
||||
|
||||
// FIXME: we could just pass SV in for mutable cf option, version and version
|
||||
// number, but this is used by SstFileReader which does not have a SV.
|
||||
void Init(Env* env, const ReadOptions& read_options,
|
||||
@@ -110,27 +141,53 @@ class ArenaWrappedDBIter : public Iterator {
|
||||
const SequenceNumber& sequence, uint64_t version_number,
|
||||
ReadCallback* read_callback, ColumnFamilyHandleImpl* cfh,
|
||||
bool expose_blob_index, bool allow_refresh,
|
||||
ReadOnlyMemTable* active_mem);
|
||||
ReadOnlyMemTable* active_mem, DBImpl* db_impl = nullptr,
|
||||
ColumnFamilyData* cfd = nullptr);
|
||||
|
||||
// Store some parameters so we can refresh the iterator at a later point
|
||||
// with these same params
|
||||
void StoreRefreshInfo(ColumnFamilyHandleImpl* cfh,
|
||||
ReadCallback* read_callback, bool expose_blob_index) {
|
||||
cfh_ = cfh;
|
||||
// Store parameters used only by explicit/auto-refresh.
|
||||
void StoreRefreshInfo(ReadCallback* read_callback, bool expose_blob_index) {
|
||||
read_callback_ = read_callback;
|
||||
expose_blob_index_ = expose_blob_index;
|
||||
}
|
||||
|
||||
void StoreDeferredInitInfo(DBImpl* db_impl, ColumnFamilyData* cfd,
|
||||
SuperVersion* sv, const SequenceNumber& sequence,
|
||||
bool allow_mark_memtable_for_flush) {
|
||||
assert(cfd != nullptr);
|
||||
db_impl_ = db_impl;
|
||||
cfd->Ref();
|
||||
cfd_ref_ = ColumnFamilyDataRef(cfd, ColumnFamilyDataUnrefDeleter{db_impl});
|
||||
deferred_cfd_ = cfd;
|
||||
deferred_sv_ = sv;
|
||||
sequence_ = sequence;
|
||||
allow_mark_memtable_for_flush_ = allow_mark_memtable_for_flush;
|
||||
}
|
||||
|
||||
private:
|
||||
Status EnsureInternalIteratorInitialized(const MultiScanArgs* scan_opts);
|
||||
void SetIterUnderDBIterImpl(InternalIterator* iter) {
|
||||
db_iter_->SetIter(iter);
|
||||
internal_iter_initialized_ = true;
|
||||
}
|
||||
void CleanupDeferredSuperVersion();
|
||||
void DestroyDBIter();
|
||||
void DestroyDBIterAndArena();
|
||||
void DoRefresh(const Snapshot* snapshot, uint64_t sv_number);
|
||||
void MaybeAutoRefresh(bool is_seek, DBIter::Direction direction);
|
||||
|
||||
DBIter* db_iter_ = nullptr;
|
||||
Arena arena_;
|
||||
uint64_t sv_number_;
|
||||
ColumnFamilyHandleImpl* cfh_ = nullptr;
|
||||
uint64_t sv_number_ = 0;
|
||||
DBImpl* db_impl_ = nullptr;
|
||||
ColumnFamilyDataRef cfd_ref_{nullptr, ColumnFamilyDataUnrefDeleter{}};
|
||||
ColumnFamilyData* deferred_cfd_ = nullptr;
|
||||
SuperVersion* deferred_sv_ = nullptr;
|
||||
SequenceNumber sequence_ = kMaxSequenceNumber;
|
||||
bool internal_iter_initialized_ = false;
|
||||
bool prepare_called_ = false;
|
||||
ReadOptions read_options_;
|
||||
ReadCallback* read_callback_;
|
||||
ReadOptions child_read_options_;
|
||||
ReadCallback* read_callback_ = nullptr;
|
||||
bool expose_blob_index_ = false;
|
||||
bool allow_refresh_ = true;
|
||||
bool allow_mark_memtable_for_flush_ = true;
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "logging/logging.h"
|
||||
#include "options/cf_options.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/status.h"
|
||||
#include "test_util/sync_point.h"
|
||||
@@ -200,6 +201,8 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
|
||||
{
|
||||
assert(file_options_);
|
||||
fo_copy = *file_options_;
|
||||
fo_copy.open_contract = FileOpenContract::kNoReopenForWrite |
|
||||
FileOpenContract::kNoReadersWhileOpenForWrite;
|
||||
fo_copy.write_hint = write_hint_;
|
||||
Status s = NewWritableFile(fs_, blob_file_path, &file, fo_copy);
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ Status BlobFileCache::OpenBlobFileReaderUncached(
|
||||
Statistics* const statistics = immutable_options_->stats;
|
||||
RecordTick(statistics, NO_FILE_OPENS);
|
||||
|
||||
assert(file_options_);
|
||||
Status s = BlobFileReader::Create(
|
||||
*immutable_options_, read_options, *file_options_, column_family_id_,
|
||||
blob_file_read_hist_, blob_file_number, io_tracer_,
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "file/read_write_util.h"
|
||||
#include "file/writable_file_writer.h"
|
||||
#include "logging/logging.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "util/aligned_buffer.h"
|
||||
#include "util/compression.h"
|
||||
#include "util/mutexlock.h"
|
||||
@@ -54,13 +55,14 @@ class RoundRobinBlobFilePartitionStrategy : public BlobFilePartitionStrategy {
|
||||
};
|
||||
|
||||
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) {
|
||||
CompressionType compression, const CompressionOptions& compression_opts) {
|
||||
assert(compression <= kLastBuiltinCompression);
|
||||
|
||||
static thread_local std::array<DirectWriteCompressionState,
|
||||
@@ -71,12 +73,15 @@ DirectWriteCompressionState& GetDirectWriteCompressionState(
|
||||
auto& compression_state =
|
||||
compression_states[static_cast<size_t>(compression)];
|
||||
if (compression != kNoCompression &&
|
||||
compression_state.compressor == nullptr) {
|
||||
// BDW v1 mirrors BlobFileBuilder by using the built-in compressor with the
|
||||
// default CompressionOptions only. Cache it per thread so repeated writes
|
||||
// do not allocate a new compressor and working area every time.
|
||||
(compression_state.compressor == 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(CompressionOptions{},
|
||||
GetBuiltinV2CompressionManager()->GetCompressor(compression_opts,
|
||||
compression);
|
||||
if (compression_state.compressor != nullptr) {
|
||||
compression_state.working_area =
|
||||
@@ -195,7 +200,9 @@ Status BlobFilePartitionManager::OpenNewBlobFile(Partition* partition,
|
||||
}
|
||||
|
||||
std::unique_ptr<FSWritableFile> file;
|
||||
Status s = NewWritableFile(fs_, blob_file_path, &file, file_options_);
|
||||
FileOptions writer_file_options = file_options_;
|
||||
writer_file_options.open_contract = FileOpenContract::kNoReopenForWrite;
|
||||
Status s = NewWritableFile(fs_, blob_file_path, &file, writer_file_options);
|
||||
if (!s.ok()) {
|
||||
RemoveFilePartitionMapping(blob_file_number);
|
||||
return s;
|
||||
@@ -204,7 +211,7 @@ Status BlobFilePartitionManager::OpenNewBlobFile(Partition* partition,
|
||||
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_,
|
||||
std::move(file), blob_file_path, writer_file_options, clock_, io_tracer_,
|
||||
statistics_, Histograms::BLOB_DB_BLOB_FILE_WRITE_MICROS, listeners_,
|
||||
file_checksum_gen_factory_, perform_data_verification);
|
||||
|
||||
@@ -390,15 +397,15 @@ bool BlobFilePartitionManager::MarkSealedFileGarbage(
|
||||
}
|
||||
|
||||
Status BlobFilePartitionManager::MaybePrepopulateBlobCache(
|
||||
const BlobDirectWriteSettings* settings, const Slice& original_value,
|
||||
const BlobDirectWriteSettings& settings, const Slice& original_value,
|
||||
uint64_t blob_file_number, uint64_t blob_offset) {
|
||||
if (settings == nullptr || settings->blob_cache == nullptr ||
|
||||
settings->prepopulate_blob_cache != PrepopulateBlobCache::kFlushOnly) {
|
||||
if (settings.blob_cache == nullptr ||
|
||||
settings.prepopulate_blob_cache != PrepopulateBlobCache::kFlushOnly) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
FullTypedCacheInterface<BlobContents, BlobContentsCreator> blob_cache{
|
||||
settings->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);
|
||||
@@ -419,7 +426,7 @@ 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) {
|
||||
const BlobDirectWriteSettings& settings, const uint32_t* partition_idx) {
|
||||
assert(blob_file_number != nullptr);
|
||||
assert(blob_offset != nullptr);
|
||||
assert(blob_size != nullptr);
|
||||
@@ -431,7 +438,8 @@ Status BlobFilePartitionManager::WriteBlob(
|
||||
GrowableBuffer compressed_value;
|
||||
Slice write_value = value;
|
||||
if (compression != kNoCompression) {
|
||||
auto& compression_state = GetDirectWriteCompressionState(compression);
|
||||
auto& compression_state =
|
||||
GetDirectWriteCompressionState(compression, settings.compression_opts);
|
||||
if (compression_state.compressor == nullptr) {
|
||||
return Status::NotSupported(
|
||||
"Blob direct write compression type not supported");
|
||||
@@ -781,13 +789,19 @@ Status BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
|
||||
}
|
||||
}
|
||||
|
||||
Status s = version != nullptr ? Status::Corruption("Invalid blob file number")
|
||||
: Status::NotFound();
|
||||
|
||||
if (blob_file_cache == nullptr) {
|
||||
return s;
|
||||
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,
|
||||
|
||||
@@ -77,7 +77,7 @@ class BlobFilePartitionManager {
|
||||
CompressionType compression, const Slice& key,
|
||||
const Slice& value, uint64_t* blob_file_number,
|
||||
uint64_t* blob_offset, uint64_t* blob_size,
|
||||
const BlobDirectWriteSettings* settings = nullptr,
|
||||
const BlobDirectWriteSettings& settings,
|
||||
const uint32_t* partition_idx = nullptr);
|
||||
|
||||
// Selects the partition to use for all blob-backed columns of one PutEntity
|
||||
@@ -239,8 +239,8 @@ class BlobFilePartitionManager {
|
||||
static bool MarkSealedFileGarbage(std::vector<SealedFile>* sealed_files,
|
||||
uint64_t file_number, uint64_t blob_count,
|
||||
uint64_t blob_bytes);
|
||||
// Optionally seeds the blob cache with the original uncompressed value.
|
||||
Status MaybePrepopulateBlobCache(const BlobDirectWriteSettings* settings,
|
||||
// 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);
|
||||
|
||||
+35
-31
@@ -12,6 +12,7 @@
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "file/file_prefetch_buffer.h"
|
||||
#include "file/filename.h"
|
||||
#include "file/read_write_util.h"
|
||||
#include "monitoring/statistics_impl.h"
|
||||
#include "options/cf_options.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
@@ -20,6 +21,7 @@
|
||||
#include "table/format.h"
|
||||
#include "table/multiget_context.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/aligned_buffer.h"
|
||||
#include "util/compression.h"
|
||||
#include "util/stop_watch.h"
|
||||
|
||||
@@ -105,24 +107,6 @@ Status BlobFileReader::OpenFile(
|
||||
|
||||
constexpr IODebugContext* dbg = nullptr;
|
||||
|
||||
{
|
||||
TEST_SYNC_POINT("BlobFileReader::OpenFile:GetFileSize");
|
||||
|
||||
const Status s =
|
||||
fs->GetFileSize(blob_file_path, IOOptions(), file_size, dbg);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -142,6 +126,24 @@ Status BlobFileReader::OpenFile(
|
||||
|
||||
assert(file);
|
||||
|
||||
{
|
||||
Status s = GetFileSizeFromOpenFileOrPath(
|
||||
file.get(), fs, blob_file_path, file_size, dbg,
|
||||
FileSizeFallback::kNotSupportedOnly,
|
||||
[]() { TEST_SYNC_POINT("BlobFileReader::OpenFile:GetFileSize"); });
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
if (immutable_options.advise_random_on_open) {
|
||||
file->Hint(FSRandomAccessFile::kRandom);
|
||||
}
|
||||
@@ -165,7 +167,7 @@ Status BlobFileReader::ReadHeader(const RandomAccessFileReader* file_reader,
|
||||
|
||||
Slice header_slice;
|
||||
Buffer buf;
|
||||
AlignedBuf aligned_buf;
|
||||
AlignedBuffer direct_io_buffer;
|
||||
|
||||
{
|
||||
TEST_SYNC_POINT("BlobFileReader::ReadHeader:ReadFromFile");
|
||||
@@ -175,7 +177,7 @@ Status BlobFileReader::ReadHeader(const RandomAccessFileReader* file_reader,
|
||||
|
||||
const Status s =
|
||||
ReadFromFile(file_reader, read_options, read_offset, read_size,
|
||||
statistics, &header_slice, &buf, &aligned_buf);
|
||||
statistics, &header_slice, &buf, &direct_io_buffer);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -216,7 +218,7 @@ Status BlobFileReader::ReadFooter(const RandomAccessFileReader* file_reader,
|
||||
|
||||
Slice footer_slice;
|
||||
Buffer buf;
|
||||
AlignedBuf aligned_buf;
|
||||
AlignedBuffer direct_io_buffer;
|
||||
|
||||
{
|
||||
TEST_SYNC_POINT("BlobFileReader::ReadFooter:ReadFromFile");
|
||||
@@ -226,7 +228,7 @@ Status BlobFileReader::ReadFooter(const RandomAccessFileReader* file_reader,
|
||||
|
||||
const Status s =
|
||||
ReadFromFile(file_reader, read_options, read_offset, read_size,
|
||||
statistics, &footer_slice, &buf, &aligned_buf);
|
||||
statistics, &footer_slice, &buf, &direct_io_buffer);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -257,10 +259,11 @@ Status BlobFileReader::ReadFromFile(const RandomAccessFileReader* file_reader,
|
||||
const ReadOptions& read_options,
|
||||
uint64_t read_offset, size_t read_size,
|
||||
Statistics* statistics, Slice* slice,
|
||||
Buffer* buf, AlignedBuf* aligned_buf) {
|
||||
Buffer* buf,
|
||||
AlignedBuffer* direct_io_buffer) {
|
||||
assert(slice);
|
||||
assert(buf);
|
||||
assert(aligned_buf);
|
||||
assert(direct_io_buffer);
|
||||
|
||||
assert(file_reader);
|
||||
|
||||
@@ -277,15 +280,15 @@ Status BlobFileReader::ReadFromFile(const RandomAccessFileReader* file_reader,
|
||||
|
||||
if (file_reader->use_direct_io()) {
|
||||
constexpr char* scratch = nullptr;
|
||||
AlignedBufferAllocationContext direct_io_context{direct_io_buffer};
|
||||
|
||||
s = file_reader->Read(io_options, read_offset, read_size, slice, scratch,
|
||||
aligned_buf, &dbg);
|
||||
&direct_io_context, &dbg);
|
||||
} else {
|
||||
buf->reset(new char[read_size]);
|
||||
constexpr AlignedBuf* aligned_scratch = nullptr;
|
||||
|
||||
s = file_reader->Read(io_options, read_offset, read_size, slice, buf->get(),
|
||||
aligned_scratch, &dbg);
|
||||
nullptr, &dbg);
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
@@ -349,7 +352,7 @@ Status BlobFileReader::GetBlob(
|
||||
|
||||
Slice record_slice;
|
||||
Buffer buf;
|
||||
AlignedBuf aligned_buf;
|
||||
AlignedBuffer direct_io_buffer;
|
||||
|
||||
bool prefetched = false;
|
||||
|
||||
@@ -379,7 +382,7 @@ Status BlobFileReader::GetBlob(
|
||||
const Status s =
|
||||
ReadFromFile(file_reader_.get(), read_options, record_offset,
|
||||
static_cast<size_t>(record_size), statistics_,
|
||||
&record_slice, &buf, &aligned_buf);
|
||||
&record_slice, &buf, &direct_io_buffer);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -477,7 +480,7 @@ void BlobFileReader::MultiGetBlob(
|
||||
}
|
||||
|
||||
Buffer buf;
|
||||
AlignedBuf aligned_buf;
|
||||
AlignedBuffer direct_io_buffer;
|
||||
|
||||
Status s;
|
||||
bool direct_io = file_reader_->use_direct_io();
|
||||
@@ -500,8 +503,9 @@ void BlobFileReader::MultiGetBlob(
|
||||
IODebugContext dbg;
|
||||
s = file_reader_->PrepareIOOptions(read_options, opts, &dbg);
|
||||
if (s.ok()) {
|
||||
AlignedBufferAllocationContext direct_io_context{&direct_io_buffer};
|
||||
s = file_reader_->MultiRead(opts, read_reqs.data(), read_reqs.size(),
|
||||
direct_io ? &aligned_buf : nullptr, &dbg);
|
||||
&direct_io_context, &dbg);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
for (auto& req : read_reqs) {
|
||||
|
||||
@@ -108,7 +108,7 @@ class BlobFileReader {
|
||||
const ReadOptions& read_options,
|
||||
uint64_t read_offset, size_t read_size,
|
||||
Statistics* statistics, Slice* slice, Buffer* buf,
|
||||
AlignedBuf* aligned_buf);
|
||||
AlignedBuffer* direct_io_buffer);
|
||||
|
||||
static Status VerifyBlob(const Slice& record_slice, const Slice& user_key,
|
||||
uint64_t value_size);
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "db/blob/blob_contents.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "db/blob/blob_log_writer.h"
|
||||
#include "env/composite_env_wrapper.h"
|
||||
#include "env/mock_env.h"
|
||||
#include "file/filename.h"
|
||||
#include "file/read_write_util.h"
|
||||
@@ -136,6 +138,167 @@ class BlobFileReaderTest : public testing::Test {
|
||||
std::unique_ptr<Env> mock_env_;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
// Returns a stale path-level size for one target blob file while leaving the
|
||||
// opened file handle untouched. This lets the test verify
|
||||
// BlobFileReader::Create prefers the opened handle's size when the path stat
|
||||
// lags behind.
|
||||
class StalePathSizeFileSystem : public FileSystemWrapper {
|
||||
public:
|
||||
static const char* kClassName() { return "StalePathSizeFileSystem"; }
|
||||
|
||||
StalePathSizeFileSystem(const std::shared_ptr<FileSystem>& target,
|
||||
std::string stale_path)
|
||||
: FileSystemWrapper(target), stale_path_(std::move(stale_path)) {}
|
||||
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
IOStatus GetFileSize(const std::string& fname, const IOOptions& options,
|
||||
uint64_t* file_size, IODebugContext* dbg) override {
|
||||
if (fname == stale_path_) {
|
||||
*file_size = 0;
|
||||
return IOStatus::OK();
|
||||
}
|
||||
return FileSystemWrapper::GetFileSize(fname, options, file_size, dbg);
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string stale_path_;
|
||||
};
|
||||
|
||||
// Makes opened blob file handles fail GetFileSize() with a hard error so the
|
||||
// reader path can verify that such errors are propagated directly.
|
||||
class FailingOpenFileSizeRandomAccessFile
|
||||
: public FSRandomAccessFileOwnerWrapper {
|
||||
public:
|
||||
explicit FailingOpenFileSizeRandomAccessFile(
|
||||
std::unique_ptr<FSRandomAccessFile>&& target)
|
||||
: FSRandomAccessFileOwnerWrapper(std::move(target)) {}
|
||||
|
||||
IOStatus GetFileSize(uint64_t* /*result*/) override {
|
||||
return IOStatus::IOError("open file size failed");
|
||||
}
|
||||
};
|
||||
|
||||
// Makes opened blob file handles report GetFileSize() as unsupported so the
|
||||
// reader path exercises its path-level fallback logic.
|
||||
class NotSupportedOpenFileSizeRandomAccessFile
|
||||
: public FSRandomAccessFileOwnerWrapper {
|
||||
public:
|
||||
explicit NotSupportedOpenFileSizeRandomAccessFile(
|
||||
std::unique_ptr<FSRandomAccessFile>&& target)
|
||||
: FSRandomAccessFileOwnerWrapper(std::move(target)) {}
|
||||
|
||||
IOStatus GetFileSize(uint64_t* /*result*/) override {
|
||||
return IOStatus::NotSupported("open file size not supported");
|
||||
}
|
||||
};
|
||||
|
||||
class OpenFileSizeFileSystem : public FileSystemWrapper {
|
||||
public:
|
||||
OpenFileSizeFileSystem(const std::shared_ptr<FileSystem>& target,
|
||||
std::string target_path)
|
||||
: FileSystemWrapper(target), target_path_(std::move(target_path)) {}
|
||||
|
||||
IOStatus NewRandomAccessFile(const std::string& fname,
|
||||
const FileOptions& options,
|
||||
std::unique_ptr<FSRandomAccessFile>* result,
|
||||
IODebugContext* dbg) override {
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
IOStatus s =
|
||||
FileSystemWrapper::NewRandomAccessFile(fname, options, &file, dbg);
|
||||
if (!s.ok()) {
|
||||
return IOStatus(std::move(s));
|
||||
}
|
||||
if (fname == target_path_) {
|
||||
*result = WrapTargetFile(std::move(file));
|
||||
} else {
|
||||
*result = std::move(file);
|
||||
}
|
||||
return IOStatus::OK();
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual std::unique_ptr<FSRandomAccessFile> WrapTargetFile(
|
||||
std::unique_ptr<FSRandomAccessFile>&& file) = 0;
|
||||
|
||||
private:
|
||||
const std::string target_path_;
|
||||
};
|
||||
|
||||
// Wraps the target blob file in FailingOpenFileSizeRandomAccessFile while
|
||||
// leaving all other files alone.
|
||||
class FailingOpenFileSizeFileSystem : public OpenFileSizeFileSystem {
|
||||
public:
|
||||
static const char* kClassName() { return "FailingOpenFileSizeFileSystem"; }
|
||||
|
||||
FailingOpenFileSizeFileSystem(const std::shared_ptr<FileSystem>& target,
|
||||
std::string target_path)
|
||||
: OpenFileSizeFileSystem(target, std::move(target_path)) {}
|
||||
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
private:
|
||||
std::unique_ptr<FSRandomAccessFile> WrapTargetFile(
|
||||
std::unique_ptr<FSRandomAccessFile>&& file) override {
|
||||
return std::unique_ptr<FSRandomAccessFile>(
|
||||
new FailingOpenFileSizeRandomAccessFile(std::move(file)));
|
||||
}
|
||||
};
|
||||
|
||||
// Wraps the target blob file in NotSupportedOpenFileSizeRandomAccessFile while
|
||||
// leaving all other files alone.
|
||||
class NotSupportedOpenFileSizeFileSystem : public OpenFileSizeFileSystem {
|
||||
public:
|
||||
static const char* kClassName() {
|
||||
return "NotSupportedOpenFileSizeFileSystem";
|
||||
}
|
||||
|
||||
NotSupportedOpenFileSizeFileSystem(const std::shared_ptr<FileSystem>& target,
|
||||
std::string target_path)
|
||||
: OpenFileSizeFileSystem(target, std::move(target_path)) {}
|
||||
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
private:
|
||||
std::unique_ptr<FSRandomAccessFile> WrapTargetFile(
|
||||
std::unique_ptr<FSRandomAccessFile>&& file) override {
|
||||
return std::unique_ptr<FSRandomAccessFile>(
|
||||
new NotSupportedOpenFileSizeRandomAccessFile(std::move(file)));
|
||||
}
|
||||
};
|
||||
|
||||
// Forces the target blob file onto the NotSupported open-handle-size branch and
|
||||
// then fails the fallback path-level GetFileSize() call.
|
||||
class FailingFallbackPathSizeFileSystem
|
||||
: public NotSupportedOpenFileSizeFileSystem {
|
||||
public:
|
||||
static const char* kClassName() {
|
||||
return "FailingFallbackPathSizeFileSystem";
|
||||
}
|
||||
|
||||
FailingFallbackPathSizeFileSystem(const std::shared_ptr<FileSystem>& target,
|
||||
std::string target_path)
|
||||
: NotSupportedOpenFileSizeFileSystem(target, target_path),
|
||||
target_path_(std::move(target_path)) {}
|
||||
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
IOStatus GetFileSize(const std::string& fname, const IOOptions& options,
|
||||
uint64_t* file_size, IODebugContext* dbg) override {
|
||||
if (fname == target_path_) {
|
||||
return IOStatus::IOError("fallback path size failed");
|
||||
}
|
||||
return FileSystemWrapper::GetFileSize(fname, options, file_size, dbg);
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string target_path_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(BlobFileReaderTest, CreateReaderAndGetBlob) {
|
||||
Options options;
|
||||
options.env = mock_env_.get();
|
||||
@@ -428,6 +591,211 @@ TEST_F(BlobFileReaderTest, CreateReaderAndGetBlob) {
|
||||
}
|
||||
}
|
||||
|
||||
// Verifies Create() prefers the file size reported by the opened handle over a
|
||||
// stale path-level size query by returning a readable reader even when the path
|
||||
// stat intentionally reports 0 for the blob file.
|
||||
TEST_F(BlobFileReaderTest, CreateReaderUsesOpenedFileSizeWhenPathSizeIsStale) {
|
||||
const std::string db_path = test::PerThreadDBPath(
|
||||
mock_env_.get(),
|
||||
"BlobFileReaderTest_CreateReaderUsesOpenedFileSizeWhenPathSizeIsStale");
|
||||
|
||||
constexpr uint64_t blob_file_number = 1;
|
||||
const std::string blob_file_path = BlobFileName(db_path, blob_file_number);
|
||||
|
||||
auto stale_size_fs = std::make_shared<StalePathSizeFileSystem>(
|
||||
mock_env_->GetFileSystem(), blob_file_path);
|
||||
CompositeEnvWrapper stale_size_env(mock_env_.get(), stale_size_fs);
|
||||
|
||||
Options options;
|
||||
options.env = &stale_size_env;
|
||||
options.cf_paths.emplace_back(db_path, 0);
|
||||
options.enable_blob_files = true;
|
||||
|
||||
ImmutableOptions immutable_options(options);
|
||||
|
||||
constexpr uint32_t column_family_id = 1;
|
||||
constexpr bool has_ttl = false;
|
||||
constexpr ExpirationRange expiration_range;
|
||||
constexpr char key[] = "key";
|
||||
constexpr char blob[] = "blob";
|
||||
|
||||
uint64_t blob_offset = 0;
|
||||
uint64_t blob_size = 0;
|
||||
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
|
||||
expiration_range, blob_file_number, key, blob, kNoCompression,
|
||||
&blob_offset, &blob_size);
|
||||
|
||||
std::unique_ptr<BlobFileReader> reader;
|
||||
ReadOptions read_options;
|
||||
read_options.verify_checksums = false;
|
||||
ASSERT_OK(BlobFileReader::Create(
|
||||
immutable_options, read_options, FileOptions(), column_family_id,
|
||||
/*blob_file_read_hist=*/nullptr, blob_file_number, nullptr /*IOTracer*/,
|
||||
&reader));
|
||||
ASSERT_NE(reader, nullptr);
|
||||
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
ASSERT_OK(reader->GetBlob(read_options, key, blob_offset, blob_size,
|
||||
kNoCompression,
|
||||
/*prefetch_buffer=*/nullptr,
|
||||
/*allocator=*/nullptr, &value, &bytes_read));
|
||||
ASSERT_NE(value, nullptr);
|
||||
ASSERT_EQ(value->data(), Slice(blob));
|
||||
ASSERT_EQ(bytes_read, blob_size);
|
||||
}
|
||||
|
||||
// Verifies Create() falls back to the path-level GetFileSize() call when the
|
||||
// opened file handle reports that size queries are unsupported.
|
||||
TEST_F(BlobFileReaderTest,
|
||||
CreateReaderFallsBackToPathSizeWhenOpenedFileSizeNotSupported) {
|
||||
const std::string db_path = test::PerThreadDBPath(
|
||||
mock_env_.get(),
|
||||
"BlobFileReaderTest_"
|
||||
"CreateReaderFallsBackToPathSizeWhenOpenedFileSizeNotSupported");
|
||||
|
||||
constexpr uint64_t blob_file_number = 1;
|
||||
const std::string blob_file_path = BlobFileName(db_path, blob_file_number);
|
||||
|
||||
auto not_supported_open_file_size_fs =
|
||||
std::make_shared<NotSupportedOpenFileSizeFileSystem>(
|
||||
mock_env_->GetFileSystem(), blob_file_path);
|
||||
CompositeEnvWrapper not_supported_open_file_size_env(
|
||||
mock_env_.get(), not_supported_open_file_size_fs);
|
||||
|
||||
Options options;
|
||||
options.env = ¬_supported_open_file_size_env;
|
||||
options.cf_paths.emplace_back(db_path, 0);
|
||||
options.enable_blob_files = true;
|
||||
|
||||
ImmutableOptions immutable_options(options);
|
||||
|
||||
constexpr uint32_t column_family_id = 1;
|
||||
constexpr bool has_ttl = false;
|
||||
constexpr ExpirationRange expiration_range;
|
||||
constexpr char key[] = "key";
|
||||
constexpr char blob[] = "blob";
|
||||
|
||||
uint64_t blob_offset = 0;
|
||||
uint64_t blob_size = 0;
|
||||
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
|
||||
expiration_range, blob_file_number, key, blob, kNoCompression,
|
||||
&blob_offset, &blob_size);
|
||||
|
||||
std::unique_ptr<BlobFileReader> reader;
|
||||
ReadOptions read_options;
|
||||
read_options.verify_checksums = false;
|
||||
ASSERT_OK(BlobFileReader::Create(
|
||||
immutable_options, read_options, FileOptions(), column_family_id,
|
||||
/*blob_file_read_hist=*/nullptr, blob_file_number, nullptr /*IOTracer*/,
|
||||
&reader));
|
||||
ASSERT_NE(reader, nullptr);
|
||||
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
ASSERT_OK(reader->GetBlob(read_options, key, blob_offset, blob_size,
|
||||
kNoCompression,
|
||||
/*prefetch_buffer=*/nullptr,
|
||||
/*allocator=*/nullptr, &value, &bytes_read));
|
||||
ASSERT_NE(value, nullptr);
|
||||
ASSERT_EQ(value->data(), Slice(blob));
|
||||
ASSERT_EQ(bytes_read, blob_size);
|
||||
}
|
||||
|
||||
// Verifies Create() propagates a real error from the opened file handle's
|
||||
// GetFileSize() rather than masking it with any path-level fallback.
|
||||
TEST_F(BlobFileReaderTest, CreateReaderPropagatesOpenedFileSizeError) {
|
||||
const std::string db_path = test::PerThreadDBPath(
|
||||
mock_env_.get(),
|
||||
"BlobFileReaderTest_CreateReaderPropagatesOpenedFileSizeError");
|
||||
|
||||
constexpr uint64_t blob_file_number = 1;
|
||||
const std::string blob_file_path = BlobFileName(db_path, blob_file_number);
|
||||
|
||||
auto failing_open_file_size_fs =
|
||||
std::make_shared<FailingOpenFileSizeFileSystem>(
|
||||
mock_env_->GetFileSystem(), blob_file_path);
|
||||
CompositeEnvWrapper failing_open_file_size_env(mock_env_.get(),
|
||||
failing_open_file_size_fs);
|
||||
|
||||
Options options;
|
||||
options.env = &failing_open_file_size_env;
|
||||
options.cf_paths.emplace_back(db_path, 0);
|
||||
options.enable_blob_files = true;
|
||||
|
||||
ImmutableOptions immutable_options(options);
|
||||
|
||||
constexpr uint32_t column_family_id = 1;
|
||||
constexpr bool has_ttl = false;
|
||||
constexpr ExpirationRange expiration_range;
|
||||
constexpr char key[] = "key";
|
||||
constexpr char blob[] = "blob";
|
||||
|
||||
uint64_t blob_offset = 0;
|
||||
uint64_t blob_size = 0;
|
||||
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
|
||||
expiration_range, blob_file_number, key, blob, kNoCompression,
|
||||
&blob_offset, &blob_size);
|
||||
|
||||
std::unique_ptr<BlobFileReader> reader;
|
||||
ReadOptions read_options;
|
||||
Status s = BlobFileReader::Create(
|
||||
immutable_options, read_options, FileOptions(), column_family_id,
|
||||
/*blob_file_read_hist=*/nullptr, blob_file_number, nullptr /*IOTracer*/,
|
||||
&reader);
|
||||
ASSERT_TRUE(s.IsIOError());
|
||||
ASSERT_EQ(reader, nullptr);
|
||||
}
|
||||
|
||||
// Verifies Create() also propagates errors from the path-level fallback size
|
||||
// query when the opened file handle only reports NotSupported for GetFileSize.
|
||||
TEST_F(
|
||||
BlobFileReaderTest,
|
||||
CreateReaderPropagatesFallbackPathSizeErrorWhenOpenedFileSizeNotSupported) {
|
||||
const std::string db_path =
|
||||
test::PerThreadDBPath(mock_env_.get(),
|
||||
"BlobFileReaderTest_"
|
||||
"CreateReaderPropagatesFallbackPathSizeErrorWhenOpe"
|
||||
"nedFileSizeNotSupported");
|
||||
|
||||
constexpr uint64_t blob_file_number = 1;
|
||||
const std::string blob_file_path = BlobFileName(db_path, blob_file_number);
|
||||
|
||||
auto failing_fallback_path_size_fs =
|
||||
std::make_shared<FailingFallbackPathSizeFileSystem>(
|
||||
mock_env_->GetFileSystem(), blob_file_path);
|
||||
CompositeEnvWrapper failing_fallback_path_size_env(
|
||||
mock_env_.get(), failing_fallback_path_size_fs);
|
||||
|
||||
Options options;
|
||||
options.env = &failing_fallback_path_size_env;
|
||||
options.cf_paths.emplace_back(db_path, 0);
|
||||
options.enable_blob_files = true;
|
||||
|
||||
ImmutableOptions immutable_options(options);
|
||||
|
||||
constexpr uint32_t column_family_id = 1;
|
||||
constexpr bool has_ttl = false;
|
||||
constexpr ExpirationRange expiration_range;
|
||||
constexpr char key[] = "key";
|
||||
constexpr char blob[] = "blob";
|
||||
|
||||
uint64_t blob_offset = 0;
|
||||
uint64_t blob_size = 0;
|
||||
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
|
||||
expiration_range, blob_file_number, key, blob, kNoCompression,
|
||||
&blob_offset, &blob_size);
|
||||
|
||||
std::unique_ptr<BlobFileReader> reader;
|
||||
ReadOptions read_options;
|
||||
Status s = BlobFileReader::Create(
|
||||
immutable_options, read_options, FileOptions(), column_family_id,
|
||||
/*blob_file_read_hist=*/nullptr, blob_file_number, nullptr /*IOTracer*/,
|
||||
&reader);
|
||||
ASSERT_TRUE(s.IsIOError());
|
||||
ASSERT_EQ(reader, nullptr);
|
||||
}
|
||||
|
||||
TEST_F(BlobFileReaderTest, Malformed) {
|
||||
// Write a blob file consisting of nothing but a header, and make sure we
|
||||
// detect the error when we open it for reading
|
||||
@@ -849,7 +1217,6 @@ class BlobFileReaderIOErrorTest
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(BlobFileReaderTest, BlobFileReaderIOErrorTest,
|
||||
::testing::ValuesIn(std::vector<std::string>{
|
||||
"BlobFileReader::OpenFile:GetFileSize",
|
||||
"BlobFileReader::OpenFile:NewRandomAccessFile",
|
||||
"BlobFileReader::ReadHeader:ReadFromFile",
|
||||
"BlobFileReader::ReadFooter:ReadFromFile",
|
||||
|
||||
@@ -74,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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -107,7 +107,7 @@ Status BlobWriteBatchTransformer::MaybePreprocessWideColumns(
|
||||
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,
|
||||
column_value, &blob_file_number, &blob_offset, &blob_size, settings,
|
||||
&entity_partition);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
@@ -169,7 +169,7 @@ Status BlobWriteBatchTransformer::PutCF(uint32_t column_family_id,
|
||||
|
||||
Status s = cached_partition_mgr_->WriteBlob(
|
||||
write_options_, column_family_id, settings.compression_type, key, value,
|
||||
&blob_file_number, &blob_offset, &blob_size, &settings);
|
||||
&blob_file_number, &blob_offset, &blob_size, settings);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -278,7 +278,7 @@ Status BlobWriteBatchTransformer::MergeCF(uint32_t column_family_id,
|
||||
Status BlobWriteBatchTransformer::PutBlobIndexCF(uint32_t column_family_id,
|
||||
const Slice& key,
|
||||
const Slice& value) {
|
||||
// Already a blob index — pass through unchanged.
|
||||
// Already a blob index -- pass through unchanged.
|
||||
return WriteBatchInternal::PutBlobIndex(output_batch_, column_family_id, key,
|
||||
value);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,9 @@ struct BlobDirectWriteSettings {
|
||||
uint64_t min_blob_size = 0;
|
||||
// Compression to use for newly written blob records.
|
||||
CompressionType compression_type = kNoCompression;
|
||||
// Raw pointer — the Cache is owned by ColumnFamilyOptions and outlives all
|
||||
// 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.
|
||||
|
||||
@@ -1620,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();
|
||||
|
||||
@@ -10,11 +10,14 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "cache/compressed_secondary_cache.h"
|
||||
@@ -26,10 +29,12 @@
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/db_with_timestamp_test_util.h"
|
||||
#include "db/wide/wide_column_test_util.h"
|
||||
#include "env/composite_env_wrapper.h"
|
||||
#include "file/filename.h"
|
||||
#include "file/random_access_file_reader.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "rocksdb/trace_reader_writer.h"
|
||||
#include "rocksdb/trace_record.h"
|
||||
#include "rocksdb/utilities/replayer.h"
|
||||
@@ -101,6 +106,206 @@ class RecordingBlobDirectWritePartitionStrategy
|
||||
std::vector<Call> calls_;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
// Matches BlobDB file names so the test filesystem can special-case only active
|
||||
// blob files without changing unrelated file opens.
|
||||
bool IsBlobFilePath(const std::string& fname) {
|
||||
const size_t basename_pos = fname.find_last_of("/\\");
|
||||
const std::string basename = basename_pos == std::string::npos
|
||||
? fname
|
||||
: fname.substr(basename_pos + 1);
|
||||
uint64_t file_number = 0;
|
||||
FileType file_type = kWalFile;
|
||||
return ParseFileName(basename, &file_number, &file_type) &&
|
||||
file_type == kBlobFile;
|
||||
}
|
||||
|
||||
// Keeps track of when an active blob file stops being writer-visible so the
|
||||
// test filesystem can model current-size visibility only while the file remains
|
||||
// active.
|
||||
class ActiveBlobVisibilityWritableFile : public FSWritableFileOwnerWrapper {
|
||||
public:
|
||||
ActiveBlobVisibilityWritableFile(std::unique_ptr<FSWritableFile>&& target,
|
||||
std::function<void()> on_close)
|
||||
: FSWritableFileOwnerWrapper(std::move(target)),
|
||||
on_close_(std::move(on_close)) {}
|
||||
|
||||
~ActiveBlobVisibilityWritableFile() override { DeactivateOnce(); }
|
||||
|
||||
IOStatus Close(const IOOptions& options, IODebugContext* dbg) override {
|
||||
IOStatus s = target()->Close(options, dbg);
|
||||
DeactivateOnce();
|
||||
return s;
|
||||
}
|
||||
|
||||
private:
|
||||
void DeactivateOnce() {
|
||||
if (on_close_) {
|
||||
on_close_();
|
||||
on_close_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::function<void()> on_close_;
|
||||
};
|
||||
|
||||
// Simulates a remote readable file whose GetFileSize() either exposes the live
|
||||
// on-disk size for default-contract active blobs or hides it for
|
||||
// append-only-no-readers files.
|
||||
class ActiveBlobVisibilityRandomAccessFile
|
||||
: public FSRandomAccessFileOwnerWrapper {
|
||||
public:
|
||||
ActiveBlobVisibilityRandomAccessFile(
|
||||
std::unique_ptr<FSRandomAccessFile>&& target, bool expose_current_size,
|
||||
std::shared_ptr<FileSystem> underlying_fs, std::string fname)
|
||||
: FSRandomAccessFileOwnerWrapper(std::move(target)),
|
||||
expose_current_size_(expose_current_size),
|
||||
underlying_fs_(std::move(underlying_fs)),
|
||||
fname_(std::move(fname)) {}
|
||||
|
||||
IOStatus GetFileSize(uint64_t* result) override {
|
||||
if (!expose_current_size_) {
|
||||
*result = 0;
|
||||
return IOStatus::OK();
|
||||
}
|
||||
// Delegate to the underlying FS path-level GetFileSize (bypassing the
|
||||
// wrapper that reports 0 for active blobs) to simulate a remote FS that
|
||||
// exposes the current file size through the open handle.
|
||||
return underlying_fs_->GetFileSize(fname_, IOOptions(), result,
|
||||
/*dbg=*/nullptr);
|
||||
}
|
||||
|
||||
private:
|
||||
const bool expose_current_size_;
|
||||
std::shared_ptr<FileSystem> underlying_fs_;
|
||||
std::string fname_;
|
||||
};
|
||||
|
||||
// Models a remote filesystem where active direct-write blobs are only
|
||||
// reader-visible when the writer does not promise no concurrent readers.
|
||||
class RemoteBlobVisibilityFileSystem : public FileSystemWrapper {
|
||||
public:
|
||||
explicit RemoteBlobVisibilityFileSystem(const std::shared_ptr<FileSystem>& fs)
|
||||
: FileSystemWrapper(fs) {}
|
||||
|
||||
static const char* kClassName() { return "RemoteBlobVisibilityFileSystem"; }
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
IOStatus NewWritableFile(const std::string& fname, const FileOptions& opts,
|
||||
std::unique_ptr<FSWritableFile>* result,
|
||||
IODebugContext* dbg) override {
|
||||
std::unique_ptr<FSWritableFile> file;
|
||||
IOStatus s = target()->NewWritableFile(fname, opts, &file, dbg);
|
||||
if (!s.ok() || !IsBlobFilePath(fname)) {
|
||||
*result = std::move(file);
|
||||
return IOStatus(std::move(s));
|
||||
}
|
||||
|
||||
const bool has_no_reopen_for_write_contract = HasFileOpenContract(
|
||||
opts.open_contract, FileOpenContract::kNoReopenForWrite);
|
||||
const bool has_no_readers_while_open_for_write_contract =
|
||||
HasFileOpenContract(opts.open_contract,
|
||||
FileOpenContract::kNoReadersWhileOpenForWrite);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
saw_no_reopen_for_write_writer_contract_ |=
|
||||
has_no_reopen_for_write_contract;
|
||||
saw_no_readers_while_open_for_write_writer_contract_ |=
|
||||
has_no_readers_while_open_for_write_contract;
|
||||
if (!has_no_readers_while_open_for_write_contract) {
|
||||
active_blob_paths_.insert(fname);
|
||||
}
|
||||
}
|
||||
|
||||
if (has_no_readers_while_open_for_write_contract) {
|
||||
*result = std::move(file);
|
||||
return IOStatus::OK();
|
||||
}
|
||||
|
||||
result->reset(new ActiveBlobVisibilityWritableFile(
|
||||
std::move(file), [this, tracked_path = std::string(fname)]() {
|
||||
DeactivateBlobPath(tracked_path);
|
||||
}));
|
||||
return IOStatus::OK();
|
||||
}
|
||||
|
||||
IOStatus NewRandomAccessFile(const std::string& fname,
|
||||
const FileOptions& opts,
|
||||
std::unique_ptr<FSRandomAccessFile>* result,
|
||||
IODebugContext* dbg) override {
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
IOStatus s = target()->NewRandomAccessFile(fname, opts, &file, dbg);
|
||||
if (!s.ok() || !IsBlobFilePath(fname)) {
|
||||
*result = std::move(file);
|
||||
return IOStatus(std::move(s));
|
||||
}
|
||||
|
||||
const bool has_no_readers_while_open_for_write_contract =
|
||||
HasFileOpenContract(opts.open_contract,
|
||||
FileOpenContract::kNoReadersWhileOpenForWrite);
|
||||
const bool active_blob = IsActiveBlobPath(fname);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
saw_no_readers_while_open_for_write_reader_contract_ |=
|
||||
has_no_readers_while_open_for_write_contract;
|
||||
}
|
||||
|
||||
if (!active_blob) {
|
||||
*result = std::move(file);
|
||||
return IOStatus::OK();
|
||||
}
|
||||
|
||||
result->reset(new ActiveBlobVisibilityRandomAccessFile(
|
||||
std::move(file), !has_no_readers_while_open_for_write_contract, target_,
|
||||
fname));
|
||||
return IOStatus::OK();
|
||||
}
|
||||
|
||||
IOStatus GetFileSize(const std::string& fname, const IOOptions& options,
|
||||
uint64_t* file_size, IODebugContext* dbg) override {
|
||||
if (IsActiveBlobPath(fname)) {
|
||||
*file_size = 0;
|
||||
return IOStatus::OK();
|
||||
}
|
||||
return target()->GetFileSize(fname, options, file_size, dbg);
|
||||
}
|
||||
|
||||
bool SawNoReopenForWriteWriterContract() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return saw_no_reopen_for_write_writer_contract_;
|
||||
}
|
||||
|
||||
bool SawNoReadersWhileOpenForWriteWriterContract() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return saw_no_readers_while_open_for_write_writer_contract_;
|
||||
}
|
||||
|
||||
bool SawNoReadersWhileOpenForWriteReaderContract() const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return saw_no_readers_while_open_for_write_reader_contract_;
|
||||
}
|
||||
|
||||
private:
|
||||
void DeactivateBlobPath(const std::string& fname) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
active_blob_paths_.erase(fname);
|
||||
}
|
||||
|
||||
bool IsActiveBlobPath(const std::string& fname) const {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return active_blob_paths_.find(fname) != active_blob_paths_.end();
|
||||
}
|
||||
|
||||
mutable std::mutex mu_;
|
||||
bool saw_no_reopen_for_write_writer_contract_ = false;
|
||||
bool saw_no_readers_while_open_for_write_writer_contract_ = false;
|
||||
bool saw_no_readers_while_open_for_write_reader_contract_ = false;
|
||||
std::unordered_set<std::string> active_blob_paths_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class DBBlobDirectWriteTest : public DBTestBase {
|
||||
protected:
|
||||
DBBlobDirectWriteTest()
|
||||
@@ -151,6 +356,48 @@ class DBBlobDirectWriteTest : public DBTestBase {
|
||||
return reader.ReadHeader(header);
|
||||
}
|
||||
|
||||
Status ReadBlobFileRecords(uint64_t blob_file_number, size_t num_records,
|
||||
std::vector<BlobLogRecord>* records) {
|
||||
assert(records != nullptr);
|
||||
records->clear();
|
||||
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
FileOptions file_options;
|
||||
constexpr IODebugContext* dbg = nullptr;
|
||||
const std::string blob_file_path = BlobFileName(dbname_, blob_file_number);
|
||||
FileSystem* fs = env_->GetFileSystem().get();
|
||||
SystemClock* clock = env_->GetSystemClock().get();
|
||||
|
||||
Status s =
|
||||
fs->NewRandomAccessFile(blob_file_path, file_options, &file, dbg);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
std::unique_ptr<RandomAccessFileReader> file_reader(
|
||||
new RandomAccessFileReader(std::move(file), blob_file_path, clock));
|
||||
BlobLogSequentialReader reader(std::move(file_reader), clock,
|
||||
/*statistics=*/nullptr);
|
||||
|
||||
BlobLogHeader header;
|
||||
s = reader.ReadHeader(&header);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
records->reserve(num_records);
|
||||
for (size_t i = 0; i < num_records; ++i) {
|
||||
BlobLogRecord record;
|
||||
s = reader.ReadRecord(&record,
|
||||
BlobLogSequentialReader::kReadHeaderKeyBlob);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
records->push_back(std::move(record));
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
CompressionType GetSupportedCompressedBlobCompression() {
|
||||
static constexpr std::array<CompressionType, 6> kCandidates{
|
||||
kSnappyCompression, kLZ4Compression, kZSTD,
|
||||
@@ -774,6 +1021,30 @@ TEST_F(DBBlobDirectWriteTest, DirectWriteImmutableMemtableRead) {
|
||||
verify_reads();
|
||||
}
|
||||
|
||||
// Verifies active blob direct-write files promise no write reopen without also
|
||||
// promising no concurrent readers.
|
||||
TEST_F(DBBlobDirectWriteTest, DirectWriteUsesReadableContractForRemoteFile) {
|
||||
auto remote_fs =
|
||||
std::make_shared<RemoteBlobVisibilityFileSystem>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> remote_env(new CompositeEnvWrapper(env_, remote_fs));
|
||||
|
||||
Options options = GetDirectWriteOptions();
|
||||
options.env = remote_env.get();
|
||||
Reopen(options);
|
||||
|
||||
const std::string key = "remote_blob_key";
|
||||
const std::string value(128, 'w');
|
||||
|
||||
ASSERT_OK(Put(key, value));
|
||||
ASSERT_TRUE(remote_fs->SawNoReopenForWriteWriterContract());
|
||||
ASSERT_FALSE(remote_fs->SawNoReadersWhileOpenForWriteWriterContract());
|
||||
|
||||
ASSERT_EQ(Get(key), value);
|
||||
ASSERT_FALSE(remote_fs->SawNoReadersWhileOpenForWriteReaderContract());
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBBlobDirectWriteTest, DirectWriteRefreshesReaderAfterFlush) {
|
||||
Options options = GetDirectWriteOptions();
|
||||
options.blob_direct_write_partitions = 1;
|
||||
@@ -944,6 +1215,70 @@ TEST_F(DBBlobDirectWriteTest,
|
||||
ASSERT_EQ(Get(third_key), third_value);
|
||||
}
|
||||
|
||||
TEST_F(DBBlobDirectWriteTest,
|
||||
DirectWriteCompressionOptionsUpdateRebuildsCachedCompressor) {
|
||||
#ifndef ZSTD
|
||||
ROCKSDB_GTEST_SKIP("This test requires ZSTD support");
|
||||
return;
|
||||
#else
|
||||
// Goal: verify BDW picks up dynamic blob_compression_opts updates instead of
|
||||
// reusing a stale per-thread compressor keyed only by compression type.
|
||||
// We toggle the ZSTD frame checksum flag on and off, then inspect the raw
|
||||
// blob records in one blob file. With identical inputs and level, enabling
|
||||
// checksum should add exactly 4 bytes to the stored compressed blob.
|
||||
Options options = GetDirectWriteOptions();
|
||||
options.blob_file_size = 1 << 20;
|
||||
options.blob_compression_type = kZSTD;
|
||||
options.blob_compression_opts.level = 1;
|
||||
options.blob_compression_opts.checksum = false;
|
||||
|
||||
Reopen(options);
|
||||
|
||||
const std::string first_key = "checksum_off_before_update";
|
||||
const std::string second_key = "checksum_on_after_update";
|
||||
const std::string third_key = "checksum_off_after_second_update";
|
||||
const std::string value(256, 'v');
|
||||
|
||||
ASSERT_OK(Put(first_key, value));
|
||||
ASSERT_EQ(Get(first_key), value);
|
||||
|
||||
ASSERT_OK(
|
||||
db_->SetOptions(db_->DefaultColumnFamily(),
|
||||
{{"blob_compression_opts", "{level=1;checksum=true}"}}));
|
||||
ASSERT_OK(Put(second_key, value));
|
||||
ASSERT_EQ(Get(second_key), value);
|
||||
|
||||
ASSERT_OK(
|
||||
db_->SetOptions(db_->DefaultColumnFamily(),
|
||||
{{"blob_compression_opts", "{level=1;checksum=false}"}}));
|
||||
ASSERT_OK(Put(third_key, value));
|
||||
ASSERT_EQ(Get(third_key), value);
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ(CountBlobFiles(), 1U);
|
||||
|
||||
const std::vector<uint64_t> blob_file_numbers = GetBlobFileNumbers();
|
||||
ASSERT_EQ(blob_file_numbers.size(), 1U);
|
||||
|
||||
std::vector<BlobLogRecord> records;
|
||||
ASSERT_OK(
|
||||
ReadBlobFileRecords(blob_file_numbers[0], /*num_records=*/3, &records));
|
||||
ASSERT_EQ(records.size(), 3U);
|
||||
ASSERT_EQ(records[0].key.ToString(), first_key);
|
||||
ASSERT_EQ(records[1].key.ToString(), second_key);
|
||||
ASSERT_EQ(records[2].key.ToString(), third_key);
|
||||
ASSERT_EQ(records[0].value.size(), records[2].value.size());
|
||||
ASSERT_EQ(records[1].value.size(), records[0].value.size() + 4);
|
||||
|
||||
Close();
|
||||
Reopen(options);
|
||||
|
||||
ASSERT_EQ(Get(first_key), value);
|
||||
ASSERT_EQ(Get(second_key), value);
|
||||
ASSERT_EQ(Get(third_key), value);
|
||||
#endif // ZSTD
|
||||
}
|
||||
|
||||
TEST_F(DBBlobDirectWriteTest, DirectWriteFailedBatchTrackedAsInitialGarbage) {
|
||||
Options options = GetDirectWriteOptions();
|
||||
options.blob_file_size = 1 << 20;
|
||||
|
||||
@@ -128,6 +128,16 @@ class DBBlobIndexTest : public DBTestBase {
|
||||
columns, s, is_blob_index, value_found);
|
||||
}
|
||||
|
||||
bool MaybeResolveMemtableBlobValueForTest(const Slice& key,
|
||||
const BlobFetcher* blob_fetcher,
|
||||
PinnableSlice* value,
|
||||
PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index,
|
||||
bool* value_found = nullptr) {
|
||||
return DBImpl::MaybeResolveMemtableBlobValue(
|
||||
key, blob_fetcher, value, columns, s, is_blob_index, value_found);
|
||||
}
|
||||
|
||||
Options GetTestOptions() {
|
||||
Options options;
|
||||
options.env = CurrentOptions().env;
|
||||
@@ -271,6 +281,55 @@ TEST_F(DBBlobIndexTest,
|
||||
ASSERT_EQ(static_cast<char>(BlobIndex::Type::kUnknown), blob_index.front());
|
||||
}
|
||||
|
||||
TEST_F(DBBlobIndexTest,
|
||||
MaybeResolveMemtableBlobValueWithoutFetcherFailsClosed) {
|
||||
// Goal: if a readonly/secondary memtable hit produces a blob-backed payload
|
||||
// but no BlobFetcher is available, the helper must fail closed instead of
|
||||
// handing raw blob-index bytes back to the caller as if they were the value.
|
||||
std::string blob_index;
|
||||
BlobIndex::EncodeBlob(&blob_index, /*file_number=*/123, /*offset=*/456,
|
||||
/*size=*/789, kNoCompression);
|
||||
|
||||
PinnableSlice value;
|
||||
value.GetSelf()->assign(blob_index.data(), blob_index.size());
|
||||
value.PinSelf();
|
||||
|
||||
Status s = Status::OK();
|
||||
bool is_blob_index = true;
|
||||
ASSERT_TRUE(MaybeResolveMemtableBlobValueForTest(
|
||||
Slice("key"), /*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr, &s,
|
||||
&is_blob_index));
|
||||
|
||||
ASSERT_TRUE(s.IsNotSupported()) << s.ToString();
|
||||
ASSERT_TRUE(value.empty());
|
||||
ASSERT_FALSE(is_blob_index);
|
||||
}
|
||||
|
||||
TEST_F(DBBlobIndexTest, ReadOnlyGetImplReturnsBlobIndexWhenRequested) {
|
||||
// Goal: cover the internal read-only GetImpl contract when the caller
|
||||
// explicitly asks for raw blob-index bytes via `is_blob_index`. Recovery
|
||||
// keeps the blob index in the memtable, and the read-only path must preserve
|
||||
// the encoded index instead of eagerly resolving or rejecting it.
|
||||
Options options = GetTestOptions();
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
std::string blob_index;
|
||||
BlobIndex::EncodeInlinedTTL(&blob_index, /*expiration=*/9876543210, "blob");
|
||||
|
||||
WriteBatch batch;
|
||||
ASSERT_OK(PutBlobIndex(&batch, "blob_key", blob_index));
|
||||
ASSERT_OK(Write(&batch));
|
||||
|
||||
Close();
|
||||
options.avoid_flush_during_recovery = true;
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
|
||||
bool is_blob_index = false;
|
||||
ASSERT_EQ(blob_index, GetImpl("blob_key", &is_blob_index));
|
||||
ASSERT_TRUE(is_blob_index);
|
||||
}
|
||||
|
||||
class PlainBlobValueFilterV3 : public CompactionFilter {
|
||||
public:
|
||||
PlainBlobValueFilterV3(std::atomic<int>* filter_call_count,
|
||||
@@ -2138,7 +2197,7 @@ TEST_F(DBBlobIndexTest, EntityBlobFilterV3Remove) {
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Compact to trigger the filter — entity should be removed
|
||||
// Compact to trigger the filter -- entity should be removed
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
// Key should be gone
|
||||
|
||||
@@ -179,6 +179,8 @@ Status BuildTable(
|
||||
TEST_SYNC_POINT_CALLBACK("BuildTable:create_file", &use_direct_writes);
|
||||
#endif // !NDEBUG
|
||||
FileOptions fo_copy = file_options;
|
||||
fo_copy.open_contract = FileOpenContract::kNoReopenForWrite |
|
||||
FileOpenContract::kNoReadersWhileOpenForWrite;
|
||||
fo_copy.write_hint = write_hint;
|
||||
IOStatus io_s = NewWritableFile(fs, fname, &file, fo_copy);
|
||||
assert(s.ok());
|
||||
|
||||
@@ -148,6 +148,7 @@ using ROCKSDB_NAMESPACE::TransactionDB;
|
||||
using ROCKSDB_NAMESPACE::TransactionDBOptions;
|
||||
using ROCKSDB_NAMESPACE::TransactionLogIterator;
|
||||
using ROCKSDB_NAMESPACE::TransactionOptions;
|
||||
using ROCKSDB_NAMESPACE::TxnDBWritePolicy;
|
||||
using ROCKSDB_NAMESPACE::WaitForCompactOptions;
|
||||
using ROCKSDB_NAMESPACE::WALRecoveryMode;
|
||||
using ROCKSDB_NAMESPACE::WritableFile;
|
||||
@@ -1384,6 +1385,10 @@ void rocksdb_backup_engine_info_destroy(
|
||||
delete info;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_stop_backup(rocksdb_backup_engine_t* be) {
|
||||
be->rep->StopBackup();
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_close(rocksdb_backup_engine_t* be) {
|
||||
delete be->rep;
|
||||
delete be;
|
||||
@@ -1465,6 +1470,20 @@ uint64_t rocksdb_backup_engine_options_get_restore_rate_limit(
|
||||
return options->rep.restore_rate_limit;
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_backup_rate_limiter(
|
||||
rocksdb_backup_engine_options_t* options, rocksdb_ratelimiter_t* limiter) {
|
||||
if (limiter) {
|
||||
options->rep.backup_rate_limiter = limiter->rep;
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_restore_rate_limiter(
|
||||
rocksdb_backup_engine_options_t* options, rocksdb_ratelimiter_t* limiter) {
|
||||
if (limiter) {
|
||||
options->rep.restore_rate_limiter = limiter->rep;
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_backup_engine_options_set_max_background_operations(
|
||||
rocksdb_backup_engine_options_t* options, int val) {
|
||||
options->rep.max_background_operations = val;
|
||||
@@ -3771,6 +3790,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;
|
||||
@@ -4242,6 +4266,15 @@ void rocksdb_set_options(rocksdb_t* db, int count, const char* const keys[],
|
||||
SaveError(errptr, db->rep->SetOptions(options_map));
|
||||
}
|
||||
|
||||
void rocksdb_set_db_options(rocksdb_t* db, int count, const char* const keys[],
|
||||
const char* const values[], char** errptr) {
|
||||
std::unordered_map<std::string, std::string> options_map;
|
||||
for (int i = 0; i < count; i++) {
|
||||
options_map[keys[i]] = values[i];
|
||||
}
|
||||
SaveError(errptr, db->rep->SetDBOptions(options_map));
|
||||
}
|
||||
|
||||
void rocksdb_set_options_cf(rocksdb_t* db,
|
||||
rocksdb_column_family_handle_t* handle, int count,
|
||||
const char* const keys[],
|
||||
@@ -5038,6 +5071,16 @@ unsigned char rocksdb_options_get_use_direct_io_for_flush_and_compaction(
|
||||
return opt->rep.use_direct_io_for_flush_and_compaction;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_use_direct_io_for_compaction_reads(
|
||||
rocksdb_options_t* opt, unsigned char v) {
|
||||
opt->rep.use_direct_io_for_compaction_reads = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_use_direct_io_for_compaction_reads(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.use_direct_io_for_compaction_reads;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_allow_mmap_reads(rocksdb_options_t* opt,
|
||||
unsigned char v) {
|
||||
opt->rep.allow_mmap_reads = v;
|
||||
@@ -5286,6 +5329,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;
|
||||
@@ -5389,6 +5441,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,
|
||||
@@ -5820,6 +5882,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:
|
||||
@@ -6238,6 +6302,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) {
|
||||
@@ -7491,6 +7565,11 @@ void rocksdb_transactiondb_options_set_default_lock_timeout(
|
||||
opt->rep.default_lock_timeout = default_lock_timeout;
|
||||
}
|
||||
|
||||
void rocksdb_transactiondb_options_set_write_policy(
|
||||
rocksdb_transactiondb_options_t* opt, int write_policy) {
|
||||
opt->rep.write_policy = static_cast<TxnDBWritePolicy>(write_policy);
|
||||
}
|
||||
|
||||
void rocksdb_transactiondb_options_set_use_per_key_point_lock_mgr(
|
||||
rocksdb_transactiondb_options_t* opt, int use_per_key_point_lock_mgr) {
|
||||
opt->rep.use_per_key_point_lock_mgr = use_per_key_point_lock_mgr;
|
||||
|
||||
+171
-16
@@ -6,6 +6,7 @@
|
||||
#include "rocksdb/c.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <inttypes.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -14,7 +15,6 @@
|
||||
#ifndef OS_WIN
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include <inttypes.h>
|
||||
|
||||
// Can not use port/port.h macros as this is a c file
|
||||
#ifdef OS_WIN
|
||||
@@ -32,6 +32,14 @@ int geteuid() {
|
||||
|
||||
#endif
|
||||
|
||||
static uint64_t GetTestId(void) {
|
||||
#ifdef OS_WIN
|
||||
return (uint64_t)geteuid();
|
||||
#else
|
||||
return ((uint64_t)geteuid() << 32) ^ (uint64_t)getpid();
|
||||
#endif
|
||||
}
|
||||
|
||||
const char* phase = "";
|
||||
static char dbname[200];
|
||||
static char sstfilename[200];
|
||||
@@ -848,20 +856,21 @@ int main(int argc, char** argv) {
|
||||
char* err = NULL;
|
||||
int run = -1;
|
||||
|
||||
snprintf(dbname, sizeof(dbname), "%s/rocksdb_c_test-%d", GetTempDir(),
|
||||
((int)geteuid()));
|
||||
snprintf(dbname, sizeof(dbname), "%s/rocksdb_c_test-%" PRIu64, GetTempDir(),
|
||||
GetTestId());
|
||||
|
||||
snprintf(dbbackupname, sizeof(dbbackupname), "%s/rocksdb_c_test-%d-backup",
|
||||
GetTempDir(), ((int)geteuid()));
|
||||
snprintf(dbbackupname, sizeof(dbbackupname),
|
||||
"%s/rocksdb_c_test-%" PRIu64 "-backup", GetTempDir(), GetTestId());
|
||||
|
||||
snprintf(dbcheckpointname, sizeof(dbcheckpointname),
|
||||
"%s/rocksdb_c_test-%d-checkpoint", GetTempDir(), ((int)geteuid()));
|
||||
"%s/rocksdb_c_test-%" PRIu64 "-checkpoint", GetTempDir(),
|
||||
GetTestId());
|
||||
|
||||
snprintf(sstfilename, sizeof(sstfilename), "%s/rocksdb_c_test-%d-sst",
|
||||
GetTempDir(), ((int)geteuid()));
|
||||
snprintf(sstfilename, sizeof(sstfilename),
|
||||
"%s/rocksdb_c_test-%" PRIu64 "-sst", GetTempDir(), GetTestId());
|
||||
|
||||
snprintf(dbpathname, sizeof(dbpathname), "%s/rocksdb_c_test-%d-dbpath",
|
||||
GetTempDir(), ((int)geteuid()));
|
||||
snprintf(dbpathname, sizeof(dbpathname),
|
||||
"%s/rocksdb_c_test-%" PRIu64 "-dbpath", GetTempDir(), GetTestId());
|
||||
|
||||
StartPhase("create_objects");
|
||||
cmp = rocksdb_comparator_create(NULL, CmpDestroy, CmpCompare, CmpName);
|
||||
@@ -888,6 +897,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);
|
||||
@@ -948,6 +960,19 @@ int main(int argc, char** argv) {
|
||||
CheckNoError(err);
|
||||
CheckGet(db, roptions, "foo", "hello");
|
||||
|
||||
StartPhase("set_db_options");
|
||||
{
|
||||
const char* keys[] = {"stats_dump_period_sec"};
|
||||
const char* values[] = {"10"};
|
||||
rocksdb_set_db_options(db, 1, keys, values, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
keys[0] = "not_a_db_option";
|
||||
rocksdb_set_db_options(db, 1, keys, values, &err);
|
||||
CheckCondition(err != NULL);
|
||||
Free(&err);
|
||||
}
|
||||
|
||||
StartPhase("backup_and_restore");
|
||||
{
|
||||
rocksdb_destroy_db(options, dbbackupname, &err);
|
||||
@@ -1015,6 +1040,12 @@ int main(int argc, char** argv) {
|
||||
Free(&before_db_id);
|
||||
Free(&after_db_id);
|
||||
|
||||
rocksdb_backup_engine_stop_backup(be);
|
||||
rocksdb_backup_engine_create_new_backup(be, db, &err);
|
||||
CheckCondition(err != NULL);
|
||||
free(err);
|
||||
err = NULL;
|
||||
|
||||
rocksdb_backup_engine_close(be);
|
||||
}
|
||||
|
||||
@@ -1144,9 +1175,11 @@ int main(int argc, char** argv) {
|
||||
static char cf_export_path[200];
|
||||
static char db_import_path[200];
|
||||
snprintf(cf_export_path, sizeof(cf_export_path),
|
||||
"%s/rocksdb_c_test-%d-cf_export", GetTempDir(), ((int)geteuid()));
|
||||
"%s/rocksdb_c_test-%" PRIu64 "-cf_export", GetTempDir(),
|
||||
GetTestId());
|
||||
snprintf(db_import_path, sizeof(db_import_path),
|
||||
"%s/rocksdb_c_test-%d-db_import", GetTempDir(), ((int)geteuid()));
|
||||
"%s/rocksdb_c_test-%" PRIu64 "-db_import", GetTempDir(),
|
||||
GetTestId());
|
||||
|
||||
rocksdb_options_t* db_options = rocksdb_options_create();
|
||||
rocksdb_column_family_handle_t* cf_export =
|
||||
@@ -2055,8 +2088,9 @@ int main(int argc, char** argv) {
|
||||
// use dbpathname2 as the cf_path for "cf1"
|
||||
rocksdb_dbpath_t* dbpath2;
|
||||
char dbpathname2[200];
|
||||
snprintf(dbpathname2, sizeof(dbpathname2), "%s/rocksdb_c_test-%d-dbpath2",
|
||||
GetTempDir(), ((int)geteuid()));
|
||||
snprintf(dbpathname2, sizeof(dbpathname2),
|
||||
"%s/rocksdb_c_test-%" PRIu64 "-dbpath2", GetTempDir(),
|
||||
GetTestId());
|
||||
dbpath2 = rocksdb_dbpath_create(dbpathname2, 1024 * 1024);
|
||||
const rocksdb_dbpath_t* cf_paths[1] = {dbpath2};
|
||||
rocksdb_options_set_cf_paths(cf_options_2, cf_paths, 1);
|
||||
@@ -2757,6 +2791,10 @@ int main(int argc, char** argv) {
|
||||
CheckCondition(
|
||||
1 == rocksdb_options_get_use_direct_io_for_flush_and_compaction(o));
|
||||
|
||||
rocksdb_options_set_use_direct_io_for_compaction_reads(o, 1);
|
||||
CheckCondition(1 ==
|
||||
rocksdb_options_get_use_direct_io_for_compaction_reads(o));
|
||||
|
||||
rocksdb_options_set_is_fd_close_on_exec(o, 1);
|
||||
CheckCondition(1 == rocksdb_options_get_is_fd_close_on_exec(o));
|
||||
|
||||
@@ -2812,6 +2850,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));
|
||||
|
||||
@@ -2869,6 +2916,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);
|
||||
@@ -2972,6 +3022,8 @@ int main(int argc, char** argv) {
|
||||
CheckCondition(1 == rocksdb_options_get_use_direct_reads(copy));
|
||||
CheckCondition(
|
||||
1 == rocksdb_options_get_use_direct_io_for_flush_and_compaction(copy));
|
||||
CheckCondition(
|
||||
1 == rocksdb_options_get_use_direct_io_for_compaction_reads(copy));
|
||||
CheckCondition(1 == rocksdb_options_get_is_fd_close_on_exec(copy));
|
||||
CheckCondition(18 == rocksdb_options_get_stats_dump_period_sec(copy));
|
||||
CheckCondition(5 == rocksdb_options_get_stats_persist_period_sec(copy));
|
||||
@@ -2995,6 +3047,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));
|
||||
@@ -3248,6 +3302,12 @@ int main(int argc, char** argv) {
|
||||
CheckCondition(
|
||||
1 == rocksdb_options_get_use_direct_io_for_flush_and_compaction(o));
|
||||
|
||||
rocksdb_options_set_use_direct_io_for_compaction_reads(copy, 0);
|
||||
CheckCondition(
|
||||
0 == rocksdb_options_get_use_direct_io_for_compaction_reads(copy));
|
||||
CheckCondition(1 ==
|
||||
rocksdb_options_get_use_direct_io_for_compaction_reads(o));
|
||||
|
||||
rocksdb_options_set_is_fd_close_on_exec(copy, 0);
|
||||
CheckCondition(0 == rocksdb_options_get_is_fd_close_on_exec(copy));
|
||||
CheckCondition(1 == rocksdb_options_get_is_fd_close_on_exec(o));
|
||||
@@ -3429,6 +3489,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);
|
||||
}
|
||||
|
||||
@@ -3674,6 +3740,52 @@ int main(int argc, char** argv) {
|
||||
CheckCondition(37 ==
|
||||
rocksdb_backup_engine_options_get_restore_rate_limit(bdo));
|
||||
|
||||
{
|
||||
rocksdb_ratelimiter_t* limiter = rocksdb_ratelimiter_create_with_mode(
|
||||
1024 * 1024, 100 * 1000, 10, 2, 0);
|
||||
rocksdb_backup_engine_options_set_backup_rate_limiter(bdo, limiter);
|
||||
rocksdb_backup_engine_options_set_restore_rate_limiter(bdo, limiter);
|
||||
rocksdb_ratelimiter_destroy(limiter);
|
||||
|
||||
rocksdb_backup_engine_options_t* rate_beo =
|
||||
rocksdb_backup_engine_options_create(dbbackupname);
|
||||
rocksdb_ratelimiter_t* backup_limiter =
|
||||
rocksdb_ratelimiter_create_with_mode(1024 * 1024, 100 * 1000, 10, 2,
|
||||
0);
|
||||
rocksdb_ratelimiter_t* restore_limiter =
|
||||
rocksdb_ratelimiter_create_with_mode(1024 * 1024, 100 * 1000, 10, 2,
|
||||
0);
|
||||
rocksdb_backup_engine_options_set_backup_rate_limiter(rate_beo,
|
||||
backup_limiter);
|
||||
rocksdb_backup_engine_options_set_restore_rate_limiter(rate_beo,
|
||||
restore_limiter);
|
||||
rocksdb_ratelimiter_destroy(backup_limiter);
|
||||
rocksdb_ratelimiter_destroy(restore_limiter);
|
||||
rocksdb_env_t* rate_benv = rocksdb_create_default_env();
|
||||
rocksdb_backup_engine_t* rate_be =
|
||||
rocksdb_backup_engine_open_opts(rate_beo, rate_benv, &err);
|
||||
rocksdb_backup_engine_options_destroy(rate_beo);
|
||||
rocksdb_env_destroy(rate_benv);
|
||||
CheckNoError(err);
|
||||
rocksdb_backup_engine_create_new_backup(rate_be, db, &err);
|
||||
CheckNoError(err);
|
||||
{
|
||||
char rate_restore_path[220];
|
||||
snprintf(rate_restore_path, sizeof(rate_restore_path),
|
||||
"%s.rate_restore", dbname);
|
||||
rocksdb_restore_options_t* rate_restore_opts =
|
||||
rocksdb_restore_options_create();
|
||||
rocksdb_backup_engine_restore_db_from_latest_backup(
|
||||
rate_be, rate_restore_path, rate_restore_path, rate_restore_opts,
|
||||
&err);
|
||||
CheckNoError(err);
|
||||
rocksdb_restore_options_destroy(rate_restore_opts);
|
||||
rocksdb_destroy_db(options, rate_restore_path, &err);
|
||||
err = NULL;
|
||||
}
|
||||
rocksdb_backup_engine_close(rate_be);
|
||||
}
|
||||
|
||||
rocksdb_backup_engine_options_set_max_background_operations(bdo, 20);
|
||||
CheckCondition(
|
||||
20 == rocksdb_backup_engine_options_get_max_background_operations(bdo));
|
||||
@@ -3785,6 +3897,43 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
}
|
||||
|
||||
StartPhase("transactiondb_set_write_policy_prepared");
|
||||
{
|
||||
char wp_dbname[200];
|
||||
rocksdb_transactiondb_t* wp_txn_db;
|
||||
rocksdb_transactiondb_options_t* wp_txn_opts;
|
||||
snprintf(wp_dbname, sizeof(wp_dbname), "%s/rocksdb_c_test-txnwp-%" PRIu64,
|
||||
GetTempDir(), GetTestId());
|
||||
rocksdb_destroy_db(options, wp_dbname, &err);
|
||||
Free(&err);
|
||||
wp_txn_opts = rocksdb_transactiondb_options_create();
|
||||
rocksdb_transactiondb_options_set_write_policy(
|
||||
wp_txn_opts, rocksdb_txndb_write_policy_write_prepared);
|
||||
rocksdb_options_set_create_if_missing(options, 1);
|
||||
// create new TransactionDB with write policy WRITE_PREPARED
|
||||
wp_txn_db =
|
||||
rocksdb_transactiondb_open(options, wp_txn_opts, wp_dbname, &err);
|
||||
CheckNoError(err);
|
||||
{
|
||||
rocksdb_transaction_options_t* wp_txn_options =
|
||||
rocksdb_transaction_options_create();
|
||||
rocksdb_transaction_t* wp_txn =
|
||||
rocksdb_transaction_begin(wp_txn_db, woptions, wp_txn_options, NULL);
|
||||
// write one record within a transaction
|
||||
rocksdb_transaction_put(wp_txn, "k", 1, "v", 1, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_transaction_commit(wp_txn, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_transaction_destroy(wp_txn);
|
||||
rocksdb_transaction_options_destroy(wp_txn_options);
|
||||
}
|
||||
CheckTxnDBGet(wp_txn_db, roptions, "k", "v");
|
||||
rocksdb_transactiondb_close(wp_txn_db);
|
||||
rocksdb_transactiondb_options_destroy(wp_txn_opts);
|
||||
rocksdb_destroy_db(options, wp_dbname, &err);
|
||||
CheckNoError(err);
|
||||
}
|
||||
|
||||
StartPhase("transactions");
|
||||
{
|
||||
rocksdb_close(db);
|
||||
@@ -4422,7 +4571,7 @@ int main(int argc, char** argv) {
|
||||
rocksdb_options_set_max_open_files(opts, -1);
|
||||
rocksdb_options_set_create_if_missing(opts, 1);
|
||||
snprintf(secondary_path, sizeof(secondary_path),
|
||||
"%s/rocksdb_c_test_secondary-%d", GetTempDir(), ((int)geteuid()));
|
||||
"%s/rocksdb_c_test_secondary-%" PRIu64, GetTempDir(), GetTestId());
|
||||
db1 = rocksdb_open_as_secondary(opts, dbname, secondary_path, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
@@ -4881,7 +5030,13 @@ int main(int argc, char** argv) {
|
||||
rocksdb_options_set_create_if_missing(null_opts, 1);
|
||||
rocksdb_options_set_compaction_service(null_opts, null_service);
|
||||
|
||||
const char* null_db = "rocksdb_c_test_null_service";
|
||||
char null_db[200];
|
||||
int null_db_len = snprintf(null_db, sizeof(null_db),
|
||||
"%s/rocksdb_c_test_null_service-%" PRIu64,
|
||||
GetTempDir(), GetTestId());
|
||||
if (null_db_len <= 0 || (size_t)null_db_len >= sizeof(null_db)) {
|
||||
abort();
|
||||
}
|
||||
|
||||
rocksdb_t* null_db_handle = rocksdb_open(null_opts, null_db, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
@@ -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_;
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ class CompactionBlobResolver : public WideColumnBlobResolver {
|
||||
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
|
||||
// (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_;
|
||||
|
||||
@@ -173,6 +173,7 @@ CompactionJob::CompactionJob(
|
||||
fs_(db_options.fs, io_tracer),
|
||||
file_options_for_read_(
|
||||
fs_->OptimizeForCompactionTableRead(file_options, db_options_)),
|
||||
file_options_for_compaction_input_read_(file_options_for_read_),
|
||||
versions_(versions),
|
||||
shutting_down_(shutting_down),
|
||||
manual_compaction_canceled_(manual_compaction_canceled),
|
||||
@@ -203,6 +204,11 @@ CompactionJob::CompactionJob(
|
||||
assert(job_context);
|
||||
assert(job_context->snapshot_context_initialized);
|
||||
|
||||
if (db_options_.use_direct_io_for_compaction_reads &&
|
||||
!db_options_.use_direct_reads) {
|
||||
file_options_for_compaction_input_read_.use_direct_reads = true;
|
||||
}
|
||||
|
||||
const auto* cfd = compact_->compaction->column_family_data();
|
||||
ThreadStatusUtil::SetEnableTracking(db_options_.enable_thread_tracking);
|
||||
ThreadStatusUtil::SetColumnFamily(cfd);
|
||||
@@ -1536,10 +1542,20 @@ InternalIterator* CompactionJob::CreateInputIterator(
|
||||
|
||||
// Although the v2 aggregator is what the level iterator(s) know about,
|
||||
// the AddTombstones calls will be propagated down to the v1 aggregator.
|
||||
const bool open_ephemeral_table_reader =
|
||||
db_options_.use_direct_io_for_compaction_reads &&
|
||||
!db_options_.use_direct_reads;
|
||||
FileOptions& input_file_options =
|
||||
open_ephemeral_table_reader ? file_options_for_compaction_input_read_
|
||||
: file_options_for_read_;
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"CompactionJob::CreateInputIterator:InputFileOptions",
|
||||
&input_file_options);
|
||||
iterators.raw_input =
|
||||
std::unique_ptr<InternalIterator>(versions_->MakeInputIterator(
|
||||
read_options, sub_compact->compaction, sub_compact->RangeDelAgg(),
|
||||
file_options_for_read_, boundaries.start, boundaries.end));
|
||||
input_file_options, boundaries.start, boundaries.end,
|
||||
open_ephemeral_table_reader));
|
||||
InternalIterator* input = iterators.raw_input.get();
|
||||
|
||||
if (boundaries.start.has_value() || boundaries.end.has_value()) {
|
||||
@@ -2065,10 +2081,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;
|
||||
@@ -2462,6 +2486,8 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
auto temperature =
|
||||
sub_compact->compaction->GetOutputTemperature(outputs.IsProximalLevel());
|
||||
fo_copy.temperature = temperature;
|
||||
fo_copy.open_contract = FileOpenContract::kNoReopenForWrite |
|
||||
FileOpenContract::kNoReadersWhileOpenForWrite;
|
||||
fo_copy.write_hint = write_hint_;
|
||||
|
||||
Status s;
|
||||
|
||||
@@ -462,6 +462,8 @@ class CompactionJob {
|
||||
FileSystemPtr fs_;
|
||||
// env_option optimized for compaction table reads
|
||||
FileOptions file_options_for_read_;
|
||||
// file_options_for_read_ plus compaction-input-only overrides.
|
||||
FileOptions file_options_for_compaction_input_read_;
|
||||
VersionSet* versions_;
|
||||
const std::atomic<bool>* shutting_down_;
|
||||
const std::atomic<bool>& manual_compaction_canceled_;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1101,7 +1101,7 @@ TEST_F(CompactionPickerTest, ReadTriggeredSkipsLastLevel) {
|
||||
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
|
||||
// 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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -421,6 +431,14 @@ Status CompactionServiceCompactionJob::Run() {
|
||||
// Please note that input stats will be updated by primary host when all
|
||||
// subcompactions are finished
|
||||
UpdateCompactionJobOutputStatsFromInternalStats(status, internal_stats_);
|
||||
// Whole-file filtering changes which keys are fed to the iterator, so the
|
||||
// iterator-based input record count should not be treated as exact.
|
||||
for (size_t level = 1; level < c->num_input_levels(); ++level) {
|
||||
if (!c->filtered_input_levels(level).empty()) {
|
||||
compaction_result_->stats.has_accurate_num_input_records = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// and set fields that are not propagated as part of the update
|
||||
compaction_result_->stats.is_manual_compaction = c->is_manual_compaction();
|
||||
compaction_result_->stats.is_full_compaction = c->is_full_compaction();
|
||||
@@ -641,6 +659,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,
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -607,6 +611,101 @@ TEST_F(CompactionServiceTest, StandaloneDeleteRangeTombstoneOptimization) {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest,
|
||||
StandaloneDeleteRangeTombstoneMakesInputCountInaccurate) {
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = CompactionStyle::kCompactionStyleUniversal;
|
||||
options.compaction_verify_record_count = true;
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
std::vector<std::string> files;
|
||||
{
|
||||
SstFileWriter sst_file_writer(EnvOptions(), options);
|
||||
std::string file1 = dbname_ + "file1.sst";
|
||||
ASSERT_OK(sst_file_writer.Open(file1));
|
||||
ASSERT_OK(sst_file_writer.Put("a", "a1"));
|
||||
ASSERT_OK(sst_file_writer.Put("b", "b1"));
|
||||
ExternalSstFileInfo file1_info;
|
||||
ASSERT_OK(sst_file_writer.Finish(&file1_info));
|
||||
files.push_back(std::move(file1));
|
||||
|
||||
std::string file2 = dbname_ + "file2.sst";
|
||||
ASSERT_OK(sst_file_writer.Open(file2));
|
||||
ASSERT_OK(sst_file_writer.Put("x", "x1"));
|
||||
ASSERT_OK(sst_file_writer.Put("y", "y1"));
|
||||
ExternalSstFileInfo file2_info;
|
||||
ASSERT_OK(sst_file_writer.Finish(&file2_info));
|
||||
files.push_back(std::move(file2));
|
||||
}
|
||||
|
||||
IngestExternalFileOptions ifo;
|
||||
ASSERT_OK(db_->IngestExternalFile(files, ifo));
|
||||
ASSERT_EQ(Get("a"), "a1");
|
||||
ASSERT_EQ(Get("b"), "b1");
|
||||
ASSERT_EQ(Get("x"), "x1");
|
||||
ASSERT_EQ(Get("y"), "y1");
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(6));
|
||||
|
||||
auto my_cs = GetCompactionService();
|
||||
uint64_t comp_num = my_cs->GetCompactionNum();
|
||||
|
||||
size_t num_files_after_filtered = 0;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::MakeInputIterator:NewCompactionMergingIterator",
|
||||
[&](void* arg) {
|
||||
num_files_after_filtered = *static_cast<size_t*>(arg);
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
{
|
||||
// The standalone range tombstone can fully cover the old versioned files,
|
||||
// so universal compaction may filter those files before iteration.
|
||||
files.clear();
|
||||
SstFileWriter sst_file_writer(EnvOptions(), options);
|
||||
std::string file2 = dbname_ + "file2.sst";
|
||||
ASSERT_OK(sst_file_writer.Open(file2));
|
||||
ASSERT_OK(sst_file_writer.DeleteRange("a", "z"));
|
||||
ExternalSstFileInfo file2_info;
|
||||
ASSERT_OK(sst_file_writer.Finish(&file2_info));
|
||||
files.push_back(std::move(file2));
|
||||
|
||||
std::string file3 = dbname_ + "file3.sst";
|
||||
ASSERT_OK(sst_file_writer.Open(file3));
|
||||
ASSERT_OK(sst_file_writer.Put("a", "a2"));
|
||||
ASSERT_OK(sst_file_writer.Put("b", "b2"));
|
||||
ExternalSstFileInfo file3_info;
|
||||
ASSERT_OK(sst_file_writer.Finish(&file3_info));
|
||||
files.push_back(std::move(file3));
|
||||
|
||||
std::string file4 = dbname_ + "file4.sst";
|
||||
ASSERT_OK(sst_file_writer.Open(file4));
|
||||
ASSERT_OK(sst_file_writer.Put("x", "x2"));
|
||||
ASSERT_OK(sst_file_writer.Put("y", "y2"));
|
||||
ExternalSstFileInfo file4_info;
|
||||
ASSERT_OK(sst_file_writer.Finish(&file4_info));
|
||||
files.push_back(std::move(file4));
|
||||
}
|
||||
|
||||
ASSERT_OK(db_->IngestExternalFile(files, ifo));
|
||||
ASSERT_OK(db_->WaitForCompact(WaitForCompactOptions()));
|
||||
ASSERT_GE(my_cs->GetCompactionNum(), comp_num + 1);
|
||||
|
||||
CompactionServiceResult result;
|
||||
my_cs->GetResult(&result);
|
||||
ASSERT_OK(result.status);
|
||||
ASSERT_TRUE(result.stats.is_remote_compaction);
|
||||
ASSERT_FALSE(result.stats.has_accurate_num_input_records);
|
||||
ASSERT_EQ(1, num_files_after_filtered);
|
||||
|
||||
ASSERT_EQ(Get("a"), "a2");
|
||||
ASSERT_EQ(Get("b"), "b2");
|
||||
ASSERT_EQ(Get("x"), "x2");
|
||||
ASSERT_EQ(Get("y"), "y2");
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, CompactionOutputFileIOError) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
@@ -939,6 +1038,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 +1411,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 +1582,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 +1590,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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+114
-9
@@ -186,14 +186,14 @@ TEST_F(DBBasicTest,
|
||||
}
|
||||
|
||||
// When the per-CF log_number actually advances, the per-CF skip must NOT
|
||||
// fire — the edit carries real information and must be emitted.
|
||||
// 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
|
||||
// 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();
|
||||
@@ -230,7 +230,7 @@ TEST_F(DBBasicTest, OptimizeManifestForRecoveryPreservesWalTracking) {
|
||||
// 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
|
||||
// best_efforts_recovery -- otherwise CURRENT can be left missing or
|
||||
// stale (regression caught by DBBasicTest.RecoverWithNoCurrentFile and
|
||||
// DBTest2.BestEffortsRecoveryWithSstUniqueIdVerification under an
|
||||
// option-on default).
|
||||
@@ -267,7 +267,7 @@ TEST_F(DBBasicTest, OptimizeManifestForRecoveryMultiCF) {
|
||||
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
|
||||
// CF 1 and 2 keep dirty memtables -- recovery must emit their per-CF
|
||||
// edits (log_number advance from WAL replay).
|
||||
Close();
|
||||
|
||||
@@ -662,7 +662,7 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenStillRotatesOnSizeCap) {
|
||||
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
|
||||
// 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"));
|
||||
@@ -685,7 +685,7 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenMultiCF) {
|
||||
Close();
|
||||
|
||||
ReopenWithColumnFamilies({"default", "alpha", "beta"}, options);
|
||||
// Trigger more writes post-reopen on each CF — these get appended to
|
||||
// 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"));
|
||||
@@ -761,6 +761,111 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenFullStackComposition) {
|
||||
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.
|
||||
@@ -777,7 +882,7 @@ TEST_F(DBBasicTest, WritableFileWriterInitialFileSizeAdoptsExistingSize) {
|
||||
ASSERT_OK(raw->Close());
|
||||
}
|
||||
|
||||
// Reopen and wrap in a WritableFileWriter without initial_file_size —
|
||||
// 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(
|
||||
@@ -805,7 +910,7 @@ TEST_F(DBBasicTest, WritableFileWriterInitialFileSizeAdoptsExistingSize) {
|
||||
}
|
||||
|
||||
// Tail corruption: appending garbage bytes to the MANIFEST after a
|
||||
// clean close must prevent reuse — the physical size exceeds the
|
||||
// clean close must prevent reuse -- the physical size exceeds the
|
||||
// last valid record end.
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
|
||||
Options options = CurrentOptions();
|
||||
@@ -6226,7 +6331,7 @@ TEST_F(DBBasicTest, DisallowMemtableWrite) {
|
||||
Options options_disallow = options_allow;
|
||||
options_disallow.disallow_memtable_writes = true;
|
||||
options_disallow.paranoid_memory_checks = true;
|
||||
options_disallow.memtable_veirfy_per_key_checksum_on_seek = true;
|
||||
options_disallow.memtable_verify_per_key_checksum_on_seek = true;
|
||||
|
||||
DestroyAndReopen(options_allow);
|
||||
// CFs allowing and disallowing memtable write
|
||||
|
||||
@@ -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"));
|
||||
@@ -1482,10 +1482,13 @@ TEST_P(DBBlockCacheTypeTest, AddRedundantStats) {
|
||||
// Access just data, forcing redundant load+insert
|
||||
ReadOptions read_options;
|
||||
std::unique_ptr<Iterator> iter{db_->NewIterator(read_options)};
|
||||
ASSERT_OK(iter->Refresh());
|
||||
|
||||
cache->SetNthLookupNotFound(1);
|
||||
iter->SeekToFirst();
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), "bar");
|
||||
ASSERT_EQ(iter->value(), "value");
|
||||
|
||||
EXPECT_EQ(2, TestGetTickerCount(options, BLOCK_CACHE_INDEX_ADD));
|
||||
EXPECT_EQ(2, TestGetTickerCount(options, BLOCK_CACHE_FILTER_ADD));
|
||||
|
||||
@@ -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;
|
||||
|
||||
+711
-5
@@ -8,11 +8,13 @@
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
#include "compaction/compaction_picker_universal.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/dbformat.h"
|
||||
#include "db/table_cache.h"
|
||||
#include "env/mock_env.h"
|
||||
#include "file/filename.h"
|
||||
#include "port/port.h"
|
||||
@@ -6651,6 +6653,572 @@ TEST_P(DBCompactionDirectIOTest, DirectIO) {
|
||||
INSTANTIATE_TEST_CASE_P(DBCompactionDirectIOTest, DBCompactionDirectIOTest,
|
||||
testing::Bool());
|
||||
|
||||
// With use_direct_io_for_compaction_reads OFF, compaction reads must stay
|
||||
// buffered: neither the compaction-input FileOptions nor a kernel O_DIRECT
|
||||
// open should fire. Runs on every platform (the sync points just don't fire
|
||||
// where O_DIRECT isn't reachable). Pairs with
|
||||
// UseDirectIoForCompactionReadsEndToEnd for the on case.
|
||||
TEST_F(DBCompactionTest, UseDirectIoForCompactionReadsOffStaysBuffered) {
|
||||
Options options = CurrentOptions();
|
||||
Destroy(options);
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
options.use_direct_reads = false;
|
||||
options.use_direct_io_for_compaction_reads = false;
|
||||
options.use_direct_io_for_flush_and_compaction = false;
|
||||
|
||||
std::atomic<bool> observed_direct_compaction_read{false};
|
||||
std::atomic<int> observed_callbacks{0};
|
||||
std::atomic<int> observed_odirect_opens{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::CreateInputIterator:InputFileOptions", [&](void* arg) {
|
||||
const auto* fo = static_cast<const FileOptions*>(arg);
|
||||
observed_callbacks.fetch_add(1, std::memory_order_relaxed);
|
||||
if (fo->use_direct_reads) {
|
||||
observed_direct_compaction_read.store(true,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"NewRandomAccessFile:O_DIRECT", [&](void* /*arg*/) {
|
||||
observed_odirect_opens.fetch_add(1, std::memory_order_relaxed);
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(TryReopen(options));
|
||||
|
||||
const std::string value(4096, 'v');
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
ASSERT_OK(Put(Key(i), value));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
ASSERT_OK(Put(Key(i), value));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
ASSERT_GT(observed_callbacks.load(), 0);
|
||||
ASSERT_FALSE(observed_direct_compaction_read.load());
|
||||
ASSERT_EQ(0, observed_odirect_opens.load());
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
Destroy(options);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest,
|
||||
UseDirectIoForCompactionReadsUsesBoundedEphemeralReaders) {
|
||||
auto fs = std::make_shared<MockFileSystem>(Env::Default()->GetSystemClock(),
|
||||
/*supports_direct_io=*/true);
|
||||
std::unique_ptr<Env> mock_env = NewCompositeEnv(fs);
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.env = mock_env.get();
|
||||
Destroy(options);
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
options.use_direct_reads = false;
|
||||
options.use_direct_io_for_compaction_reads = true;
|
||||
options.use_direct_io_for_flush_and_compaction = false;
|
||||
options.max_subcompactions = 3;
|
||||
options.statistics = CreateDBStatistics();
|
||||
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.block_cache = NewLRUCache(64 << 20);
|
||||
table_options.cache_index_and_filter_blocks = true;
|
||||
table_options.pin_l0_filter_and_index_blocks_in_cache = true;
|
||||
table_options.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
|
||||
std::atomic<int> fresh_reader_opens{0};
|
||||
std::atomic<int> malformed_fresh_reader_options{0};
|
||||
std::atomic<int> avoid_shared_metadata_cache_opens{0};
|
||||
std::atomic<int> shared_metadata_cache_uses{0};
|
||||
std::atomic<size_t> input_iterator_file_bound{0};
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"TableCache::FindTable:FreshTableReader", [&](void* arg) {
|
||||
const auto* open_options = static_cast<TableCacheOpenOptions*>(arg);
|
||||
fresh_reader_opens.fetch_add(1, std::memory_order_relaxed);
|
||||
if (!open_options->open_ephemeral_table_reader ||
|
||||
!open_options->avoid_shared_metadata_cache ||
|
||||
!open_options->skip_filters) {
|
||||
malformed_fresh_reader_options.fetch_add(1,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlockBasedTable::PrefetchIndexAndFilterBlocks:SharedMetadataCache",
|
||||
[&](void* arg) {
|
||||
const auto* context = static_cast<std::pair<bool, bool>*>(arg);
|
||||
const bool avoid_shared_metadata_cache = context->first;
|
||||
const bool use_cache = context->second;
|
||||
if (avoid_shared_metadata_cache) {
|
||||
avoid_shared_metadata_cache_opens.fetch_add(
|
||||
1, std::memory_order_relaxed);
|
||||
if (use_cache) {
|
||||
shared_metadata_cache_uses.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::MakeInputIterator:NewCompactionMergingIterator",
|
||||
[&](void* arg) {
|
||||
input_iterator_file_bound.fetch_add(*static_cast<size_t*>(arg),
|
||||
std::memory_order_relaxed);
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(TryReopen(options));
|
||||
|
||||
const std::string value(4096, 'v');
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
ASSERT_OK(Put(Key(i), value));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
ASSERT_OK(Put(Key(i), value));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
ASSERT_GT(fresh_reader_opens.load(), 0);
|
||||
EXPECT_EQ(0, malformed_fresh_reader_options.load());
|
||||
ASSERT_GT(avoid_shared_metadata_cache_opens.load(), 0);
|
||||
EXPECT_EQ(0, shared_metadata_cache_uses.load());
|
||||
ASSERT_GT(input_iterator_file_bound.load(), 0);
|
||||
EXPECT_LE(static_cast<size_t>(fresh_reader_opens.load()),
|
||||
input_iterator_file_bound.load());
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
Destroy(options);
|
||||
}
|
||||
|
||||
// End-to-end check that use_direct_io_for_compaction_reads opens compaction
|
||||
// inputs with O_DIRECT while use_direct_reads stays off (user reads buffered).
|
||||
// The NewRandomAccessFile:O_DIRECT sync point in env/fs_posix.cc fires once
|
||||
// per fresh open with the O_DIRECT flag, so this proves the kernel path, not
|
||||
// just the FileOptions. Only runs on platforms that take the O_DIRECT path.
|
||||
#if !defined(OS_MACOSX) && !defined(OS_OPENBSD) && !defined(OS_SOLARIS) && \
|
||||
!defined(OS_WIN)
|
||||
TEST_F(DBCompactionTest, UseDirectIoForCompactionReadsEndToEnd) {
|
||||
if (!IsDirectIOSupported()) {
|
||||
ROCKSDB_GTEST_BYPASS("Direct IO not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
Options options = CurrentOptions();
|
||||
Destroy(options);
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
// User reads stay buffered, compaction reads should switch to O_DIRECT.
|
||||
options.use_direct_reads = false;
|
||||
options.use_direct_io_for_compaction_reads = true;
|
||||
// Isolate the read-side change; leave the compaction write path buffered.
|
||||
options.use_direct_io_for_flush_and_compaction = false;
|
||||
|
||||
// Sync-point callbacks fire on compaction threads while the test thread
|
||||
// reads these counters, so use atomics to avoid a data race.
|
||||
std::atomic<int> observed_run_starts{0};
|
||||
std::atomic<int> observed_odirect_opens{0};
|
||||
std::atomic<bool> observed_direct_compaction_read{false};
|
||||
std::atomic<int> observed_callbacks{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({});
|
||||
// Plumbing-level probe: the compaction-input FileOptions should carry
|
||||
// use_direct_reads = true when the new flag is enabled.
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::CreateInputIterator:InputFileOptions", [&](void* arg) {
|
||||
const auto* fo = static_cast<const FileOptions*>(arg);
|
||||
observed_callbacks.fetch_add(1, std::memory_order_relaxed);
|
||||
if (fo != nullptr && fo->use_direct_reads) {
|
||||
observed_direct_compaction_read.store(true,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::Run():Start", [&](void* /*arg*/) {
|
||||
observed_run_starts.fetch_add(1, std::memory_order_relaxed);
|
||||
});
|
||||
// Kernel-level probe: this fires only when open() is issued with O_DIRECT,
|
||||
// proving we change the actual cache mode, not just the FileOptions struct.
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"NewRandomAccessFile:O_DIRECT", [&](void* /*arg*/) {
|
||||
observed_odirect_opens.fetch_add(1, std::memory_order_relaxed);
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status s = TryReopen(options);
|
||||
if (s.IsNotSupported() || s.IsInvalidArgument()) {
|
||||
ROCKSDB_GTEST_BYPASS(
|
||||
"Direct IO reads not supported in this test environment");
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
return;
|
||||
}
|
||||
ASSERT_OK(s);
|
||||
|
||||
// Produce two L0 files with OVERLAPPING key ranges so that CompactRange has
|
||||
// actual merge work to do (otherwise RocksDB performs a trivial file move
|
||||
// and never constructs a CompactionJob).
|
||||
const std::string value(4096, 'v');
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
ASSERT_OK(Put(Key(i), value));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
ASSERT_OK(Put(Key(i), value));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// User reads should still go through the buffered path. Confirm that the
|
||||
// option does not silently flip use_direct_reads for user reads.
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
std::string actual;
|
||||
ASSERT_OK(db_->Get(ReadOptions(), Key(i), &actual));
|
||||
ASSERT_EQ(value, actual);
|
||||
}
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
// Wait for compaction to complete and CompactionJob to be constructed.
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
// Confirm the compaction actually ran; otherwise the missing sync-point hits
|
||||
// would be a test-setup problem, not an option regression.
|
||||
ASSERT_GT(observed_run_starts.load(), 0)
|
||||
<< "CompactionJob::Run():Start never fired; CompactRange did not "
|
||||
"schedule a compaction.";
|
||||
ASSERT_GT(observed_callbacks.load(), 0);
|
||||
ASSERT_TRUE(observed_direct_compaction_read.load());
|
||||
// At least one compaction-input open went through O_DIRECT. Without the
|
||||
// TableCache bypass this would be zero, since compaction would reuse the
|
||||
// buffered handles cached for user reads.
|
||||
EXPECT_GT(observed_odirect_opens.load(), 0)
|
||||
<< "no compaction-input opens went through O_DIRECT; "
|
||||
"observed_odirect_opens="
|
||||
<< observed_odirect_opens.load();
|
||||
|
||||
// Quick sanity sweep after compaction to confirm data is intact.
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
std::string actual;
|
||||
ASSERT_OK(db_->Get(ReadOptions(), Key(i), &actual));
|
||||
ASSERT_EQ(value, actual);
|
||||
}
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
Destroy(options);
|
||||
}
|
||||
|
||||
// Exercise the LevelIterator bypass path (L1+ compactions) with range
|
||||
// tombstones, where the ephemeral TableReader's lifetime is coupled to the
|
||||
// range_tombstone_iter the file iterator returns. The end-to-end test above
|
||||
// only makes two L0 files, which take the direct NewIterator path and never
|
||||
// hit LevelIterator. Here we build L1/L2 with tombstones and compact L1->L2 so
|
||||
// LevelIterator::NewFileIterator drives the bypass; a wrong reader/iterator
|
||||
// lifetime would crash or trip sanitizers as LevelIterator switches files.
|
||||
// Correctness is checked by computing the expected state of every key and
|
||||
// asserting Get() matches: wave-2 puts beat wave-1, and each key's most-recent
|
||||
// covering tombstone shadows older puts.
|
||||
TEST_F(DBCompactionTest,
|
||||
UseDirectIoForCompactionReadsLevelIteratorWithTombstones) {
|
||||
if (!IsDirectIOSupported()) {
|
||||
ROCKSDB_GTEST_BYPASS("Direct IO not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
Options options = CurrentOptions();
|
||||
Destroy(options);
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
options.use_direct_reads = false;
|
||||
options.use_direct_io_for_compaction_reads = true;
|
||||
options.use_direct_io_for_flush_and_compaction = false;
|
||||
// Small files / small level base so we can pack data into L1 and L2 with
|
||||
// a few flushes and CompactRange calls instead of needing millions of keys.
|
||||
options.write_buffer_size = 64 * 1024;
|
||||
options.target_file_size_base = 64 * 1024;
|
||||
options.max_bytes_for_level_base = 256 * 1024;
|
||||
options.level0_file_num_compaction_trigger = 100; // never auto-trigger
|
||||
|
||||
std::atomic<int> observed_odirect_opens{0};
|
||||
std::atomic<int> observed_run_starts{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"NewRandomAccessFile:O_DIRECT", [&](void* /*arg*/) {
|
||||
observed_odirect_opens.fetch_add(1, std::memory_order_relaxed);
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::Run():Start", [&](void* /*arg*/) {
|
||||
observed_run_starts.fetch_add(1, std::memory_order_relaxed);
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status s = TryReopen(options);
|
||||
if (s.IsNotSupported() || s.IsInvalidArgument()) {
|
||||
ROCKSDB_GTEST_BYPASS(
|
||||
"Direct IO reads not supported in this test environment");
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
return;
|
||||
}
|
||||
ASSERT_OK(s);
|
||||
|
||||
// Use distinguishable values per wave so we can verify which wave's put
|
||||
// won the merge for each key, not just "some put won".
|
||||
const std::string wave1_value(1024, '1');
|
||||
const std::string wave2_value(1024, '2');
|
||||
|
||||
auto write_batch = [&](int begin, int end, const std::string& value,
|
||||
bool with_range_tombstone) {
|
||||
for (int i = begin; i < end; ++i) {
|
||||
ASSERT_OK(Put(Key(i), value));
|
||||
}
|
||||
if (with_range_tombstone) {
|
||||
// Drop a slice in the middle of the just-written range. The
|
||||
// DeleteRange follows the puts in this batch, so its sequence number
|
||||
// is higher and it shadows the puts on [del_lo, del_hi) within this
|
||||
// SST.
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(),
|
||||
Key(begin + (end - begin) / 4),
|
||||
Key(begin + 3 * (end - begin) / 4)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
};
|
||||
|
||||
// Wave 1: four flushed SSTs covering [0, 800), each with a tombstone
|
||||
// covering the middle half of its own range. Then compact down so the
|
||||
// next phase exercises LevelIterator over L1+ files.
|
||||
constexpr int kWave1Begin = 0;
|
||||
constexpr int kWave1End = 800;
|
||||
constexpr int kWave1BatchSize = 200;
|
||||
for (int batch_begin = kWave1Begin; batch_begin < kWave1End;
|
||||
batch_begin += kWave1BatchSize) {
|
||||
write_batch(batch_begin, batch_begin + kWave1BatchSize, wave1_value,
|
||||
/*with_range_tombstone=*/true);
|
||||
}
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
// Wave 2: two more flushed SSTs at L0 that overlap wave 1, each with their
|
||||
// own range tombstone. The puts in wave 2 have higher seqno than wave 1,
|
||||
// so they win when not tombstoned.
|
||||
struct Wave2Range {
|
||||
int begin;
|
||||
int end;
|
||||
};
|
||||
const Wave2Range wave2_ranges[] = {{50, 250}, {350, 550}};
|
||||
for (const auto& r : wave2_ranges) {
|
||||
write_batch(r.begin, r.end, wave2_value, /*with_range_tombstone=*/true);
|
||||
}
|
||||
|
||||
const int run_starts_before = observed_run_starts.load();
|
||||
const int odirect_before = observed_odirect_opens.load();
|
||||
|
||||
// Compact everything together, forcing a LevelIterator over the lower-level
|
||||
// files on the bypass path. Wrong ephemeral reader / tombstone-iter
|
||||
// lifetimes should trip sanitizers here.
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
ASSERT_GT(observed_run_starts.load(), run_starts_before)
|
||||
<< "expected at least one compaction to run during the L1+ phase";
|
||||
EXPECT_GT(observed_odirect_opens.load(), odirect_before)
|
||||
<< "no compaction-input opens went through O_DIRECT during L1+ "
|
||||
"compaction; LevelIterator bypass path may be broken";
|
||||
|
||||
// Compute the precise expected state per key from the test parameters.
|
||||
// For each k in the wave-1 range:
|
||||
// - If k is inside a wave-2 batch: wave-2 wins. NotFound iff k is in
|
||||
// that batch's tombstone range; otherwise present with wave2_value.
|
||||
// - Else: wave 1 wins. NotFound iff k is in its batch's tombstone
|
||||
// range; otherwise present with wave1_value.
|
||||
enum class Expectation { kAbsent, kWave1Value, kWave2Value };
|
||||
auto classify = [&](int k) -> Expectation {
|
||||
// Wave 2 first (it shadows wave 1 wherever it covers).
|
||||
for (const auto& r : wave2_ranges) {
|
||||
if (k >= r.begin && k < r.end) {
|
||||
const int width = r.end - r.begin;
|
||||
const int del_lo = r.begin + width / 4;
|
||||
const int del_hi = r.begin + 3 * width / 4;
|
||||
if (k >= del_lo && k < del_hi) {
|
||||
return Expectation::kAbsent;
|
||||
}
|
||||
return Expectation::kWave2Value;
|
||||
}
|
||||
}
|
||||
// Fall back to wave 1.
|
||||
const int batch = (k - kWave1Begin) / kWave1BatchSize;
|
||||
const int batch_begin = kWave1Begin + batch * kWave1BatchSize;
|
||||
const int del_lo = batch_begin + kWave1BatchSize / 4;
|
||||
const int del_hi = batch_begin + 3 * kWave1BatchSize / 4;
|
||||
if (k >= del_lo && k < del_hi) {
|
||||
return Expectation::kAbsent;
|
||||
}
|
||||
return Expectation::kWave1Value;
|
||||
};
|
||||
|
||||
int present_w1 = 0;
|
||||
int present_w2 = 0;
|
||||
int absent = 0;
|
||||
std::string actual;
|
||||
for (int k = kWave1Begin; k < kWave1End; ++k) {
|
||||
const Status get_s = db_->Get(ReadOptions(), Key(k), &actual);
|
||||
const Expectation exp = classify(k);
|
||||
switch (exp) {
|
||||
case Expectation::kAbsent:
|
||||
EXPECT_TRUE(get_s.IsNotFound())
|
||||
<< "key " << k << " expected NotFound (covered by tombstone); "
|
||||
<< "got status=" << get_s.ToString()
|
||||
<< " value_len=" << (get_s.ok() ? actual.size() : 0);
|
||||
++absent;
|
||||
break;
|
||||
case Expectation::kWave1Value:
|
||||
ASSERT_OK(get_s) << "key " << k << " expected wave-1 value";
|
||||
EXPECT_EQ(wave1_value, actual)
|
||||
<< "key " << k << " expected wave-1 value but got a different one";
|
||||
++present_w1;
|
||||
break;
|
||||
case Expectation::kWave2Value:
|
||||
ASSERT_OK(get_s) << "key " << k << " expected wave-2 value";
|
||||
EXPECT_EQ(wave2_value, actual)
|
||||
<< "key " << k << " expected wave-2 value but got a different one";
|
||||
++present_w2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// All three buckets should be non-empty, so the test really exercises both
|
||||
// tombstone paths and the wave-2-wins path. A zero here means the setup
|
||||
// drifted and the setup (not these checks) should be fixed.
|
||||
EXPECT_GT(present_w1, 0);
|
||||
EXPECT_GT(present_w2, 0);
|
||||
EXPECT_GT(absent, 0);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
Destroy(options);
|
||||
}
|
||||
|
||||
// With the option enabled, run reader threads against the live DB while manual
|
||||
// compactions run in the background. Each compaction-input file is open through
|
||||
// an ephemeral O_DIRECT handle while the shared TableCache still serves user
|
||||
// reads through a buffered handle, so the same SST is open in two cache modes
|
||||
// at once. This stresses that coexistence under TSAN/ASAN/UBSAN; it asserts
|
||||
// reads stay consistent and final values match the last writes, not anything
|
||||
// about timing.
|
||||
TEST_F(DBCompactionTest, UseDirectIoForCompactionReadsConcurrentReadStress) {
|
||||
if (!IsDirectIOSupported()) {
|
||||
ROCKSDB_GTEST_BYPASS("Direct IO not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
Options options = CurrentOptions();
|
||||
Destroy(options);
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
options.use_direct_reads = false;
|
||||
options.use_direct_io_for_compaction_reads = true;
|
||||
options.use_direct_io_for_flush_and_compaction = false;
|
||||
options.write_buffer_size = 64 * 1024;
|
||||
options.target_file_size_base = 64 * 1024;
|
||||
options.max_bytes_for_level_base = 256 * 1024;
|
||||
options.level0_file_num_compaction_trigger = 100; // never auto-trigger
|
||||
|
||||
Status s = TryReopen(options);
|
||||
if (s.IsNotSupported() || s.IsInvalidArgument()) {
|
||||
ROCKSDB_GTEST_BYPASS(
|
||||
"Direct IO reads not supported in this test environment");
|
||||
return;
|
||||
}
|
||||
ASSERT_OK(s);
|
||||
|
||||
constexpr int kNumKeys = 512;
|
||||
constexpr int kValueSize = 256;
|
||||
const std::string base_value(kValueSize, 'b');
|
||||
|
||||
// Initial population, flushed into a few L0 SSTs.
|
||||
for (int i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(Put(Key(i), base_value));
|
||||
if (i > 0 && i % 128 == 0) {
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::atomic<bool> stop{false};
|
||||
std::atomic<int64_t> reads_observed{0};
|
||||
std::atomic<int64_t> read_errors{0};
|
||||
|
||||
// Reader threads loop until stop is set, recording any status that isn't OK
|
||||
// or NotFound. The point is to surface Corruption/IOError/use-after-free, so
|
||||
// the main thread can fail the test.
|
||||
constexpr int kNumReaders = 4;
|
||||
std::vector<port::Thread> readers;
|
||||
readers.reserve(kNumReaders);
|
||||
for (int t = 0; t < kNumReaders; ++t) {
|
||||
readers.emplace_back([&, t]() {
|
||||
std::string value;
|
||||
while (!stop.load(std::memory_order_acquire)) {
|
||||
const int k =
|
||||
(t * 7919 +
|
||||
static_cast<int>(reads_observed.load(std::memory_order_relaxed))) %
|
||||
kNumKeys;
|
||||
Status read_s = db_->Get(ReadOptions(), Key(k), &value);
|
||||
if (!read_s.ok() && !read_s.IsNotFound()) {
|
||||
read_errors.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
reads_observed.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Drive a few rounds of writes and compactions on the main thread while
|
||||
// the readers hammer Get(). Each round overwrites every key with a
|
||||
// round-tagged value and then compacts.
|
||||
constexpr int kRounds = 4;
|
||||
std::string last_value = base_value;
|
||||
for (int round = 0; round < kRounds; ++round) {
|
||||
const std::string round_value(kValueSize,
|
||||
static_cast<char>('A' + (round % 26)));
|
||||
for (int i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(Put(Key(i), round_value));
|
||||
if (i > 0 && i % 64 == 0) {
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
last_value = round_value;
|
||||
}
|
||||
|
||||
stop.store(true, std::memory_order_release);
|
||||
for (auto& reader : readers) {
|
||||
reader.join();
|
||||
}
|
||||
|
||||
// Every key must now hold the final round's value. Catches wholesale
|
||||
// corruption the sampling readers might miss, and confirms compaction
|
||||
// finished under the bypass path.
|
||||
std::string value;
|
||||
for (int i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(db_->Get(ReadOptions(), Key(i), &value));
|
||||
EXPECT_EQ(last_value, value);
|
||||
}
|
||||
EXPECT_GT(reads_observed.load(), 0)
|
||||
<< "reader threads never observed a single read; test was a no-op";
|
||||
EXPECT_EQ(0, read_errors.load())
|
||||
<< "concurrent reads against compaction with bypass-path saw "
|
||||
<< read_errors.load() << " non-OK/non-NotFound status returns";
|
||||
|
||||
Destroy(options);
|
||||
}
|
||||
#endif // !defined(OS_MACOSX) && !defined(OS_OPENBSD) && ...
|
||||
|
||||
class CompactionPriTest : public DBTestBase,
|
||||
public testing::WithParamInterface<uint32_t> {
|
||||
public:
|
||||
@@ -11833,7 +12401,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
|
||||
@@ -11864,7 +12432,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) {
|
||||
@@ -11876,7 +12444,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*) {
|
||||
@@ -11889,7 +12457,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);
|
||||
|
||||
@@ -11971,7 +12539,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnInstallFailure) {
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Trigger compaction — Run() succeeds but Install() fails.
|
||||
// Trigger compaction -- Run() succeeds but Install() fails.
|
||||
Status s = dbfull()->TEST_CompactRange(0, nullptr, nullptr);
|
||||
ASSERT_NOK(s);
|
||||
|
||||
@@ -11993,6 +12561,144 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnInstallFailure) {
|
||||
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;
|
||||
|
||||
@@ -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.
|
||||
@@ -8173,7 +8232,7 @@ TEST_F(DBTest2, FastSstOpenDisableAfterMetadataPersisted) {
|
||||
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
|
||||
db.reset();
|
||||
|
||||
// Reopen with fast_sst_open DISABLED — metadata is in the MANIFEST
|
||||
// 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();
|
||||
+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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-1
@@ -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();
|
||||
}
|
||||
|
||||
@@ -3883,7 +3890,7 @@ TEST_F(DBFlushTest, LeakedTableCacheEntryOnFlushInstallFailure) {
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Trigger flush — BuildTable succeeds but LogAndApply fails.
|
||||
// Trigger flush -- BuildTable succeeds but LogAndApply fails.
|
||||
Status s = Flush();
|
||||
ASSERT_NOK(s);
|
||||
|
||||
|
||||
+703
-153
File diff suppressed because it is too large.
Load diff
+187
-11
@@ -15,6 +15,8 @@
|
||||
#include <limits>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -74,6 +76,7 @@ namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class Arena;
|
||||
class ArenaWrappedDBIter;
|
||||
class FileIngestionHandleImpl;
|
||||
class InMemoryStatsHistoryIterator;
|
||||
class MemTable;
|
||||
class PersistentStatsHistoryIterator;
|
||||
@@ -573,6 +576,10 @@ class DBImpl : public DB {
|
||||
const LiveFilesStorageInfoOptions& opts,
|
||||
std::vector<LiveFileStorageInfo>* files) override;
|
||||
|
||||
Status GetPreparedFileInfoForExternalSstIngestion(
|
||||
const std::string& file_path,
|
||||
std::shared_ptr<const PreparedFileInfo>* file_info) override;
|
||||
|
||||
// Obtains the meta data of the specified column family of the DB.
|
||||
// TODO(yhchiang): output parameter is placed in the end in this codebase.
|
||||
void GetColumnFamilyMetaData(ColumnFamilyHandle* column_family,
|
||||
@@ -602,6 +609,14 @@ class DBImpl : public DB {
|
||||
Status IngestExternalFiles(
|
||||
const std::vector<IngestExternalFileArg>& args) override;
|
||||
|
||||
using DB::PrepareFileIngestion;
|
||||
Status PrepareFileIngestion(
|
||||
const std::vector<IngestExternalFileArg>& args,
|
||||
std::unique_ptr<FileIngestionHandle>* handle) override;
|
||||
|
||||
Status CommitFileIngestionHandles(
|
||||
std::vector<std::unique_ptr<FileIngestionHandle>> handles) override;
|
||||
|
||||
using DB::CreateColumnFamilyWithImport;
|
||||
Status CreateColumnFamilyWithImport(
|
||||
const ColumnFamilyOptions& options, const std::string& column_family_name,
|
||||
@@ -855,12 +870,22 @@ class DBImpl : public DB {
|
||||
// memtable range tombstone iterator used by the underlying merging iterator.
|
||||
// This range tombstone iterator can be refreshed later by db_iter.
|
||||
// @param read_options Must outlive the returned iterator.
|
||||
InternalIterator* NewInternalIterator(const ReadOptions& read_options,
|
||||
ColumnFamilyData* cfd,
|
||||
SuperVersion* super_version,
|
||||
Arena* arena, SequenceNumber sequence,
|
||||
bool allow_unprepared_value,
|
||||
ArenaWrappedDBIter* db_iter = nullptr);
|
||||
// @param sequence The snapshot sequence captured when the DB iterator was
|
||||
// created. Child iterators must use this instead of dereferencing
|
||||
// read_options.snapshot, which may be released before lazy initialization.
|
||||
// @param scan_opts Optional bounded scan ranges used only to prune the
|
||||
// iterator tree during lazy Prepare() initialization.
|
||||
InternalIterator* NewInternalIterator(
|
||||
const ReadOptions& read_options, ColumnFamilyData* cfd,
|
||||
SuperVersion* super_version, Arena* arena, SequenceNumber sequence,
|
||||
bool allow_unprepared_value, ArenaWrappedDBIter* db_iter = nullptr,
|
||||
const MultiScanArgs* scan_opts = nullptr);
|
||||
|
||||
// Release a SuperVersion held by an iterator. This preserves the cleanup
|
||||
// behavior used by materialized internal iterators even when the DB iterator
|
||||
// never needed to lazily build its child iterator tree.
|
||||
void CleanupIteratorSuperVersion(SuperVersion* super_version,
|
||||
bool background_purge);
|
||||
|
||||
LogsWithPrepTracker* logs_with_prep_tracker() {
|
||||
return &logs_with_prep_tracker_;
|
||||
@@ -1096,6 +1121,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.
|
||||
@@ -1453,6 +1479,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
|
||||
@@ -1527,6 +1560,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);
|
||||
|
||||
@@ -1855,6 +1898,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
|
||||
@@ -1870,6 +1918,7 @@ class DBImpl : public DB {
|
||||
|
||||
private:
|
||||
friend class DB;
|
||||
friend class DBImplReadOnly;
|
||||
friend class DBImplSecondary;
|
||||
friend class ErrorHandler;
|
||||
friend class InternalStats;
|
||||
@@ -1881,6 +1930,7 @@ class DBImpl : public DB {
|
||||
friend class WriteBatchWithIndex;
|
||||
friend class WriteUnpreparedTxnDB;
|
||||
friend class WriteUnpreparedTxn;
|
||||
friend class FileIngestionHandleImpl;
|
||||
|
||||
friend class ForwardIterator;
|
||||
friend struct SuperVersion;
|
||||
@@ -2218,6 +2268,10 @@ class DBImpl : public DB {
|
||||
void ReleaseFileNumberFromPendingOutputs(
|
||||
std::unique_ptr<std::list<uint64_t>::iterator>& v);
|
||||
|
||||
// Rolls back one prepared file ingestion (delete its staged files, release
|
||||
// the reserved file numbers)
|
||||
void RollbackPreparedFileIngestion(FileIngestionHandleImpl* const h);
|
||||
|
||||
// Similar to pending_outputs, preserve OPTIONS file. Used for remote
|
||||
// compaction.
|
||||
std::list<uint64_t>::iterator CaptureOptionsFileNumber();
|
||||
@@ -2568,10 +2622,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,
|
||||
@@ -2804,7 +2863,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);
|
||||
@@ -2947,6 +3067,29 @@ class DBImpl : public DB {
|
||||
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>
|
||||
@@ -3277,6 +3420,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
|
||||
@@ -3326,6 +3491,10 @@ class DBImpl : public DB {
|
||||
// REQUIRES: mutex held
|
||||
int num_running_ingest_file_ = 0;
|
||||
|
||||
// Number of FileIngestionHandle objects produced by PrepareFileIngestion()
|
||||
// that have not been committed or destroyed yet.
|
||||
std::atomic<uint32_t> num_outstanding_prepared_ingestions_{0};
|
||||
|
||||
WalManager wal_manager_;
|
||||
|
||||
// A value of > 0 temporarily disables scheduling of background work
|
||||
@@ -3351,6 +3520,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
|
||||
|
||||
@@ -874,6 +874,24 @@ 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) {
|
||||
@@ -1563,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",
|
||||
@@ -1598,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);
|
||||
@@ -1722,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(
|
||||
@@ -1991,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
|
||||
@@ -4006,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);
|
||||
@@ -4013,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()) {
|
||||
@@ -4237,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(),
|
||||
@@ -4455,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(),
|
||||
@@ -4531,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());
|
||||
@@ -4655,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()) {
|
||||
@@ -4674,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
|
||||
@@ -5114,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
|
||||
@@ -5147,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));
|
||||
|
||||
@@ -833,6 +833,7 @@ void DBImpl::PurgeObsoleteFiles(JobContext& state, bool schedule_only) {
|
||||
}
|
||||
wal_manager_.PurgeObsoleteWALFiles();
|
||||
LogFlush(immutable_db_options_.info_log);
|
||||
TEST_SYNC_POINT("DBImpl::PurgeObsoleteFiles:BeforePendingPurgeFinished");
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
--pending_purge_obsolete_files_;
|
||||
assert(pending_purge_obsolete_files_ >= 0);
|
||||
@@ -1206,7 +1207,7 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
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
|
||||
// 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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
+212
-23
@@ -21,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"
|
||||
@@ -122,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()) {
|
||||
@@ -244,6 +255,17 @@ Status DBImpl::ValidateOptions(const DBOptions& db_options) {
|
||||
"then direct I/O reads (use_direct_reads) must be disabled. ");
|
||||
}
|
||||
|
||||
if (db_options.allow_mmap_reads &&
|
||||
db_options.use_direct_io_for_compaction_reads) {
|
||||
// mmap reads and direct I/O share the same EnvOptions field, so enabling
|
||||
// both would try to mmap and O_DIRECT the same reads. Reject it here rather
|
||||
// than tripping a lower-level assert.
|
||||
return Status::NotSupported(
|
||||
"If memory mapped reads (allow_mmap_reads) are enabled "
|
||||
"then compaction-only direct I/O reads "
|
||||
"(use_direct_io_for_compaction_reads) must be disabled. ");
|
||||
}
|
||||
|
||||
if (db_options.allow_mmap_writes &&
|
||||
db_options.use_direct_io_for_flush_and_compaction) {
|
||||
return Status::NotSupported(
|
||||
@@ -495,7 +517,8 @@ Status DBImpl::Recover(
|
||||
std::unique_ptr<FSRandomAccessFile> idfile;
|
||||
FileOptions customized_fs(file_options_);
|
||||
customized_fs.use_direct_reads |=
|
||||
immutable_db_options_.use_direct_io_for_flush_and_compaction;
|
||||
immutable_db_options_.use_direct_io_for_flush_and_compaction ||
|
||||
immutable_db_options_.use_direct_io_for_compaction_reads;
|
||||
const std::string& fname =
|
||||
manifest_path.empty() ? current_fname : manifest_path;
|
||||
s = fs_->NewRandomAccessFile(fname, customized_fs, &idfile, nullptr);
|
||||
@@ -799,9 +822,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;
|
||||
@@ -2213,6 +2246,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,
|
||||
@@ -2377,16 +2420,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();
|
||||
@@ -2424,21 +2468,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);
|
||||
@@ -2534,8 +2719,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()) {
|
||||
@@ -2740,6 +2927,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);
|
||||
|
||||
@@ -134,6 +134,19 @@ class DBImplReadOnly : public DBImpl {
|
||||
return Status::NotSupported("Not supported operation in read only mode.");
|
||||
}
|
||||
|
||||
using DB::PrepareFileIngestion;
|
||||
Status PrepareFileIngestion(
|
||||
const std::vector<IngestExternalFileArg>& /*args*/,
|
||||
std::unique_ptr<FileIngestionHandle>* /*handle*/) override {
|
||||
return Status::NotSupported("Not supported operation in read only mode.");
|
||||
}
|
||||
|
||||
using DB::CommitFileIngestionHandles;
|
||||
Status CommitFileIngestionHandles(
|
||||
std::vector<std::unique_ptr<FileIngestionHandle>> /*handles*/) override {
|
||||
return Status::NotSupported("Not supported operation in read only mode.");
|
||||
}
|
||||
|
||||
using DB::CreateColumnFamilyWithImport;
|
||||
Status CreateColumnFamilyWithImport(
|
||||
const ColumnFamilyOptions& /*options*/,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -239,6 +239,19 @@ class DBImplSecondary : public DBImpl {
|
||||
return Status::NotSupported("Not supported operation in secondary mode.");
|
||||
}
|
||||
|
||||
using DB::PrepareFileIngestion;
|
||||
Status PrepareFileIngestion(
|
||||
const std::vector<IngestExternalFileArg>& /*args*/,
|
||||
std::unique_ptr<FileIngestionHandle>* /*handle*/) override {
|
||||
return Status::NotSupported("Not supported operation in secondary mode.");
|
||||
}
|
||||
|
||||
using DB::CommitFileIngestionHandles;
|
||||
Status CommitFileIngestionHandles(
|
||||
std::vector<std::unique_ptr<FileIngestionHandle>> /*handles*/) override {
|
||||
return Status::NotSupported("Not supported operation in secondary mode.");
|
||||
}
|
||||
|
||||
// Try to catch up with the primary by reading as much as possible from the
|
||||
// log files until there is nothing more to read or encounters an error. If
|
||||
// the amount of information in the log files to process is huge, this
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "db/error_handler.h"
|
||||
#include "db/event_helpers.h"
|
||||
#include "db/wide/wide_columns_helper.h"
|
||||
#include "file/filename.h"
|
||||
#include "logging/logging.h"
|
||||
#include "memtable/wbwi_memtable.h"
|
||||
#include "monitoring/perf_context_imp.h"
|
||||
@@ -509,6 +510,7 @@ struct DBImpl::BlobDirectWriteContext {
|
||||
sv->cfd->ioptions().enable_blob_direct_write;
|
||||
settings.min_blob_size = sv->mutable_cf_options.min_blob_size;
|
||||
settings.compression_type = sv->mutable_cf_options.blob_compression_type;
|
||||
settings.compression_opts = sv->mutable_cf_options.blob_compression_opts;
|
||||
settings.blob_cache = sv->cfd->ioptions().blob_cache.get();
|
||||
settings.prepopulate_blob_cache =
|
||||
sv->mutable_cf_options.prepopulate_blob_cache;
|
||||
@@ -1545,6 +1547,10 @@ Status DBImpl::WriteImpl(
|
||||
// Note: if we are to resume after non-OK statuses we need to revisit how
|
||||
// we react to non-OK statuses here.
|
||||
if (w.status.ok()) { // Don't publish a partial batch write
|
||||
TEST_SYNC_POINT(
|
||||
"DBImpl::WriteImpl:AfterWBWIIngestBeforeSetLastSequence:pause");
|
||||
TEST_SYNC_POINT(
|
||||
"DBImpl::WriteImpl:AfterWBWIIngestBeforeSetLastSequence:resume");
|
||||
versions_->SetLastSequence(last_sequence);
|
||||
}
|
||||
}
|
||||
@@ -3102,6 +3108,7 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
|
||||
const WriteOptions write_options;
|
||||
|
||||
log::Writer* new_log = nullptr;
|
||||
UnpublishedWAL prepared_wal;
|
||||
MemTable* new_mem = nullptr;
|
||||
IOStatus io_s;
|
||||
|
||||
@@ -3129,8 +3136,16 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
|
||||
!wal_recycle_files_.empty() && IsFileDeletionsEnabled()) {
|
||||
recycle_log_number = wal_recycle_files_.front();
|
||||
}
|
||||
uint64_t new_log_number =
|
||||
creating_new_log ? versions_->NewFileNumber() : cur_wal_number_;
|
||||
uint64_t new_log_number = cur_wal_number_;
|
||||
if (creating_new_log) {
|
||||
prepared_wal = WaitForAsyncWALPrecreate();
|
||||
if (prepared_wal.writer) {
|
||||
new_log_number = prepared_wal.log_number;
|
||||
} else {
|
||||
assert(prepared_wal.log_number == 0);
|
||||
new_log_number = versions_->NewFileNumber();
|
||||
}
|
||||
}
|
||||
// For use outside of holding DB mutex
|
||||
const MutableCFOptions mutable_cf_options_copy =
|
||||
cfd->GetLatestMutableCFOptions();
|
||||
@@ -3151,6 +3166,8 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
|
||||
// Log this later after lock release. It may be outdated, e.g., if background
|
||||
// flush happens before logging, but that should be ok.
|
||||
int num_imm_unflushed = cfd->imm()->NumNotFlushed();
|
||||
const DBOptions db_options_snapshot =
|
||||
BuildDBOptions(immutable_db_options_, mutable_db_options_);
|
||||
const auto preallocate_block_size =
|
||||
GetWalPreallocateBlockSize(mutable_cf_options_copy.write_buffer_size);
|
||||
mutex_.Unlock();
|
||||
@@ -3166,13 +3183,35 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
|
||||
wal_write_mutex_.Unlock();
|
||||
// TODO: Write buffer size passed in should be max of all CF's instead
|
||||
// of mutable_cf_options.write_buffer_size.
|
||||
io_s = CreateWAL(write_options, new_log_number, recycle_log_number,
|
||||
preallocate_block_size, info, &new_log);
|
||||
if (prepared_wal.writer) {
|
||||
io_s = StartWALFile(write_options, info, prepared_wal.writer.get());
|
||||
if (io_s.ok()) {
|
||||
new_log = prepared_wal.writer.release();
|
||||
}
|
||||
} else {
|
||||
io_s =
|
||||
CreateWAL(db_options_snapshot, write_options, new_log_number,
|
||||
recycle_log_number, preallocate_block_size, info, &new_log);
|
||||
}
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::SwitchMemtable:AfterCreateWAL", &io_s);
|
||||
if (s.ok()) {
|
||||
s = io_s;
|
||||
}
|
||||
}
|
||||
if (!s.ok() && prepared_wal.writer) {
|
||||
const uint64_t prepared_wal_number = prepared_wal.log_number;
|
||||
const std::string prepared_wal_fname =
|
||||
LogFileName(immutable_db_options_.GetWalDir(), prepared_wal_number);
|
||||
prepared_wal.Reset();
|
||||
Status delete_s = env_->DeleteFile(prepared_wal_fname);
|
||||
if (!delete_s.ok() && !delete_s.IsNotFound()) {
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"Failed to delete unstarted async precreated WAL #%" PRIu64
|
||||
": %s",
|
||||
prepared_wal_number, delete_s.ToString().c_str());
|
||||
}
|
||||
delete_s.PermitUncheckedError();
|
||||
}
|
||||
if (s.ok()) {
|
||||
// FIXME: from the comment for GetEarliestSequenceNumber(), any key with
|
||||
// seqno >= earliest_seqno should be in this or later memtable. This means
|
||||
@@ -3354,6 +3393,7 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
|
||||
new_mem->Ref();
|
||||
cfd->SetMemtable(new_mem);
|
||||
InstallSuperVersionAndScheduleWork(cfd, &context->superversion_context);
|
||||
MaybeScheduleAsyncWALPrecreate(preallocate_block_size);
|
||||
|
||||
// Notify client that memtable is sealed, now that we have successfully
|
||||
// installed a new memtable
|
||||
|
||||
+89
-79
@@ -67,8 +67,9 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
const Comparator* cmp, InternalIterator* iter,
|
||||
const Version* version, SequenceNumber s, bool arena_mode,
|
||||
ReadCallback* read_callback, ColumnFamilyHandleImpl* cfh,
|
||||
bool expose_blob_index, ReadOnlyMemTable* active_mem)
|
||||
ReadCallback* read_callback, DBImpl* db_impl,
|
||||
ColumnFamilyData* cfd, bool expose_blob_index,
|
||||
ReadOnlyMemTable* active_mem)
|
||||
: prefix_extractor_(mutable_cf_options.prefix_extractor.get()),
|
||||
env_(_env),
|
||||
clock_(ioptions.clock),
|
||||
@@ -76,21 +77,26 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
|
||||
user_comparator_(cmp),
|
||||
merge_operator_(ioptions.merge_operator.get()),
|
||||
iter_(iter),
|
||||
blob_reader_(
|
||||
version, read_options.read_tier, read_options.verify_checksums,
|
||||
read_options.fill_cache, read_options.io_activity,
|
||||
cfh ? cfh->cfd()->blob_file_cache() : nullptr,
|
||||
cfh != nullptr && cfh->cfd()->blob_partition_manager() != nullptr),
|
||||
blob_state_(version, read_options.read_tier,
|
||||
read_options.verify_checksums, read_options.fill_cache,
|
||||
read_options.io_activity,
|
||||
cfd ? cfd->blob_file_cache() : nullptr,
|
||||
cfd != nullptr && cfd->blob_partition_manager() != nullptr),
|
||||
read_callback_(read_callback),
|
||||
sequence_(s),
|
||||
value_columns_state_(version, read_options, cfh),
|
||||
value_columns_state_(version, read_options, cfd),
|
||||
statistics_(ioptions.stats),
|
||||
max_skip_(mutable_cf_options.max_sequential_skip_in_iterations),
|
||||
max_skippable_internal_keys_(read_options.max_skippable_internal_keys),
|
||||
num_internal_keys_skipped_(0),
|
||||
iterate_lower_bound_(read_options.iterate_lower_bound),
|
||||
iterate_upper_bound_(read_options.iterate_upper_bound),
|
||||
cfh_(cfh),
|
||||
trace_db_(db_impl),
|
||||
trace_cf_id_(cfd != nullptr ? cfd->GetID() : 0),
|
||||
has_trace_state_(db_impl != nullptr && cfd != nullptr),
|
||||
allow_blob_write_path_fallback_(cfd != nullptr &&
|
||||
cfd->blob_partition_manager() != nullptr),
|
||||
ingest_sst_lock_(cfd != nullptr ? &cfd->GetIngestSstLock() : nullptr),
|
||||
timestamp_ub_(read_options.timestamp),
|
||||
timestamp_lb_(read_options.iter_start_ts),
|
||||
timestamp_size_(timestamp_ub_ ? timestamp_ub_->size() : 0),
|
||||
@@ -125,7 +131,6 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
|
||||
: 0),
|
||||
expose_blob_index_(expose_blob_index),
|
||||
allow_unprepared_value_(read_options.allow_unprepared_value),
|
||||
is_blob_(false),
|
||||
arena_mode_(arena_mode) {
|
||||
RecordTick(statistics_, NO_ITERATOR_CREATED);
|
||||
if (pin_thru_lifetime_) {
|
||||
@@ -178,7 +183,7 @@ Status DBIter::GetProperty(std::string prop_name, std::string* prop) {
|
||||
} else if (prop_name == "rocksdb.iterator.is-value-pinned") {
|
||||
if (valid_) {
|
||||
*prop = (pin_thru_lifetime_ && iter_.Valid() &&
|
||||
iter_.value().data() == value_columns_state_.value().data())
|
||||
iter_.value().data() == value_columns_state_->value().data())
|
||||
? "1"
|
||||
: "0";
|
||||
} else {
|
||||
@@ -306,26 +311,24 @@ bool DBIter::SetValueAndColumnsFromBlobImpl(const Slice& user_key,
|
||||
// Keep the non-BDW iterator path on the pre-existing Version::GetBlob()
|
||||
// fast path. Only enable the direct-write fallback when this CF actually
|
||||
// has a write-path partition manager.
|
||||
const bool allow_write_path_fallback =
|
||||
cfh_ != nullptr && cfh_->cfd()->blob_partition_manager() != nullptr;
|
||||
const Status s = blob_reader_.RetrieveAndSetBlobValue(
|
||||
user_key, blob_index, allow_write_path_fallback);
|
||||
const Status s = blob_state_.mut()->reader.RetrieveAndSetBlobValue(
|
||||
user_key, blob_index, allow_blob_write_path_fallback_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
valid_ = false;
|
||||
is_blob_ = false;
|
||||
blob_state_.mut()->is_blob = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
SetValueAndColumnsFromPlain(blob_reader_.GetBlobValue());
|
||||
SetValueAndColumnsFromPlain(blob_state_->reader.GetBlobValue());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DBIter::SetValueAndColumnsFromBlob(const Slice& user_key,
|
||||
const Slice& blob_index) {
|
||||
assert(!is_blob_);
|
||||
is_blob_ = true;
|
||||
assert(!blob_state_->is_blob);
|
||||
blob_state_.mut()->is_blob = true;
|
||||
|
||||
if (expose_blob_index_) {
|
||||
SetValueAndColumnsFromPlain(blob_index);
|
||||
@@ -333,11 +336,11 @@ bool DBIter::SetValueAndColumnsFromBlob(const Slice& user_key,
|
||||
}
|
||||
|
||||
if (allow_unprepared_value_) {
|
||||
assert(value_columns_state_.value().empty());
|
||||
assert(value_columns_state_.wide_columns().empty());
|
||||
assert(value_columns_state_->value().empty());
|
||||
assert(value_columns_state_->wide_columns().empty());
|
||||
|
||||
assert(lazy_blob_index_.empty());
|
||||
lazy_blob_index_ = blob_index;
|
||||
assert(blob_state_->lazy_blob_index.empty());
|
||||
blob_state_.mut()->lazy_blob_index = blob_index;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -346,7 +349,10 @@ bool DBIter::SetValueAndColumnsFromBlob(const Slice& user_key,
|
||||
}
|
||||
|
||||
bool DBIter::SetValueAndColumnsFromEntity(Slice slice) {
|
||||
auto& state = value_columns_state_;
|
||||
// Auto-marks dirty via mut() up front since every successful path below
|
||||
// populates wide_columns_ (and possibly the lazy entity/blob column
|
||||
// vectors).
|
||||
auto& state = *value_columns_state_.mut();
|
||||
state.AssertReadyForEntity();
|
||||
|
||||
// Fast path: if no blob columns, use the simpler Deserialize
|
||||
@@ -409,7 +415,7 @@ bool DBIter::SetValueAndColumnsFromEntity(Slice slice) {
|
||||
}
|
||||
|
||||
bool DBIter::MaterializeLazyEntityColumns() const {
|
||||
const auto& state = value_columns_state_;
|
||||
const auto& state = *value_columns_state_;
|
||||
if (state.lazy_entity_columns().empty() || !state.wide_columns().empty()) {
|
||||
return true;
|
||||
}
|
||||
@@ -420,7 +426,7 @@ bool DBIter::MaterializeLazyEntityColumns() const {
|
||||
}
|
||||
|
||||
DBIter* const mutable_this = const_cast<DBIter*>(this);
|
||||
auto& mutable_state = mutable_this->value_columns_state_;
|
||||
auto& mutable_state = *mutable_this->value_columns_state_.mut();
|
||||
WideColumns materialized_columns;
|
||||
materialized_columns.reserve(state.lazy_entity_columns().size());
|
||||
for (const auto& col : state.lazy_entity_columns()) {
|
||||
@@ -454,7 +460,7 @@ bool DBIter::SetValueAndColumnsFromMergeResult(const Status& merge_status,
|
||||
}
|
||||
|
||||
if (result_type == kTypeWideColumnEntity) {
|
||||
if (!SetValueAndColumnsFromEntity(value_columns_state_.saved_value())) {
|
||||
if (!SetValueAndColumnsFromEntity(value_columns_state_->saved_value())) {
|
||||
assert(!valid_);
|
||||
return false;
|
||||
}
|
||||
@@ -466,7 +472,7 @@ bool DBIter::SetValueAndColumnsFromMergeResult(const Status& merge_status,
|
||||
assert(result_type == kTypeValue);
|
||||
SetValueAndColumnsFromPlain(pinned_value_.data()
|
||||
? pinned_value_
|
||||
: value_columns_state_.saved_value());
|
||||
: value_columns_state_->saved_value());
|
||||
valid_ = true;
|
||||
return true;
|
||||
}
|
||||
@@ -474,17 +480,17 @@ bool DBIter::SetValueAndColumnsFromMergeResult(const Status& merge_status,
|
||||
bool DBIter::PrepareValue() {
|
||||
assert(valid_);
|
||||
|
||||
if (lazy_blob_index_.empty()) {
|
||||
if (blob_state_->lazy_blob_index.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
assert(allow_unprepared_value_);
|
||||
assert(is_blob_);
|
||||
assert(blob_state_->is_blob);
|
||||
|
||||
const bool result =
|
||||
SetValueAndColumnsFromBlobImpl(saved_key_.GetUserKey(), lazy_blob_index_);
|
||||
const bool result = SetValueAndColumnsFromBlobImpl(
|
||||
saved_key_.GetUserKey(), blob_state_->lazy_blob_index);
|
||||
|
||||
lazy_blob_index_.clear();
|
||||
blob_state_.mut()->lazy_blob_index.clear();
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -621,7 +627,7 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key) {
|
||||
PERF_COUNTER_ADD(internal_delete_skipped_count, 1);
|
||||
MarkMemtableForFlushForPerOpTrigger(mem_hidden_op_scanned);
|
||||
// Track contiguous tombstones for range conversion.
|
||||
// Skip if outside seek prefix — the top-of-loop check
|
||||
// Skip if outside seek prefix -- the top-of-loop check
|
||||
// flushed any pending run, but we must also avoid starting
|
||||
// a new run outside the prefix.
|
||||
if (min_tombstones_for_range_conversion_ > 0 &&
|
||||
@@ -774,25 +780,18 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key) {
|
||||
}
|
||||
} while (iter_.Valid());
|
||||
|
||||
// If we accumulated tombstones, insert the range tombstone. Use
|
||||
// iterate_upper_bound_ if within the seek prefix, otherwise fall back to
|
||||
// saved_key_ (the last tracked delete, covering n-1 deletes).
|
||||
// If we accumulated tombstones, use the last tracked tombstone as the
|
||||
// exclusive end key. It may be more optimal to use iterator upper bound if it
|
||||
// exists, but the current iterator API makes that dangerous as upper bound
|
||||
// points to user memory which is not guaranteed immutable.
|
||||
if (contiguous_tombstone_count_ > 0 && iter_.status().ok()) {
|
||||
if (iterate_upper_bound_ != nullptr && PrefixCheck(*iterate_upper_bound_)) {
|
||||
if (timestamp_size_ == 0) {
|
||||
MaybeInsertRangeTombstone(*iterate_upper_bound_);
|
||||
} else {
|
||||
// iterate_upper_bound_ is a plain user key without a timestamp
|
||||
// suffix. Pad with min timestamp so it sorts after all entries with
|
||||
// this user key, preserving the exclusive bound semantics.
|
||||
std::string end_key_with_ts;
|
||||
AppendKeyWithMinTimestamp(&end_key_with_ts, *iterate_upper_bound_,
|
||||
timestamp_size_);
|
||||
MaybeInsertRangeTombstone(end_key_with_ts);
|
||||
}
|
||||
} else if (prefix_.has_value()) {
|
||||
MaybeInsertRangeTombstone(saved_key_.GetUserKey());
|
||||
}
|
||||
// It is unsafe to use iter_.key() here even when iter_.Valid() and the key
|
||||
// is within the seek prefix. This is because memtable iterators are still
|
||||
// valid past the upper bound, but sst iterators are not. So iter_.key() can
|
||||
// point to a memtable entry that has skipped past real live entries in
|
||||
// ssts.
|
||||
assert(PrefixCheck(saved_key_.GetUserKey()));
|
||||
MaybeInsertRangeTombstone(saved_key_.GetUserKey());
|
||||
}
|
||||
ResetContiguousTombstoneTracking();
|
||||
|
||||
@@ -1102,7 +1101,7 @@ void DBIter::PrevInternal() {
|
||||
if (min_tombstones_for_range_conversion_ > 0 &&
|
||||
range_tomb_end_key_.Size() > 0 && timestamp_lb_ == nullptr) {
|
||||
if (!valid_ && found_visible && PrefixCheck(saved_key_without_ts)) {
|
||||
// Key was deleted and is within the seek prefix — track it.
|
||||
// Key was deleted and is within the seek prefix -- track it.
|
||||
TrackContiguousTombstone(saved_key_.GetUserKey(),
|
||||
/*always_update_first_key=*/true);
|
||||
} else if (valid_) {
|
||||
@@ -1146,7 +1145,7 @@ void DBIter::PrevInternal() {
|
||||
// Sets ikey_ to the last visible entry's internal key. When found_visible
|
||||
// is false, ikey_ is not updated and may contain stale data.
|
||||
// Sets found_visible to true if at least one entry passed the IsVisible()
|
||||
// check (seqno <= snapshot). When false, no entry was visible — the key
|
||||
// check (seqno <= snapshot). When false, no entry was visible -- the key
|
||||
// does not exist at this snapshot and should not be treated as a tombstone.
|
||||
// Returns false if an error occurred, and !status().ok() and !valid_.
|
||||
//
|
||||
@@ -1201,7 +1200,7 @@ bool DBIter::FindValueForCurrentKey(bool& found_visible) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Entry survived the visibility check — at least one visible version
|
||||
// Entry survived the visibility check -- at least one visible version
|
||||
// exists for this user key.
|
||||
found_visible = true;
|
||||
|
||||
@@ -1485,7 +1484,9 @@ bool DBIter::FindValueForCurrentKeyUsingSeek() {
|
||||
if (timestamp_lb_ != nullptr) {
|
||||
saved_key_.SetInternalKey(ikey);
|
||||
} else {
|
||||
saved_key_.SetUserKey(ikey.user_key);
|
||||
saved_key_.SetUserKey(
|
||||
ikey.user_key,
|
||||
!pin_thru_lifetime_ || !iter_.iter()->IsKeyPinned() /* copy */);
|
||||
}
|
||||
|
||||
valid_ = true;
|
||||
@@ -1592,7 +1593,7 @@ bool DBIter::MergeWithNoBaseValue(const Slice& user_key) {
|
||||
merge_operator_, user_key, MergeHelper::kNoBaseValue,
|
||||
merge_context_.GetOperands(), logger_, statistics_, clock_,
|
||||
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
|
||||
&value_columns_state_.saved_value(), &pinned_value_, &result_type);
|
||||
&value_columns_state_.mut()->saved_value(), &pinned_value_, &result_type);
|
||||
return SetValueAndColumnsFromMergeResult(s, result_type);
|
||||
}
|
||||
|
||||
@@ -1605,13 +1606,13 @@ bool DBIter::MergeWithPlainBaseValue(const Slice& value,
|
||||
merge_operator_, user_key, MergeHelper::kPlainBaseValue, value,
|
||||
merge_context_.GetOperands(), logger_, statistics_, clock_,
|
||||
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
|
||||
&value_columns_state_.saved_value(), &pinned_value_, &result_type);
|
||||
&value_columns_state_.mut()->saved_value(), &pinned_value_, &result_type);
|
||||
return SetValueAndColumnsFromMergeResult(s, result_type);
|
||||
}
|
||||
|
||||
bool DBIter::MergeWithBlobBaseValue(const Slice& blob_index,
|
||||
const Slice& user_key) {
|
||||
assert(!is_blob_);
|
||||
assert(!blob_state_->is_blob);
|
||||
|
||||
if (expose_blob_index_) {
|
||||
status_ =
|
||||
@@ -1620,10 +1621,8 @@ bool DBIter::MergeWithBlobBaseValue(const Slice& blob_index,
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool allow_write_path_fallback =
|
||||
cfh_ != nullptr && cfh_->cfd()->blob_partition_manager() != nullptr;
|
||||
const Status s = blob_reader_.RetrieveAndSetBlobValue(
|
||||
user_key, blob_index, allow_write_path_fallback);
|
||||
const Status s = blob_state_.mut()->reader.RetrieveAndSetBlobValue(
|
||||
user_key, blob_index, allow_blob_write_path_fallback_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
valid_ = false;
|
||||
@@ -1632,11 +1631,11 @@ bool DBIter::MergeWithBlobBaseValue(const Slice& blob_index,
|
||||
|
||||
valid_ = true;
|
||||
|
||||
if (!MergeWithPlainBaseValue(blob_reader_.GetBlobValue(), user_key)) {
|
||||
if (!MergeWithPlainBaseValue(blob_state_->reader.GetBlobValue(), user_key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
blob_reader_.ResetBlobValue();
|
||||
blob_state_.Reset();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1645,7 +1644,7 @@ bool DBIter::MergeWithWideColumnBaseValue(const Slice& entity,
|
||||
const Slice& user_key) {
|
||||
// Resolve V2 entity blob columns if present, since TimedFullMerge only
|
||||
// supports V1 format.
|
||||
BlobFetcher blob_fetcher = blob_reader_.CreateBlobFetcher();
|
||||
BlobFetcher blob_fetcher = blob_state_->reader.CreateBlobFetcher();
|
||||
std::string resolved_entity;
|
||||
Slice effective_entity;
|
||||
Status s_resolve = WideColumnSerialization::ResolveEntityForMerge(
|
||||
@@ -1664,7 +1663,7 @@ bool DBIter::MergeWithWideColumnBaseValue(const Slice& entity,
|
||||
merge_operator_, user_key, MergeHelper::kWideBaseValue, effective_entity,
|
||||
merge_context_.GetOperands(), logger_, statistics_, clock_,
|
||||
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
|
||||
&value_columns_state_.saved_value(), &pinned_value_, &result_type);
|
||||
&value_columns_state_.mut()->saved_value(), &pinned_value_, &result_type);
|
||||
return SetValueAndColumnsFromMergeResult(s, result_type);
|
||||
}
|
||||
|
||||
@@ -1819,10 +1818,10 @@ void DBIter::MaybeInsertRangeTombstone(const Slice& end_key) {
|
||||
}
|
||||
}
|
||||
|
||||
assert(cfh_ != nullptr);
|
||||
assert(ingest_sst_lock_ != nullptr);
|
||||
if (active_mem_->AddLogicallyRedundantRangeTombstone(
|
||||
insert_seq, range_tomb_first_key_.GetUserKey(), end_key,
|
||||
cfh_->cfd()->GetIngestSstLock())) {
|
||||
*ingest_sst_lock_)) {
|
||||
RecordTick(statistics_, READ_PATH_RANGE_TOMBSTONES_INSERTED);
|
||||
ROCKS_LOG_DEBUG(logger_,
|
||||
"Inserted range tombstone [%s, %s) @ seq %" PRIu64
|
||||
@@ -1962,10 +1961,10 @@ Status DBIter::ValidateScanOptions(const MultiScanArgs& multiscan_opts) const {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void DBIter::Prepare(const MultiScanArgs& scan_opts) {
|
||||
Status DBIter::SetScanOptionsForPrepare(const MultiScanArgs& scan_opts) {
|
||||
status_ = ValidateScanOptions(scan_opts);
|
||||
if (!status_.ok()) {
|
||||
return;
|
||||
return status_;
|
||||
}
|
||||
std::optional<MultiScanArgs> new_scan_opts;
|
||||
new_scan_opts.emplace(scan_opts);
|
||||
@@ -1978,14 +1977,27 @@ void DBIter::Prepare(const MultiScanArgs& scan_opts) {
|
||||
if (!scan_opts_.value().io_dispatcher) {
|
||||
scan_opts_->io_dispatcher.reset(NewIODispatcher());
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
if (!scan_opts.empty()) {
|
||||
void DBIter::PrepareInternalChildren() {
|
||||
if (!scan_opts_.has_value() || !status_.ok()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (scan_opts_.value().HasBoundedScanRanges()) {
|
||||
iter_.Prepare(&scan_opts_.value());
|
||||
} else {
|
||||
iter_.Prepare(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void DBIter::Prepare(const MultiScanArgs& scan_opts) {
|
||||
if (SetScanOptionsForPrepare(scan_opts).ok()) {
|
||||
PrepareInternalChildren();
|
||||
}
|
||||
}
|
||||
|
||||
void DBIter::Seek(const Slice& target) {
|
||||
PERF_COUNTER_ADD(iter_seek_count, 1);
|
||||
PERF_CPU_TIMER_GUARD(iter_seek_cpu_nanos, clock_);
|
||||
@@ -2032,7 +2044,7 @@ void DBIter::Seek(const Slice& target) {
|
||||
scan_index_++;
|
||||
}
|
||||
|
||||
if (cfh_ != nullptr) {
|
||||
if (has_trace_state_) {
|
||||
// TODO: What do we do if this returns an error?
|
||||
Slice lower_bound, upper_bound;
|
||||
if (iterate_lower_bound_ != nullptr) {
|
||||
@@ -2045,9 +2057,7 @@ void DBIter::Seek(const Slice& target) {
|
||||
} else {
|
||||
upper_bound = Slice("");
|
||||
}
|
||||
cfh_->db()
|
||||
->TraceIteratorSeek(cfh_->cfd()->GetID(), target, lower_bound,
|
||||
upper_bound)
|
||||
trace_db_->TraceIteratorSeek(trace_cf_id_, target, lower_bound, upper_bound)
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
|
||||
@@ -2100,7 +2110,7 @@ void DBIter::SeekForPrev(const Slice& target) {
|
||||
PERF_CPU_TIMER_GUARD(iter_seek_cpu_nanos, clock_);
|
||||
StopWatch sw(clock_, statistics_, DB_SEEK);
|
||||
|
||||
if (cfh_ != nullptr) {
|
||||
if (has_trace_state_) {
|
||||
// TODO: What do we do if this returns an error?
|
||||
Slice lower_bound, upper_bound;
|
||||
if (iterate_lower_bound_ != nullptr) {
|
||||
@@ -2113,8 +2123,8 @@ void DBIter::SeekForPrev(const Slice& target) {
|
||||
} else {
|
||||
upper_bound = Slice("");
|
||||
}
|
||||
cfh_->db()
|
||||
->TraceIteratorSeekForPrev(cfh_->cfd()->GetID(), target, lower_bound,
|
||||
trace_db_
|
||||
->TraceIteratorSeekForPrev(trace_cf_id_, target, lower_bound,
|
||||
upper_bound)
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
|
||||
+60
-29
@@ -27,10 +27,14 @@
|
||||
#include "rocksdb/wide_columns.h"
|
||||
#include "table/iterator_wrapper.h"
|
||||
#include "util/autovector.h"
|
||||
#include "util/dirty_tracked.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
class BlobFileCache;
|
||||
class Version;
|
||||
namespace port {
|
||||
class RWMutex;
|
||||
}
|
||||
|
||||
// This file declares the factory functions of DBIter, in its original form
|
||||
// or a wrapped form with class ArenaWrappedDBIter, which is defined here.
|
||||
@@ -82,14 +86,19 @@ class DBIter final : public Iterator {
|
||||
ReadCallback* read_callback,
|
||||
ReadOnlyMemTable* active_mem,
|
||||
ColumnFamilyHandleImpl* cfh = nullptr,
|
||||
bool expose_blob_index = false,
|
||||
Arena* arena = nullptr) {
|
||||
bool expose_blob_index = false, Arena* arena = nullptr,
|
||||
DBImpl* db_impl = nullptr,
|
||||
ColumnFamilyData* cfd = nullptr) {
|
||||
if (cfh != nullptr) {
|
||||
db_impl = cfh->db();
|
||||
cfd = cfh->cfd();
|
||||
}
|
||||
void* mem = arena ? arena->AllocateAligned(sizeof(DBIter))
|
||||
: operator new(sizeof(DBIter));
|
||||
DBIter* db_iter = new (mem)
|
||||
DBIter(env, read_options, ioptions, mutable_cf_options,
|
||||
user_key_comparator, internal_iter, version, sequence, arena,
|
||||
read_callback, cfh, expose_blob_index, active_mem);
|
||||
read_callback, db_impl, cfd, expose_blob_index, active_mem);
|
||||
return db_iter;
|
||||
}
|
||||
|
||||
@@ -195,18 +204,18 @@ class DBIter final : public Iterator {
|
||||
Slice value() const override {
|
||||
assert(valid_);
|
||||
|
||||
return value_columns_state_.value();
|
||||
return value_columns_state_->value();
|
||||
}
|
||||
|
||||
const WideColumns& columns() const override {
|
||||
assert(valid_);
|
||||
assert(!value_columns_state_.HasLazyEntityColumns() ||
|
||||
value_columns_state_.HasMaterializedColumns());
|
||||
return value_columns_state_.wide_columns();
|
||||
assert(!value_columns_state_->HasLazyEntityColumns() ||
|
||||
value_columns_state_->HasMaterializedColumns());
|
||||
return value_columns_state_->wide_columns();
|
||||
}
|
||||
|
||||
Status status() const override {
|
||||
if (status_.ok()) {
|
||||
if (status_.ok() && iter_.iter() != nullptr) {
|
||||
return iter_.status();
|
||||
} else {
|
||||
assert(!valid_);
|
||||
@@ -225,7 +234,7 @@ class DBIter final : public Iterator {
|
||||
}
|
||||
bool IsBlob() const {
|
||||
assert(valid_);
|
||||
return is_blob_;
|
||||
return blob_state_->is_blob;
|
||||
}
|
||||
|
||||
Status GetProperty(std::string prop_name, std::string* prop) override;
|
||||
@@ -247,19 +256,22 @@ class DBIter final : public Iterator {
|
||||
iter_.SetRangeDelReadSeqno(s);
|
||||
}
|
||||
void set_valid(bool v) { valid_ = v; }
|
||||
void set_status(Status s) { status_ = std::move(s); }
|
||||
|
||||
bool PrepareValue() override;
|
||||
|
||||
void Prepare(const MultiScanArgs& scan_opts) override;
|
||||
Status ValidateScanOptions(const MultiScanArgs& multiscan_opts) const;
|
||||
Status SetScanOptionsForPrepare(const MultiScanArgs& scan_opts);
|
||||
void PrepareInternalChildren();
|
||||
|
||||
private:
|
||||
DBIter(Env* _env, const ReadOptions& read_options,
|
||||
const ImmutableOptions& ioptions,
|
||||
const MutableCFOptions& mutable_cf_options, const Comparator* cmp,
|
||||
InternalIterator* iter, const Version* version, SequenceNumber s,
|
||||
bool arena_mode, ReadCallback* read_callback,
|
||||
ColumnFamilyHandleImpl* cfh, bool expose_blob_index,
|
||||
bool arena_mode, ReadCallback* read_callback, DBImpl* db_impl,
|
||||
ColumnFamilyData* cfd, bool expose_blob_index,
|
||||
ReadOnlyMemTable* active_mem);
|
||||
|
||||
class BlobReader {
|
||||
@@ -296,6 +308,20 @@ class DBIter final : public Iterator {
|
||||
BlobFileCache* blob_file_cache_;
|
||||
bool allow_write_path_fallback_;
|
||||
};
|
||||
struct BlobState {
|
||||
BlobReader reader;
|
||||
Slice lazy_blob_index;
|
||||
bool is_blob = false;
|
||||
|
||||
template <typename... Args>
|
||||
explicit BlobState(Args&&... args) : reader(std::forward<Args>(args)...) {}
|
||||
|
||||
void Reset() {
|
||||
reader.ResetBlobValue();
|
||||
lazy_blob_index.clear();
|
||||
is_blob = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Groups the current iterator result together with the backing storage and
|
||||
// lazy entity-resolution metadata it depends on. Resetting this object drops
|
||||
@@ -303,13 +329,12 @@ class DBIter final : public Iterator {
|
||||
class ValueColumnsState {
|
||||
public:
|
||||
ValueColumnsState(const Version* version, const ReadOptions& read_options,
|
||||
ColumnFamilyHandleImpl* cfh)
|
||||
ColumnFamilyData* cfd)
|
||||
: entity_blob_resolver_(
|
||||
version, read_options.read_tier, read_options.verify_checksums,
|
||||
read_options.fill_cache, read_options.io_activity,
|
||||
cfh ? cfh->cfd()->blob_file_cache() : nullptr,
|
||||
cfh != nullptr &&
|
||||
cfh->cfd()->blob_partition_manager() != nullptr) {}
|
||||
cfd ? cfd->blob_file_cache() : nullptr,
|
||||
cfd != nullptr && cfd->blob_partition_manager() != nullptr) {}
|
||||
|
||||
Slice& value() { return value_; }
|
||||
const Slice& value() const { return value_; }
|
||||
@@ -483,7 +508,9 @@ class DBIter final : public Iterator {
|
||||
}
|
||||
}
|
||||
|
||||
inline void ClearSavedValue() { value_columns_state_.ClearSavedValue(); }
|
||||
inline void ClearSavedValue() {
|
||||
value_columns_state_.mut()->ClearSavedValue();
|
||||
}
|
||||
|
||||
inline void ResetInternalKeysSkippedCounter() {
|
||||
local_stats_.skip_count_ += num_internal_keys_skipped_;
|
||||
@@ -508,7 +535,7 @@ class DBIter final : public Iterator {
|
||||
}
|
||||
|
||||
void SetValueAndColumnsFromPlain(const Slice& slice) {
|
||||
value_columns_state_.SetFromPlain(slice);
|
||||
value_columns_state_.mut()->SetFromPlain(slice);
|
||||
}
|
||||
|
||||
bool SetValueAndColumnsFromBlobImpl(const Slice& user_key,
|
||||
@@ -524,11 +551,7 @@ class DBIter final : public Iterator {
|
||||
|
||||
void ResetValueAndColumns() { value_columns_state_.Reset(); }
|
||||
|
||||
void ResetBlobData() {
|
||||
blob_reader_.ResetBlobValue();
|
||||
lazy_blob_index_.clear();
|
||||
is_blob_ = false;
|
||||
}
|
||||
void ResetBlobData() { blob_state_.Reset(); }
|
||||
|
||||
// The following methods perform the actual merge operation for the
|
||||
// no/plain/blob/wide-column base value cases.
|
||||
@@ -539,6 +562,12 @@ class DBIter final : public Iterator {
|
||||
bool MergeWithWideColumnBaseValue(const Slice& entity, const Slice& user_key);
|
||||
|
||||
bool PrepareValueInternal() {
|
||||
// Capture this before PrepareValue(): PrepareValue() updates the wrapper
|
||||
// state to "prepared" on success. We still call PrepareValue()
|
||||
// unconditionally to preserve its contract/error handling, but only need
|
||||
// to re-parse ikey_ when this call may have actually materialized the
|
||||
// underlying iterator value/key.
|
||||
const bool value_was_prepared = iter_.IsValuePrepared();
|
||||
if (!iter_.PrepareValue()) {
|
||||
assert(!iter_.status().ok());
|
||||
valid_ = false;
|
||||
@@ -548,7 +577,7 @@ class DBIter final : public Iterator {
|
||||
// lookup and index_iter_ could point to different block resulting
|
||||
// in ikey_ pointing to wrong key. So ikey_ needs to be updated in
|
||||
// case of Seek/Next calls to point to right key again.
|
||||
if (!ParseKey(&ikey_)) {
|
||||
if (!value_was_prepared && !ParseKey(&ikey_)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -570,7 +599,7 @@ class DBIter final : public Iterator {
|
||||
|
||||
// If enough contiguous tombstones have been tracked, insert a range
|
||||
// tombstone [first_key, end_key) into the mutable memtable.
|
||||
// end_key is the exclusive upper bound — typically the next live key.
|
||||
// end_key is the exclusive upper bound -- typically the next live key.
|
||||
void MaybeInsertRangeTombstone(const Slice& end_key);
|
||||
void ResetContiguousTombstoneTracking() {
|
||||
contiguous_tombstone_count_ = 0;
|
||||
@@ -644,7 +673,7 @@ class DBIter final : public Iterator {
|
||||
UserComparatorWrapper user_comparator_;
|
||||
const MergeOperator* const merge_operator_;
|
||||
IteratorWrapper iter_;
|
||||
BlobReader blob_reader_;
|
||||
DirtyTracked<BlobState> blob_state_;
|
||||
ReadCallback* read_callback_;
|
||||
// Max visible sequence number. It is normally the snapshot seq unless we have
|
||||
// uncommitted data in db as in WriteUnCommitted.
|
||||
@@ -661,7 +690,7 @@ class DBIter final : public Iterator {
|
||||
// kTypeValuePreferredSeqno entry, this is the write time specified by the
|
||||
// user.
|
||||
uint64_t saved_write_unix_time_;
|
||||
ValueColumnsState value_columns_state_;
|
||||
DirtyTracked<ValueColumnsState> value_columns_state_;
|
||||
Slice pinned_value_;
|
||||
Statistics* statistics_;
|
||||
uint64_t max_skip_;
|
||||
@@ -682,13 +711,16 @@ class DBIter final : public Iterator {
|
||||
std::optional<IterKey> prefix_;
|
||||
|
||||
Status status_;
|
||||
Slice lazy_blob_index_;
|
||||
|
||||
// List of operands for merge operator.
|
||||
MergeContext merge_context_;
|
||||
LocalStatistics local_stats_;
|
||||
PinnedIteratorsManager pinned_iters_mgr_;
|
||||
ColumnFamilyHandleImpl* cfh_;
|
||||
DBImpl* trace_db_;
|
||||
uint32_t trace_cf_id_;
|
||||
bool has_trace_state_;
|
||||
bool allow_blob_write_path_fallback_;
|
||||
port::RWMutex* ingest_sst_lock_;
|
||||
const Slice* const timestamp_ub_;
|
||||
const Slice* const timestamp_lb_;
|
||||
const size_t timestamp_size_;
|
||||
@@ -721,7 +753,6 @@ class DBIter final : public Iterator {
|
||||
// the stacked BlobDB implementation is used, false otherwise.
|
||||
bool expose_blob_index_;
|
||||
bool allow_unprepared_value_;
|
||||
bool is_blob_;
|
||||
bool arena_mode_;
|
||||
|
||||
IterKey range_tomb_first_key_;
|
||||
|
||||
@@ -216,12 +216,19 @@ class TestIterator : public InternalIterator {
|
||||
bool IsKeyPinned() const override { return true; }
|
||||
bool IsValuePinned() const override { return true; }
|
||||
|
||||
void Prepare(const MultiScanArgs* /*scan_opts*/) override {
|
||||
++prepare_call_count_;
|
||||
}
|
||||
|
||||
size_t prepare_call_count() const { return prepare_call_count_; }
|
||||
|
||||
private:
|
||||
bool initialized_;
|
||||
bool valid_;
|
||||
size_t sequence_number_;
|
||||
size_t iter_;
|
||||
size_t steps_ = 0;
|
||||
size_t prepare_call_count_ = 0;
|
||||
|
||||
InternalKeyComparator cmp;
|
||||
std::vector<std::pair<std::string, std::string>> data_;
|
||||
@@ -243,6 +250,53 @@ class DBIteratorTest : public testing::Test {
|
||||
DBIteratorTest() : env_(Env::Default()) {}
|
||||
};
|
||||
|
||||
TEST_F(DBIteratorTest, PrepareForwardsValidatedScanRanges) {
|
||||
Options options;
|
||||
ImmutableOptions ioptions = ImmutableOptions(options);
|
||||
MutableCFOptions mutable_cf_options = MutableCFOptions(options);
|
||||
TestIterator* internal_iter = new TestIterator(BytewiseComparator());
|
||||
internal_iter->AddPut("a", "val_a");
|
||||
internal_iter->AddPut("b", "val_b");
|
||||
internal_iter->Finish();
|
||||
|
||||
ReadOptions ro;
|
||||
std::unique_ptr<Iterator> db_iter(DBIter::NewIter(
|
||||
env_, ro, ioptions, mutable_cf_options, BytewiseComparator(),
|
||||
internal_iter, nullptr /* version */, 10 /* sequence */,
|
||||
nullptr /* read_callback */, /*active_mem=*/nullptr));
|
||||
|
||||
MultiScanArgs scan_opts(BytewiseComparator());
|
||||
scan_opts.insert("a", "c");
|
||||
|
||||
db_iter->Prepare(scan_opts);
|
||||
ASSERT_OK(db_iter->status());
|
||||
EXPECT_EQ(internal_iter->prepare_call_count(), 1);
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorTest, PrepareDoesNotForwardInvalidScanRanges) {
|
||||
Options options;
|
||||
ImmutableOptions ioptions = ImmutableOptions(options);
|
||||
MutableCFOptions mutable_cf_options = MutableCFOptions(options);
|
||||
TestIterator* internal_iter = new TestIterator(BytewiseComparator());
|
||||
internal_iter->AddPut("a", "val_a");
|
||||
internal_iter->AddPut("b", "val_b");
|
||||
internal_iter->Finish();
|
||||
|
||||
ReadOptions ro;
|
||||
std::unique_ptr<Iterator> db_iter(DBIter::NewIter(
|
||||
env_, ro, ioptions, mutable_cf_options, BytewiseComparator(),
|
||||
internal_iter, nullptr /* version */, 10 /* sequence */,
|
||||
nullptr /* read_callback */, /*active_mem=*/nullptr));
|
||||
|
||||
MultiScanArgs scan_opts(BytewiseComparator());
|
||||
scan_opts.insert("b", "d");
|
||||
scan_opts.insert("a", "c");
|
||||
|
||||
db_iter->Prepare(scan_opts);
|
||||
EXPECT_TRUE(db_iter->status().IsInvalidArgument());
|
||||
EXPECT_EQ(internal_iter->prepare_call_count(), 0);
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorTest, DBIteratorPrevNext) {
|
||||
Options options;
|
||||
ImmutableOptions ioptions = ImmutableOptions(options);
|
||||
|
||||
+708
-40
@@ -10,6 +10,9 @@
|
||||
#include <functional>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "db/arena_wrapped_db_iter.h"
|
||||
#include "db/db_iter.h"
|
||||
@@ -87,6 +90,392 @@ TEST_F(DBIteratorBaseTest, APICallsWithPerfContext) {
|
||||
delete iter;
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorBaseTest, PrepareWithMultiScanPrunesNonIntersectingFiles) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("a", "va"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("z", "vz"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(0));
|
||||
|
||||
int table_iterators_created = 0;
|
||||
int files_added = 0;
|
||||
int block_based_iterators = 0;
|
||||
int level_iterators = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"TableCache::NewIterator::BeforeFindTable",
|
||||
[&](void* /*arg*/) { ++table_iterators_created; });
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Version::AddIteratorsForLevel:AddedFile",
|
||||
[&](void* /*arg*/) { ++files_added; });
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Version::AddIteratorsForLevel:IteratorType", [&](void* arg) {
|
||||
auto* iterator_type = static_cast<std::pair<bool, bool>*>(arg);
|
||||
if (iterator_type->first) {
|
||||
++block_based_iterators;
|
||||
}
|
||||
if (iterator_type->second) {
|
||||
++level_iterators;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
MultiScanArgs scan_opts(BytewiseComparator());
|
||||
scan_opts.insert(Slice("a"), Slice("b"));
|
||||
Slice upper_bound("b");
|
||||
ReadOptions read_options;
|
||||
read_options.iterate_upper_bound = &upper_bound;
|
||||
std::unique_ptr<Iterator> iter(
|
||||
db_->NewIterator(read_options, db_->DefaultColumnFamily()));
|
||||
|
||||
ASSERT_EQ(0, table_iterators_created);
|
||||
ASSERT_EQ(0, files_added);
|
||||
ASSERT_EQ(0, block_based_iterators);
|
||||
ASSERT_EQ(0, level_iterators);
|
||||
iter->Prepare(scan_opts);
|
||||
ASSERT_EQ(1, table_iterators_created);
|
||||
ASSERT_EQ(1, files_added);
|
||||
ASSERT_EQ(1, block_based_iterators);
|
||||
ASSERT_EQ(0, level_iterators);
|
||||
std::vector<std::string> keys;
|
||||
for (iter->Seek("a"); iter->Valid(); iter->Next()) {
|
||||
keys.push_back(iter->key().ToString());
|
||||
}
|
||||
ASSERT_EQ(keys, std::vector<std::string>({"a"}));
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorBaseTest, PrepareWithMultiScanPrunesNonIntersectingLevels) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("z", "vz"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(2);
|
||||
ASSERT_OK(Put("a", "va"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_OK(Put("m", "vm"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(1);
|
||||
|
||||
int table_iterators_created = 0;
|
||||
int files_added = 0;
|
||||
int block_based_iterators = 0;
|
||||
int level_iterators = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"TableCache::NewIterator::BeforeFindTable",
|
||||
[&](void* /*arg*/) { ++table_iterators_created; });
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Version::AddIteratorsForLevel:AddedFile",
|
||||
[&](void* /*arg*/) { ++files_added; });
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Version::AddIteratorsForLevel:IteratorType", [&](void* arg) {
|
||||
auto* iterator_type = static_cast<std::pair<bool, bool>*>(arg);
|
||||
if (iterator_type->first) {
|
||||
++block_based_iterators;
|
||||
}
|
||||
if (iterator_type->second) {
|
||||
++level_iterators;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
MultiScanArgs scan_opts(BytewiseComparator());
|
||||
scan_opts.insert(Slice("a"), Slice("n"));
|
||||
Slice upper_bound("n");
|
||||
ReadOptions read_options;
|
||||
read_options.iterate_upper_bound = &upper_bound;
|
||||
std::unique_ptr<Iterator> iter(
|
||||
db_->NewIterator(read_options, db_->DefaultColumnFamily()));
|
||||
|
||||
ASSERT_EQ(0, table_iterators_created);
|
||||
ASSERT_EQ(0, files_added);
|
||||
ASSERT_EQ(0, block_based_iterators);
|
||||
ASSERT_EQ(0, level_iterators);
|
||||
iter->Prepare(scan_opts);
|
||||
ASSERT_EQ(0, table_iterators_created);
|
||||
ASSERT_EQ(2, files_added);
|
||||
ASSERT_EQ(0, block_based_iterators);
|
||||
ASSERT_EQ(1, level_iterators);
|
||||
std::vector<std::string> keys;
|
||||
for (iter->Seek("a"); iter->Valid(); iter->Next()) {
|
||||
keys.push_back(iter->key().ToString());
|
||||
}
|
||||
ASSERT_EQ(keys, std::vector<std::string>({"a", "m"}));
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorBaseTest, PrepareWithMultiScanAllowsSingleUnboundedRange) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("a", "va"));
|
||||
ASSERT_OK(Put("b", "vb"));
|
||||
ASSERT_OK(Put("c", "vc"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(1);
|
||||
|
||||
MultiScanArgs scan_opts(BytewiseComparator());
|
||||
scan_opts.insert(Slice("b"));
|
||||
ReadOptions read_options;
|
||||
std::unique_ptr<Iterator> iter(
|
||||
db_->NewIterator(read_options, db_->DefaultColumnFamily()));
|
||||
|
||||
iter->Prepare(scan_opts);
|
||||
std::vector<std::string> keys;
|
||||
for (iter->Seek("b"); iter->Valid(); iter->Next()) {
|
||||
keys.push_back(iter->key().ToString());
|
||||
}
|
||||
ASSERT_EQ(keys, std::vector<std::string>({"b", "c"}));
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorBaseTest, PrepareWithMultiScanRejectsRepeatedPrepare) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("a", "va"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(1);
|
||||
|
||||
MultiScanArgs scan_opts(BytewiseComparator());
|
||||
scan_opts.insert(Slice("a"), Slice("b"));
|
||||
Slice upper_bound("b");
|
||||
ReadOptions read_options;
|
||||
read_options.iterate_upper_bound = &upper_bound;
|
||||
std::unique_ptr<Iterator> iter(
|
||||
db_->NewIterator(read_options, db_->DefaultColumnFamily()));
|
||||
|
||||
iter->Prepare(scan_opts);
|
||||
ASSERT_OK(iter->status());
|
||||
iter->Prepare(scan_opts);
|
||||
ASSERT_NOK(iter->status());
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorBaseTest, PrepareWithMultiScanDedupsMultipleRangesInSameFile) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("a", "va"));
|
||||
ASSERT_OK(Put("m", "vm"));
|
||||
ASSERT_OK(Put("z", "vz"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(1);
|
||||
|
||||
int table_iterators_created = 0;
|
||||
int files_added = 0;
|
||||
int block_based_iterators = 0;
|
||||
int level_iterators = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"TableCache::NewIterator::BeforeFindTable",
|
||||
[&](void* /*arg*/) { ++table_iterators_created; });
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Version::AddIteratorsForLevel:AddedFile",
|
||||
[&](void* /*arg*/) { ++files_added; });
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Version::AddIteratorsForLevel:IteratorType", [&](void* arg) {
|
||||
auto* iterator_type = static_cast<std::pair<bool, bool>*>(arg);
|
||||
if (iterator_type->first) {
|
||||
++block_based_iterators;
|
||||
}
|
||||
if (iterator_type->second) {
|
||||
++level_iterators;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
MultiScanArgs scan_opts(BytewiseComparator());
|
||||
scan_opts.insert(Slice("a"), Slice("b"));
|
||||
scan_opts.insert(Slice("m"), Slice("n"));
|
||||
Slice upper_bound("b");
|
||||
ReadOptions read_options;
|
||||
read_options.iterate_upper_bound = &upper_bound;
|
||||
std::unique_ptr<Iterator> iter(
|
||||
db_->NewIterator(read_options, db_->DefaultColumnFamily()));
|
||||
|
||||
ASSERT_EQ(0, table_iterators_created);
|
||||
ASSERT_EQ(0, files_added);
|
||||
ASSERT_EQ(0, block_based_iterators);
|
||||
ASSERT_EQ(0, level_iterators);
|
||||
iter->Prepare(scan_opts);
|
||||
ASSERT_EQ(1, table_iterators_created);
|
||||
ASSERT_EQ(1, files_added);
|
||||
ASSERT_EQ(1, block_based_iterators);
|
||||
ASSERT_EQ(0, level_iterators);
|
||||
|
||||
std::vector<std::string> keys;
|
||||
for (iter->Seek("a"); iter->Valid(); iter->Next()) {
|
||||
keys.push_back(iter->key().ToString());
|
||||
}
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_EQ(keys, std::vector<std::string>({"a"}));
|
||||
|
||||
upper_bound = "n";
|
||||
for (iter->Seek("m"); iter->Valid(); iter->Next()) {
|
||||
keys.push_back(iter->key().ToString());
|
||||
}
|
||||
ASSERT_EQ(keys, std::vector<std::string>({"a", "m"}));
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorBaseTest, PrepareWithMultiScanPrunesOverlappingL0Files) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("a", "va"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("m", "vm"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(0));
|
||||
|
||||
int table_iterators_created = 0;
|
||||
int files_added = 0;
|
||||
int block_based_iterators = 0;
|
||||
int level_iterators = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"TableCache::NewIterator::BeforeFindTable",
|
||||
[&](void* /*arg*/) { ++table_iterators_created; });
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Version::AddIteratorsForLevel:AddedFile",
|
||||
[&](void* /*arg*/) { ++files_added; });
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Version::AddIteratorsForLevel:IteratorType", [&](void* arg) {
|
||||
auto* iterator_type = static_cast<std::pair<bool, bool>*>(arg);
|
||||
if (iterator_type->first) {
|
||||
++block_based_iterators;
|
||||
}
|
||||
if (iterator_type->second) {
|
||||
++level_iterators;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
MultiScanArgs scan_opts(BytewiseComparator());
|
||||
scan_opts.insert(Slice("a"), Slice("n"));
|
||||
Slice upper_bound("n");
|
||||
ReadOptions read_options;
|
||||
read_options.iterate_upper_bound = &upper_bound;
|
||||
std::unique_ptr<Iterator> iter(
|
||||
db_->NewIterator(read_options, db_->DefaultColumnFamily()));
|
||||
|
||||
ASSERT_EQ(0, table_iterators_created);
|
||||
ASSERT_EQ(0, files_added);
|
||||
ASSERT_EQ(0, block_based_iterators);
|
||||
ASSERT_EQ(0, level_iterators);
|
||||
iter->Prepare(scan_opts);
|
||||
ASSERT_EQ(2, table_iterators_created);
|
||||
ASSERT_EQ(2, files_added);
|
||||
ASSERT_EQ(2, block_based_iterators);
|
||||
ASSERT_EQ(0, level_iterators);
|
||||
std::vector<std::string> keys;
|
||||
for (iter->Seek("a"); iter->Valid(); iter->Next()) {
|
||||
keys.push_back(iter->key().ToString());
|
||||
}
|
||||
ASSERT_EQ(keys, std::vector<std::string>({"a", "m"}));
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorBaseTest, PrepareWithMultiScanPrunesNonIntersectingMemTables) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("a", "va"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(1);
|
||||
|
||||
ASSERT_OK(db_->PauseBackgroundWork());
|
||||
ASSERT_OK(Put("z_imm", "vz"));
|
||||
ASSERT_OK(dbfull()->TEST_SwitchMemtable());
|
||||
ASSERT_OK(Put("z_mem", "vz"));
|
||||
|
||||
int table_iterators_created = 0;
|
||||
int files_added = 0;
|
||||
int block_based_iterators = 0;
|
||||
int level_iterators = 0;
|
||||
int merging_iterators = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"TableCache::NewIterator::BeforeFindTable",
|
||||
[&](void* /*arg*/) { ++table_iterators_created; });
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Version::AddIteratorsForLevel:AddedFile",
|
||||
[&](void* /*arg*/) { ++files_added; });
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Version::AddIteratorsForLevel:IteratorType", [&](void* arg) {
|
||||
auto* iterator_type = static_cast<std::pair<bool, bool>*>(arg);
|
||||
if (iterator_type->first) {
|
||||
++block_based_iterators;
|
||||
}
|
||||
if (iterator_type->second) {
|
||||
++level_iterators;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"MergeIteratorBuilder::Finish:UseMergingIterator", [&](void* arg) {
|
||||
if (*static_cast<bool*>(arg)) {
|
||||
++merging_iterators;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
MultiScanArgs scan_opts(BytewiseComparator());
|
||||
scan_opts.insert(Slice("a"), Slice("b"));
|
||||
Slice upper_bound("b");
|
||||
ReadOptions read_options;
|
||||
read_options.iterate_upper_bound = &upper_bound;
|
||||
std::unique_ptr<Iterator> iter(
|
||||
db_->NewIterator(read_options, db_->DefaultColumnFamily()));
|
||||
|
||||
ASSERT_EQ(0, table_iterators_created);
|
||||
ASSERT_EQ(0, files_added);
|
||||
ASSERT_EQ(0, block_based_iterators);
|
||||
ASSERT_EQ(0, level_iterators);
|
||||
ASSERT_EQ(0, merging_iterators);
|
||||
iter->Prepare(scan_opts);
|
||||
ASSERT_EQ(1, table_iterators_created);
|
||||
ASSERT_EQ(1, files_added);
|
||||
ASSERT_EQ(1, block_based_iterators);
|
||||
ASSERT_EQ(0, level_iterators);
|
||||
ASSERT_EQ(0, merging_iterators);
|
||||
std::vector<std::string> keys;
|
||||
for (iter->Seek("a"); iter->Valid(); iter->Next()) {
|
||||
keys.push_back(iter->key().ToString());
|
||||
}
|
||||
ASSERT_EQ(keys, std::vector<std::string>({"a"}));
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
ASSERT_OK(db_->ContinueBackgroundWork());
|
||||
}
|
||||
|
||||
// Test param:
|
||||
// bool: whether to pass read_callback to NewIterator().
|
||||
class DBIteratorTest : public DBIteratorBaseTest,
|
||||
@@ -945,6 +1334,68 @@ TEST_P(DBIteratorTest, IteratorDeleteAfterCfDelete) {
|
||||
delete iter;
|
||||
}
|
||||
|
||||
TEST_P(DBIteratorTest, IteratorSeekAfterCfDelete) {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
|
||||
ASSERT_OK(Put(1, "foo", "delete-cf-then-seek-iter"));
|
||||
ASSERT_OK(Put(1, "hello", "value2"));
|
||||
|
||||
ColumnFamilyHandle* cf = handles_[1];
|
||||
ReadOptions ro;
|
||||
|
||||
auto* iter = db_->NewIterator(ro, cf);
|
||||
|
||||
// Delete the CF handle before the lazy iterator tree is materialized.
|
||||
EXPECT_OK(db_->DestroyColumnFamilyHandle(cf));
|
||||
handles_.erase(std::begin(handles_) + 1);
|
||||
|
||||
iter->Seek("foo");
|
||||
ASSERT_EQ(IterStatus(iter), "foo->delete-cf-then-seek-iter");
|
||||
iter->SeekForPrev("hello");
|
||||
ASSERT_EQ(IterStatus(iter), "hello->value2");
|
||||
iter->SeekToFirst();
|
||||
ASSERT_EQ(IterStatus(iter), "foo->delete-cf-then-seek-iter");
|
||||
iter->Next();
|
||||
ASSERT_EQ(IterStatus(iter), "hello->value2");
|
||||
delete iter;
|
||||
}
|
||||
|
||||
TEST_P(DBIteratorTest, IteratorAutoRefreshAfterCfDeleteBeforeLazyInit) {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
|
||||
ASSERT_OK(Put(1, "foo", "delete-cf-then-auto-refresh"));
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
ColumnFamilyHandle* cf = handles_[1];
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.snapshot = snapshot;
|
||||
ro.auto_refresh_iterator_with_snapshot = true;
|
||||
|
||||
auto* iter = db_->NewIterator(ro, cf);
|
||||
|
||||
ASSERT_OK(Put(1, "zzz", "after-snapshot"));
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
// Delete the CF handle before the lazy iterator tree is materialized. The
|
||||
// following operations force auto-refresh to acquire a newer SuperVersion.
|
||||
EXPECT_OK(db_->DestroyColumnFamilyHandle(cf));
|
||||
handles_.erase(std::begin(handles_) + 1);
|
||||
|
||||
iter->Seek("foo");
|
||||
ASSERT_EQ(IterStatus(iter), "foo->delete-cf-then-auto-refresh");
|
||||
ASSERT_OK(iter->status());
|
||||
iter->SeekForPrev("foo");
|
||||
ASSERT_EQ(IterStatus(iter), "foo->delete-cf-then-auto-refresh");
|
||||
ASSERT_OK(iter->status());
|
||||
iter->Next();
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
delete iter;
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_P(DBIteratorTest, IteratorDeleteAfterCfDrop) {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
|
||||
@@ -966,6 +1417,30 @@ TEST_P(DBIteratorTest, IteratorDeleteAfterCfDrop) {
|
||||
delete iter;
|
||||
}
|
||||
|
||||
TEST_P(DBIteratorTest, IteratorSeekAfterCfDrop) {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
|
||||
ASSERT_OK(Put(1, "foo", "drop-cf-then-seek-iter"));
|
||||
|
||||
ReadOptions ro;
|
||||
ColumnFamilyHandle* cf = handles_[1];
|
||||
|
||||
auto* iter = db_->NewIterator(ro, cf);
|
||||
|
||||
// Drop and delete the CF before the lazy iterator tree is materialized.
|
||||
EXPECT_OK(db_->DropColumnFamily(cf));
|
||||
EXPECT_OK(db_->DestroyColumnFamilyHandle(cf));
|
||||
handles_.erase(std::begin(handles_) + 1);
|
||||
|
||||
iter->Seek("foo");
|
||||
ASSERT_EQ(IterStatus(iter), "foo->drop-cf-then-seek-iter");
|
||||
iter->SeekForPrev("foo");
|
||||
ASSERT_EQ(IterStatus(iter), "foo->drop-cf-then-seek-iter");
|
||||
iter->SeekToFirst();
|
||||
ASSERT_EQ(IterStatus(iter), "foo->drop-cf-then-seek-iter");
|
||||
delete iter;
|
||||
}
|
||||
|
||||
// SetOptions not defined in ROCKSDB LITE
|
||||
TEST_P(DBIteratorTest, DBIteratorBoundTest) {
|
||||
Options options = CurrentOptions();
|
||||
@@ -2277,6 +2752,10 @@ TEST_P(DBIteratorTest, ReadAhead) {
|
||||
options.env = env_;
|
||||
options.disable_auto_compactions = true;
|
||||
options.write_buffer_size = 4 << 20;
|
||||
// Pin compression so the readahead byte thresholds below don't depend on the
|
||||
// default compression type. kNoCompression keeps the SST files large enough
|
||||
// to exercise readahead and is always available.
|
||||
options.compression = kNoCompression;
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.block_size = 1024;
|
||||
@@ -2693,6 +3172,8 @@ TEST_P(DBIteratorTest, CreationFailure) {
|
||||
|
||||
Iterator* iter = NewIterator(ReadOptions());
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
iter->SeekToFirst();
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
ASSERT_TRUE(iter->status().IsCorruption());
|
||||
delete iter;
|
||||
}
|
||||
@@ -4151,7 +4632,7 @@ TEST_P(DBIteratorTest, PrefixSameAsStartSeekToNonInDomainKey) {
|
||||
ASSERT_OK(Put("abc2", "v2"));
|
||||
ASSERT_OK(Put("abc3", "v3"));
|
||||
|
||||
// Seek to "ab" (2 bytes) — out-of-domain for FixedPrefixTransform(3).
|
||||
// Seek to "ab" (2 bytes) -- out-of-domain for FixedPrefixTransform(3).
|
||||
// ShouldSetPrefix returns false for out-of-domain targets, so no prefix
|
||||
// constraint is set. The seek should find "abc1" and iteration should
|
||||
// proceed without prefix_same_as_start enforcement.
|
||||
@@ -4395,6 +4876,71 @@ TEST_P(DBMultiScanIteratorTest, RangeAcrossFiles) {
|
||||
iter.reset();
|
||||
}
|
||||
|
||||
TEST_P(DBMultiScanIteratorTest, SortedRangesSkipIODispatcherSort) {
|
||||
auto options = CurrentOptions();
|
||||
options.target_file_size_base = 100 << 10;
|
||||
options.compaction_style = kCompactionStyleUniversal;
|
||||
options.num_levels = 50;
|
||||
options.compression = kNoCompression;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
auto key = [](int i) {
|
||||
std::stringstream ss;
|
||||
ss << "k" << std::setw(5) << std::setfill('0') << i;
|
||||
return ss.str();
|
||||
};
|
||||
|
||||
auto rnd = Random::GetTLSInstance();
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
ASSERT_OK(Put(key(i), rnd->RandomString(2 << 10)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(db_->CompactRange({}, nullptr, nullptr));
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(49));
|
||||
|
||||
int sort_count = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"IODispatcherImpl::SubmitJob:SortBlockHandles",
|
||||
[&](void* /*arg*/) { ++sort_count; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
auto tracking_dispatcher = std::make_shared<TrackingIODispatcher>();
|
||||
std::vector<std::string> key_ranges({key(10), key(90)});
|
||||
MultiScanArgs scan_options(BytewiseComparator());
|
||||
scan_options.io_dispatcher = tracking_dispatcher;
|
||||
scan_options.use_async_io = false;
|
||||
scan_options.insert(key_ranges[0], key_ranges[1]);
|
||||
|
||||
ReadOptions ro;
|
||||
ro.fill_cache = GetParam();
|
||||
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
|
||||
std::unique_ptr<MultiScan> iter =
|
||||
dbfull()->NewMultiScan(ro, cfh, scan_options);
|
||||
|
||||
try {
|
||||
int i = 10;
|
||||
for (auto range : *iter) {
|
||||
for (auto it : range) {
|
||||
ASSERT_EQ(it.first.ToString(), key(i));
|
||||
++i;
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(i, 90);
|
||||
} catch (MultiScanException& ex) {
|
||||
ASSERT_NOK(ex.status());
|
||||
std::cerr << "Iterator returned status " << ex.what();
|
||||
abort();
|
||||
} catch (std::logic_error& ex) {
|
||||
std::cerr << "Iterator returned logic error " << ex.what();
|
||||
abort();
|
||||
}
|
||||
|
||||
ASSERT_GT(tracking_dispatcher->GetReadSets().size(), 0);
|
||||
EXPECT_EQ(sort_count, 0);
|
||||
iter.reset();
|
||||
}
|
||||
|
||||
TEST_P(DBMultiScanIteratorTest, FailureTest) {
|
||||
auto options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
@@ -5820,11 +6366,13 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedIteratorWithBounds) {
|
||||
// Both directions encounter two tombstone runs (a-d and g-j).
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 2);
|
||||
if (Forward()) {
|
||||
// Forward: sees a-d tombstones first → [a, e), then g-j → [g, z).
|
||||
// Forward: sees a-d tombstones first -> live e terminates -> [a, e).
|
||||
// Then g-j -> exhaustion past j, saved_key_="j" fallback -> [g, j),
|
||||
// covering g,h,i. j remains a point tombstone.
|
||||
AssertRange(0, "a", "e");
|
||||
AssertRange(1, "g", "z");
|
||||
AssertRange(1, "g", "j");
|
||||
} else {
|
||||
// Reverse: sees j-g tombstones first, then f,e live, then d-a → exhausts.
|
||||
// Reverse: sees j-g tombstones first, then f,e live, then d-a -> exhausts.
|
||||
AssertRange(0, "g", "z");
|
||||
AssertRange(1, "a", "e");
|
||||
}
|
||||
@@ -5860,13 +6408,24 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedIteratorNoBounds) {
|
||||
|
||||
VerifyIteration({"e", "f"});
|
||||
|
||||
// Without bounds, only the a-d run (which has a live key boundary) gets
|
||||
// inserted. The g-j run at the end has no upper bound → dropped.
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
AssertRange(0, "a", "e");
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
1);
|
||||
if (Forward()) {
|
||||
// Forward: a-d run terminates at live e -> [a, e). g-j run exhausts
|
||||
// past j -> saved_key_ fallback -> [g, j), covering g,h,i with j as a
|
||||
// point tombstone.
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 2);
|
||||
AssertRange(0, "a", "e");
|
||||
AssertRange(1, "g", "j");
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
2);
|
||||
} else {
|
||||
// Reverse fallback path remains prefix-gated.
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
AssertRange(0, "a", "e");
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(ReadPathRangeTombstoneTest, DirectionChange) {
|
||||
@@ -6048,7 +6607,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterDefaultReadOptions) {
|
||||
}
|
||||
|
||||
TEST_P(ReadPathRangeTombstoneTest, PrefixFilterTotalOrderSeek) {
|
||||
// total_order_seek=true disables prefix filtering — all files visible.
|
||||
// total_order_seek=true disables prefix filtering -- all files visible.
|
||||
// The delete run spans from prefix 'b' into prefix 'c', terminated by
|
||||
// live key "cb" from L0. The tombstone [ba, cb) crosses prefix boundaries,
|
||||
// which is safe because total_order_seek makes all files visible.
|
||||
@@ -6090,7 +6649,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterTotalOrderSeek) {
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Memtable: contiguous deletes spanning prefixes 'b' and 'c'.
|
||||
// No live key between "bb" and "cb" — the run crosses prefix boundaries.
|
||||
// No live key between "bb" and "cb" -- the run crosses prefix boundaries.
|
||||
ASSERT_OK(put("aa", "below"));
|
||||
ASSERT_OK(del("ba"));
|
||||
ASSERT_OK(del("bb"));
|
||||
@@ -6218,6 +6777,55 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterPrefixSameAsStart) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(ReadPathRangeTombstoneTest,
|
||||
PrefixFilterUpperBoundDoesNotCoverSkippedLiveKey) {
|
||||
if (!Forward()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.min_tombstones_for_range_conversion = 3;
|
||||
options.statistics = CreateDBStatistics();
|
||||
options.disable_auto_compactions = true;
|
||||
options.prefix_extractor.reset(NewFixedPrefixTransform(1));
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
table_options.whole_key_filtering = false;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Keep a live in-prefix key above the user upper bound in an SST so the
|
||||
// bounded prefix scan cannot return it, then leave a later same-prefix
|
||||
// memtable key available as the internal stop key. If cleanup uses that
|
||||
// late memtable key as the synthetic range end, it will cover "be".
|
||||
ASSERT_OK(Put("be", "live_b"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(Delete("ba"));
|
||||
ASSERT_OK(Delete("bb"));
|
||||
ASSERT_OK(Delete("bc"));
|
||||
ASSERT_OK(Put("bz", "live_z"));
|
||||
|
||||
attempted_insert_ranges_.clear();
|
||||
ReadOptions ro;
|
||||
ro.prefix_same_as_start = true;
|
||||
std::string upper_str = "bd";
|
||||
Slice upper(upper_str);
|
||||
ro.iterate_upper_bound = &upper;
|
||||
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ro));
|
||||
|
||||
std::vector<std::string> keys;
|
||||
for (iter->Seek("ba"); iter->Valid(); iter->Next()) {
|
||||
keys.push_back(iter->key().ToString());
|
||||
}
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_TRUE(keys.empty());
|
||||
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1u);
|
||||
|
||||
ASSERT_EQ(Get("be"), "live_b");
|
||||
}
|
||||
|
||||
TEST_P(ReadPathRangeTombstoneTest, PrefixFilterOutOfDomainSeek) {
|
||||
// With an out-of-domain seek target, prefix_same_as_start cannot establish
|
||||
// a seek-prefix bound. The iterator therefore behaves like an unrestricted
|
||||
@@ -6249,7 +6857,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterOutOfDomainSeek) {
|
||||
ro.prefix_same_as_start = true;
|
||||
auto it = std::unique_ptr<Iterator>(db_->NewIterator(ro));
|
||||
if (Forward()) {
|
||||
// Seek with a 1-byte key — out-of-domain for FixedPrefixTransform(4).
|
||||
// Seek with a 1-byte key -- out-of-domain for FixedPrefixTransform(4).
|
||||
// prefix_same_as_start cannot set a prefix bound for this target.
|
||||
it->Seek("b");
|
||||
while (it->Valid()) {
|
||||
@@ -6612,8 +7220,8 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedWithUDT) {
|
||||
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
if (Forward()) {
|
||||
// Forward exhaustion: range is [e+ts, z+min_ts).
|
||||
AssertRange(0, std::string("e") + ts, std::string("z") + min_ts);
|
||||
// Forward exhaustion past h: saved_key_="h"+ts fallback -> [e+ts, h+ts).
|
||||
AssertRange(0, std::string("e") + ts, std::string("h") + ts);
|
||||
} else {
|
||||
// Reverse exhaustion: range is [a+ts, e+ts).
|
||||
AssertRange(0, std::string("a") + ts, std::string("e") + ts);
|
||||
@@ -6624,9 +7232,10 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedWithUDT) {
|
||||
}
|
||||
|
||||
TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
|
||||
// SeekForPrev lands directly on tombstones. No upper bound, so forward
|
||||
// exhaustion drops the trailing tombstone run (no end key available).
|
||||
// Reverse: SeekForPrev("h") → Delete(h,g,f,e) → live "d" → range [e, h).
|
||||
// SeekForPrev lands directly on tombstones. No upper bound. Forward
|
||||
// exhaustion past h falls back to saved_key_="h" -> [e, h), covering
|
||||
// e,f,g with h as a point tombstone.
|
||||
// Reverse: SeekForPrev("h") -> Delete(h,g,f,e) -> live "d" -> range [e, h).
|
||||
for (bool use_udt : {false, true}) {
|
||||
SCOPED_TRACE(use_udt ? "with UDT" : "without UDT");
|
||||
Options options = CurrentOptions();
|
||||
@@ -6664,7 +7273,7 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
|
||||
keys.push_back(iter->key().ToString());
|
||||
}
|
||||
} else {
|
||||
// SeekForPrev("h") lands on Delete(h), traverses g,f,e → finds "d".
|
||||
// SeekForPrev("h") lands on Delete(h), traverses g,f,e -> finds "d".
|
||||
for (iter->SeekForPrev("h"); iter->Valid(); iter->Prev()) {
|
||||
keys.push_back(iter->key().ToString());
|
||||
}
|
||||
@@ -6673,11 +7282,16 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_EQ(keys, (std::vector<std::string>{"a", "b", "c", "d"}));
|
||||
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
if (Forward()) {
|
||||
// No upper bound → trailing tombstone run has no end key → dropped.
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 0);
|
||||
// Forward exhausts past h -> saved_key_="h" fallback -> [e, h),
|
||||
// covering e,f,g with h as a point tombstone.
|
||||
if (use_udt) {
|
||||
AssertRange(0, std::string("e") + ts, std::string("h") + ts);
|
||||
} else {
|
||||
AssertRange(0, "e", "h");
|
||||
}
|
||||
} else {
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
if (use_udt) {
|
||||
std::string min_ts(sizeof(uint64_t), '\0');
|
||||
AssertRange(0, std::string("e") + ts, std::string("h") + min_ts);
|
||||
@@ -6687,15 +7301,18 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
|
||||
}
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
Forward() ? 0 : 1);
|
||||
1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(ReadPathRangeTombstoneTest, UpperBoundTombstone) {
|
||||
// iterate_upper_bound lands directly on tombstones. Both directions see
|
||||
// tombstones e-h and use upper bound "i" as end key for the range.
|
||||
// Forward: exhaustion at bound → [e, i). Reverse: SeekToLast delegates to
|
||||
// SeekForPrev("i") → [e, i).
|
||||
// iterate_upper_bound lands past the data. Both directions see tombstones
|
||||
// e-h. Forward exhausts naturally past h (no key in DB >= upper) so the
|
||||
// forward path falls back to saved_key_ ("h") as the exclusive end_key,
|
||||
// covering n-1 of n deletes -- [e, h). The remaining tombstone for h
|
||||
// stays as a point delete. Reverse captures the live key "d" before the
|
||||
// run and uses it via range_tomb_end_key_ -- [e, i) when SeekToLast
|
||||
// delegates to SeekForPrev("i").
|
||||
for (bool use_udt : {false, true}) {
|
||||
SCOPED_TRACE(use_udt ? "with UDT" : "without UDT");
|
||||
Options options = CurrentOptions();
|
||||
@@ -6746,13 +7363,17 @@ TEST_P(ReadPathRangeTombstoneTest, UpperBoundTombstone) {
|
||||
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
if (use_udt) {
|
||||
// Forward end key: upper bound padded with min_ts.
|
||||
// Reverse end key: upper bound padded with max_ts (via
|
||||
// SetSavedKeyToSeekForPrevTarget).
|
||||
std::string end_ts(sizeof(uint64_t), Forward() ? '\0' : '\xff');
|
||||
AssertRange(0, std::string("e") + ts, std::string("i") + end_ts);
|
||||
if (Forward()) {
|
||||
// Forward end key: saved_key_ ("h") with the tombstone's own ts.
|
||||
AssertRange(0, std::string("e") + ts, std::string("h") + ts);
|
||||
} else {
|
||||
// Reverse end key: upper bound padded with max_ts (via
|
||||
// SetSavedKeyToSeekForPrevTarget).
|
||||
std::string end_ts(sizeof(uint64_t), '\xff');
|
||||
AssertRange(0, std::string("e") + ts, std::string("i") + end_ts);
|
||||
}
|
||||
} else {
|
||||
AssertRange(0, "e", "i");
|
||||
AssertRange(0, "e", Forward() ? "h" : "i");
|
||||
}
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
@@ -6762,8 +7383,8 @@ TEST_P(ReadPathRangeTombstoneTest, UpperBoundTombstone) {
|
||||
|
||||
TEST_P(ReadPathRangeTombstoneTest, LowerBoundTruncatesReverse) {
|
||||
// Keys a-j, delete a-h. Lower bound "e" truncates reverse iteration
|
||||
// mid-tombstone-run. Forward: tombstones e-h ended by live key i → [e, i).
|
||||
// Reverse: tombstones h,g,f,e then lower_bound hit → flush [e, i).
|
||||
// mid-tombstone-run. Forward: tombstones e-h ended by live key i -> [e, i).
|
||||
// Reverse: tombstones h,g,f,e then lower_bound hit -> flush [e, i).
|
||||
for (bool use_udt : {false, true}) {
|
||||
SCOPED_TRACE(use_udt ? "with UDT" : "without UDT");
|
||||
Options options = CurrentOptions();
|
||||
@@ -6900,14 +7521,14 @@ TEST_P(ReadPathRangeTombstoneTest, InvisibleKeysDontBreakTombstoneRun) {
|
||||
ASSERT_OK(Put("f", "vf"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Delete b, d — visible tombstones.
|
||||
// Delete b, d -- visible tombstones.
|
||||
ASSERT_OK(Delete("b"));
|
||||
ASSERT_OK(Delete("d"));
|
||||
|
||||
// Snapshot S. At S: a(live), b(del), d(del), f(live).
|
||||
const Snapshot* snap = db_->GetSnapshot();
|
||||
|
||||
// Write c, e AFTER the snapshot — invisible at S.
|
||||
// Write c, e AFTER the snapshot -- invisible at S.
|
||||
// At S the iterator sees: a(live), b(del), c(invis), d(del),
|
||||
// e(invis), f(live).
|
||||
ASSERT_OK(Put("c", "vc"));
|
||||
@@ -6919,7 +7540,7 @@ TEST_P(ReadPathRangeTombstoneTest, InvisibleKeysDontBreakTombstoneRun) {
|
||||
VerifyIteration({"a", "f"}, ro);
|
||||
|
||||
// Invisible keys c and e should not break the tombstone run.
|
||||
// 2 tombstones (b, d) ≥ threshold → range [b, f).
|
||||
// 2 tombstones (b, d) >= threshold -> range [b, f).
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
AssertRange(0, "b", "f");
|
||||
|
||||
@@ -7022,7 +7643,8 @@ TEST_P(ReadPathRangeTombstoneTest, SeekToLastStaleSavedKey) {
|
||||
|
||||
TEST_P(ReadPathRangeTombstoneTest, SeekToLastTombstones) {
|
||||
if (Forward()) {
|
||||
ROCKSDB_GTEST_SKIP("SeekToLast tombstone materialization is reverse-only.");
|
||||
ROCKSDB_GTEST_BYPASS(
|
||||
"SeekToLast tombstone materialization is reverse-only.");
|
||||
}
|
||||
|
||||
Options options = CurrentOptions();
|
||||
@@ -7105,6 +7727,52 @@ TEST_P(ReadPathRangeTombstoneTest,
|
||||
db_->ReleaseSnapshot(snap);
|
||||
}
|
||||
|
||||
// SeekForPrev uses the reverse path: PrevInternal -> FindValueForCurrentKey.
|
||||
// When a key has more versions than max_sequential_skip_in_iterations,
|
||||
// FindValueForCurrentKey delegates to FindValueForCurrentKeyUsingSeek which
|
||||
// re-seeks forward to the newest visible version. That function must preserve
|
||||
// key pinning by passing the correct `copy` parameter to
|
||||
// saved_key_.SetUserKey().
|
||||
TEST_P(DBIteratorTest, SeekForPrevKeyPinnedWithManyVersions) {
|
||||
Options options = CurrentOptions();
|
||||
options.max_sequential_skip_in_iterations = 8;
|
||||
options.statistics = CreateDBStatistics();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// 20 versions > max_sequential_skip (8), forcing the reseek path.
|
||||
// No flush: keeps all versions alive in the memtable.
|
||||
for (int i = 0; i < 20; i++) {
|
||||
ASSERT_OK(Put("key1", "value" + std::to_string(i)));
|
||||
}
|
||||
|
||||
ReadOptions ro;
|
||||
ro.pin_data = true;
|
||||
|
||||
std::unique_ptr<Iterator> iter(NewIterator(ro));
|
||||
std::string prop_value;
|
||||
|
||||
// Seek (forward) correctly reports key as pinned.
|
||||
iter->Seek("key1");
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_OK(iter->GetProperty("rocksdb.iterator.is-key-pinned", &prop_value));
|
||||
ASSERT_EQ("1", prop_value);
|
||||
|
||||
uint64_t reseeks_before =
|
||||
options.statistics->getTickerCount(NUMBER_OF_RESEEKS_IN_ITERATION);
|
||||
|
||||
iter->SeekForPrev("key1");
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ("key1", iter->key().ToString());
|
||||
|
||||
// Confirm FindValueForCurrentKeyUsingSeek was actually taken.
|
||||
uint64_t reseeks_after =
|
||||
options.statistics->getTickerCount(NUMBER_OF_RESEEKS_IN_ITERATION);
|
||||
ASSERT_GT(reseeks_after, reseeks_before);
|
||||
|
||||
ASSERT_OK(iter->GetProperty("rocksdb.iterator.is-key-pinned", &prop_value));
|
||||
ASSERT_EQ("1", prop_value);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -359,7 +359,7 @@ class DBMemTableTestForSeek : public DBMemTableTest,
|
||||
TEST_P(DBMemTableTestForSeek, IntegrityChecks) {
|
||||
// Validate key corruption could be detected during seek.
|
||||
// We insert many keys into skiplist. Then we corrupt the each key one at a
|
||||
// time. With memtable_veirfy_per_key_checksum_on_seek enabled, when the
|
||||
// time. With memtable_verify_per_key_checksum_on_seek enabled, when the
|
||||
// corrupted key is searched, the checksum of every key visited during the
|
||||
// seek is validated. It will report data corruption. Otherwise seek returns
|
||||
// not found.
|
||||
@@ -367,7 +367,7 @@ TEST_P(DBMemTableTestForSeek, IntegrityChecks) {
|
||||
Options options = CurrentOptions();
|
||||
options.allow_data_in_errors = allow_data_in_error;
|
||||
options.paranoid_memory_checks = std::get<1>(GetParam());
|
||||
options.memtable_veirfy_per_key_checksum_on_seek = std::get<2>(GetParam());
|
||||
options.memtable_verify_per_key_checksum_on_seek = std::get<2>(GetParam());
|
||||
options.memtable_protection_bytes_per_key = 8;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
@@ -410,7 +410,7 @@ TEST_P(DBMemTableTestForSeek, IntegrityChecks) {
|
||||
ASSERT_EQ(raw_data_pointer.size(), key_count);
|
||||
|
||||
bool enable_key_validation_on_seek =
|
||||
options.memtable_veirfy_per_key_checksum_on_seek;
|
||||
options.memtable_verify_per_key_checksum_on_seek;
|
||||
|
||||
// For each key, corrupt it, validate corruption is detected correctly, then
|
||||
// revert it.
|
||||
|
||||
@@ -151,8 +151,8 @@ TEST_F(DBOpenWithConfigTest, OpenWithComprehensiveConfig) {
|
||||
// memtable_op_scan_flush_trigger=0
|
||||
options.memtable_op_scan_flush_trigger = 0;
|
||||
|
||||
// memtable_veirfy_per_key_checksum_on_seek=false
|
||||
options.memtable_veirfy_per_key_checksum_on_seek = false;
|
||||
// memtable_verify_per_key_checksum_on_seek=false
|
||||
options.memtable_verify_per_key_checksum_on_seek = false;
|
||||
|
||||
// Level and compaction settings
|
||||
// num_levels=50
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "db/column_family.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/db_test_util.h"
|
||||
#include "env/mock_env.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/cache.h"
|
||||
@@ -102,6 +103,40 @@ TEST_F(DBOptionsTest, ImmutableTrackAndVerifyWalsInManifest) {
|
||||
ASSERT_FALSE(s.ok());
|
||||
}
|
||||
|
||||
TEST_F(DBOptionsTest, ImmutableAsyncWalPrecreate) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
options.async_wal_precreate = true;
|
||||
|
||||
ImmutableDBOptions db_options(options);
|
||||
ASSERT_TRUE(db_options.async_wal_precreate);
|
||||
|
||||
Reopen(options);
|
||||
ASSERT_TRUE(dbfull()->GetDBOptions().async_wal_precreate);
|
||||
|
||||
Status s = dbfull()->SetDBOptions({{"async_wal_precreate", "false"}});
|
||||
ASSERT_FALSE(s.ok());
|
||||
}
|
||||
|
||||
TEST_F(DBOptionsTest, SanitizeAsyncWalPrecreateWithWalRecycle) {
|
||||
// This checks the option-level contract for the incompatible WAL paths:
|
||||
// SanitizeOptions() preserves WAL recycling and disables only the
|
||||
// opportunistic async precreation path, so the immutable options reflect
|
||||
// effective behavior.
|
||||
Options options;
|
||||
options.env = env_;
|
||||
options.async_wal_precreate = true;
|
||||
options.recycle_log_file_num = 1;
|
||||
|
||||
Options sanitized_options = SanitizeOptions(dbname_, options);
|
||||
ASSERT_FALSE(sanitized_options.async_wal_precreate);
|
||||
ASSERT_EQ(1U, sanitized_options.recycle_log_file_num);
|
||||
|
||||
options.recycle_log_file_num = 0;
|
||||
sanitized_options = SanitizeOptions(dbname_, options);
|
||||
ASSERT_TRUE(sanitized_options.async_wal_precreate);
|
||||
}
|
||||
|
||||
TEST_F(DBOptionsTest, ImmutableVerifySstUniqueIdInManifest) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
@@ -1729,6 +1764,171 @@ TEST_F(DBOptionsTest, SetOptionsMultipleColumnFamilies) {
|
||||
ASSERT_TRUE(dbfull()->GetOptions(handles_[2]).disable_auto_compactions);
|
||||
}
|
||||
|
||||
// Confirms the default value and serialization/parse round-trip of the new
|
||||
// option. No DB open required; exercises only the options-metadata layer.
|
||||
TEST_F(DBOptionsTest, UseDirectIoForCompactionReadsRoundTrip) {
|
||||
// Default value must remain false to preserve existing semantics.
|
||||
ASSERT_FALSE(DBOptions().use_direct_io_for_compaction_reads);
|
||||
|
||||
DBOptions parsed;
|
||||
ConfigOptions config_options;
|
||||
ASSERT_OK(GetDBOptionsFromString(config_options, DBOptions(),
|
||||
"use_direct_io_for_compaction_reads=true",
|
||||
&parsed));
|
||||
ASSERT_TRUE(parsed.use_direct_io_for_compaction_reads);
|
||||
ASSERT_OK(GetDBOptionsFromString(config_options, DBOptions(),
|
||||
"use_direct_io_for_compaction_reads=false",
|
||||
&parsed));
|
||||
ASSERT_FALSE(parsed.use_direct_io_for_compaction_reads);
|
||||
}
|
||||
|
||||
// Validates that Open rejects the documented incompatible combination.
|
||||
TEST_F(DBOptionsTest, UseDirectIoForCompactionReadsValidation) {
|
||||
// mmap_reads + use_direct_io_for_compaction_reads is rejected at Open
|
||||
// time, the same way mmap_reads + use_direct_reads has always been
|
||||
// rejected.
|
||||
Options bad_options = CurrentOptions();
|
||||
bad_options.create_if_missing = true;
|
||||
bad_options.allow_mmap_reads = true;
|
||||
bad_options.use_direct_io_for_compaction_reads = true;
|
||||
Status bad_status = TryReopen(bad_options);
|
||||
ASSERT_TRUE(bad_status.IsNotSupported()) << bad_status.ToString();
|
||||
}
|
||||
|
||||
// Confirms the option is plumbed all the way to the live DB's options API:
|
||||
// after opening with the flag set, GetDBOptions() reports it back. Only runs
|
||||
// when the environment supports direct I/O.
|
||||
TEST_F(DBOptionsTest, UseDirectIoForCompactionReadsLiveReopen) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.use_direct_io_for_compaction_reads = true;
|
||||
// Use a buffered user-read setup so the new flag is the one doing the work.
|
||||
options.use_direct_reads = false;
|
||||
options.use_direct_io_for_flush_and_compaction = false;
|
||||
Status s = TryReopen(options);
|
||||
if (s.IsNotSupported() || s.IsInvalidArgument()) {
|
||||
ROCKSDB_GTEST_BYPASS(
|
||||
"Direct I/O not supported in this test environment; live reopen "
|
||||
"cannot be exercised.");
|
||||
return;
|
||||
}
|
||||
ASSERT_OK(s);
|
||||
ASSERT_TRUE(dbfull()->GetDBOptions().use_direct_io_for_compaction_reads);
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBOptionsTest, UseDirectIoForCompactionReadsUnsupportedFileSystem) {
|
||||
auto fs = std::make_shared<MockFileSystem>(Env::Default()->GetSystemClock(),
|
||||
/*supports_direct_io=*/false);
|
||||
std::unique_ptr<Env> mock_env = NewCompositeEnv(fs);
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.env = mock_env.get();
|
||||
options.create_if_missing = true;
|
||||
options.use_direct_reads = false;
|
||||
options.use_direct_io_for_flush_and_compaction = false;
|
||||
options.use_direct_io_for_compaction_reads = true;
|
||||
|
||||
Status s = TryReopen(options);
|
||||
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
|
||||
ASSERT_NE(s.ToString().find("Direct I/O is not supported"), std::string::npos)
|
||||
<< s.ToString();
|
||||
}
|
||||
|
||||
TEST_F(
|
||||
DBOptionsTest,
|
||||
UseDirectIoForCompactionReadsSetDBOptionsKeepsVerificationReadsBuffered) {
|
||||
auto fs = std::make_shared<MockFileSystem>(Env::Default()->GetSystemClock(),
|
||||
/*supports_direct_io=*/true);
|
||||
std::unique_ptr<Env> mock_env = NewCompositeEnv(fs);
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.env = mock_env.get();
|
||||
options.create_if_missing = true;
|
||||
options.use_direct_reads = false;
|
||||
options.use_direct_io_for_flush_and_compaction = false;
|
||||
options.use_direct_io_for_compaction_reads = true;
|
||||
|
||||
ASSERT_OK(TryReopen(options));
|
||||
|
||||
bool observed = false;
|
||||
FileOptions observed_file_options;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::SetDBOptions:FileOptionsForCompaction", [&](void* arg) {
|
||||
observed = true;
|
||||
observed_file_options = *static_cast<FileOptions*>(arg);
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(dbfull()->SetDBOptions({{"bytes_per_sync", "1024"}}));
|
||||
ASSERT_TRUE(observed);
|
||||
ASSERT_FALSE(observed_file_options.use_direct_reads);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
Close();
|
||||
}
|
||||
|
||||
// Exercises the FileSystem::OptimizeForCompactionTableRead and
|
||||
// OptimizeForBlobFileRead helpers directly, asserting that the compaction-only
|
||||
// flag does not affect shared FileSystem hooks.
|
||||
TEST_F(DBOptionsTest, OptimizeForCompactionTableReadUsesGlobalDirectReadsOnly) {
|
||||
FileOptions in_opts;
|
||||
in_opts.use_direct_reads = false;
|
||||
|
||||
{
|
||||
Options check_options;
|
||||
check_options.use_direct_reads = false;
|
||||
check_options.use_direct_io_for_compaction_reads = true;
|
||||
check_options.use_direct_io_for_flush_and_compaction = false;
|
||||
ImmutableDBOptions immutable(check_options);
|
||||
FileOptions sst_read =
|
||||
env_->GetFileSystem()->OptimizeForCompactionTableRead(in_opts,
|
||||
immutable);
|
||||
FileOptions blob_read =
|
||||
env_->GetFileSystem()->OptimizeForBlobFileRead(in_opts, immutable);
|
||||
EXPECT_FALSE(sst_read.use_direct_reads);
|
||||
EXPECT_FALSE(blob_read.use_direct_reads);
|
||||
EXPECT_FALSE(sst_read.use_direct_writes);
|
||||
}
|
||||
|
||||
{
|
||||
Options off_options;
|
||||
off_options.use_direct_reads = false;
|
||||
off_options.use_direct_io_for_compaction_reads = false;
|
||||
off_options.use_direct_io_for_flush_and_compaction = false;
|
||||
ImmutableDBOptions immutable_off(off_options);
|
||||
FileOptions sst_read_off =
|
||||
env_->GetFileSystem()->OptimizeForCompactionTableRead(in_opts,
|
||||
immutable_off);
|
||||
EXPECT_FALSE(sst_read_off.use_direct_reads);
|
||||
}
|
||||
|
||||
{
|
||||
Options global_on_options;
|
||||
global_on_options.use_direct_reads = true;
|
||||
global_on_options.use_direct_io_for_compaction_reads = false;
|
||||
global_on_options.use_direct_io_for_flush_and_compaction = false;
|
||||
ImmutableDBOptions immutable_global(global_on_options);
|
||||
FileOptions sst_read_global =
|
||||
env_->GetFileSystem()->OptimizeForCompactionTableRead(in_opts,
|
||||
immutable_global);
|
||||
EXPECT_TRUE(sst_read_global.use_direct_reads);
|
||||
}
|
||||
|
||||
{
|
||||
Options both_on;
|
||||
both_on.use_direct_reads = true;
|
||||
both_on.use_direct_io_for_compaction_reads = true;
|
||||
both_on.use_direct_io_for_flush_and_compaction = false;
|
||||
ImmutableDBOptions immutable_both(both_on);
|
||||
FileOptions sst_read_both =
|
||||
env_->GetFileSystem()->OptimizeForCompactionTableRead(in_opts,
|
||||
immutable_both);
|
||||
EXPECT_TRUE(sst_read_both.use_direct_reads);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+238
-2
@@ -7,9 +7,13 @@
|
||||
// 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 "db/blob/blob_index.h"
|
||||
#include "db/db_impl/db_impl_secondary.h"
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/db_with_timestamp_test_util.h"
|
||||
#include "db/wide/wide_column_test_util.h"
|
||||
#include "db/write_batch_internal.h"
|
||||
#include "file/filename.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/utilities/transaction_db.h"
|
||||
#include "test_util/sync_point.h"
|
||||
@@ -357,8 +361,7 @@ TEST_F(DBSecondaryTest, GetMergeOperands) {
|
||||
const Status s = db_secondary_->GetMergeOperands(
|
||||
ReadOptions(), cfh, "k1", values.data(), &merge_operands_info,
|
||||
&number_of_operands);
|
||||
ASSERT_NOK(s);
|
||||
ASSERT_TRUE(s.IsMergeInProgress());
|
||||
ASSERT_OK(s);
|
||||
|
||||
ASSERT_EQ(number_of_operands, 4);
|
||||
ASSERT_EQ(values[0].ToString(), "v1");
|
||||
@@ -367,6 +370,197 @@ TEST_F(DBSecondaryTest, GetMergeOperands) {
|
||||
ASSERT_EQ(values[3].ToString(), "v4");
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, GetMergeOperandsWithBlobBackedEntityDefaultColumn) {
|
||||
// Goal: exercise the secondary read path with a blob-backed V2 entity base
|
||||
// in SST and a newer merge operand applied through catch-up. The secondary
|
||||
// DB must resolve the blob-backed default column for both Get() and
|
||||
// GetMergeOperands() while combining the newer memtable merge with the older
|
||||
// SST-backed base entity.
|
||||
Options options = GetDefaultOptions();
|
||||
options.create_if_missing = true;
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 50;
|
||||
options.disable_auto_compactions = true;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
|
||||
options.env = env_;
|
||||
Reopen(options);
|
||||
|
||||
const std::string key = "secondary_blob_entity";
|
||||
const std::string default_value(100, 'd');
|
||||
const std::string large_value(120, 'l');
|
||||
const std::string merge_operand = "suffix";
|
||||
const std::string expected_merged = default_value + "|" + merge_operand;
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"meta", "inline"}};
|
||||
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
options.max_open_files = -1;
|
||||
OpenSecondary(options);
|
||||
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
|
||||
merge_operand));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
|
||||
auto* cfh = db_secondary_->DefaultColumnFamily();
|
||||
|
||||
{
|
||||
PinnableSlice result;
|
||||
ASSERT_OK(db_secondary_->Get(ReadOptions(), cfh, key, &result));
|
||||
ASSERT_EQ(result, expected_merged);
|
||||
}
|
||||
|
||||
{
|
||||
GetMergeOperandsOptions get_merge_opts;
|
||||
get_merge_opts.expected_max_number_of_operands = 2;
|
||||
|
||||
std::array<PinnableSlice, 2> merge_operands;
|
||||
int number_of_operands = 0;
|
||||
|
||||
ASSERT_OK(db_secondary_->GetMergeOperands(
|
||||
ReadOptions(), cfh, key, merge_operands.data(), &get_merge_opts,
|
||||
&number_of_operands));
|
||||
ASSERT_EQ(number_of_operands, 2);
|
||||
ASSERT_EQ(merge_operands[0], default_value);
|
||||
ASSERT_EQ(merge_operands[1], merge_operand);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, GetImplReturnsBlobIndexWhenRequested) {
|
||||
// Goal: cover the internal secondary GetImpl contract when the caller
|
||||
// explicitly asks for raw blob-index bytes via `is_blob_index`. Catch-up
|
||||
// rebuilds the blob index into the secondary memtable, and this path must
|
||||
// preserve the encoded index instead of eagerly resolving or rejecting it.
|
||||
Options options = GetDefaultOptions();
|
||||
options.create_if_missing = true;
|
||||
options.env = env_;
|
||||
|
||||
Reopen(options);
|
||||
|
||||
options.max_open_files = -1;
|
||||
OpenSecondary(options);
|
||||
|
||||
std::string blob_index;
|
||||
BlobIndex::EncodeInlinedTTL(&blob_index, /*expiration=*/9876543210, "blob");
|
||||
|
||||
WriteBatch batch;
|
||||
ASSERT_OK(WriteBatchInternal::PutBlobIndex(
|
||||
&batch, db_->DefaultColumnFamily()->GetID(), "blob_key", blob_index));
|
||||
ASSERT_OK(db_->Write(WriteOptions(), &batch));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
|
||||
PinnableSlice value;
|
||||
bool is_blob_index = false;
|
||||
DBImpl::GetImplOptions get_impl_options;
|
||||
get_impl_options.column_family = db_secondary_->DefaultColumnFamily();
|
||||
get_impl_options.value = &value;
|
||||
get_impl_options.is_blob_index = &is_blob_index;
|
||||
|
||||
ASSERT_OK(db_secondary_full()->GetImpl(ReadOptions(), Slice("blob_key"),
|
||||
get_impl_options));
|
||||
ASSERT_TRUE(is_blob_index);
|
||||
ASSERT_EQ(value, blob_index);
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest,
|
||||
GetAndGetEntityWithBlobBackedDefaultColumnDirectWriteMemtable) {
|
||||
// Goal: cover the secondary memtable path after catch-up replays a blob
|
||||
// direct-write entity from the primary WAL. The test checks both `Get()`,
|
||||
// which must resolve the blob-backed default column, and `GetEntity()`,
|
||||
// which must eagerly resolve all unresolved blob columns instead of exposing
|
||||
// encoded blob indices to the caller.
|
||||
Options options =
|
||||
wide_column_test_util::GetDirectWriteOptions(GetDefaultOptions());
|
||||
options.create_if_missing = true;
|
||||
options.min_blob_size = 50;
|
||||
options.env = env_;
|
||||
|
||||
Reopen(options);
|
||||
|
||||
options.max_open_files = -1;
|
||||
OpenSecondary(options);
|
||||
|
||||
const std::string key = "secondary_direct_write_memtable_entity";
|
||||
const std::string default_value =
|
||||
wide_column_test_util::GenerateLargeValue(100, 'd');
|
||||
const std::string large_value =
|
||||
wide_column_test_util::GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = wide_column_test_util::GenerateSmallValue();
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
|
||||
auto* cfh = db_secondary_->DefaultColumnFamily();
|
||||
|
||||
{
|
||||
PinnableSlice result;
|
||||
ASSERT_OK(db_secondary_->Get(ReadOptions(), cfh, key, &result));
|
||||
ASSERT_EQ(result, default_value);
|
||||
}
|
||||
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_secondary_->GetEntity(ReadOptions(), cfh, key, &result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, SecondaryDirectWriteMemtableBlobBlockCacheTier) {
|
||||
// Goal: cover the secondary memtable path under kBlockCacheTier. Catch-up
|
||||
// rebuilds the blob-backed direct-write entity from WAL, so serving Get() or
|
||||
// GetEntity() would require blob I/O through the memtable resolution path,
|
||||
// which must return Incomplete instead of issuing that I/O.
|
||||
Options options =
|
||||
wide_column_test_util::GetDirectWriteOptions(GetDefaultOptions());
|
||||
options.create_if_missing = true;
|
||||
options.min_blob_size = 50;
|
||||
options.env = env_;
|
||||
|
||||
Reopen(options);
|
||||
|
||||
options.max_open_files = -1;
|
||||
OpenSecondary(options);
|
||||
|
||||
const std::string key = "secondary_direct_write_memtable_block_cache_tier";
|
||||
const std::string default_value =
|
||||
wide_column_test_util::GenerateLargeValue(100, 'd');
|
||||
const std::string large_value =
|
||||
wide_column_test_util::GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = wide_column_test_util::GenerateSmallValue();
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
|
||||
auto* cfh = db_secondary_->DefaultColumnFamily();
|
||||
ReadOptions read_opts;
|
||||
read_opts.read_tier = kBlockCacheTier;
|
||||
|
||||
{
|
||||
PinnableSlice result;
|
||||
const Status s = db_secondary_->Get(read_opts, cfh, key, &result);
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
ASSERT_TRUE(result.empty());
|
||||
}
|
||||
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
const Status s = db_secondary_->GetEntity(read_opts, cfh, key, &result);
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
ASSERT_TRUE(result.columns().empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, InternalCompactionCompactedFiles) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
@@ -729,6 +923,48 @@ TEST_F(DBSecondaryTest, OpenAsSecondaryWALTailing) {
|
||||
verify_db_func("new_foo_value_1", "new_bar_value");
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, CatchUpTailsCurrentWalWhenFutureWalExists) {
|
||||
// Goal: secondary catch-up must keep tailing the current WAL even when a
|
||||
// higher-number empty WAL is present. This reproduces the async WAL
|
||||
// precreation shape by creating a future WAL before opening the secondary,
|
||||
// then appending another write to the current WAL and catching up again.
|
||||
Options options;
|
||||
options.env = env_;
|
||||
Reopen(options);
|
||||
|
||||
ASSERT_OK(Put("foo", "value0"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("foo", "value1"));
|
||||
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
|
||||
|
||||
std::unique_ptr<WalFile> current_wal;
|
||||
ASSERT_OK(db_->GetCurrentWalFile(¤t_wal));
|
||||
ASSERT_NE(nullptr, current_wal);
|
||||
|
||||
const uint64_t future_wal_number = current_wal->LogNumber() + 1000;
|
||||
std::unique_ptr<WritableFile> future_wal_file;
|
||||
ASSERT_OK(env_->NewWritableFile(LogFileName(dbname_, future_wal_number),
|
||||
&future_wal_file, EnvOptions()));
|
||||
ASSERT_OK(future_wal_file->Close());
|
||||
|
||||
Options secondary_options;
|
||||
secondary_options.env = env_;
|
||||
secondary_options.max_open_files = -1;
|
||||
OpenSecondary(secondary_options);
|
||||
|
||||
ReadOptions read_options;
|
||||
std::string value;
|
||||
ASSERT_OK(db_secondary_->Get(read_options, "foo", &value));
|
||||
ASSERT_EQ("value1", value);
|
||||
|
||||
ASSERT_OK(Put("foo", "value2"));
|
||||
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
|
||||
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
ASSERT_OK(db_secondary_->Get(read_options, "foo", &value));
|
||||
ASSERT_EQ("value2", value);
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, SecondaryTailingBug_ISSUE_8467) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
|
||||
@@ -8,14 +8,17 @@
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "port/port.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/advanced_compression.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/types.h"
|
||||
#include "rocksdb/utilities/object_registry.h"
|
||||
#include "rocksdb/utilities/table_properties_collectors.h"
|
||||
#include "table/format.h"
|
||||
#include "table/meta_blocks.h"
|
||||
@@ -51,6 +54,42 @@ void VerifyTableProperties(DB* db, uint64_t expected_entries_size) {
|
||||
|
||||
VerifySstUniqueIds(props);
|
||||
}
|
||||
|
||||
class ParseCompressionDisplayNameManager : public CompressionManagerWrapper {
|
||||
public:
|
||||
static constexpr const char* kCompatibilityName =
|
||||
"ParseCompressionDisplayNameManager";
|
||||
|
||||
ParseCompressionDisplayNameManager()
|
||||
: CompressionManagerWrapper(GetBuiltinV2CompressionManager()) {}
|
||||
|
||||
const char* Name() const override { return kCompatibilityName; }
|
||||
const char* CompatibilityName() const override { return kCompatibilityName; }
|
||||
|
||||
std::string CompressionTypeToString(CompressionType type) const override {
|
||||
if (type == kCustomCompression8A) {
|
||||
return "CustomAlpha";
|
||||
}
|
||||
if (type == kCustomCompression8B) {
|
||||
return "CustomBeta";
|
||||
}
|
||||
return CompressionManagerWrapper::CompressionTypeToString(type);
|
||||
}
|
||||
|
||||
static void Register() {
|
||||
static std::once_flag loaded;
|
||||
std::call_once(loaded, []() {
|
||||
auto& library = *ObjectLibrary::Default();
|
||||
library.AddFactory<CompressionManager>(
|
||||
kCompatibilityName, [](const std::string& /*uri*/,
|
||||
std::unique_ptr<CompressionManager>* guard,
|
||||
std::string* /*errmsg*/) {
|
||||
*guard = std::make_unique<ParseCompressionDisplayNameManager>();
|
||||
return guard->get();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
} // anonymous namespace
|
||||
|
||||
class DBTablePropertiesTest : public DBTestBase,
|
||||
@@ -812,6 +851,90 @@ TEST_F(DBTablePropertiesTest, KeyLargestSmallestSeqno) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, ParseCompressionNameForDisplay) {
|
||||
auto custom_mgr = std::make_shared<ParseCompressionDisplayNameManager>();
|
||||
|
||||
// Test empty string
|
||||
EXPECT_EQ("NoCompression", ParseCompressionNameForDisplay(""));
|
||||
|
||||
// Test old format (no semicolon)
|
||||
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay("ZSTD"));
|
||||
EXPECT_EQ("Snappy", ParseCompressionNameForDisplay("Snappy"));
|
||||
EXPECT_EQ("LZ4", ParseCompressionNameForDisplay("LZ4"));
|
||||
EXPECT_EQ("kZSTD", ParseCompressionNameForDisplay("kZSTD"));
|
||||
EXPECT_EQ("kSnappyCompression",
|
||||
ParseCompressionNameForDisplay("kSnappyCompression"));
|
||||
|
||||
// Test new format version 7 with ZSTD
|
||||
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay("zstd;07;"));
|
||||
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay("BuiltinV2;07;"));
|
||||
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay("builtin_v2;07;"));
|
||||
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay(";07;"));
|
||||
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay("custom_mgr;07;extra;"));
|
||||
|
||||
// Test new format with LZ4
|
||||
EXPECT_EQ("LZ4", ParseCompressionNameForDisplay("zstd;04;"));
|
||||
EXPECT_EQ("LZ4", ParseCompressionNameForDisplay("lz4;04;"));
|
||||
|
||||
// Test lowercase hex for reserved/custom values
|
||||
EXPECT_EQ("Reserved7F", ParseCompressionNameForDisplay("test;7f;"));
|
||||
EXPECT_EQ("Custom8A", ParseCompressionNameForDisplay("test;8a;"));
|
||||
|
||||
// Test multiple compression types
|
||||
EXPECT_EQ("LZ4,ZSTD", ParseCompressionNameForDisplay("test;0407;"));
|
||||
EXPECT_EQ(
|
||||
"ZSTD,LZ4",
|
||||
ParseCompressionNameForDisplay("test;0704;")); // sorted by appearance
|
||||
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay("test;0007;"));
|
||||
|
||||
// Test all standard compression types (01-07)
|
||||
EXPECT_EQ("NoCompression", ParseCompressionNameForDisplay("test;00;"));
|
||||
EXPECT_EQ("Snappy", ParseCompressionNameForDisplay("test;01;"));
|
||||
EXPECT_EQ("Zlib", ParseCompressionNameForDisplay("test;02;"));
|
||||
EXPECT_EQ("BZip2", ParseCompressionNameForDisplay("test;03;"));
|
||||
EXPECT_EQ("LZ4", ParseCompressionNameForDisplay("test;04;"));
|
||||
EXPECT_EQ("LZ4HC", ParseCompressionNameForDisplay("test;05;"));
|
||||
EXPECT_EQ("Xpress", ParseCompressionNameForDisplay("test;06;"));
|
||||
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay("test;07;"));
|
||||
|
||||
// Test custom/reserved types (>= 0x08)
|
||||
// 0x08-0x7F are Reserved, 0x80-0xFE are Custom
|
||||
EXPECT_EQ("Reserved08", ParseCompressionNameForDisplay("test;08;"));
|
||||
EXPECT_EQ("Custom80", ParseCompressionNameForDisplay("test;80;"));
|
||||
EXPECT_EQ("Reserved7F", ParseCompressionNameForDisplay("test;7F;"));
|
||||
EXPECT_EQ("CustomFE", ParseCompressionNameForDisplay("test;FE;"));
|
||||
EXPECT_EQ("CustomAlpha,CustomBeta",
|
||||
ParseCompressionNameForDisplay(
|
||||
"ParseCompressionDisplayNameManager;8A8B;", custom_mgr));
|
||||
EXPECT_EQ("CustomAlpha,LZ4,CustomBeta",
|
||||
ParseCompressionNameForDisplay(
|
||||
"ParseCompressionDisplayNameManager;8A048B;", custom_mgr));
|
||||
|
||||
ParseCompressionDisplayNameManager::Register();
|
||||
EXPECT_EQ("CustomAlpha", ParseCompressionNameForDisplay(
|
||||
"ParseCompressionDisplayNameManager;8A;"));
|
||||
|
||||
// Test DisableOption (0xFF) - filtered out
|
||||
EXPECT_EQ("NoCompression", ParseCompressionNameForDisplay("test;FF;"));
|
||||
|
||||
// Test NoCompression (empty hex field would be caught by validation, but test
|
||||
// "00")
|
||||
EXPECT_EQ("NoCompression", ParseCompressionNameForDisplay("test;00;"));
|
||||
EXPECT_EQ("NoCompression", ParseCompressionNameForDisplay("BuiltinV2;;"));
|
||||
EXPECT_EQ("NoCompression", ParseCompressionNameForDisplay("test;;"));
|
||||
|
||||
// Test three+ semicolons (future fields ignored)
|
||||
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay("test;07;extra;fields;"));
|
||||
|
||||
// Test malformed inputs -> "Unknown"
|
||||
EXPECT_EQ("Unknown", ParseCompressionNameForDisplay("only_one;"));
|
||||
EXPECT_EQ("Unknown", ParseCompressionNameForDisplay("bad;0;")); // odd length
|
||||
EXPECT_EQ("Unknown",
|
||||
ParseCompressionNameForDisplay("bad;0G;")); // invalid hex
|
||||
EXPECT_EQ("Unknown",
|
||||
ParseCompressionNameForDisplay("bad;GG;")); // invalid hex
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(DBTablePropertiesTest, DBTablePropertiesTest,
|
||||
::testing::Values("kCompactionStyleLevel",
|
||||
"kCompactionStyleUniversal"));
|
||||
|
||||
+110
@@ -3516,6 +3516,18 @@ class ModelDB : public DB {
|
||||
return Status::NotSupported("Not implemented");
|
||||
}
|
||||
|
||||
using DB::PrepareFileIngestion;
|
||||
Status PrepareFileIngestion(
|
||||
const std::vector<IngestExternalFileArg>& /*args*/,
|
||||
std::unique_ptr<FileIngestionHandle>* /*handle*/) override {
|
||||
return Status::NotSupported("Not implemented.");
|
||||
}
|
||||
|
||||
Status CommitFileIngestionHandles(
|
||||
std::vector<std::unique_ptr<FileIngestionHandle>> /*handles*/) override {
|
||||
return Status::NotSupported("Not implemented.");
|
||||
}
|
||||
|
||||
using DB::CreateColumnFamilyWithImport;
|
||||
Status CreateColumnFamilyWithImport(
|
||||
const ColumnFamilyOptions& /*options*/,
|
||||
@@ -3773,6 +3785,12 @@ class ModelDB : public DB {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status GetPreparedFileInfoForExternalSstIngestion(
|
||||
const std::string& /*file_path*/,
|
||||
std::shared_ptr<const PreparedFileInfo>* /*file_info*/) override {
|
||||
return Status::NotSupported();
|
||||
}
|
||||
|
||||
Status GetSortedWalFiles(VectorLogPtr& /*files*/) override {
|
||||
return Status::OK();
|
||||
}
|
||||
@@ -8238,6 +8256,98 @@ TEST_P(OpenFilesAsyncTest, BeforeRead) {
|
||||
}
|
||||
}
|
||||
|
||||
// For modern DBs (manifest carries file_creation_time),
|
||||
// GetCreationTimeOfOldestFile returns the real value without waiting for async
|
||||
// file open.
|
||||
TEST_P(OpenFilesAsyncTest, GetCreationTimeOfOldestFileSkipsWaitForModernDB) {
|
||||
if (max_open_files_ != -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
Options options = CurrentOptions();
|
||||
ASSERT_NO_FATAL_FAILURE(SetupData(options));
|
||||
|
||||
// If WaitForAsyncFileOpen is entered, the test fails -- modern DBs must
|
||||
// never need the wait.
|
||||
std::atomic<bool> waited{false};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::WaitForAsyncFileOpen::BeforeWait",
|
||||
[&](void* /*arg*/) { waited.store(true); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
OpenTestDB(options);
|
||||
|
||||
uint64_t creation_time = std::numeric_limits<uint64_t>::max();
|
||||
ASSERT_OK(dbfull()->GetCreationTimeOfOldestFile(&creation_time));
|
||||
EXPECT_GT(creation_time, 0);
|
||||
EXPECT_LT(creation_time, std::numeric_limits<uint64_t>::max());
|
||||
EXPECT_FALSE(waited.load());
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
Close();
|
||||
}
|
||||
|
||||
// For legacy DBs (manifest lacks file_creation_time, value comes from the
|
||||
// pinned reader), GetCreationTimeOfOldestFile must block until
|
||||
// BGWorkAsyncFileOpen pins the readers; otherwise it returns 0 (the "info
|
||||
// unavailable" sentinel) instead of the real value.
|
||||
TEST_P(OpenFilesAsyncTest,
|
||||
GetCreationTimeOfOldestFileBlocksOnAsyncOpenForLegacyDB) {
|
||||
if (max_open_files_ != -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
Options options = CurrentOptions();
|
||||
ASSERT_NO_FATAL_FAILURE(SetupData(options));
|
||||
|
||||
// Simulate a legacy DB: force TryGetFileCreationTime to return
|
||||
// kUnknownFileCreationTime until BGWorkAsyncFileOpen completes (i.e., what
|
||||
// would happen if the manifest had no file_creation_time and the pinned
|
||||
// reader was the only source).
|
||||
std::atomic<bool> async_open_done{false};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::BGWorkAsyncFileOpen:Done",
|
||||
[&](void* /*arg*/) { async_open_done.store(true); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"Version::GetCreationTimeOfOldestFile::FileCreationTime", [&](void* arg) {
|
||||
if (!async_open_done.load()) {
|
||||
*static_cast<uint64_t*>(arg) = kUnknownFileCreationTime;
|
||||
}
|
||||
});
|
||||
|
||||
// Dependency chain: caller's first pass returns 0, enters
|
||||
// WaitForAsyncFileOpen -> main thread confirms wait -> main thread releases
|
||||
// async open -> background worker completes -> caller wakes -> second pass
|
||||
// sees the real value.
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"DBImpl::WaitForAsyncFileOpen::BeforeWait",
|
||||
"OpenFilesAsyncTest::CreationTime::CallerBlocked"},
|
||||
{"OpenFilesAsyncTest::CreationTime::CallerBlocked",
|
||||
"OpenFilesAsyncTest::CreationTime::ReleaseAsyncOpen"},
|
||||
{"OpenFilesAsyncTest::CreationTime::ReleaseAsyncOpen",
|
||||
"DBImpl::BGWorkAsyncFileOpen::Start"}});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
OpenTestDB(options);
|
||||
|
||||
uint64_t creation_time = std::numeric_limits<uint64_t>::max();
|
||||
port::Thread caller([&]() {
|
||||
ASSERT_OK(dbfull()->GetCreationTimeOfOldestFile(&creation_time));
|
||||
});
|
||||
|
||||
TEST_SYNC_POINT("OpenFilesAsyncTest::CreationTime::CallerBlocked");
|
||||
TEST_SYNC_POINT("OpenFilesAsyncTest::CreationTime::ReleaseAsyncOpen");
|
||||
caller.join();
|
||||
|
||||
EXPECT_GT(creation_time, 0);
|
||||
EXPECT_LT(creation_time, std::numeric_limits<uint64_t>::max());
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(OpenFilesAsyncTest, Shutdown) {
|
||||
Options options = CurrentOptions();
|
||||
ASSERT_NO_FATAL_FAILURE(SetupData(options));
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "db/forward_iterator.h"
|
||||
#include "env/fs_readonly.h"
|
||||
#include "env/mock_env.h"
|
||||
#include "file/file_util.h"
|
||||
#include "port/lang.h"
|
||||
#include "rocksdb/cache.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
@@ -90,6 +91,8 @@ DBTestBase::DBTestBase(const std::string path, bool env_do_fsync)
|
||||
dbname_ = test::PerThreadDBPath(env_, path);
|
||||
alternative_wal_dir_ = dbname_ + "/wal";
|
||||
alternative_db_log_dir_ = dbname_ + "/db_log_dir";
|
||||
EXPECT_OK(DestroyDir(env_, alternative_wal_dir_));
|
||||
EXPECT_OK(DestroyDir(env_, alternative_db_log_dir_));
|
||||
auto options = CurrentOptions();
|
||||
options.env = env_;
|
||||
auto delete_options = options;
|
||||
@@ -117,6 +120,8 @@ DBTestBase::~DBTestBase() {
|
||||
if (getenv("KEEP_DB")) {
|
||||
printf("DB is still at %s\n", dbname_.c_str());
|
||||
} else {
|
||||
EXPECT_OK(DestroyDir(env_, alternative_wal_dir_));
|
||||
EXPECT_OK(DestroyDir(env_, alternative_db_log_dir_));
|
||||
EXPECT_OK(DestroyDB(dbname_, options));
|
||||
}
|
||||
// Ensure SstFileManager (and its DeleteScheduler background thread) is
|
||||
|
||||
+445
-14
@@ -7,6 +7,8 @@
|
||||
// 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 <algorithm>
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/db_with_timestamp_test_util.h"
|
||||
#include "options/options_helper.h"
|
||||
@@ -19,12 +21,62 @@
|
||||
#include "utilities/fault_injection_env.h"
|
||||
#include "utilities/fault_injection_fs.h"
|
||||
|
||||
#if defined(OS_LINUX)
|
||||
#include <sys/statfs.h>
|
||||
#endif
|
||||
|
||||
#if defined(OS_LINUX) && !defined(BTRFS_SUPER_MAGIC)
|
||||
#define BTRFS_SUPER_MAGIC 0x9123683E
|
||||
#endif
|
||||
|
||||
#if defined(OS_LINUX) && !defined(TMPFS_MAGIC)
|
||||
#define TMPFS_MAGIC 0x01021994
|
||||
#endif
|
||||
|
||||
#if defined(OS_LINUX) && !defined(OVERLAYFS_SUPER_MAGIC)
|
||||
#define OVERLAYFS_SUPER_MAGIC 0x794c7630
|
||||
#endif
|
||||
|
||||
#if defined(OS_LINUX) && !defined(ZFS_SUPER_MAGIC)
|
||||
#define ZFS_SUPER_MAGIC 0x2fc12fc1
|
||||
#endif
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
class DBWALTestBase : public DBTestBase {
|
||||
protected:
|
||||
explicit DBWALTestBase(const std::string& dir_name)
|
||||
: DBTestBase(dir_name, /*env_do_fsync=*/true) {}
|
||||
|
||||
std::vector<uint64_t> ListWalNumbers() {
|
||||
std::vector<uint64_t> wal_numbers;
|
||||
std::vector<std::string> files;
|
||||
EXPECT_OK(env_->GetChildren(dbname_, &files));
|
||||
for (const auto& file : files) {
|
||||
uint64_t number = 0;
|
||||
FileType type = kWalFile;
|
||||
if (ParseFileName(file, &number, &type) && type == kWalFile) {
|
||||
wal_numbers.push_back(number);
|
||||
}
|
||||
}
|
||||
std::sort(wal_numbers.begin(), wal_numbers.end());
|
||||
return wal_numbers;
|
||||
}
|
||||
|
||||
void CreateEmptyWal(uint64_t wal_number) {
|
||||
std::unique_ptr<WritableFile> file;
|
||||
ASSERT_OK(env_->NewWritableFile(LogFileName(dbname_, wal_number), &file,
|
||||
EnvOptions()));
|
||||
ASSERT_OK(file->Close());
|
||||
}
|
||||
|
||||
void CreateWalWithContents(uint64_t wal_number, const Slice& contents) {
|
||||
std::unique_ptr<WritableFile> file;
|
||||
ASSERT_OK(env_->NewWritableFile(LogFileName(dbname_, wal_number), &file,
|
||||
EnvOptions()));
|
||||
ASSERT_OK(file->Append(contents));
|
||||
ASSERT_OK(file->Close());
|
||||
}
|
||||
|
||||
#if defined(ROCKSDB_PLATFORM_POSIX)
|
||||
public:
|
||||
#if defined(ROCKSDB_FALLOCATE_PRESENT)
|
||||
@@ -57,6 +109,31 @@ class DBWALTestBase : public DBTestBase {
|
||||
assert(err == 0);
|
||||
return sbuf.st_blocks * 512;
|
||||
}
|
||||
|
||||
#if defined(ROCKSDB_FALLOCATE_PRESENT)
|
||||
bool ShouldSkipAllocationCheck(const std::string& file_name) {
|
||||
(void)file_name;
|
||||
#if defined(OS_LINUX)
|
||||
struct statfs fs_stat;
|
||||
if (statfs(file_name.c_str(), &fs_stat) == 0) {
|
||||
if (fs_stat.f_type ==
|
||||
static_cast<decltype(fs_stat.f_type)>(BTRFS_SUPER_MAGIC)) {
|
||||
return true;
|
||||
} else if (fs_stat.f_type ==
|
||||
static_cast<decltype(fs_stat.f_type)>(ZFS_SUPER_MAGIC)) {
|
||||
return true;
|
||||
} else if (fs_stat.f_type ==
|
||||
static_cast<decltype(fs_stat.f_type)>(TMPFS_MAGIC)) {
|
||||
return true;
|
||||
} else if (fs_stat.f_type ==
|
||||
static_cast<decltype(fs_stat.f_type)>(OVERLAYFS_SUPER_MAGIC)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
#endif // ROCKSDB_FALLOCATE_PRESENT
|
||||
#endif // ROCKSDB_PLATFORM_POSIX
|
||||
};
|
||||
|
||||
@@ -783,6 +860,8 @@ TEST_F(DBWALTest, WALWithChecksumHandoff) {
|
||||
std::shared_ptr<FaultInjectionTestFS> fault_fs(
|
||||
new FaultInjectionTestFS(FileSystem::Default()));
|
||||
std::unique_ptr<Env> fault_fs_env(NewCompositeEnv(fault_fs));
|
||||
// Close the DB before the local Env is destroyed if an ASSERT exits early.
|
||||
Defer close_db_on_exit([this]() { Close(); });
|
||||
do {
|
||||
Options options = CurrentOptions();
|
||||
|
||||
@@ -2752,16 +2831,35 @@ TEST_F(DBWALTest, TruncateLastLogAfterRecoverWithoutFlush) {
|
||||
auto& file_before = log_files_before[0];
|
||||
ASSERT_LT(file_before->SizeFileBytes(), 1 * kKB);
|
||||
// The log file has preallocated space.
|
||||
ASSERT_GE(GetAllocatedFileSize(dbname_ + file_before->PathName()),
|
||||
preallocated_size);
|
||||
{
|
||||
std::string fname = dbname_ + file_before->PathName();
|
||||
uint64_t allocated = GetAllocatedFileSize(fname);
|
||||
if (ShouldSkipAllocationCheck(fname)) {
|
||||
fprintf(stderr,
|
||||
"Skipping preallocation check on this filesystem for %s\n",
|
||||
fname.c_str());
|
||||
} else if (allocated < preallocated_size) {
|
||||
fprintf(stderr,
|
||||
"Warning: allocated size (%lu) less than preallocated size (%lu) "
|
||||
"for %s, skipping check. This may indicate the filesystem does "
|
||||
"not support preallocation or does not report it.\n",
|
||||
allocated, preallocated_size, fname.c_str());
|
||||
} else {
|
||||
ASSERT_GE(allocated, preallocated_size);
|
||||
}
|
||||
}
|
||||
Reopen(options);
|
||||
VectorLogPtr log_files_after;
|
||||
ASSERT_OK(dbfull()->GetSortedWalFiles(log_files_after));
|
||||
ASSERT_EQ(1, log_files_after.size());
|
||||
ASSERT_LT(log_files_after[0]->SizeFileBytes(), 1 * kKB);
|
||||
// The preallocated space should be truncated.
|
||||
ASSERT_LT(GetAllocatedFileSize(dbname_ + file_before->PathName()),
|
||||
preallocated_size);
|
||||
{
|
||||
std::string fname = dbname_ + file_before->PathName();
|
||||
if (!ShouldSkipAllocationCheck(fname)) {
|
||||
ASSERT_LT(GetAllocatedFileSize(fname), preallocated_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tests that we will truncate the preallocated space of the last log from
|
||||
// previous.
|
||||
@@ -2788,8 +2886,23 @@ TEST_F(DBWALTest, TruncateLastLogAfterRecoverWithFlush) {
|
||||
ASSERT_EQ(1, log_files_before.size());
|
||||
auto& file_before = log_files_before[0];
|
||||
ASSERT_LT(file_before->SizeFileBytes(), 1 * kKB);
|
||||
ASSERT_GE(GetAllocatedFileSize(dbname_ + file_before->PathName()),
|
||||
preallocated_size);
|
||||
{
|
||||
std::string fname = dbname_ + file_before->PathName();
|
||||
uint64_t allocated = GetAllocatedFileSize(fname);
|
||||
if (ShouldSkipAllocationCheck(fname)) {
|
||||
fprintf(stderr,
|
||||
"Skipping preallocation check on this filesystem for %s\n",
|
||||
fname.c_str());
|
||||
} else if (allocated < preallocated_size) {
|
||||
fprintf(stderr,
|
||||
"Warning: allocated size (%lu) less than preallocated size (%lu) "
|
||||
"for %s, skipping check. This may indicate the filesystem does "
|
||||
"not support preallocation or does not report it.\n",
|
||||
allocated, preallocated_size, fname.c_str());
|
||||
} else {
|
||||
ASSERT_GE(allocated, preallocated_size);
|
||||
}
|
||||
}
|
||||
// The log file has preallocated space.
|
||||
Close();
|
||||
|
||||
@@ -2807,8 +2920,12 @@ TEST_F(DBWALTest, TruncateLastLogAfterRecoverWithFlush) {
|
||||
// if the process is in a crash loop, the log file may not get
|
||||
// deleted and thte preallocated space will keep accumulating. So we need
|
||||
// to ensure it gets trtuncated.
|
||||
EXPECT_LT(GetAllocatedFileSize(dbname_ + file_before->PathName()),
|
||||
preallocated_size);
|
||||
{
|
||||
std::string fname = dbname_ + file_before->PathName();
|
||||
if (!ShouldSkipAllocationCheck(fname)) {
|
||||
EXPECT_LT(GetAllocatedFileSize(fname), preallocated_size);
|
||||
}
|
||||
}
|
||||
TEST_SYNC_POINT(
|
||||
"DBWALTest::TruncateLastLogAfterRecoverWithFlush:AfterTruncate");
|
||||
reopen_thread.join();
|
||||
@@ -2863,14 +2980,31 @@ TEST_F(DBWALTest, TruncateLastLogAfterRecoverWALEmpty) {
|
||||
log_file->PrepareWrite(0, 4096);
|
||||
log_file.reset();
|
||||
|
||||
ASSERT_GE(GetAllocatedFileSize(last_log), preallocated_size);
|
||||
{
|
||||
uint64_t allocated = GetAllocatedFileSize(last_log);
|
||||
if (ShouldSkipAllocationCheck(last_log)) {
|
||||
fprintf(stderr,
|
||||
"Skipping preallocation check on this filesystem for %s\n",
|
||||
last_log.c_str());
|
||||
} else if (allocated < preallocated_size) {
|
||||
fprintf(stderr,
|
||||
"Warning: allocated size (%lu) less than preallocated size (%lu) "
|
||||
"for %s, skipping check. This may indicate the filesystem does "
|
||||
"not support preallocation or does not report it.\n",
|
||||
allocated, preallocated_size, last_log.c_str());
|
||||
} else {
|
||||
ASSERT_GE(allocated, preallocated_size);
|
||||
}
|
||||
}
|
||||
|
||||
port::Thread reopen_thread([&]() { Reopen(options); });
|
||||
|
||||
TEST_SYNC_POINT(
|
||||
"DBWALTest::TruncateLastLogAfterRecoverWithFlush:AfterRecover");
|
||||
// The preallocated space should be truncated.
|
||||
EXPECT_LT(GetAllocatedFileSize(last_log), preallocated_size);
|
||||
if (!ShouldSkipAllocationCheck(last_log)) {
|
||||
EXPECT_LT(GetAllocatedFileSize(last_log), preallocated_size);
|
||||
}
|
||||
TEST_SYNC_POINT(
|
||||
"DBWALTest::TruncateLastLogAfterRecoverWithFlush:AfterTruncate");
|
||||
reopen_thread.join();
|
||||
@@ -2912,8 +3046,23 @@ TEST_F(DBWALTest, ReadOnlyRecoveryNoTruncate) {
|
||||
auto& file_before = log_files_before[0];
|
||||
ASSERT_LT(file_before->SizeFileBytes(), 1 * kKB);
|
||||
// The log file has preallocated space.
|
||||
auto db_size = GetAllocatedFileSize(dbname_ + file_before->PathName());
|
||||
ASSERT_GE(db_size, preallocated_size);
|
||||
std::string fname = dbname_ + file_before->PathName();
|
||||
auto db_size = GetAllocatedFileSize(fname);
|
||||
{
|
||||
if (ShouldSkipAllocationCheck(fname)) {
|
||||
fprintf(stderr,
|
||||
"Skipping preallocation check on this filesystem for %s\n",
|
||||
fname.c_str());
|
||||
} else if (db_size < preallocated_size) {
|
||||
fprintf(stderr,
|
||||
"Warning: allocated size (%lu) less than preallocated size (%lu) "
|
||||
"for %s, skipping check. This may indicate the filesystem does "
|
||||
"not support preallocation or does not report it.\n",
|
||||
db_size, preallocated_size, fname.c_str());
|
||||
} else {
|
||||
ASSERT_GE(db_size, preallocated_size);
|
||||
}
|
||||
}
|
||||
Close();
|
||||
|
||||
// enable truncate and open DB as readonly, the file should not be truncated
|
||||
@@ -2927,8 +3076,12 @@ TEST_F(DBWALTest, ReadOnlyRecoveryNoTruncate) {
|
||||
ASSERT_EQ(log_files_after[0]->PathName(), file_before->PathName());
|
||||
// The preallocated space should NOT be truncated.
|
||||
// the DB size is almost the same.
|
||||
ASSERT_NEAR(GetAllocatedFileSize(dbname_ + file_before->PathName()), db_size,
|
||||
db_size / 100);
|
||||
{
|
||||
std::string fname2 = dbname_ + file_before->PathName();
|
||||
if (!ShouldSkipAllocationCheck(fname2)) {
|
||||
ASSERT_NEAR(GetAllocatedFileSize(fname2), db_size, db_size / 100);
|
||||
}
|
||||
}
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
@@ -3071,6 +3224,284 @@ TEST_F(DBWALTest, EmptyWalReopenTest) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBWALTest, AsyncWalPrecreateWaitsForPendingFile) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.statistics = CreateDBStatistics();
|
||||
options.async_wal_precreate = true;
|
||||
options.recycle_log_file_num = 0;
|
||||
options.write_buffer_size = 1024;
|
||||
options.max_write_buffer_number = 4;
|
||||
|
||||
// Goal: cover the race where foreground WAL rotation reaches the switch
|
||||
// before the background precreate publishes its result. The sync points hold
|
||||
// the async worker after it has created the file but before it marks it
|
||||
// ready, then verify the foreground writer waits instead of allocating a
|
||||
// higher WAL number.
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImpl::BGWorkAsyncWALPrecreate:BeforePublish",
|
||||
"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:WorkerBlocked"},
|
||||
{"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:AllowPublish",
|
||||
"DBImpl::BGWorkAsyncWALPrecreate:Publish"},
|
||||
{"DBImpl::WaitForAsyncWALPrecreate:BeforeWait",
|
||||
"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:ForegroundWaiting"},
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
Defer cleanup_sync_point([] {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
});
|
||||
|
||||
Reopen(options);
|
||||
TEST_SYNC_POINT(
|
||||
"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:WorkerBlocked");
|
||||
|
||||
ASSERT_OK(Put("fill", "value"));
|
||||
Status writer_status;
|
||||
port::Thread writer(
|
||||
[&]() { writer_status = dbfull()->TEST_SwitchMemtable(); });
|
||||
|
||||
TEST_SYNC_POINT(
|
||||
"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:ForegroundWaiting");
|
||||
TEST_SYNC_POINT(
|
||||
"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:AllowPublish");
|
||||
writer.join();
|
||||
ASSERT_OK(writer_status);
|
||||
ASSERT_OK(Put("trigger", "value"));
|
||||
ASSERT_EQ("value", Get("trigger"));
|
||||
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_HIT));
|
||||
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_WAITED));
|
||||
ASSERT_EQ(0, options.statistics->getTickerCount(WAL_PRECREATE_MISS));
|
||||
}
|
||||
|
||||
TEST_F(DBWALTest, AsyncWalPrecreateConsumesReadyFile) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.statistics = CreateDBStatistics();
|
||||
options.async_wal_precreate = true;
|
||||
options.recycle_log_file_num = 0;
|
||||
options.write_buffer_size = 1024;
|
||||
options.max_write_buffer_number = 4;
|
||||
|
||||
// Goal: exercise the no-wait consumption path. The background task publishes
|
||||
// a prepared future WAL before the foreground memtable switch, and the switch
|
||||
// must install exactly that reserved WAL number instead of allocating
|
||||
// another.
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImpl::BGWorkAsyncWALPrecreate:Done",
|
||||
"DBWALTest::AsyncWalPrecreateConsumesReadyFile:PrecreateDone"},
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
Defer cleanup_sync_point([] {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
});
|
||||
|
||||
Reopen(options);
|
||||
TEST_SYNC_POINT(
|
||||
"DBWALTest::AsyncWalPrecreateConsumesReadyFile:PrecreateDone");
|
||||
|
||||
const std::vector<uint64_t> wal_numbers = ListWalNumbers();
|
||||
ASSERT_GE(wal_numbers.size(), 2);
|
||||
const uint64_t prepared_wal_number = wal_numbers.back();
|
||||
ASSERT_NE(prepared_wal_number, dbfull()->TEST_GetCurrentLogNumber());
|
||||
|
||||
ASSERT_OK(Put("fill", "value"));
|
||||
ASSERT_OK(dbfull()->TEST_SwitchMemtable());
|
||||
ASSERT_EQ(prepared_wal_number, dbfull()->TEST_GetCurrentLogNumber());
|
||||
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_HIT));
|
||||
ASSERT_EQ(0, options.statistics->getTickerCount(WAL_PRECREATE_MISS));
|
||||
ASSERT_EQ(0, options.statistics->getTickerCount(WAL_PRECREATE_WAITED));
|
||||
}
|
||||
|
||||
TEST_F(DBWALTest, AsyncWalPrecreateRecoveryToleratesActualFutureWal) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.async_wal_precreate = true;
|
||||
options.recycle_log_file_num = 0;
|
||||
options.track_and_verify_wals = true;
|
||||
options.track_and_verify_wals_in_manifest = true;
|
||||
options.avoid_flush_during_shutdown = true;
|
||||
options.avoid_flush_during_recovery = true;
|
||||
|
||||
// Goal: cover the actual async precreation path, not a hand-forged empty WAL.
|
||||
// The background task opens the future WAL, clean close releases the
|
||||
// unpublished writer and leaves the empty future WAL behind, and recovery
|
||||
// must tolerate that file while replaying the preceding WAL with real user
|
||||
// data.
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImpl::BGWorkAsyncWALPrecreate:Done",
|
||||
"DBWALTest::AsyncWalPrecreateRecoveryToleratesActualFutureWal:"
|
||||
"PrecreateDone"},
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
Defer cleanup_sync_point([] {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
});
|
||||
|
||||
DestroyAndReopen(options);
|
||||
TEST_SYNC_POINT(
|
||||
"DBWALTest::AsyncWalPrecreateRecoveryToleratesActualFutureWal:"
|
||||
"PrecreateDone");
|
||||
|
||||
const std::vector<uint64_t> wal_numbers = ListWalNumbers();
|
||||
ASSERT_GE(wal_numbers.size(), 2);
|
||||
const uint64_t future_wal_number = wal_numbers.back();
|
||||
uint64_t future_wal_size = 0;
|
||||
ASSERT_OK(env_->GetFileSize(LogFileName(dbname_, future_wal_number),
|
||||
&future_wal_size));
|
||||
ASSERT_EQ(0, future_wal_size);
|
||||
|
||||
ASSERT_OK(Put("key", "value"));
|
||||
Close();
|
||||
|
||||
ASSERT_OK(TryReopen(options));
|
||||
ASSERT_EQ("value", Get("key"));
|
||||
ASSERT_OK(Put("after-recovery", "value2"));
|
||||
ASSERT_EQ("value2", Get("after-recovery"));
|
||||
}
|
||||
|
||||
TEST_F(DBWALTest, AsyncWalPrecreateDeletesFileOnStartFailure) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.statistics = CreateDBStatistics();
|
||||
options.async_wal_precreate = true;
|
||||
options.recycle_log_file_num = 0;
|
||||
options.write_buffer_size = 1024;
|
||||
options.max_write_buffer_number = 4;
|
||||
|
||||
// Goal: force failure after consuming a prepared WAL but before it becomes a
|
||||
// logical WAL. The failed, unpublished file must be deleted and its writer
|
||||
// destroyed outside mutex_.
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImpl::BGWorkAsyncWALPrecreate:Done",
|
||||
"DBWALTest::AsyncWalPrecreateDeletesFileOnStartFailure:PrecreateDone"},
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
Defer cleanup_sync_point([] {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
});
|
||||
|
||||
Reopen(options);
|
||||
TEST_SYNC_POINT(
|
||||
"DBWALTest::AsyncWalPrecreateDeletesFileOnStartFailure:PrecreateDone");
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::StartWALFile:AfterCompressionTypeRecord", [](void* arg) {
|
||||
*static_cast<IOStatus*>(arg) =
|
||||
IOStatus::IOError("injected async WAL start failure");
|
||||
});
|
||||
const std::vector<uint64_t> wal_numbers = ListWalNumbers();
|
||||
ASSERT_GE(wal_numbers.size(), 2);
|
||||
const uint64_t prepared_wal_number = wal_numbers.back();
|
||||
|
||||
ASSERT_OK(Put("fill", "value"));
|
||||
ASSERT_NOK(dbfull()->TEST_SwitchMemtable());
|
||||
ASSERT_TRUE(
|
||||
env_->FileExists(LogFileName(dbname_, prepared_wal_number)).IsNotFound());
|
||||
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_HIT));
|
||||
}
|
||||
|
||||
TEST_F(DBWALTest, AsyncWalPrecreateFailureFallsBackToSyncCreation) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.statistics = CreateDBStatistics();
|
||||
options.async_wal_precreate = true;
|
||||
options.recycle_log_file_num = 0;
|
||||
options.write_buffer_size = 1024;
|
||||
options.max_write_buffer_number = 4;
|
||||
|
||||
// Goal: fail the background precreate file open after normal DB open has
|
||||
// created the current WAL. The foreground switch should observe no prepared
|
||||
// WAL, create one synchronously, and expose both failure and miss counters.
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImpl::BGWorkAsyncWALPrecreate:Done",
|
||||
"DBWALTest::AsyncWalPrecreateFailureFallsBackToSyncCreation:"
|
||||
"PrecreateDone"},
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::Open:AfterDeleteFiles",
|
||||
[&](void*) { env_->non_writable_count_ = 1; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
Defer cleanup_sync_point([&] {
|
||||
env_->non_writable_count_ = 0;
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
});
|
||||
|
||||
Reopen(options);
|
||||
TEST_SYNC_POINT(
|
||||
"DBWALTest::AsyncWalPrecreateFailureFallsBackToSyncCreation:"
|
||||
"PrecreateDone");
|
||||
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_FAILED));
|
||||
|
||||
ASSERT_OK(Put("fill", "value"));
|
||||
ASSERT_OK(dbfull()->TEST_SwitchMemtable());
|
||||
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_MISS));
|
||||
}
|
||||
|
||||
TEST_F(DBWALTest, ReadOnlyErrorIfWalFileExistsIgnoresEmptyWals) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
DestroyAndReopen(options);
|
||||
Close();
|
||||
|
||||
// Goal: preserve error_if_wal_file_exists for WALs containing data while
|
||||
// allowing empty future WALs left by async precreation. This creates empty
|
||||
// WAL files directly so the test isolates the read-only open contract.
|
||||
const std::vector<uint64_t> existing_wal_numbers = ListWalNumbers();
|
||||
const uint64_t empty_wal_number =
|
||||
existing_wal_numbers.empty() ? 1 : existing_wal_numbers.back() + 1;
|
||||
CreateEmptyWal(empty_wal_number);
|
||||
|
||||
DBOptions db_options(options);
|
||||
std::vector<ColumnFamilyDescriptor> column_families;
|
||||
column_families.emplace_back(kDefaultColumnFamilyName,
|
||||
ColumnFamilyOptions(options));
|
||||
std::vector<ColumnFamilyHandle*> handles;
|
||||
std::unique_ptr<DB> read_only_db;
|
||||
ASSERT_OK(DB::OpenForReadOnly(db_options, dbname_, column_families, &handles,
|
||||
&read_only_db,
|
||||
true /* error_if_wal_file_exists */));
|
||||
for (ColumnFamilyHandle* handle : handles) {
|
||||
ASSERT_OK(read_only_db->DestroyColumnFamilyHandle(handle));
|
||||
}
|
||||
read_only_db.reset();
|
||||
handles.clear();
|
||||
|
||||
CreateWalWithContents(empty_wal_number + 1, Slice("not empty"));
|
||||
ASSERT_NOK(DB::OpenForReadOnly(db_options, dbname_, column_families, &handles,
|
||||
&read_only_db,
|
||||
true /* error_if_wal_file_exists */));
|
||||
}
|
||||
|
||||
TEST_F(DBWALTest, RecoveryToleratesEmptyFutureWal) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.track_and_verify_wals = true;
|
||||
options.track_and_verify_wals_in_manifest = true;
|
||||
options.avoid_flush_during_recovery = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("key", "value"));
|
||||
Close();
|
||||
|
||||
const std::vector<uint64_t> wal_numbers = ListWalNumbers();
|
||||
ASSERT_FALSE(wal_numbers.empty());
|
||||
const uint64_t future_wal_number = wal_numbers.back() + 1;
|
||||
CreateEmptyWal(future_wal_number);
|
||||
|
||||
// Goal: model a crash after async WAL precreation but before consumption.
|
||||
// Recovery should process the zero-byte future WAL, mark its file number
|
||||
// used, and keep replaying the real preceding WAL without reporting a WAL
|
||||
// chain or MANIFEST-tracking error.
|
||||
ASSERT_OK(TryReopen(options));
|
||||
ASSERT_EQ("value", Get("key"));
|
||||
ASSERT_OK(Put("after-recovery", "value2"));
|
||||
ASSERT_EQ("value2", Get("after-recovery"));
|
||||
}
|
||||
|
||||
TEST_F(DBWALTest, RecoveryFlushSwitchWALOnEmptyMemtable) {
|
||||
Options options = CurrentOptions();
|
||||
auto fault_fs = std::make_shared<FaultInjectionTestFS>(FileSystem::Default());
|
||||
|
||||
+2
-2
@@ -218,7 +218,7 @@ TEST_P(IterKeySwapTest, SwapAndDestroy) {
|
||||
b_use_secondary] = GetParam();
|
||||
|
||||
std::string expected_a, expected_b;
|
||||
// Storage for pinned keys — must outlive the IterKeys.
|
||||
// Storage for pinned keys -- must outlive the IterKeys.
|
||||
std::string a_storage, b_storage;
|
||||
IterKey a;
|
||||
expected_a =
|
||||
@@ -236,7 +236,7 @@ TEST_P(IterKeySwapTest, SwapAndDestroy) {
|
||||
// After swap: a has b's old data, b has a's old data.
|
||||
ASSERT_EQ(a.GetUserKey().ToString(), expected_b);
|
||||
ASSERT_EQ(b.GetUserKey().ToString(), expected_a);
|
||||
} // b destroyed here — must not corrupt a's data
|
||||
} // b destroyed here -- must not corrupt a's data
|
||||
|
||||
// a must still hold valid data after b's destruction.
|
||||
ASSERT_EQ(a.GetUserKey().ToString(), expected_b);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// 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 <atomic>
|
||||
#include <cstdlib>
|
||||
#include <map>
|
||||
#include <string>
|
||||
@@ -258,6 +259,108 @@ TEST_F(DeleteFileTest, BackgroundPurgeIteratorTest) {
|
||||
CheckFileTypeCounts(dbname_, 0, 1, 1);
|
||||
}
|
||||
|
||||
// Regression test for a use-after-free where an obsolete-file purge started
|
||||
// DURING DB close -- after CloseHelper's early purge drain -- continued using
|
||||
// DBImpl after ~DBImpl destroyed mutex_. Seen as a vhost-user-blk SIGABRT when
|
||||
// many RocksDB instances closed concurrently under a Warm Storage fault, with
|
||||
// avoid_unnecessary_blocking_io=true on a shared, process-wide Env.
|
||||
//
|
||||
// Here we make the straggler deterministic: keep a dropped column family handle
|
||||
// alive, delete it only after CloseHelper has passed its early purge drain,
|
||||
// then park that cleanup after FindObsoleteFiles() has incremented
|
||||
// pending_purge_obsolete_files_ but before PurgeObsoleteFiles(..., true)
|
||||
// schedules BGWorkPurge. Without the fix, the final drain sees
|
||||
// bg_purge_scheduled_ == 0 and can destroy DBImpl while the pending cleanup is
|
||||
// still about to lock/use it. With the fix, CloseHelper waits through the
|
||||
// pending handoff and then through the scheduled purge.
|
||||
TEST_F(DeleteFileTest, CloseWaitsForPurgeScheduledDuringClose) {
|
||||
Options options = CurrentOptions();
|
||||
SetOptions(&options);
|
||||
Destroy(options);
|
||||
options.create_if_missing = true;
|
||||
// Route obsolete-file deletion through the background purge queue, matching
|
||||
// the production config that hit the bug.
|
||||
options.avoid_unnecessary_blocking_io = true;
|
||||
Reopen(options);
|
||||
|
||||
// A dropped CF whose handle we keep alive: deleting the handle schedules a
|
||||
// *background* purge (the dropped-CF teardown path).
|
||||
ColumnFamilyHandle* cfh = nullptr;
|
||||
ASSERT_OK(db_->CreateColumnFamily(ColumnFamilyOptions(), "dropme", &cfh));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), cfh, "key", "value"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions(), cfh));
|
||||
ASSERT_OK(db_->DropColumnFamily(cfh));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForPurge()); // settle setup purges
|
||||
|
||||
std::atomic<bool> block_pending_handoff{false};
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::PurgeObsoleteFiles:BeforePendingPurgeFinished", [&](void*) {
|
||||
if (!block_pending_handoff.exchange(false)) {
|
||||
return;
|
||||
}
|
||||
TEST_SYNC_POINT(
|
||||
"DeleteFileTest::CloseWaitsForPurge:PendingHandoffReady");
|
||||
TEST_SYNC_POINT(
|
||||
"DeleteFileTest::CloseWaitsForPurge:PendingHandoffBlocked");
|
||||
});
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
// Delete the dropped CF handle only after CloseHelper has passed its
|
||||
// early purge drain and entered a mutex-unlocked close window.
|
||||
{"DBImpl::CloseHelper:CFHandleCleanupUnlocked",
|
||||
"DeleteFileTest::CloseWaitsForPurge:DropDuringCloseUnlock"},
|
||||
// Let the test thread observe that the deleter is parked in the pending
|
||||
// handoff window.
|
||||
{"DeleteFileTest::CloseWaitsForPurge:PendingHandoffReady",
|
||||
"DeleteFileTest::CloseWaitsForPurge:WaitForPendingHandoff"},
|
||||
// Keep CloseHelper in the mutex-unlocked close window until the
|
||||
// dropped-CF cleanup reaches the pending-to-scheduled handoff.
|
||||
{"DeleteFileTest::CloseWaitsForPurge:PendingHandoffReady",
|
||||
"DBImpl::CloseHelper:CFHandleCleanupAllowed"},
|
||||
// Observe that CloseHelper is actually waiting in the final purge drain
|
||||
// while the dropped-CF cleanup is still parked in the pending handoff.
|
||||
{"DBImpl::CloseHelper:FinalPurgeDrainWait",
|
||||
"DeleteFileTest::CloseWaitsForPurge:WaitForFinalPurgeDrain"},
|
||||
// Keep the dropped-CF cleanup parked in the pending handoff window until
|
||||
// the test releases it.
|
||||
{"DeleteFileTest::CloseWaitsForPurge:ReleasePendingHandoff",
|
||||
"DeleteFileTest::CloseWaitsForPurge:PendingHandoffBlocked"},
|
||||
// Park the scheduled BGWorkPurge before it locks mutex_ until we release
|
||||
// it, so CloseHelper also has to wait for bg_purge_scheduled_.
|
||||
{"DeleteFileTest::CloseWaitsForPurge:ReleaseBackgroundPurge",
|
||||
"DBImpl::BackgroundCallPurge:beforeMutexLock"},
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
port::Thread closer([&]() { db_.reset(); });
|
||||
|
||||
// Unblocks once CloseHelper is in a mutex-unlocked close window; then
|
||||
// deleting the dropped CF handle starts purge work the early drain missed.
|
||||
TEST_SYNC_POINT("DeleteFileTest::CloseWaitsForPurge:DropDuringCloseUnlock");
|
||||
block_pending_handoff.store(true);
|
||||
ColumnFamilyHandle* dropped_cfh = cfh;
|
||||
cfh = nullptr;
|
||||
port::Thread dropped_cf_deleter([dropped_cfh]() { delete dropped_cfh; });
|
||||
|
||||
TEST_SYNC_POINT("DeleteFileTest::CloseWaitsForPurge:WaitForPendingHandoff");
|
||||
|
||||
// Wait until CloseHelper is blocked in the final drain with no scheduled
|
||||
// purge yet. Without the fix, it would proceed while the dropped-CF cleanup
|
||||
// is still pending. With the fix, it waits for pending_purge_obsolete_files_.
|
||||
TEST_SYNC_POINT("DeleteFileTest::CloseWaitsForPurge:WaitForFinalPurgeDrain");
|
||||
|
||||
// Release the pending cleanup, then the BGWorkPurge it schedules. With the
|
||||
// fix, the closer waits for both transitions and completes cleanly.
|
||||
TEST_SYNC_POINT("DeleteFileTest::CloseWaitsForPurge:ReleasePendingHandoff");
|
||||
dropped_cf_deleter.join();
|
||||
TEST_SYNC_POINT("DeleteFileTest::CloseWaitsForPurge:ReleaseBackgroundPurge");
|
||||
closer.join();
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(DeleteFileTest, PurgeDuringOpen) {
|
||||
Options options = CurrentOptions();
|
||||
CheckFileTypeCounts(dbname_, -1, 0, -1);
|
||||
|
||||
@@ -1311,7 +1311,7 @@ TEST_F(ExternalSSTFileBasicTest, SyncFailure) {
|
||||
});
|
||||
if (i == 0) {
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"ExternalSstFileIngestionJob::Prepare:Reopen", [&](void* s) {
|
||||
"ExternalSstFileIngestionJob::CheckSyncReturnCode", [&](void* s) {
|
||||
Status* status = static_cast<Status*>(s);
|
||||
if (status->IsNotSupported()) {
|
||||
no_sync = true;
|
||||
@@ -1367,13 +1367,13 @@ TEST_F(ExternalSSTFileBasicTest, SyncFailure) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileBasicTest, ReopenNotSupported) {
|
||||
TEST_F(ExternalSSTFileBasicTest, SyncFileNotSupported) {
|
||||
Options options;
|
||||
options.create_if_missing = true;
|
||||
options.env = env_;
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"ExternalSstFileIngestionJob::Prepare:Reopen", [&](void* arg) {
|
||||
"ExternalSstFileIngestionJob::CheckSyncReturnCode", [&](void* arg) {
|
||||
Status* s = static_cast<Status*>(arg);
|
||||
*s = Status::NotSupported();
|
||||
});
|
||||
@@ -1386,7 +1386,7 @@ TEST_F(ExternalSSTFileBasicTest, ReopenNotSupported) {
|
||||
std::unique_ptr<SstFileWriter> sst_file_writer(
|
||||
new SstFileWriter(EnvOptions(), sst_file_writer_options));
|
||||
std::string file_name =
|
||||
sst_files_dir_ + "reopen_not_supported_test_" + ".sst";
|
||||
sst_files_dir_ + "sync_file_not_supported_test_" + ".sst";
|
||||
ASSERT_OK(sst_file_writer->Open(file_name));
|
||||
ASSERT_OK(sst_file_writer->Put("bar", "v2"));
|
||||
ASSERT_OK(sst_file_writer->Finish());
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "logging/logging.h"
|
||||
#include "monitoring/statistics_impl.h"
|
||||
#include "table/merging_iterator.h"
|
||||
#include "table/prepared_file_info.h"
|
||||
#include "table/sst_file_writer_collectors.h"
|
||||
#include "table/table_builder.h"
|
||||
#include "table/unique_id_impl.h"
|
||||
@@ -31,18 +32,24 @@ Status ExternalSstFileIngestionJob::Prepare(
|
||||
const std::vector<std::string>& external_files_paths,
|
||||
const std::vector<std::string>& files_checksums,
|
||||
const std::vector<std::string>& files_checksum_func_names,
|
||||
const std::vector<const PreparedFileInfo*>& file_infos,
|
||||
const std::optional<RangeOpt>& atomic_replace_range,
|
||||
const Temperature& file_temperature, uint64_t next_file_number,
|
||||
SuperVersion* sv) {
|
||||
Status status;
|
||||
|
||||
// Read the information of files we are ingesting
|
||||
for (const std::string& file_path : external_files_paths) {
|
||||
// Read the information of files we are ingesting.
|
||||
for (size_t i = 0; i < external_files_paths.size(); i++) {
|
||||
const std::string& file_path = external_files_paths[i];
|
||||
IngestedFileInfo file_to_ingest;
|
||||
// For temperature, first assume it matches provided hint
|
||||
file_to_ingest.file_temperature = file_temperature;
|
||||
status =
|
||||
GetIngestedFileInfo(file_path, next_file_number++, &file_to_ingest, sv);
|
||||
file_to_ingest.prefetch_lmax_index_and_filter_blocks =
|
||||
ingestion_options_.prefetch_lmax_index_and_filter_blocks;
|
||||
const PreparedFileInfo* prepared_file_info =
|
||||
file_infos.empty() ? nullptr : file_infos[i];
|
||||
status = GetIngestedFileInfo(file_path, next_file_number++,
|
||||
prepared_file_info, &file_to_ingest, sv);
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"Failed to get ingested file info: %s: %s",
|
||||
@@ -76,23 +83,10 @@ Status ExternalSstFileIngestionJob::Prepare(
|
||||
auto num_files = files_to_ingest_.size();
|
||||
if (num_files == 0) {
|
||||
return Status::InvalidArgument("The list of files is empty");
|
||||
} else if (num_files > 1) {
|
||||
// Verify that passed files don't have overlapping ranges
|
||||
autovector<const IngestedFileInfo*> sorted_files;
|
||||
for (size_t i = 0; i < num_files; i++) {
|
||||
sorted_files.push_back(&files_to_ingest_[i]);
|
||||
}
|
||||
|
||||
std::sort(sorted_files.begin(), sorted_files.end(), file_range_checker_);
|
||||
|
||||
for (size_t i = 0; i + 1 < num_files; i++) {
|
||||
if (file_range_checker_.Overlaps(*sorted_files[i], *sorted_files[i + 1],
|
||||
/* known_sorted= */ true)) {
|
||||
files_overlap_ = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Detect whether the input files overlap one another; this drives how they
|
||||
// are divided into batches below.
|
||||
files_overlap_ = ComputeFilesOverlap(files_to_ingest_);
|
||||
|
||||
if (atomic_replace_range.has_value()) {
|
||||
atomic_replace_range_.emplace();
|
||||
@@ -165,35 +159,21 @@ Status ExternalSstFileIngestionJob::Prepare(
|
||||
// It is unsafe to assume application had sync the file and file
|
||||
// directory before ingest the file. For integrity of RocksDB we need
|
||||
// to sync the file.
|
||||
|
||||
// TODO(xingbo), We should in general be moving away from production
|
||||
// uses of ReuseWritableFile (except explicitly for WAL recycling),
|
||||
// ReopenWritableFile, and NewRandomRWFile. We should create a
|
||||
// FileSystem::SyncFile/FsyncFile API that by default does the
|
||||
// re-open+sync+close combo but can (a) be reused easily, and (b) be
|
||||
// overridden to do that more cleanly, e.g. in EncryptedEnv.
|
||||
// https://github.com/facebook/rocksdb/issues/13741
|
||||
std::unique_ptr<FSWritableFile> file_to_sync;
|
||||
Status s = fs_->ReopenWritableFile(path_inside_db, env_options_,
|
||||
&file_to_sync, nullptr);
|
||||
TEST_SYNC_POINT_CALLBACK("ExternalSstFileIngestionJob::Prepare:Reopen",
|
||||
&s);
|
||||
TEST_SYNC_POINT("ExternalSstFileIngestionJob::BeforeSyncIngestedFile");
|
||||
Status s = fs_->SyncFile(path_inside_db, env_options_, IOOptions(),
|
||||
db_options_.use_fsync, nullptr);
|
||||
TEST_SYNC_POINT("ExternalSstFileIngestionJob::AfterSyncIngestedFile");
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"ExternalSstFileIngestionJob::CheckSyncReturnCode", &s);
|
||||
// Some file systems (especially remote/distributed) don't support
|
||||
// reopening a file for writing and don't require reopening and
|
||||
// syncing the file. Ignore the NotSupported error in that case.
|
||||
// explicitly syncing the file and don't require it. Ignore the
|
||||
// NotSupported error in that case.
|
||||
if (!s.IsNotSupported()) {
|
||||
status = s;
|
||||
if (status.ok()) {
|
||||
TEST_SYNC_POINT(
|
||||
"ExternalSstFileIngestionJob::BeforeSyncIngestedFile");
|
||||
status = SyncIngestedFile(file_to_sync.get());
|
||||
TEST_SYNC_POINT(
|
||||
"ExternalSstFileIngestionJob::AfterSyncIngestedFile");
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"Failed to sync ingested file %s: %s",
|
||||
path_inside_db.c_str(), status.ToString().c_str());
|
||||
}
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"Failed to sync ingested file %s: %s",
|
||||
path_inside_db.c_str(), status.ToString().c_str());
|
||||
}
|
||||
}
|
||||
} else if (status.IsNotSupported() &&
|
||||
@@ -447,6 +427,60 @@ void ExternalSstFileIngestionJob::DivideInputFilesIntoBatches() {
|
||||
}
|
||||
}
|
||||
|
||||
bool ExternalSstFileIngestionJob::ComputeFilesOverlap(
|
||||
const autovector<IngestedFileInfo>& files) const {
|
||||
const size_t num_files = files.size();
|
||||
if (num_files <= 1) {
|
||||
return false;
|
||||
}
|
||||
// Verify whether the files have overlapping ranges by sorting copies of the
|
||||
// file ranges and checking adjacent pairs.
|
||||
autovector<const IngestedFileInfo*> sorted_files;
|
||||
for (size_t i = 0; i < num_files; i++) {
|
||||
sorted_files.push_back(&files[i]);
|
||||
}
|
||||
std::sort(sorted_files.begin(), sorted_files.end(), file_range_checker_);
|
||||
for (size_t i = 0; i + 1 < num_files; i++) {
|
||||
if (file_range_checker_.Overlaps(*sorted_files[i], *sorted_files[i + 1],
|
||||
/* known_sorted= */ true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Status ExternalSstFileIngestionJob::MergeForSameColumnFamily(
|
||||
ExternalSstFileIngestionJob* other) {
|
||||
assert(other != nullptr);
|
||||
assert(other != this);
|
||||
assert(cfd_ == other->cfd_);
|
||||
if (atomic_replace_range_.has_value() ||
|
||||
other->atomic_replace_range_.has_value()) {
|
||||
return Status::NotSupported(
|
||||
"cannot merge file ingestion handles for the same column family when "
|
||||
"atomic_replace_range is used");
|
||||
}
|
||||
if (!(ingestion_options_ == other->ingestion_options_)) {
|
||||
return Status::InvalidArgument(
|
||||
"file ingestion handles for the same column family must be prepared "
|
||||
"with the same IngestExternalFileOptions");
|
||||
}
|
||||
// Append the other job's prepared files after this job's so that, for any
|
||||
// overlapping keys, the other job's data wins via a higher assigned sequence
|
||||
// number -- the same semantics as passing all the files to a single ingestion
|
||||
// call in this order. Recompute overlap and rebuild the batches over the
|
||||
// union.
|
||||
for (IngestedFileInfo& file : other->files_to_ingest_) {
|
||||
files_to_ingest_.push_back(std::move(file));
|
||||
}
|
||||
other->files_to_ingest_.clear();
|
||||
other->file_batches_to_ingest_.clear();
|
||||
files_overlap_ = ComputeFilesOverlap(files_to_ingest_);
|
||||
file_batches_to_ingest_.clear();
|
||||
DivideInputFilesIntoBatches();
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status ExternalSstFileIngestionJob::NeedsFlush(bool* flush_needed,
|
||||
SuperVersion* super_version) {
|
||||
Status status;
|
||||
@@ -716,6 +750,9 @@ Status ExternalSstFileIngestionJob::AssignLevelsForOneBatch(
|
||||
tail_size, file->user_defined_timestamps_persisted, "", "");
|
||||
f_metadata.temperature = file->file_temperature;
|
||||
f_metadata.marked_for_compaction = marked_for_compaction;
|
||||
f_metadata.skip_index_and_filter_blocks_prefetch =
|
||||
!file->prefetch_lmax_index_and_filter_blocks &&
|
||||
file->picked_level == cfd_->NumberLevels() - 1;
|
||||
// Extract min/max timestamps from table properties for UDT support.
|
||||
// This ensures ingested files have proper timestamp ranges in FileMetaData,
|
||||
// similar to files created by flush and compaction.
|
||||
@@ -970,18 +1007,14 @@ Status ExternalSstFileIngestionJob::ResetTableReader(
|
||||
}
|
||||
|
||||
Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
|
||||
const std::string& external_file, uint64_t new_file_number,
|
||||
SuperVersion* sv, IngestedFileInfo* file_to_ingest,
|
||||
std::unique_ptr<TableReader>* table_reader) {
|
||||
// Get the external file properties
|
||||
auto props = table_reader->get()->GetTableProperties();
|
||||
assert(props.get());
|
||||
const auto& uprops = props->user_collected_properties;
|
||||
const std::string& external_file, const TableProperties& props,
|
||||
IngestedFileInfo* file_to_ingest) {
|
||||
const auto& uprops = props.user_collected_properties;
|
||||
|
||||
// Get table version
|
||||
auto version_iter = uprops.find(ExternalSstFilePropertyNames::kVersion);
|
||||
if (version_iter == uprops.end()) {
|
||||
assert(!SstFileWriter::CreatedBySstFileWriter(*props));
|
||||
assert(!SstFileWriter::CreatedBySstFileWriter(props));
|
||||
if (!ingestion_options_.allow_db_generated_files) {
|
||||
return Status::Corruption("External file version not found");
|
||||
} else {
|
||||
@@ -990,7 +1023,7 @@ Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
|
||||
file_to_ingest->version = 0;
|
||||
}
|
||||
} else {
|
||||
assert(SstFileWriter::CreatedBySstFileWriter(*props));
|
||||
assert(SstFileWriter::CreatedBySstFileWriter(props));
|
||||
file_to_ingest->version = DecodeFixed32(version_iter->second.c_str());
|
||||
}
|
||||
|
||||
@@ -1004,12 +1037,17 @@ Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
|
||||
|
||||
// Set the global sequence number
|
||||
file_to_ingest->original_seqno = DecodeFixed64(seqno_iter->second.c_str());
|
||||
if (props->external_sst_file_global_seqno_offset == 0) {
|
||||
file_to_ingest->global_seqno_offset = 0;
|
||||
file_to_ingest->global_seqno_offset =
|
||||
static_cast<size_t>(props.external_sst_file_global_seqno_offset);
|
||||
// The on-disk offset is only needed if we will write the global seqno back
|
||||
// into the file (write_global_seqno). The metadata fast-path does not open
|
||||
// the file, and its in-memory table properties do not carry the offset
|
||||
// (it is only computed while reading the file back); that is fine as long
|
||||
// as write_global_seqno is not requested.
|
||||
if (ingestion_options_.write_global_seqno &&
|
||||
file_to_ingest->global_seqno_offset == 0) {
|
||||
return Status::Corruption("Was not able to find file global seqno field");
|
||||
}
|
||||
file_to_ingest->global_seqno_offset =
|
||||
static_cast<size_t>(props->external_sst_file_global_seqno_offset);
|
||||
} else if (file_to_ingest->version == 1) {
|
||||
// SST file V1 should not have global seqno field
|
||||
assert(seqno_iter == uprops.end());
|
||||
@@ -1030,23 +1068,20 @@ Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
|
||||
" is not supported");
|
||||
}
|
||||
|
||||
file_to_ingest->cf_id = static_cast<uint32_t>(props->column_family_id);
|
||||
// This assignment works fine even though `table_reader` may later be reset,
|
||||
// since that will not affect how table properties are parsed, and this
|
||||
// assignment is making a copy.
|
||||
file_to_ingest->table_properties = *props;
|
||||
file_to_ingest->cf_id = static_cast<uint32_t>(props.column_family_id);
|
||||
file_to_ingest->table_properties = props;
|
||||
|
||||
// Get number of entries in table
|
||||
file_to_ingest->num_entries = props->num_entries;
|
||||
file_to_ingest->num_range_deletions = props->num_range_deletions;
|
||||
file_to_ingest->num_entries = props.num_entries;
|
||||
file_to_ingest->num_range_deletions = props.num_range_deletions;
|
||||
|
||||
// Validate table properties related to comparator name and user defined
|
||||
// timestamps persisted flag.
|
||||
file_to_ingest->user_defined_timestamps_persisted =
|
||||
static_cast<bool>(props->user_defined_timestamps_persisted);
|
||||
static_cast<bool>(props.user_defined_timestamps_persisted);
|
||||
bool mark_sst_file_has_no_udt = false;
|
||||
Status s = ValidateUserDefinedTimestampsOptions(
|
||||
cfd_->user_comparator(), props->comparator_name,
|
||||
cfd_->user_comparator(), props.comparator_name,
|
||||
cfd_->ioptions().persist_user_defined_timestamps,
|
||||
file_to_ingest->user_defined_timestamps_persisted,
|
||||
&mark_sst_file_has_no_udt);
|
||||
@@ -1054,7 +1089,9 @@ Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
|
||||
// A column family that enables user-defined timestamps in Memtable only
|
||||
// feature can also ingest external files created by a setting that disables
|
||||
// user-defined timestamps. In that case, we need to re-mark the
|
||||
// user_defined_timestamps_persisted flag for the file.
|
||||
// user_defined_timestamps_persisted flag for the file. The open-and-scan
|
||||
// caller is then responsible for reopening its `TableReader` with the
|
||||
// updated flag.
|
||||
file_to_ingest->user_defined_timestamps_persisted = false;
|
||||
} else if (!s.ok()) {
|
||||
ROCKS_LOG_WARN(
|
||||
@@ -1064,22 +1101,84 @@ Status ExternalSstFileIngestionJob::SanityCheckTableProperties(
|
||||
return s;
|
||||
}
|
||||
|
||||
// `TableReader` is initialized with `user_defined_timestamps_persisted` flag
|
||||
// to be true. If its value changed to false after this sanity check, we
|
||||
// need to reset the `TableReader`.
|
||||
if (ucmp_->timestamp_size() > 0 &&
|
||||
!file_to_ingest->user_defined_timestamps_persisted) {
|
||||
s = ResetTableReader(external_file, new_file_number,
|
||||
file_to_ingest->user_defined_timestamps_persisted, sv,
|
||||
file_to_ingest, table_reader);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
Status ExternalSstFileIngestionJob::GetIngestedFileInfoFromFileInfo(
|
||||
const std::string& external_file,
|
||||
const PreparedFileInfo& prepared_file_info,
|
||||
IngestedFileInfo* file_to_ingest) {
|
||||
// Boundaries, size, and table properties were obtained without opening the
|
||||
// file (e.g. produced by SstFileWriter::Finish). We reuse them directly.
|
||||
file_to_ingest->file_size = prepared_file_info.file_size;
|
||||
Status status = SanityCheckTableProperties(
|
||||
external_file, prepared_file_info.table_properties, file_to_ingest);
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(
|
||||
db_options_.info_log,
|
||||
"Failed to sanity check table properties for external file %s: %s",
|
||||
external_file.c_str(), status.ToString().c_str());
|
||||
return status;
|
||||
}
|
||||
|
||||
const size_t ts_sz = ucmp_->timestamp_size();
|
||||
if (ts_sz > 0 && !file_to_ingest->user_defined_timestamps_persisted) {
|
||||
auto pad_timestamp = [ts_sz](std::string* result, const Slice& key) {
|
||||
assert(result->empty());
|
||||
if (ExtractValueType(key) == kTypeRangeDeletion) {
|
||||
PadInternalKeyWithMaxTimestamp(result, key, ts_sz);
|
||||
} else {
|
||||
PadInternalKeyWithMinTimestamp(result, key, ts_sz);
|
||||
}
|
||||
};
|
||||
pad_timestamp(file_to_ingest->smallest_internal_key.rep(),
|
||||
prepared_file_info.smallest.Encode());
|
||||
pad_timestamp(file_to_ingest->largest_internal_key.rep(),
|
||||
prepared_file_info.largest.Encode());
|
||||
} else {
|
||||
file_to_ingest->smallest_internal_key = prepared_file_info.smallest;
|
||||
file_to_ingest->largest_internal_key = prepared_file_info.largest;
|
||||
}
|
||||
|
||||
if (ingestion_options_.allow_db_generated_files) {
|
||||
// Sequence numbers are preserved (not reassigned), so the bounds must be
|
||||
// known before we skip the GetSeqnoBoundaryForFile scan.
|
||||
if (!file_to_ingest->table_properties.HasKeyLargestSeqno()) {
|
||||
return Status::Corruption("Unknown largest seqno for db generated file.");
|
||||
}
|
||||
file_to_ingest->largest_seqno =
|
||||
file_to_ingest->table_properties.key_largest_seqno;
|
||||
if (file_to_ingest->largest_seqno == 0) {
|
||||
file_to_ingest->smallest_seqno = 0;
|
||||
} else {
|
||||
if (!file_to_ingest->table_properties.HasKeySmallestSeqno()) {
|
||||
return Status::Corruption(
|
||||
"Unknown smallest seqno for db generated file.");
|
||||
}
|
||||
file_to_ingest->smallest_seqno =
|
||||
file_to_ingest->table_properties.key_smallest_seqno;
|
||||
}
|
||||
} else {
|
||||
// Normal ingestion reassigns a global sequence number later, so the file's
|
||||
// keys must currently be at seqno 0 (mirror of the open-and-scan check).
|
||||
SequenceNumber largest_seqno =
|
||||
file_to_ingest->table_properties.key_largest_seqno;
|
||||
// UINT64_MAX means unknown and the file is generated before table property
|
||||
// `key_largest_seqno` is introduced.
|
||||
if (largest_seqno != UINT64_MAX && largest_seqno > 0) {
|
||||
return Status::Corruption(
|
||||
"External file has non zero largest sequence number " +
|
||||
std::to_string(largest_seqno));
|
||||
}
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status ExternalSstFileIngestionJob::GetIngestedFileInfoFromFile(
|
||||
const std::string& external_file, uint64_t new_file_number,
|
||||
IngestedFileInfo* file_to_ingest, SuperVersion* sv) {
|
||||
file_to_ingest->external_file_path = external_file;
|
||||
IngestedFileInfo* file_to_ingest, SuperVersion* sv,
|
||||
std::unique_ptr<TableReader>* out_table_reader) {
|
||||
TEST_SYNC_POINT("ExternalSstFileIngestionJob::GetIngestedFileInfo:ReadPath");
|
||||
|
||||
// Get external file size
|
||||
Status status = fs_->GetFileSize(external_file, IOOptions(),
|
||||
@@ -1091,15 +1190,11 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
return status;
|
||||
}
|
||||
|
||||
// Assign FD with number
|
||||
file_to_ingest->fd =
|
||||
FileDescriptor(new_file_number, 0, file_to_ingest->file_size);
|
||||
|
||||
// Create TableReader for external file
|
||||
// Create TableReader for external file.
|
||||
std::unique_ptr<TableReader> table_reader;
|
||||
// Initially create the `TableReader` with flag
|
||||
// `user_defined_timestamps_persisted` to be true since that's the most common
|
||||
// case
|
||||
// `user_defined_timestamps_persisted` to be true since that's the most
|
||||
// common case
|
||||
status = ResetTableReader(external_file, new_file_number,
|
||||
/*user_defined_timestamps_persisted=*/true, sv,
|
||||
file_to_ingest, &table_reader);
|
||||
@@ -1110,8 +1205,8 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
return status;
|
||||
}
|
||||
|
||||
status = SanityCheckTableProperties(external_file, new_file_number, sv,
|
||||
file_to_ingest, &table_reader);
|
||||
status = SanityCheckTableProperties(
|
||||
external_file, *table_reader->GetTableProperties(), file_to_ingest);
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(
|
||||
db_options_.info_log,
|
||||
@@ -1120,6 +1215,23 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
return status;
|
||||
}
|
||||
|
||||
// The `TableReader` above was opened with `user_defined_timestamps_persisted`
|
||||
// assumed true. If the sanity check determined the file has no persisted
|
||||
// timestamps (UDT-in-Memtable-only feature), reopen it with the corrected
|
||||
// flag so keys are parsed properly by the scan below.
|
||||
if (ucmp_->timestamp_size() > 0 &&
|
||||
!file_to_ingest->user_defined_timestamps_persisted) {
|
||||
status = ResetTableReader(external_file, new_file_number,
|
||||
file_to_ingest->user_defined_timestamps_persisted,
|
||||
sv, file_to_ingest, &table_reader);
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"Failed to reset table reader for external file %s: %s",
|
||||
external_file.c_str(), status.ToString().c_str());
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
const bool allow_data_in_errors = db_options_.allow_data_in_errors;
|
||||
ParsedInternalKey key;
|
||||
if (ingestion_options_.allow_db_generated_files) {
|
||||
@@ -1141,8 +1253,8 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
} else {
|
||||
SequenceNumber largest_seqno =
|
||||
table_reader.get()->GetTableProperties()->key_largest_seqno;
|
||||
// UINT64_MAX means unknown and the file is generated before table property
|
||||
// `key_largest_seqno` is introduced.
|
||||
// UINT64_MAX means unknown and the file is generated before table
|
||||
// property `key_largest_seqno` is introduced.
|
||||
if (largest_seqno != UINT64_MAX && largest_seqno > 0) {
|
||||
return Status::Corruption(
|
||||
"External file has non zero largest sequence number " +
|
||||
@@ -1150,24 +1262,6 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
}
|
||||
}
|
||||
|
||||
if (ingestion_options_.verify_checksums_before_ingest) {
|
||||
// If customized readahead size is needed, we can pass a user option
|
||||
// all the way to here. Right now we just rely on the default readahead
|
||||
// to keep things simple.
|
||||
// TODO: plumb Env::IOActivity, Env::IOPriority
|
||||
ReadOptions ro;
|
||||
ro.readahead_size = ingestion_options_.verify_checksums_readahead_size;
|
||||
ro.fill_cache = ingestion_options_.fill_cache;
|
||||
status = table_reader->VerifyChecksum(
|
||||
ro, TableReaderCaller::kExternalSSTIngestion);
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"Failed to verify checksum for table reader: %s",
|
||||
status.ToString().c_str());
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: plumb Env::IOActivity, Env::IOPriority
|
||||
ReadOptions ro;
|
||||
ro.fill_cache = ingestion_options_.fill_cache;
|
||||
@@ -1247,6 +1341,22 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
"External file has a range deletion with non zero sequence "
|
||||
"number.");
|
||||
}
|
||||
#ifndef NDEBUG
|
||||
// To keep aligned with the fast path that expects range deletion keys to
|
||||
// always use max ts.
|
||||
const size_t ts_sz = ucmp_->timestamp_size();
|
||||
if (ts_sz > 0) {
|
||||
const std::string max_ts(ts_sz, '\xff');
|
||||
assert(key.user_key.size() >= ts_sz);
|
||||
assert(ucmp_->CompareTimestamp(
|
||||
ExtractTimestampFromUserKey(key.user_key, ts_sz), max_ts) ==
|
||||
0);
|
||||
assert(range_del_iter->value().size() >= ts_sz);
|
||||
assert(ucmp_->CompareTimestamp(
|
||||
ExtractTimestampFromUserKey(range_del_iter->value(), ts_sz),
|
||||
max_ts) == 0);
|
||||
}
|
||||
#endif
|
||||
RangeTombstone tombstone(key, range_del_iter->value());
|
||||
file_range_checker_.MaybeUpdateRange(tombstone.SerializeKey(),
|
||||
tombstone.SerializeEndKey(),
|
||||
@@ -1254,6 +1364,75 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
}
|
||||
}
|
||||
|
||||
*out_table_reader = std::move(table_reader);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
const std::string& external_file, uint64_t new_file_number,
|
||||
const PreparedFileInfo* prepared_file_info,
|
||||
IngestedFileInfo* file_to_ingest, SuperVersion* sv) {
|
||||
file_to_ingest->external_file_path = external_file;
|
||||
|
||||
std::unique_ptr<TableReader> table_reader;
|
||||
Status status =
|
||||
prepared_file_info != nullptr
|
||||
? GetIngestedFileInfoFromFileInfo(external_file, *prepared_file_info,
|
||||
file_to_ingest)
|
||||
: GetIngestedFileInfoFromFile(external_file, new_file_number,
|
||||
file_to_ingest, sv, &table_reader);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
assert(file_to_ingest->file_size > 0);
|
||||
assert(!file_to_ingest->unset());
|
||||
assert(file_to_ingest->table_properties.num_entries > 0 ||
|
||||
file_to_ingest->table_properties.num_range_deletions > 0);
|
||||
if (ingestion_options_.allow_db_generated_files) {
|
||||
// These files keep their original sequence numbers (derived, not
|
||||
// reassigned), so the bounds must be valid here.
|
||||
assert(file_to_ingest->smallest_seqno <= file_to_ingest->largest_seqno);
|
||||
assert(file_to_ingest->largest_seqno < kMaxSequenceNumber);
|
||||
}
|
||||
|
||||
// Verify the file checksum if requested. The open-and-scan path already has a
|
||||
// `TableReader` open; the fast-path opens one here only when verification is
|
||||
// requested -- otherwise it performs no file I/O.
|
||||
if (ingestion_options_.verify_checksums_before_ingest) {
|
||||
if (table_reader == nullptr) {
|
||||
status =
|
||||
ResetTableReader(external_file, new_file_number,
|
||||
file_to_ingest->user_defined_timestamps_persisted,
|
||||
sv, file_to_ingest, &table_reader);
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"Failed to reset table reader for external file %s: %s",
|
||||
external_file.c_str(), status.ToString().c_str());
|
||||
return status;
|
||||
}
|
||||
}
|
||||
// If customized readahead size is needed, we can pass a user option all the
|
||||
// way to here. Right now we just rely on the default readahead to keep
|
||||
// things simple.
|
||||
// TODO: plumb Env::IOActivity, Env::IOPriority
|
||||
ReadOptions ro;
|
||||
ro.readahead_size = ingestion_options_.verify_checksums_readahead_size;
|
||||
ro.fill_cache = ingestion_options_.fill_cache;
|
||||
status = table_reader->VerifyChecksum(
|
||||
ro, TableReaderCaller::kExternalSSTIngestion);
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"Failed to verify checksum for external file %s: %s",
|
||||
external_file.c_str(), status.ToString().c_str());
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign FD with number.
|
||||
file_to_ingest->fd =
|
||||
FileDescriptor(new_file_number, 0, file_to_ingest->file_size);
|
||||
|
||||
const size_t ts_sz = ucmp_->timestamp_size();
|
||||
Slice smallest = file_to_ingest->smallest_internal_key.user_key();
|
||||
Slice largest = file_to_ingest->largest_internal_key.user_key();
|
||||
|
||||
@@ -181,6 +181,9 @@ struct IngestedFileInfo : public KeyRangeInfo {
|
||||
// setting.
|
||||
bool user_defined_timestamps_persisted = true;
|
||||
|
||||
// Whether Lmax commit-time table opening should prefetch index/filter blocks.
|
||||
bool prefetch_lmax_index_and_filter_blocks = true;
|
||||
|
||||
SequenceNumber largest_seqno = kMaxSequenceNumber;
|
||||
SequenceNumber smallest_seqno = kMaxSequenceNumber;
|
||||
};
|
||||
@@ -248,6 +251,7 @@ class ExternalSstFileIngestionJob {
|
||||
Status Prepare(const std::vector<std::string>& external_files_paths,
|
||||
const std::vector<std::string>& files_checksums,
|
||||
const std::vector<std::string>& files_checksum_func_names,
|
||||
const std::vector<const PreparedFileInfo*>& file_infos,
|
||||
const std::optional<RangeOpt>& atomic_replace_range,
|
||||
const Temperature& file_temperature, uint64_t next_file_number,
|
||||
SuperVersion* sv);
|
||||
@@ -301,6 +305,20 @@ class ExternalSstFileIngestionJob {
|
||||
return max_assigned_seqno_;
|
||||
}
|
||||
|
||||
// Max threads requested for opening this job's files during commit, from the
|
||||
// per-CF IngestExternalFileOptions.
|
||||
int file_opening_threads() const {
|
||||
return ingestion_options_.file_opening_threads;
|
||||
}
|
||||
|
||||
// Merge another already-Prepare()d job for the SAME column family into this
|
||||
// one so both sets of files are committed by a single Run(). The other job's
|
||||
// files are appended after this job's, so for any overlapping keys the other
|
||||
// job's data wins via a higher assigned sequence number -- the same semantics
|
||||
// as passing all the files to a single ingestion call in this order. The
|
||||
// other job is left empty.
|
||||
Status MergeForSameColumnFamily(ExternalSstFileIngestionJob* other);
|
||||
|
||||
private:
|
||||
Status ResetTableReader(const std::string& external_file,
|
||||
uint64_t new_file_number,
|
||||
@@ -315,17 +333,37 @@ class ExternalSstFileIngestionJob {
|
||||
// different options. For example: when external file does not contain
|
||||
// timestamps while column family enables UDT in Memtables only feature.
|
||||
Status SanityCheckTableProperties(const std::string& external_file,
|
||||
uint64_t new_file_number, SuperVersion* sv,
|
||||
IngestedFileInfo* file_to_ingest,
|
||||
std::unique_ptr<TableReader>* table_reader);
|
||||
const TableProperties& props,
|
||||
IngestedFileInfo* file_to_ingest);
|
||||
|
||||
// Open the external file and populate `file_to_ingest` with all the
|
||||
// external information we need to ingest this file.
|
||||
// external information we need to ingest this file. When
|
||||
// `prepared_file_info` is non-null, its caller-supplied metadata is reused
|
||||
// instead of opening and scanning the file.
|
||||
Status GetIngestedFileInfo(const std::string& external_file,
|
||||
uint64_t new_file_number,
|
||||
const PreparedFileInfo* prepared_file_info,
|
||||
IngestedFileInfo* file_to_ingest,
|
||||
SuperVersion* sv);
|
||||
|
||||
// Acquire the per-file metadata from the caller-supplied opaque
|
||||
// `PreparedFileInfo` (produced by SstFileWriter::Finish) instead of opening
|
||||
// the file.
|
||||
Status GetIngestedFileInfoFromFileInfo(
|
||||
const std::string& external_file,
|
||||
const PreparedFileInfo& prepared_file_info,
|
||||
IngestedFileInfo* file_to_ingest);
|
||||
|
||||
// Acquire the per-file metadata by opening the external file and scanning it
|
||||
// (table properties, sequence number bounds, and boundary keys including any
|
||||
// range-tombstone extensions). Used when no file_info is available. The
|
||||
// opened `TableReader` is returned via `*table_reader` so the caller can
|
||||
// reuse it (e.g. to verify the file checksum) without re-opening the file.
|
||||
Status GetIngestedFileInfoFromFile(
|
||||
const std::string& external_file, uint64_t new_file_number,
|
||||
IngestedFileInfo* file_to_ingest, SuperVersion* sv,
|
||||
std::unique_ptr<TableReader>* out_table_reader);
|
||||
|
||||
// If the input files' key range overlaps themselves, this function divides
|
||||
// them in the user specified order into multiple batches. Where the files
|
||||
// within a batch do not overlap with each other, but key range could overlap
|
||||
@@ -334,6 +372,10 @@ class ExternalSstFileIngestionJob {
|
||||
// make one batch.
|
||||
void DivideInputFilesIntoBatches();
|
||||
|
||||
// Returns whether any two files in `files` have overlapping key ranges, by
|
||||
// sorting the file ranges and checking adjacent pairs.
|
||||
bool ComputeFilesOverlap(const autovector<IngestedFileInfo>& files) const;
|
||||
|
||||
// Assign level for the files in one batch. The files within one batch are not
|
||||
// overlapping, and we assign level to each file one after another.
|
||||
// If `prev_batch_uppermost_level` is specified, all files in this batch will
|
||||
@@ -407,7 +449,7 @@ class ExternalSstFileIngestionJob {
|
||||
SnapshotList* db_snapshots_;
|
||||
autovector<IngestedFileInfo> files_to_ingest_;
|
||||
std::vector<FileBatchInfo> file_batches_to_ingest_;
|
||||
const IngestExternalFileOptions& ingestion_options_;
|
||||
const IngestExternalFileOptions ingestion_options_;
|
||||
std::optional<KeyRangeInfo> atomic_replace_range_;
|
||||
Directories* directories_;
|
||||
EventLogger* event_logger_;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <table/block_based/block_based_table_factory.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
@@ -17,6 +18,7 @@
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/sst_file_reader.h"
|
||||
#include "rocksdb/sst_file_writer.h"
|
||||
#include "table/prepared_file_info.h"
|
||||
#include "test_util/testutil.h"
|
||||
#include "util/random.h"
|
||||
#include "util/thread_guard.h"
|
||||
@@ -92,10 +94,17 @@ class ExternSSTFileLinkFailFallbackTest
|
||||
|
||||
class ExternalSSTFileTest
|
||||
: public ExternalSSTFileTestBase,
|
||||
public ::testing::WithParamInterface<std::tuple<bool, bool>> {
|
||||
public ::testing::WithParamInterface<std::tuple<bool, bool, bool>> {
|
||||
public:
|
||||
ExternalSSTFileTest() = default;
|
||||
|
||||
void SetUp() override {
|
||||
const auto* info = ::testing::UnitTest::GetInstance()->current_test_info();
|
||||
if (info != nullptr && info->value_param() != nullptr) {
|
||||
two_phase_ingest_ = std::get<2>(GetParam());
|
||||
}
|
||||
}
|
||||
|
||||
Status GenerateOneExternalFile(
|
||||
const Options& options, ColumnFamilyHandle* cfh,
|
||||
std::vector<std::pair<std::string, std::string>>& data, int file_id,
|
||||
@@ -152,7 +161,8 @@ class ExternalSSTFileTest
|
||||
bool verify_checksums_before_ingest = true, bool ingest_behind = false,
|
||||
bool sort_data = false,
|
||||
std::map<std::string, std::string>* true_data = nullptr,
|
||||
ColumnFamilyHandle* cfh = nullptr, bool fill_cache = false) {
|
||||
ColumnFamilyHandle* cfh = nullptr, bool fill_cache = false,
|
||||
bool ingest_with_file_info = false) {
|
||||
// Generate a file id if not provided
|
||||
if (file_id == -1) {
|
||||
file_id = last_file_id_ + 1;
|
||||
@@ -188,7 +198,12 @@ class ExternalSSTFileTest
|
||||
return s;
|
||||
}
|
||||
}
|
||||
s = sst_file_writer.Finish();
|
||||
ExternalSstFileInfo file_info;
|
||||
if (ingest_with_file_info) {
|
||||
s = sst_file_writer.Finish(&file_info);
|
||||
} else {
|
||||
s = sst_file_writer.Finish();
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
IngestExternalFileOptions ifo;
|
||||
@@ -197,7 +212,21 @@ class ExternalSSTFileTest
|
||||
ifo.verify_checksums_before_ingest = verify_checksums_before_ingest;
|
||||
ifo.ingest_behind = ingest_behind;
|
||||
ifo.fill_cache = fill_cache;
|
||||
if (cfh) {
|
||||
if (two_phase_ingest_) {
|
||||
std::unique_ptr<FileIngestionHandle> handle;
|
||||
s = db_->PrepareFileIngestion(cfh ? cfh : db_->DefaultColumnFamily(),
|
||||
{file_path}, ifo, &handle);
|
||||
if (s.ok()) {
|
||||
s = db_->CommitFileIngestionHandle(std::move(handle));
|
||||
}
|
||||
} else if (ingest_with_file_info) {
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = cfh ? cfh : db_->DefaultColumnFamily();
|
||||
arg.external_files = {file_path};
|
||||
arg.file_infos = {file_info.prepared_file_info.get()};
|
||||
arg.options = ifo;
|
||||
s = db_->IngestExternalFiles({arg});
|
||||
} else if (cfh) {
|
||||
s = db_->IngestExternalFile(cfh, {file_path}, ifo);
|
||||
} else {
|
||||
s = db_->IngestExternalFile({file_path}, ifo);
|
||||
@@ -243,6 +272,14 @@ class ExternalSSTFileTest
|
||||
args[i].external_files.push_back(external_file_path);
|
||||
args[i].options = ifos[i];
|
||||
}
|
||||
if (two_phase_ingest_) {
|
||||
std::unique_ptr<FileIngestionHandle> handle;
|
||||
Status s = db_->PrepareFileIngestion(args, &handle);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
return db_->CommitFileIngestionHandle(std::move(handle));
|
||||
}
|
||||
return db_->IngestExternalFiles(args);
|
||||
}
|
||||
|
||||
@@ -294,10 +331,619 @@ class ExternalSSTFileTest
|
||||
return db_->IngestExternalFile(files, opts);
|
||||
}
|
||||
|
||||
// Writes `data` (sorted by user key) into a new external SST and returns its
|
||||
// path in *file_path WITHOUT ingesting it, so a test can drive the two-phase
|
||||
// PrepareFileIngestion()/CommitFileIngestion() API directly.
|
||||
Status GenerateExternalFileOnly(
|
||||
const Options& options,
|
||||
std::vector<std::pair<std::string, std::string>> data,
|
||||
std::string* file_path) {
|
||||
std::sort(data.begin(), data.end(),
|
||||
[&](const std::pair<std::string, std::string>& e1,
|
||||
const std::pair<std::string, std::string>& e2) {
|
||||
return options.comparator->Compare(e1.first, e2.first) < 0;
|
||||
});
|
||||
*file_path = sst_files_dir_ + "only_" + std::to_string(++last_file_id_);
|
||||
SstFileWriter sst_file_writer(EnvOptions(), options);
|
||||
Status s = sst_file_writer.Open(*file_path);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
for (const auto& entry : data) {
|
||||
s = sst_file_writer.Put(entry.first, entry.second);
|
||||
if (!s.ok()) {
|
||||
sst_file_writer.Finish().PermitUncheckedError();
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return sst_file_writer.Finish();
|
||||
}
|
||||
|
||||
protected:
|
||||
int last_file_id_ = 0;
|
||||
bool two_phase_ingest_ = false;
|
||||
};
|
||||
|
||||
TEST_F(ExternalSSTFileTest, IngestionTimingHistogram) {
|
||||
Options options = CurrentOptions();
|
||||
options.statistics = CreateDBStatistics();
|
||||
options.statistics->set_stats_level(StatsLevel::kAll); // enable timers
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(GenerateAndAddExternalFile(
|
||||
options, {{"k1", "v1"}, {"k2", "v2"}, {"k3", "v3"}}, /*file_id=*/-1,
|
||||
/*allow_global_seqno=*/true, /*write_global_seqno=*/false,
|
||||
/*verify_checksums_before_ingest=*/true, /*ingest_behind=*/false,
|
||||
/*sort_data=*/true));
|
||||
|
||||
auto hist = [&](Histograms h) {
|
||||
HistogramData hd;
|
||||
options.statistics->histogramData(h, &hd);
|
||||
return hd;
|
||||
};
|
||||
|
||||
// One sample per call (not per CF) in each phase histogram.
|
||||
HistogramData prepare_hd = hist(INGEST_EXTERNAL_FILE_PREPARE_TIME);
|
||||
HistogramData run_hd = hist(INGEST_EXTERNAL_FILE_RUN_TIME);
|
||||
ASSERT_EQ(1, prepare_hd.count);
|
||||
ASSERT_EQ(1, run_hd.count);
|
||||
ASSERT_GT(prepare_hd.max, 0.0);
|
||||
ASSERT_GT(run_hd.max, 0.0);
|
||||
|
||||
// A second call adds exactly one more sample to each histogram.
|
||||
ASSERT_OK(GenerateAndAddExternalFile(
|
||||
options, {{"k4", "v4"}, {"k5", "v5"}}, /*file_id=*/-1,
|
||||
/*allow_global_seqno=*/true, /*write_global_seqno=*/false,
|
||||
/*verify_checksums_before_ingest=*/true, /*ingest_behind=*/false,
|
||||
/*sort_data=*/true));
|
||||
ASSERT_EQ(2, hist(INGEST_EXTERNAL_FILE_PREPARE_TIME).count);
|
||||
ASSERT_EQ(2, hist(INGEST_EXTERNAL_FILE_RUN_TIME).count);
|
||||
|
||||
// A failed ingestion records nothing (latency is logged only on success).
|
||||
ASSERT_NOK(db_->IngestExternalFile({"/path/does/not/exist.sst"},
|
||||
IngestExternalFileOptions()));
|
||||
ASSERT_EQ(2, hist(INGEST_EXTERNAL_FILE_PREPARE_TIME).count);
|
||||
ASSERT_EQ(2, hist(INGEST_EXTERNAL_FILE_RUN_TIME).count);
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, PrepareThenCommit) {
|
||||
Options options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
std::string file_path;
|
||||
ASSERT_OK(GenerateExternalFileOnly(
|
||||
options, {{"k1", "v1"}, {"k2", "v2"}, {"k3", "v3"}}, &file_path));
|
||||
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {file_path};
|
||||
|
||||
std::unique_ptr<FileIngestionHandle> handle;
|
||||
ASSERT_OK(db_->PrepareFileIngestion({arg}, &handle));
|
||||
ASSERT_TRUE(handle != nullptr);
|
||||
|
||||
// The prepared file is staged in the DB but not yet visible.
|
||||
ASSERT_EQ("NOT_FOUND", Get("k1"));
|
||||
|
||||
ASSERT_OK(db_->CommitFileIngestionHandle(std::move(handle)));
|
||||
ASSERT_EQ("v1", Get("k1"));
|
||||
ASSERT_EQ("v2", Get("k2"));
|
||||
ASSERT_EQ("v3", Get("k3"));
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, ParallelFileOpenWithFileOpeningThreads) {
|
||||
// Ingest many non-overlapping files with file_opening_threads > 1 so commit
|
||||
// opens table readers with multiple threads. max_open_files == -1 makes
|
||||
// commit open every new file.
|
||||
Options options = CurrentOptions();
|
||||
options.max_open_files = -1;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
constexpr int kNumFiles = 8;
|
||||
constexpr int kKeysPerFile = 50;
|
||||
std::vector<std::string> files;
|
||||
std::map<std::string, std::string> true_data;
|
||||
for (int f = 0; f < kNumFiles; f++) {
|
||||
std::vector<std::pair<std::string, std::string>> data;
|
||||
for (int k = 0; k < kKeysPerFile; k++) {
|
||||
const int key = f * kKeysPerFile + k;
|
||||
const std::string value = "val" + std::to_string(key);
|
||||
data.emplace_back(Key(key), value);
|
||||
true_data[Key(key)] = value;
|
||||
}
|
||||
std::string file_path;
|
||||
ASSERT_OK(GenerateExternalFileOnly(options, data, &file_path));
|
||||
files.push_back(file_path);
|
||||
}
|
||||
|
||||
IngestExternalFileOptions ifo;
|
||||
ifo.file_opening_threads = 4;
|
||||
ASSERT_OK(db_->IngestExternalFile(files, ifo));
|
||||
|
||||
for (const auto& [key, value] : true_data) {
|
||||
ASSERT_EQ(value, Get(key));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, LmaxPrefetchSkipDoesNotDisableL0Prefetch) {
|
||||
LRUCacheOptions co;
|
||||
co.capacity = 32 << 20;
|
||||
std::shared_ptr<Cache> cache = NewLRUCache(co);
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.block_cache = cache;
|
||||
table_options.cache_index_and_filter_blocks = true;
|
||||
table_options.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.max_open_files = -1;
|
||||
options.num_levels = 2;
|
||||
options.optimize_filters_for_hits = false;
|
||||
options.statistics = CreateDBStatistics();
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const auto write_file = [&](const std::string& file_path,
|
||||
const std::string& value,
|
||||
ExternalSstFileInfo* file_info) {
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.Open(file_path));
|
||||
ASSERT_OK(writer.Put(Key(10), value));
|
||||
ASSERT_OK(writer.Finish(file_info));
|
||||
};
|
||||
|
||||
const auto ingest_file = [&](const std::string& file_path,
|
||||
const ExternalSstFileInfo& file_info,
|
||||
bool fail_if_not_bottommost_level) {
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {file_path};
|
||||
arg.file_infos = {file_info.prepared_file_info.get()};
|
||||
arg.options.fail_if_not_bottommost_level = fail_if_not_bottommost_level;
|
||||
arg.options.verify_checksums_before_ingest = false;
|
||||
arg.options.prefetch_lmax_index_and_filter_blocks = false;
|
||||
ASSERT_OK(db_->IngestExternalFiles({arg}));
|
||||
};
|
||||
|
||||
const std::string lmax_file_path = sst_files_dir_ + "lazy_lmax.sst";
|
||||
ExternalSstFileInfo lmax_file_info;
|
||||
write_file(lmax_file_path, "v10", &lmax_file_info);
|
||||
|
||||
ASSERT_EQ(0, options.statistics->getAndResetTickerCount(
|
||||
Tickers::BLOCK_CACHE_INDEX_ADD));
|
||||
ASSERT_EQ(0, options.statistics->getAndResetTickerCount(
|
||||
Tickers::BLOCK_CACHE_FILTER_ADD));
|
||||
|
||||
ingest_file(lmax_file_path, lmax_file_info,
|
||||
true /* fail_if_not_bottommost_level */);
|
||||
ASSERT_EQ("0,1", FilesPerLevel());
|
||||
|
||||
ASSERT_EQ(0, options.statistics->getAndResetTickerCount(
|
||||
Tickers::BLOCK_CACHE_INDEX_ADD));
|
||||
ASSERT_EQ(0, options.statistics->getAndResetTickerCount(
|
||||
Tickers::BLOCK_CACHE_FILTER_ADD));
|
||||
|
||||
const std::string l0_file_path = sst_files_dir_ + "l0.sst";
|
||||
ExternalSstFileInfo l0_file_info;
|
||||
write_file(l0_file_path, "v11", &l0_file_info);
|
||||
|
||||
ASSERT_EQ(0, options.statistics->getAndResetTickerCount(
|
||||
Tickers::BLOCK_CACHE_INDEX_ADD));
|
||||
ASSERT_EQ(0, options.statistics->getAndResetTickerCount(
|
||||
Tickers::BLOCK_CACHE_FILTER_ADD));
|
||||
|
||||
ingest_file(l0_file_path, l0_file_info,
|
||||
false /* fail_if_not_bottommost_level */);
|
||||
ASSERT_EQ("1,1", FilesPerLevel());
|
||||
|
||||
EXPECT_GT(options.statistics->getAndResetTickerCount(
|
||||
Tickers::BLOCK_CACHE_INDEX_ADD),
|
||||
0);
|
||||
EXPECT_GT(options.statistics->getAndResetTickerCount(
|
||||
Tickers::BLOCK_CACHE_FILTER_ADD),
|
||||
0);
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, AbortPreparedIngestion) {
|
||||
Options options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
std::string file_path;
|
||||
ASSERT_OK(GenerateExternalFileOnly(options, {{"x", "1"}}, &file_path));
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {file_path};
|
||||
|
||||
std::unique_ptr<FileIngestionHandle> handle;
|
||||
ASSERT_OK(db_->PrepareFileIngestion({arg}, &handle));
|
||||
ASSERT_OK(handle->Abort());
|
||||
ASSERT_EQ("NOT_FOUND", Get("x"));
|
||||
|
||||
// Committing an already-aborted handle returns an error.
|
||||
ASSERT_TRUE(
|
||||
db_->CommitFileIngestionHandle(std::move(handle)).IsInvalidArgument());
|
||||
|
||||
// The DB remains healthy after an abort: a fresh prepare + commit works.
|
||||
std::unique_ptr<FileIngestionHandle> handle2;
|
||||
ASSERT_OK(db_->PrepareFileIngestion({arg}, &handle2));
|
||||
ASSERT_OK(db_->CommitFileIngestionHandle(std::move(handle2)));
|
||||
ASSERT_EQ("1", Get("x"));
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, MultiHandleAtomicCommit) {
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"cf1", "cf2", "cf3"}, options);
|
||||
|
||||
// Two handles whose column families overlap on cf2:
|
||||
// handle 1 -> cf1, cf2 handle 2 -> cf2, cf3
|
||||
// Committing them together installs cf1 and cf3 each from one handle, while
|
||||
// cf2 receives files from BOTH handles, merged into a single per-CF commit.
|
||||
std::string f_cf1;
|
||||
std::string f_cf2_h1;
|
||||
std::string f_cf2_h2;
|
||||
std::string f_cf3;
|
||||
ASSERT_OK(GenerateExternalFileOnly(options, {{"a", "1"}}, &f_cf1));
|
||||
// The cf2 files share key "b": handle 2 is committed later, so its value must
|
||||
// win via a higher assigned sequence number.
|
||||
ASSERT_OK(GenerateExternalFileOnly(options, {{"b", "h1"}, {"b1", "10"}},
|
||||
&f_cf2_h1));
|
||||
ASSERT_OK(GenerateExternalFileOnly(options, {{"b", "h2"}, {"b2", "20"}},
|
||||
&f_cf2_h2));
|
||||
ASSERT_OK(GenerateExternalFileOnly(options, {{"c", "3"}}, &f_cf3));
|
||||
|
||||
std::vector<IngestExternalFileArg> args1(2);
|
||||
args1[0].column_family = handles_[1]; // cf1
|
||||
args1[0].external_files = {f_cf1};
|
||||
args1[1].column_family = handles_[2]; // cf2
|
||||
args1[1].external_files = {f_cf2_h1};
|
||||
|
||||
std::vector<IngestExternalFileArg> args2(2);
|
||||
args2[0].column_family =
|
||||
handles_[2]; // cf2 -> merges with handle 1's cf2 job
|
||||
args2[0].external_files = {f_cf2_h2};
|
||||
args2[1].column_family = handles_[3]; // cf3
|
||||
args2[1].external_files = {f_cf3};
|
||||
|
||||
std::unique_ptr<FileIngestionHandle> h1;
|
||||
std::unique_ptr<FileIngestionHandle> h2;
|
||||
ASSERT_OK(db_->PrepareFileIngestion(args1, &h1));
|
||||
ASSERT_OK(db_->PrepareFileIngestion(args2, &h2));
|
||||
ASSERT_EQ("NOT_FOUND", Get(1, "a"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(2, "b1"));
|
||||
ASSERT_EQ("NOT_FOUND", Get(3, "c"));
|
||||
|
||||
// Commit both handles atomically.
|
||||
std::vector<std::unique_ptr<FileIngestionHandle>> batch;
|
||||
batch.push_back(std::move(h1));
|
||||
batch.push_back(std::move(h2));
|
||||
ASSERT_OK(db_->CommitFileIngestionHandles(std::move(batch)));
|
||||
|
||||
ASSERT_EQ("1", Get(1, "a"));
|
||||
ASSERT_EQ("h2", Get(2, "b")); // overlapping key: later handle (h2) wins
|
||||
ASSERT_EQ("10", Get(2, "b1")); // cf2 from handle 1
|
||||
ASSERT_EQ("20", Get(2, "b2")); // cf2 from handle 2 (merged)
|
||||
ASSERT_EQ("3", Get(3, "c"));
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, MultiHandleSameColumnFamilyIncompatibleOptions) {
|
||||
Options options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
std::string f1;
|
||||
std::string f2;
|
||||
ASSERT_OK(GenerateExternalFileOnly(options, {{"a", "1"}}, &f1));
|
||||
ASSERT_OK(GenerateExternalFileOnly(options, {{"b", "2"}}, &f2));
|
||||
|
||||
// Same column family but with options the merged Run() would consult set
|
||||
// differently across the two handles.
|
||||
IngestExternalFileOptions opts_fill;
|
||||
IngestExternalFileOptions opts_no_fill;
|
||||
opts_no_fill.fill_cache = false;
|
||||
std::unique_ptr<FileIngestionHandle> h1;
|
||||
std::unique_ptr<FileIngestionHandle> h2;
|
||||
ASSERT_OK(db_->PrepareFileIngestion(db_->DefaultColumnFamily(), {f1},
|
||||
opts_fill, &h1));
|
||||
ASSERT_OK(db_->PrepareFileIngestion(db_->DefaultColumnFamily(), {f2},
|
||||
opts_no_fill, &h2));
|
||||
|
||||
std::vector<std::unique_ptr<FileIngestionHandle>> batch;
|
||||
batch.push_back(std::move(h1));
|
||||
batch.push_back(std::move(h2));
|
||||
ASSERT_TRUE(
|
||||
db_->CommitFileIngestionHandles(std::move(batch)).IsInvalidArgument());
|
||||
|
||||
// The rejected commit rolled both handles back; the CF is still usable.
|
||||
ASSERT_EQ("NOT_FOUND", Get("a"));
|
||||
ASSERT_EQ("NOT_FOUND", Get("b"));
|
||||
std::unique_ptr<FileIngestionHandle> h3;
|
||||
ASSERT_OK(db_->PrepareFileIngestion(db_->DefaultColumnFamily(), {f1},
|
||||
IngestExternalFileOptions(), &h3));
|
||||
ASSERT_OK(db_->CommitFileIngestionHandle(std::move(h3)));
|
||||
ASSERT_EQ("1", Get("a"));
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, PrepareFileIngestionValidationErrors) {
|
||||
Options options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
std::unique_ptr<FileIngestionHandle> handle;
|
||||
|
||||
ASSERT_TRUE(
|
||||
db_->PrepareFileIngestion(std::vector<IngestExternalFileArg>{}, nullptr)
|
||||
.IsInvalidArgument());
|
||||
|
||||
// Empty arg list.
|
||||
ASSERT_TRUE(
|
||||
db_->PrepareFileIngestion(std::vector<IngestExternalFileArg>{}, &handle)
|
||||
.IsInvalidArgument());
|
||||
ASSERT_TRUE(handle == nullptr);
|
||||
|
||||
// move_files and link_files are mutually exclusive.
|
||||
std::string file_path;
|
||||
ASSERT_OK(GenerateExternalFileOnly(options, {{"k", "v"}}, &file_path));
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {file_path};
|
||||
arg.options.move_files = true;
|
||||
arg.options.link_files = true;
|
||||
ASSERT_TRUE(db_->PrepareFileIngestion({arg}, &handle).IsInvalidArgument());
|
||||
ASSERT_TRUE(handle == nullptr);
|
||||
}
|
||||
|
||||
// Ingestion that reuses the SstFileWriter's metadata via
|
||||
// IngestExternalFileArg::file_infos skips the open-and-scan path and still
|
||||
// produces correct data, including overlapping data that is reassigned a new
|
||||
// global sequence number.
|
||||
TEST_F(ExternalSSTFileTest, IngestWithFileInfo) {
|
||||
Options options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
std::map<std::string, std::string> true_data;
|
||||
|
||||
// Count entries into the open-and-scan path so we can assert it is skipped.
|
||||
std::atomic<int> read_path_count{0};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"ExternalSstFileIngestionJob::GetIngestedFileInfo:ReadPath",
|
||||
[&](void*) { read_path_count.fetch_add(1); });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Reuse the writer's metadata, with the normal ingestion options: a global
|
||||
// sequence number is assigned (allow_db_generated_files is NOT set) and the
|
||||
// checksum is verified (the fast-path opens the file only to verify it, which
|
||||
// is not a metadata scan).
|
||||
auto ingest_fi = [&](std::vector<std::pair<std::string, std::string>> data) {
|
||||
return GenerateAndAddExternalFile(
|
||||
options, std::move(data), /*file_id=*/-1, /*allow_global_seqno=*/true,
|
||||
/*write_global_seqno=*/false, /*verify_checksums_before_ingest=*/true,
|
||||
/*ingest_behind=*/false, /*sort_data=*/true, &true_data,
|
||||
/*cfh=*/nullptr, /*fill_cache=*/false, /*ingest_with_file_info=*/true);
|
||||
};
|
||||
|
||||
// Non-overlapping ingest: data reads back and the file is not scanned.
|
||||
ASSERT_OK(ingest_fi({{Key(1), "a1"}, {Key(2), "a2"}, {Key(3), "a3"}}));
|
||||
ASSERT_EQ(read_path_count.load(), 0);
|
||||
|
||||
// Overlap existing (flushed) data so a non-zero global seqno is assigned and
|
||||
// the newer values win -- the normal seqno-reassignment path through
|
||||
// file_infos.
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(2), "old2"));
|
||||
true_data[Key(2)] = "old2";
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(ingest_fi({{Key(2), "new2"}, {Key(4), "new4"}}));
|
||||
ASSERT_EQ(read_path_count.load(), 0);
|
||||
|
||||
// Control: ingesting the same way but WITHOUT file_infos does enter the scan.
|
||||
ASSERT_OK(GenerateAndAddExternalFile(
|
||||
options, {{Key(8), "c8"}}, /*file_id=*/-1, /*allow_global_seqno=*/true,
|
||||
/*write_global_seqno=*/false, /*verify_checksums_before_ingest=*/true,
|
||||
/*ingest_behind=*/false, /*sort_data=*/true, &true_data));
|
||||
ASSERT_GT(read_path_count.load(), 0);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
size_t kcnt = 0;
|
||||
VerifyDBFromMap(true_data, &kcnt, false);
|
||||
}
|
||||
|
||||
// Range deletions are honored on the file_infos fast-path: the writer's
|
||||
// range-del bounds are carried in the metadata, so a DeleteRange covering
|
||||
// existing keys still shadows them.
|
||||
TEST_F(ExternalSSTFileTest, IngestWithFileInfoRangeDeletion) {
|
||||
Options options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(20), "live20"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(25), "live25"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
const std::string f = sst_files_dir_ + "rangedel.sst";
|
||||
SstFileWriter w(EnvOptions(), options);
|
||||
ASSERT_OK(w.Open(f));
|
||||
ASSERT_OK(w.Put(Key(30), "v30"));
|
||||
ASSERT_OK(w.DeleteRange(Key(20), Key(26))); // covers keys 20..25
|
||||
ExternalSstFileInfo file_info;
|
||||
ASSERT_OK(w.Finish(&file_info));
|
||||
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {f};
|
||||
arg.file_infos = {file_info.prepared_file_info.get()};
|
||||
arg.options.allow_global_seqno = true;
|
||||
ASSERT_OK(db_->IngestExternalFiles({arg}));
|
||||
|
||||
ReadOptions ro;
|
||||
std::string val;
|
||||
ASSERT_TRUE(db_->Get(ro, Key(20), &val).IsNotFound());
|
||||
ASSERT_TRUE(db_->Get(ro, Key(25), &val).IsNotFound());
|
||||
ASSERT_OK(db_->Get(ro, Key(30), &val));
|
||||
ASSERT_EQ(val, "v30");
|
||||
}
|
||||
|
||||
// verify_checksums_before_ingest is honored on the file_infos fast-path: it
|
||||
// opens the file just to verify, so corruption is still detected.
|
||||
TEST_F(ExternalSSTFileTest, IngestWithFileInfoVerifiesChecksum) {
|
||||
Options options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string f = sst_files_dir_ + "corrupt.sst";
|
||||
SstFileWriter w(EnvOptions(), options);
|
||||
ASSERT_OK(w.Open(f));
|
||||
for (int k = 0; k < 100; k++) {
|
||||
ASSERT_OK(w.Put(Key(k), "v" + Key(k)));
|
||||
}
|
||||
ExternalSstFileInfo file_info;
|
||||
ASSERT_OK(w.Finish(&file_info));
|
||||
ASSERT_OK(test::CorruptFile(options.env, f, /*offset=*/64,
|
||||
/*bytes_to_corrupt=*/8));
|
||||
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {f};
|
||||
arg.file_infos = {file_info.prepared_file_info.get()};
|
||||
arg.options.verify_checksums_before_ingest = true;
|
||||
ASSERT_TRUE(db_->IngestExternalFiles({arg}).IsCorruption());
|
||||
}
|
||||
|
||||
// write_global_seqno is incompatible with file_infos: the file is not opened,
|
||||
// so the seqno cannot be written back into it.
|
||||
TEST_F(ExternalSSTFileTest, IngestWithFileInfoRejectsWriteGlobalSeqno) {
|
||||
Options options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string f = sst_files_dir_ + "wgs.sst";
|
||||
SstFileWriter w(EnvOptions(), options);
|
||||
ASSERT_OK(w.Open(f));
|
||||
ASSERT_OK(w.Put(Key(1), "v1"));
|
||||
ExternalSstFileInfo file_info;
|
||||
ASSERT_OK(w.Finish(&file_info));
|
||||
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {f};
|
||||
arg.file_infos = {file_info.prepared_file_info.get()};
|
||||
arg.options.write_global_seqno = true;
|
||||
ASSERT_TRUE(db_->IngestExternalFiles({arg}).IsInvalidArgument());
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, GetPreparedFileInfoForExternalSstIngestion) {
|
||||
Options options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string source_dbname = dbname_ + "_source";
|
||||
ASSERT_OK(DestroyDB(source_dbname, options));
|
||||
std::unique_ptr<DB> source_db;
|
||||
ASSERT_OK(DB::Open(options, source_dbname, &source_db));
|
||||
|
||||
ASSERT_OK(source_db->Put(WriteOptions(), Key(10), "v10"));
|
||||
ASSERT_OK(source_db->Flush(FlushOptions()));
|
||||
|
||||
std::vector<LiveFileMetaData> live_meta;
|
||||
source_db->GetLiveFilesMetaData(&live_meta);
|
||||
ASSERT_EQ(live_meta.size(), 1);
|
||||
ASSERT_GT(live_meta[0].largest_seqno, 0);
|
||||
const std::string file_path =
|
||||
live_meta[0].directory + "/" + live_meta[0].relative_filename;
|
||||
|
||||
std::shared_ptr<const PreparedFileInfo> prepared_file_info;
|
||||
ASSERT_TRUE(source_db
|
||||
->GetPreparedFileInfoForExternalSstIngestion(
|
||||
live_meta[0].relative_filename, &prepared_file_info)
|
||||
.IsInvalidArgument());
|
||||
ASSERT_EQ(prepared_file_info, nullptr);
|
||||
prepared_file_info = std::make_shared<PreparedFileInfo>();
|
||||
ASSERT_TRUE(source_db
|
||||
->GetPreparedFileInfoForExternalSstIngestion(
|
||||
dbname_ + "_wrong/" + live_meta[0].relative_filename,
|
||||
&prepared_file_info)
|
||||
.IsInvalidArgument());
|
||||
ASSERT_EQ(prepared_file_info, nullptr);
|
||||
|
||||
ASSERT_OK(source_db->GetPreparedFileInfoForExternalSstIngestion(
|
||||
file_path, &prepared_file_info));
|
||||
ASSERT_TRUE(prepared_file_info != nullptr);
|
||||
ASSERT_EQ(prepared_file_info->file_size, live_meta[0].size);
|
||||
ASSERT_EQ(prepared_file_info->table_properties.key_largest_seqno,
|
||||
live_meta[0].largest_seqno);
|
||||
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {file_path};
|
||||
arg.file_infos = {prepared_file_info.get()};
|
||||
arg.options.allow_db_generated_files = true;
|
||||
arg.options.snapshot_consistency = false;
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"ExternalSstFileIngestionJob::GetIngestedFileInfo:ReadPath",
|
||||
[](void*) { FAIL() << "Prepared file info should skip the read path"; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(db_->IngestExternalFiles({arg}));
|
||||
|
||||
ASSERT_EQ(Get(Key(10)), "v10");
|
||||
|
||||
ASSERT_OK(source_db->Close());
|
||||
source_db.reset();
|
||||
ASSERT_OK(DestroyDB(source_dbname, options));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest,
|
||||
IngestWithFileInfoRejectsMissingDbGeneratedSeqnoBounds) {
|
||||
Options options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string f = sst_files_dir_ + "missing_seqno_bounds.sst";
|
||||
SstFileWriter w(EnvOptions(), options);
|
||||
ASSERT_OK(w.Open(f));
|
||||
ASSERT_OK(w.Put(Key(1), "v1"));
|
||||
ExternalSstFileInfo file_info;
|
||||
ASSERT_OK(w.Finish(&file_info));
|
||||
auto prepared_file_info = file_info.prepared_file_info;
|
||||
|
||||
auto missing_seqno_bounds =
|
||||
std::make_shared<PreparedFileInfo>(*prepared_file_info);
|
||||
missing_seqno_bounds->table_properties.key_smallest_seqno = UINT64_MAX;
|
||||
missing_seqno_bounds->table_properties.key_largest_seqno = UINT64_MAX;
|
||||
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {f};
|
||||
arg.file_infos = {missing_seqno_bounds.get()};
|
||||
arg.options.allow_db_generated_files = true;
|
||||
arg.options.snapshot_consistency = false;
|
||||
Status s = db_->IngestExternalFiles({arg});
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_NE(s.ToString().find("Unknown largest seqno"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, IngestWithFileInfoPreparedKeyBoundTypes) {
|
||||
Options options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string f = sst_files_dir_ + "prepared_key_bound_types.sst";
|
||||
SstFileWriter w(EnvOptions(), options);
|
||||
ASSERT_OK(w.Open(f));
|
||||
ASSERT_OK(w.Put(Key(10), "v10"));
|
||||
ASSERT_OK(w.DeleteRange(Key(20), Key(30)));
|
||||
ExternalSstFileInfo file_info;
|
||||
ASSERT_OK(w.Finish(&file_info));
|
||||
auto prepared_file_info = file_info.prepared_file_info;
|
||||
|
||||
ParsedInternalKey smallest;
|
||||
ASSERT_OK(ParseInternalKey(prepared_file_info->smallest.Encode(), &smallest,
|
||||
false /* log_err_key */));
|
||||
ASSERT_EQ(0, smallest.sequence);
|
||||
ASSERT_EQ(kTypeValue, smallest.type);
|
||||
ASSERT_EQ(Key(10), smallest.user_key.ToString());
|
||||
|
||||
ParsedInternalKey largest;
|
||||
ASSERT_OK(ParseInternalKey(prepared_file_info->largest.Encode(), &largest,
|
||||
false /* log_err_key */));
|
||||
ASSERT_EQ(kMaxSequenceNumber, largest.sequence);
|
||||
ASSERT_EQ(kTypeRangeDeletion, largest.type);
|
||||
ASSERT_EQ(Key(30), largest.user_key.ToString());
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, ComparatorMismatch) {
|
||||
Options options = CurrentOptions();
|
||||
Options options_diff_ucmp = options;
|
||||
@@ -1816,7 +2462,8 @@ TEST_P(ExternalSSTFileTest, IngestFileWithGlobalSeqnoRandomized) {
|
||||
for (int i = 0; i < 500; i++) {
|
||||
std::vector<std::pair<std::string, std::string>> random_data;
|
||||
for (int j = 0; j < 100; j++) {
|
||||
std::string k = rnd.RandomString(rnd.Next() % 20);
|
||||
std::string k =
|
||||
rnd.RandomString(rnd.Next() % 20 + 1); // requires non-empty keys
|
||||
std::string v = rnd.RandomString(rnd.Next() % 50);
|
||||
random_data.emplace_back(k, v);
|
||||
}
|
||||
@@ -3247,6 +3894,20 @@ class ExternalSSTFileWithTimestampTest : public ExternalSSTFileTest {
|
||||
return dst;
|
||||
}
|
||||
|
||||
std::string MaxTimestamp() {
|
||||
Slice ts = MaxU64Ts();
|
||||
return ts.ToString();
|
||||
}
|
||||
|
||||
Options UDTMemtableOnlyOptions() {
|
||||
Options options = CurrentOptions();
|
||||
options.comparator = test::BytewiseComparatorWithU64TsWrapper();
|
||||
options.persist_user_defined_timestamps = false;
|
||||
options.allow_concurrent_memtable_write = false;
|
||||
options.disable_auto_compactions = true;
|
||||
return options;
|
||||
}
|
||||
|
||||
Status IngestExternalUDTFile(const std::vector<std::string>& files,
|
||||
bool allow_global_seqno = true) {
|
||||
IngestExternalFileOptions opts;
|
||||
@@ -3815,8 +4476,74 @@ TEST_F(ExternalSSTFileWithTimestampTest, TimestampsNotPersistedBasic) {
|
||||
kRangeDelSkipConfigs));
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileWithTimestampTest,
|
||||
TimestampsPersistedIngestWithFileInfoRangeDeletionBounds) {
|
||||
Options options = CurrentOptions();
|
||||
options.comparator = test::BytewiseComparatorWithU64TsWrapper();
|
||||
options.persist_user_defined_timestamps = true;
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string ts = EncodeAsUint64(10);
|
||||
const std::string f = sst_files_dir_ + "udt_persisted_rangedel_bounds.sst";
|
||||
SstFileWriter w(EnvOptions(), options);
|
||||
ASSERT_OK(w.Open(f));
|
||||
ASSERT_OK(w.Put(Key(30), ts, "v30"));
|
||||
ASSERT_OK(w.DeleteRange(Key(20), Key(26), ts));
|
||||
ExternalSstFileInfo file_info;
|
||||
ASSERT_OK(w.Finish(&file_info));
|
||||
auto prepared_file_info = file_info.prepared_file_info;
|
||||
ASSERT_EQ(Key(20) + MaxTimestamp(),
|
||||
prepared_file_info->smallest.user_key().ToString());
|
||||
ASSERT_EQ(Key(30) + ts, prepared_file_info->largest.user_key().ToString());
|
||||
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {f};
|
||||
arg.file_infos = {file_info.prepared_file_info.get()};
|
||||
ASSERT_OK(db_->IngestExternalFiles({arg}));
|
||||
|
||||
std::vector<LiveFileMetaData> metadata;
|
||||
db_->GetLiveFilesMetaData(&metadata);
|
||||
ASSERT_EQ(1, metadata.size());
|
||||
ASSERT_EQ(Key(20) + MaxTimestamp(), metadata[0].smallestkey);
|
||||
ASSERT_EQ(Key(30) + ts, metadata[0].largestkey);
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileWithTimestampTest,
|
||||
TimestampsNotPersistedIngestWithFileInfoRangeDeletionBounds) {
|
||||
Options options = UDTMemtableOnlyOptions();
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string f = sst_files_dir_ + "udt_rangedel_bounds.sst";
|
||||
SstFileWriter w(EnvOptions(), options);
|
||||
ASSERT_OK(w.Open(f));
|
||||
ASSERT_OK(w.Put(Key(30), EncodeAsUint64(0), "v30"));
|
||||
ASSERT_OK(w.DeleteRange(Key(20), Key(26), EncodeAsUint64(0)));
|
||||
ExternalSstFileInfo file_info;
|
||||
ASSERT_OK(w.Finish(&file_info));
|
||||
auto prepared_file_info = file_info.prepared_file_info;
|
||||
ASSERT_EQ(Key(20), prepared_file_info->smallest.user_key().ToString());
|
||||
ASSERT_EQ(Key(30), prepared_file_info->largest.user_key().ToString());
|
||||
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {f};
|
||||
arg.file_infos = {file_info.prepared_file_info.get()};
|
||||
ASSERT_OK(db_->IngestExternalFiles({arg}));
|
||||
|
||||
std::vector<LiveFileMetaData> metadata;
|
||||
db_->GetLiveFilesMetaData(&metadata);
|
||||
ASSERT_EQ(1, metadata.size());
|
||||
ASSERT_EQ(Key(20) + MaxTimestamp(), metadata[0].smallestkey);
|
||||
ASSERT_EQ(Key(30) + EncodeAsUint64(0), metadata[0].largestkey);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(ExternalSSTFileTest, ExternalSSTFileTest,
|
||||
testing::Combine(testing::Bool(), testing::Bool()));
|
||||
testing::Combine(testing::Bool(), testing::Bool(),
|
||||
testing::Bool()));
|
||||
|
||||
class IngestDBGeneratedFileTest
|
||||
: public ExternalSSTFileTestBase,
|
||||
|
||||
@@ -266,6 +266,9 @@ class FaultInjectionTest
|
||||
CloseDB();
|
||||
env_->ResetState();
|
||||
Status s = DB::Open(options_, dbname_, &db_);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
assert(db_ != nullptr);
|
||||
return s;
|
||||
}
|
||||
@@ -643,6 +646,78 @@ TEST(FaultInjectionFSTest, ReadUnsyncedData) {
|
||||
ASSERT_EQ(0, sl.compare(Slice(data.data() + first_read_len, sl.size())));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FaultInjectionFSTest, FileOpenContractRejectsReadersWhileOpenForWrite) {
|
||||
std::shared_ptr<FaultInjectionTestFS> fault_fs =
|
||||
std::make_shared<FaultInjectionTestFS>(FileSystem::Default());
|
||||
const std::string fname = test::PerThreadDBPath(
|
||||
"file_open_contract_no_readers_while_open_for_write");
|
||||
|
||||
FileOptions writer_options;
|
||||
writer_options.open_contract = FileOpenContract::kNoReadersWhileOpenForWrite;
|
||||
|
||||
std::unique_ptr<FSWritableFile> writer;
|
||||
ASSERT_OK(fault_fs->NewWritableFile(fname, writer_options, &writer, nullptr));
|
||||
|
||||
std::unique_ptr<FSRandomAccessFile> reader;
|
||||
IOStatus s =
|
||||
fault_fs->NewRandomAccessFile(fname, FileOptions(), &reader, nullptr);
|
||||
ASSERT_TRUE(s.IsNotSupported());
|
||||
|
||||
ASSERT_OK(writer->Close(IOOptions(), nullptr));
|
||||
ASSERT_OK(
|
||||
fault_fs->NewRandomAccessFile(fname, FileOptions(), &reader, nullptr));
|
||||
}
|
||||
|
||||
TEST(FaultInjectionFSTest,
|
||||
FileOpenContractAllowsSyncFileButRejectsReopenedWrite) {
|
||||
std::shared_ptr<FaultInjectionTestFS> fault_fs =
|
||||
std::make_shared<FaultInjectionTestFS>(FileSystem::Default());
|
||||
const std::string fname =
|
||||
test::PerThreadDBPath("file_open_contract_no_reopen_for_write");
|
||||
|
||||
FileOptions writer_options;
|
||||
writer_options.open_contract = FileOpenContract::kNoReopenForWrite;
|
||||
|
||||
std::unique_ptr<FSWritableFile> writer;
|
||||
ASSERT_OK(fault_fs->NewWritableFile(fname, writer_options, &writer, nullptr));
|
||||
ASSERT_OK(writer->Append("abc", IOOptions(), nullptr));
|
||||
ASSERT_OK(writer->Close(IOOptions(), nullptr));
|
||||
|
||||
ASSERT_OK(fault_fs->SyncFile(fname, FileOptions(), IOOptions(),
|
||||
/*use_fsync=*/false, nullptr));
|
||||
|
||||
std::unique_ptr<FSWritableFile> reopened_writer;
|
||||
ASSERT_OK(fault_fs->ReopenWritableFile(fname, FileOptions(), &reopened_writer,
|
||||
nullptr));
|
||||
IOStatus s = reopened_writer->Append("d", IOOptions(), nullptr);
|
||||
ASSERT_TRUE(s.IsNotSupported());
|
||||
ASSERT_OK(reopened_writer->Close(IOOptions(), nullptr));
|
||||
}
|
||||
|
||||
TEST(FaultInjectionFSTest, FileOpenContractAllowsCreateAfterDelete) {
|
||||
std::shared_ptr<FaultInjectionTestFS> fault_fs =
|
||||
std::make_shared<FaultInjectionTestFS>(FileSystem::Default());
|
||||
const std::string fname =
|
||||
test::PerThreadDBPath("file_open_contract_recreate_after_delete");
|
||||
|
||||
FileOptions writer_options;
|
||||
writer_options.open_contract = FileOpenContract::kNoReopenForWrite;
|
||||
|
||||
std::unique_ptr<FSWritableFile> writer;
|
||||
ASSERT_OK(fault_fs->NewWritableFile(fname, writer_options, &writer, nullptr));
|
||||
ASSERT_OK(writer->Append("abc", IOOptions(), nullptr));
|
||||
ASSERT_OK(writer->Close(IOOptions(), nullptr));
|
||||
|
||||
ASSERT_OK(FileSystem::Default()->DeleteFile(fname, IOOptions(), nullptr));
|
||||
|
||||
std::unique_ptr<FSWritableFile> recreated_writer;
|
||||
ASSERT_OK(fault_fs->NewWritableFile(fname, writer_options, &recreated_writer,
|
||||
nullptr));
|
||||
ASSERT_OK(recreated_writer->Append("def", IOOptions(), nullptr));
|
||||
ASSERT_OK(recreated_writer->Close(IOOptions(), nullptr));
|
||||
ASSERT_OK(fault_fs->DeleteFile(fname, IOOptions(), nullptr));
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+193
-4
@@ -31,6 +31,7 @@
|
||||
#include "util/mutexlock.h"
|
||||
#include "util/rate_limiter_impl.h"
|
||||
#include "util/string_util.h"
|
||||
#include "utilities/fault_injection_fs.h"
|
||||
#include "utilities/merge_operators.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -201,6 +202,194 @@ TEST_F(EventListenerTest, OnSingleDBCompactionTest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Listener that asserts OnCompactionPreCommit fires strictly between
|
||||
// OnCompactionBegin and OnCompactionCompleted, and that input files'
|
||||
// being_compacted flag is still true at that point.
|
||||
class TestCompactionPreCommitListener : public EventListener {
|
||||
public:
|
||||
explicit TestCompactionPreCommitListener(EventListenerTest* test)
|
||||
: test_(test) {}
|
||||
|
||||
void OnCompactionBegin(DB* /*db*/, const CompactionJobInfo& ci) override {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
++begin_count_;
|
||||
last_begin_job_id_ = ci.job_id;
|
||||
EXPECT_EQ(begin_count_, pre_commit_count_ + 1);
|
||||
EXPECT_EQ(begin_count_, completed_count_ + 1);
|
||||
}
|
||||
|
||||
void OnCompactionPreCommit(DB* db, const CompactionJobInfo& ci) override {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
++pre_commit_count_;
|
||||
// Must fire after Begin and before Completed for this compaction.
|
||||
EXPECT_EQ(pre_commit_count_, begin_count_);
|
||||
EXPECT_EQ(pre_commit_count_, completed_count_ + 1);
|
||||
EXPECT_EQ(ci.job_id, last_begin_job_id_);
|
||||
EXPECT_GT(ci.input_files.size(), 0U);
|
||||
|
||||
// Verify input files are still marked being_compacted.
|
||||
std::vector<std::vector<FileMetaData>> files_by_level;
|
||||
test_->dbfull()->TEST_GetFilesMetaData(test_->handles_[ci.cf_id],
|
||||
&files_by_level);
|
||||
EXPECT_EQ(test_->db_.get(), db);
|
||||
for (const auto& info : ci.input_file_infos) {
|
||||
bool found = false;
|
||||
for (const auto& level_files : files_by_level) {
|
||||
for (const auto& meta : level_files) {
|
||||
if (meta.fd.GetNumber() == info.file_number) {
|
||||
found = true;
|
||||
EXPECT_TRUE(meta.being_compacted)
|
||||
<< "input file " << info.file_number
|
||||
<< " should still be being_compacted in "
|
||||
"OnCompactionPreCommit";
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(found) << "input file " << info.file_number
|
||||
<< " not found in DB";
|
||||
}
|
||||
}
|
||||
|
||||
void OnCompactionCompleted(DB* /*db*/, const CompactionJobInfo& ci) override {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
++completed_count_;
|
||||
// Must fire after the matching PreCommit.
|
||||
EXPECT_EQ(completed_count_, pre_commit_count_);
|
||||
EXPECT_EQ(completed_count_, begin_count_);
|
||||
EXPECT_EQ(ci.job_id, last_begin_job_id_);
|
||||
}
|
||||
|
||||
size_t BeginCount() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return begin_count_;
|
||||
}
|
||||
size_t PreCommitCount() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return pre_commit_count_;
|
||||
}
|
||||
size_t CompletedCount() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return completed_count_;
|
||||
}
|
||||
|
||||
private:
|
||||
EventListenerTest* test_;
|
||||
std::mutex mutex_;
|
||||
size_t begin_count_ = 0;
|
||||
size_t pre_commit_count_ = 0;
|
||||
size_t completed_count_ = 0;
|
||||
int last_begin_job_id_ = -1;
|
||||
};
|
||||
|
||||
TEST_F(EventListenerTest, OnCompactionPreCommitOrdering) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.compression = kNoCompression;
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
auto* listener = new TestCompactionPreCommitListener(this);
|
||||
options.listeners.emplace_back(listener);
|
||||
|
||||
// Verify sync-point ordering: Begin -> PreCommit -> Completed.
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"DBImpl::NotifyOnCompactionBegin::UnlockMutex",
|
||||
"DBImpl::NotifyOnCompactionPreCommit::UnlockMutex"},
|
||||
{"DBImpl::NotifyOnCompactionPreCommit::UnlockMutex",
|
||||
"DBImpl::NotifyOnCompactionCompleted::UnlockMutex"}});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
Random rnd(301);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 10; j++) {
|
||||
ASSERT_OK(Put(1, rnd.RandomString(10), rnd.RandomString(10)));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
EXPECT_GT(listener->BeginCount(), 0U);
|
||||
EXPECT_EQ(listener->PreCommitCount(), listener->BeginCount());
|
||||
EXPECT_EQ(listener->CompletedCount(), listener->BeginCount());
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
class TestDBShutdownBeginListener : public EventListener {
|
||||
public:
|
||||
void OnDBShutdownBegin(DB* db) override {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
++shutdown_count_;
|
||||
last_db_ = db;
|
||||
}
|
||||
|
||||
int ShutdownCount() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return shutdown_count_;
|
||||
}
|
||||
|
||||
DB* LastDB() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return last_db_;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex mutex_;
|
||||
int shutdown_count_ = 0;
|
||||
DB* last_db_ = nullptr;
|
||||
};
|
||||
|
||||
TEST_F(EventListenerTest, OnDBShutdownBeginOnceForCancelAndClose) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
auto listener = std::make_shared<TestDBShutdownBeginListener>();
|
||||
options.listeners.emplace_back(listener);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
DB* db = db_.get();
|
||||
dbfull()->CancelAllBackgroundWork(false);
|
||||
EXPECT_EQ(1, listener->ShutdownCount());
|
||||
EXPECT_EQ(db, listener->LastDB());
|
||||
|
||||
Close();
|
||||
EXPECT_EQ(1, listener->ShutdownCount());
|
||||
}
|
||||
|
||||
TEST_F(EventListenerTest, OnDBShutdownBeginOnFailedOpen) {
|
||||
Close();
|
||||
|
||||
std::shared_ptr<FaultInjectionTestFS> fs(
|
||||
new FaultInjectionTestFS(env_->GetFileSystem()));
|
||||
std::unique_ptr<Env> env(NewCompositeEnv(fs));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.env = env.get();
|
||||
options.create_if_missing = true;
|
||||
auto listener = std::make_shared<TestDBShutdownBeginListener>();
|
||||
options.listeners.emplace_back(listener);
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"PersistRocksDBOptions:create",
|
||||
[&](void* /*arg*/) { fs->SetFilesystemActive(false); });
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"PersistRocksDBOptions:written",
|
||||
[&](void* /*arg*/) { fs->SetFilesystemActive(true); });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
std::unique_ptr<DB> db;
|
||||
Status s = DB::Open(options, dbname_, &db);
|
||||
ASSERT_TRUE(s.IsIOError()) << s.ToString();
|
||||
EXPECT_EQ(1, listener->ShutdownCount());
|
||||
EXPECT_NE(nullptr, listener->LastDB());
|
||||
EXPECT_EQ(nullptr, db.get());
|
||||
|
||||
fs->SetFilesystemActive(true);
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
// This simple Listener can only handle one flush at a time.
|
||||
class TestFlushListener : public EventListener {
|
||||
public:
|
||||
@@ -1696,7 +1885,7 @@ TEST_F(EventListenerTest, BackgroundJobPressure) {
|
||||
Env::Priority::LOW);
|
||||
sleeping_task.WaitUntilSleeping();
|
||||
|
||||
// Phase 1: No pressure — 3 SST files (below slowdown trigger=4).
|
||||
// Phase 1: No pressure -- 3 SST files (below slowdown trigger=4).
|
||||
for (int i = 0; i < 3; i++) {
|
||||
ASSERT_OK(Put("k" + std::to_string(i), std::string(100, 'x')));
|
||||
ASSERT_OK(Flush());
|
||||
@@ -1714,7 +1903,7 @@ TEST_F(EventListenerTest, BackgroundJobPressure) {
|
||||
// MaybeScheduleFlushOrCompaction() runs before the pressure callback and
|
||||
// may schedule new flush work, making flush counts non-deterministic.
|
||||
|
||||
// Phase 2: Build pressure — flush past slowdown trigger (4 L0 SST files).
|
||||
// Phase 2: Build pressure -- flush past slowdown trigger (4 L0 SST files).
|
||||
// Compaction is blocked, so L0 SST files pile up.
|
||||
listener->Reset();
|
||||
{
|
||||
@@ -1750,11 +1939,11 @@ TEST_F(EventListenerTest, BackgroundJobPressure) {
|
||||
ASSERT_TRUE(found_compaction_scheduled);
|
||||
ASSERT_TRUE(found_high_proximity);
|
||||
|
||||
// Phase 3: Relieve pressure — unblock compaction, wait for completion.
|
||||
// Phase 3: Relieve pressure -- unblock compaction, wait for completion.
|
||||
listener->Reset();
|
||||
sleeping_task.WakeUp();
|
||||
sleeping_task.WaitUntilDone();
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForBackgroundWork());
|
||||
|
||||
snapshots = listener->GetSnapshots();
|
||||
CheckInvariants(snapshots);
|
||||
|
||||
+1
-4
@@ -48,15 +48,12 @@ Reader::Reader(std::shared_ptr<Logger> info_log,
|
||||
first_record_read_(false),
|
||||
compression_type_(kNoCompression),
|
||||
compression_type_record_read_(false),
|
||||
uncompress_(nullptr),
|
||||
uncompress_(),
|
||||
hash_state_(nullptr),
|
||||
uncompress_hash_state_(nullptr) {}
|
||||
|
||||
Reader::~Reader() {
|
||||
delete[] backing_store_;
|
||||
if (uncompress_) {
|
||||
delete uncompress_;
|
||||
}
|
||||
if (hash_state_) {
|
||||
XXH3_freeState(hash_state_);
|
||||
}
|
||||
|
||||
+1
-1
@@ -175,7 +175,7 @@ class Reader {
|
||||
CompressionType compression_type_;
|
||||
// Track whether the compression type record has been read or not.
|
||||
bool compression_type_record_read_;
|
||||
StreamingUncompress* uncompress_;
|
||||
std::unique_ptr<StreamingUncompress> uncompress_;
|
||||
// Reusable uncompressed output buffer
|
||||
std::unique_ptr<char[]> uncompressed_buffer_;
|
||||
// Reusable uncompressed record
|
||||
|
||||
+3
-6
@@ -1224,11 +1224,11 @@ TEST_P(StreamingCompressionTest, Basic) {
|
||||
}
|
||||
CompressionOptions opts;
|
||||
constexpr uint32_t compression_format_version = 2;
|
||||
StreamingCompress* compress = StreamingCompress::Create(
|
||||
auto compress = StreamingCompress::Create(
|
||||
compression_type, opts, compression_format_version, kBlockSize);
|
||||
StreamingUncompress* uncompress = StreamingUncompress::Create(
|
||||
auto uncompress = StreamingUncompress::Create(
|
||||
compression_type, compression_format_version, kBlockSize);
|
||||
MemoryAllocator* allocator = new DefaultMemoryAllocator();
|
||||
std::unique_ptr<MemoryAllocator> allocator(new DefaultMemoryAllocator());
|
||||
std::string input_buffer = BigString("abc", input_size);
|
||||
std::vector<std::string> compressed_buffers;
|
||||
size_t remaining;
|
||||
@@ -1266,9 +1266,6 @@ TEST_P(StreamingCompressionTest, Basic) {
|
||||
} while (ret_val > 0 || output_pos == kBlockSize);
|
||||
}
|
||||
allocator->Deallocate((void*)uncompressed_output_buffer);
|
||||
delete allocator;
|
||||
delete compress;
|
||||
delete uncompress;
|
||||
// The final return value from uncompress() should be 0.
|
||||
ASSERT_EQ(ret_val, 0);
|
||||
ASSERT_EQ(input_buffer, uncompressed_buffer);
|
||||
|
||||
+1
-4
@@ -32,7 +32,7 @@ Writer::Writer(std::unique_ptr<WritableFileWriter>&& dest, uint64_t log_number,
|
||||
header_size_(recycle_log_files ? kRecyclableHeaderSize : kHeaderSize),
|
||||
manual_flush_(manual_flush),
|
||||
compression_type_(compression_type),
|
||||
compress_(nullptr),
|
||||
compress_(),
|
||||
track_and_verify_wals_(track_and_verify_wals),
|
||||
last_seqno_recorded_(0) {
|
||||
for (uint8_t i = 0; i <= kMaxRecordType; i++) {
|
||||
@@ -48,9 +48,6 @@ Writer::~Writer() {
|
||||
if (dest_) {
|
||||
WriteBuffer(WriteOptions()).PermitUncheckedError();
|
||||
}
|
||||
if (compress_) {
|
||||
delete compress_;
|
||||
}
|
||||
ThreadStatusUtil::SetThreadOperation(cur_op_type);
|
||||
}
|
||||
|
||||
|
||||
Loaded 100 of 317 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user