mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-06 02:52:35 -05:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
937bcacfe9 | ||
|
|
f051e35ce8 | ||
|
|
9ed9d29162 | ||
|
|
336e4f9db1 | ||
|
|
b8672a8101 | ||
|
|
53f215f6d4 | ||
|
|
75da341340 | ||
|
|
e603f55e5f | ||
|
|
1c634ade4a | ||
|
|
81ee59210c | ||
|
|
08d8122809 | ||
|
|
7be9ffc81a | ||
|
|
1d37a88852 | ||
|
|
7b0019a1b8 | ||
|
|
d3a28293a5 | ||
|
|
2b3cdc5764 | ||
|
|
6c65d8046d | ||
|
|
2126b6e3ba |
No files matched your search
@@ -1,27 +0,0 @@
|
||||
// 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,25 +7,11 @@
|
||||
// Parameters:
|
||||
// executionFile - path to the JSON execution log from claude-code-base-action
|
||||
// conclusion - 'success' or 'failure' from the action output
|
||||
// meta - { trigger, autoMode, headSha, reviewer, isQuery, isPartial
|
||||
// }
|
||||
// meta - { trigger, 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 {
|
||||
@@ -94,25 +80,36 @@ 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 = getTriggerLine();
|
||||
const triggerLine = meta.trigger === 'auto' ?
|
||||
`*Auto-triggered after CI passed — reviewing commit ${
|
||||
meta.headSha.substring(0, 7)}*` :
|
||||
`*Requested by @${meta.reviewer}*`;
|
||||
|
||||
return buildComment({
|
||||
icon,
|
||||
headerTitle,
|
||||
return [
|
||||
`## ${icon} ${headerTitle}`,
|
||||
'',
|
||||
triggerLine,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
responseBody,
|
||||
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',
|
||||
],
|
||||
});
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'<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');
|
||||
};
|
||||
@@ -1,98 +0,0 @@
|
||||
// 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,31 +14,9 @@
|
||||
// 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,
|
||||
legacyMarkers = [],
|
||||
prunePrefix = '',
|
||||
preserveLatest = 0,
|
||||
obsoleteMarker = '',
|
||||
obsoleteTitle = '',
|
||||
}) {
|
||||
module.exports = async function postPrComment(
|
||||
{github, context, core, prNumber, body, marker}) {
|
||||
if (!prNumber || !body) {
|
||||
core.warning('Missing prNumber or body; skipping comment.');
|
||||
return;
|
||||
@@ -50,155 +28,30 @@ module.exports = async function postPrComment({
|
||||
// Ensure marker is embedded in the body
|
||||
const markedBody = body.includes(marker) ? body : `${marker}\n${body}`;
|
||||
|
||||
async function listComments() {
|
||||
return await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
// 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) {
|
||||
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({
|
||||
await github.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: existing.id,
|
||||
body: markedBody,
|
||||
});
|
||||
core.info(`Updated existing comment ${existing.id}`);
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: markedBody,
|
||||
});
|
||||
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');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,347 +0,0 @@
|
||||
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
@@ -1,185 +0,0 @@
|
||||
# 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,8 +1,12 @@
|
||||
# Claude 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.
|
||||
# Companion to claude-review.yml. Downloads the review artifact and posts
|
||||
# it as a PR comment. This workflow has write permissions but never runs
|
||||
# AI or processes untrusted code.
|
||||
#
|
||||
# Security: Separating analysis (has ANTHROPIC_API_KEY, no write perms)
|
||||
# from posting (has GITHUB_TOKEN write perms, no AI) prevents a crafted
|
||||
# PR from tricking Claude into exfiltrating tokens.
|
||||
|
||||
name: Post Claude Review Comment
|
||||
|
||||
@@ -16,10 +20,127 @@ permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
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
|
||||
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.');
|
||||
@@ -1,8 +1,20 @@
|
||||
# Claude 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.
|
||||
# This workflow runs Claude to review PRs. It produces a markdown review
|
||||
# as an artifact but does NOT post comments (no write permissions).
|
||||
# The companion workflow (claude-review-comment.yml) handles posting.
|
||||
#
|
||||
# Triggers:
|
||||
# 1. workflow_run: Auto-review after pr-jobs passes
|
||||
# 2. issue_comment: Manual /claude-review or /claude-query by maintainers
|
||||
# 3. workflow_dispatch: Manual testing
|
||||
#
|
||||
# Security:
|
||||
# - This job has contents:read ONLY (no PR write, no issue write)
|
||||
# - ANTHROPIC_API_KEY is used here but never coexists with write tokens
|
||||
# - Claude uses read-only tools (View, GlobTool, GrepTool) plus Write
|
||||
# (only for incremental findings file, not repo modifications)
|
||||
# - Review methodology: claude_md/code_review.md
|
||||
|
||||
name: Claude Code Review
|
||||
|
||||
@@ -11,11 +23,6 @@ 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]
|
||||
|
||||
@@ -26,44 +33,560 @@ on:
|
||||
required: true
|
||||
type: number
|
||||
model:
|
||||
description: Claude model to use (defaults to latest Opus)
|
||||
description: Claude model to use
|
||||
required: false
|
||||
type: choice
|
||||
options:
|
||||
- claude-opus-4-6
|
||||
- claude-sonnet-4-6
|
||||
- claude-sonnet-4-20250514
|
||||
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:
|
||||
review:
|
||||
# ======================== Auto Review ======================== #
|
||||
auto-review:
|
||||
name: Auto Claude Review
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
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: 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
|
||||
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
|
||||
@@ -1,25 +0,0 @@
|
||||
# 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
|
||||
@@ -1,68 +0,0 @@
|
||||
# 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
|
||||
+14
-1
@@ -1,6 +1,20 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 11.2.4 (06/24/2026)
|
||||
### Bug Fixes
|
||||
* Fixed a bug where closing a read-only DB instance could delete live SST files created by a concurrent read-write DB sharing the same directory.
|
||||
|
||||
## 11.2.3 (05/07/2026)
|
||||
### New Features
|
||||
* Add experimental mutable DBOption `optimize_manifest_for_recovery` (default false). When enabled, RocksDB can reduce recovery work after a clean shutdown, which may lower DB::Open latency on warm reopens.
|
||||
* Add experimental `reuse_manifest_on_open` DBOption (default false). When enabled, DB::Open reuses the existing MANIFEST file for append instead of creating a fresh one, avoiding the cost of serializing the entire database state into a new MANIFEST on the first post-open write.
|
||||
|
||||
## 11.2.2 (05/07/2026)
|
||||
## 11.2.1 (04/29/2026)
|
||||
### Bug Fixes
|
||||
* Fix bug in range tombstone synthesis that covers live keys added during an IngestExternalFile
|
||||
|
||||
## 11.2.0 (04/18/2026)
|
||||
### New Features
|
||||
* Added experimental `DBOptions::fast_sst_open` option. When enabled, RocksDB retrieves opaque file system metadata for SST files after flush, compaction, and external file ingestion, persists it in the MANIFEST, and passes it back to the file system on subsequent file opens to accelerate DB open time.
|
||||
@@ -21,7 +35,6 @@
|
||||
* 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.
|
||||
|
||||
@@ -384,10 +384,6 @@ 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
|
||||
@@ -1272,8 +1268,6 @@ 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 \
|
||||
@@ -1289,7 +1283,6 @@ 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 \
|
||||
|
||||
@@ -37,7 +37,7 @@ if [ "$?" != "1" ]; then
|
||||
BAD=1
|
||||
fi
|
||||
|
||||
LC_ALL=C git grep -n $'[\x80-\xff]' -- ':!docs' ':!*.md' ':!.github'
|
||||
git grep -n -P "[\x80-\xFF]" -- ':!docs' ':!*.md'
|
||||
if [ "$?" != "1" ]; then
|
||||
echo '^^^^ Use only ASCII characters in source files'
|
||||
BAD=1
|
||||
|
||||
@@ -7,13 +7,6 @@ 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,61 +0,0 @@
|
||||
# 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,54 +1,14 @@
|
||||
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.
|
||||
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).
|
||||
|
||||
## Required Output Structure
|
||||
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`.
|
||||
|
||||
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.
|
||||
Write ONLY the formatted review as your final response -- no
|
||||
commentary or explanation outside the review itself.
|
||||
@@ -481,61 +481,6 @@ 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,13 +7,6 @@ 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
|
||||
|
||||
|
||||
@@ -54,14 +54,13 @@ 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, const CompressionOptions& compression_opts) {
|
||||
CompressionType compression) {
|
||||
assert(compression <= kLastBuiltinCompression);
|
||||
|
||||
static thread_local std::array<DirectWriteCompressionState,
|
||||
@@ -72,15 +71,12 @@ DirectWriteCompressionState& GetDirectWriteCompressionState(
|
||||
auto& compression_state =
|
||||
compression_states[static_cast<size_t>(compression)];
|
||||
if (compression != kNoCompression &&
|
||||
(compression_state.compressor == nullptr ||
|
||||
compression_state.compression_opts != compression_opts)) {
|
||||
// BDW compression settings are mutable, so rebuild the per-thread cached
|
||||
// compressor when the latest published opts for this type change.
|
||||
compression_state.working_area = Compressor::ManagedWorkingArea{};
|
||||
compression_state.compressor.reset();
|
||||
compression_state.compression_opts = compression_opts;
|
||||
compression_state.compressor == nullptr) {
|
||||
// BDW v1 mirrors BlobFileBuilder by using the built-in compressor with the
|
||||
// default CompressionOptions only. Cache it per thread so repeated writes
|
||||
// do not allocate a new compressor and working area every time.
|
||||
compression_state.compressor =
|
||||
GetBuiltinV2CompressionManager()->GetCompressor(compression_opts,
|
||||
GetBuiltinV2CompressionManager()->GetCompressor(CompressionOptions{},
|
||||
compression);
|
||||
if (compression_state.compressor != nullptr) {
|
||||
compression_state.working_area =
|
||||
@@ -394,15 +390,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.blob_cache == nullptr ||
|
||||
settings.prepopulate_blob_cache != PrepopulateBlobCache::kFlushOnly) {
|
||||
if (settings == nullptr || 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);
|
||||
@@ -423,7 +419,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);
|
||||
@@ -435,8 +431,7 @@ Status BlobFilePartitionManager::WriteBlob(
|
||||
GrowableBuffer compressed_value;
|
||||
Slice write_value = value;
|
||||
if (compression != kNoCompression) {
|
||||
auto& compression_state =
|
||||
GetDirectWriteCompressionState(compression, settings.compression_opts);
|
||||
auto& compression_state = GetDirectWriteCompressionState(compression);
|
||||
if (compression_state.compressor == nullptr) {
|
||||
return Status::NotSupported(
|
||||
"Blob direct write compression type not supported");
|
||||
@@ -786,19 +781,13 @@ Status BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
|
||||
}
|
||||
}
|
||||
|
||||
Status s = version != nullptr ? Status::Corruption("Invalid blob file number")
|
||||
: Status::NotFound();
|
||||
|
||||
if (blob_file_cache == nullptr) {
|
||||
return version != nullptr ? Status::Corruption("Invalid blob file number")
|
||||
: Status::NotFound();
|
||||
return s;
|
||||
}
|
||||
|
||||
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,
|
||||
const BlobDirectWriteSettings* settings = nullptr,
|
||||
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);
|
||||
// Seeds the blob cache with the original uncompressed value when configured.
|
||||
Status MaybePrepopulateBlobCache(const BlobDirectWriteSettings& settings,
|
||||
// Optionally seeds the blob cache with the original uncompressed value.
|
||||
Status MaybePrepopulateBlobCache(const BlobDirectWriteSettings* settings,
|
||||
const Slice& original_value,
|
||||
uint64_t blob_file_number,
|
||||
uint64_t blob_offset);
|
||||
|
||||
@@ -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,9 +33,7 @@ struct BlobDirectWriteSettings {
|
||||
uint64_t min_blob_size = 0;
|
||||
// Compression to use for newly written blob records.
|
||||
CompressionType compression_type = kNoCompression;
|
||||
// Compression options for newly written blob records.
|
||||
CompressionOptions compression_opts;
|
||||
// Raw pointer -- the Cache is owned by ColumnFamilyOptions and outlives all
|
||||
// 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,47 +1620,6 @@ 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();
|
||||
|
||||
@@ -151,48 +151,6 @@ 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,
|
||||
@@ -986,70 +944,6 @@ 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,16 +128,6 @@ 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;
|
||||
@@ -281,55 +271,6 @@ 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,
|
||||
@@ -2197,7 +2138,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
|
||||
|
||||
@@ -734,42 +734,6 @@ 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;
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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(
|
||||
|
||||
+8
-8
@@ -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"));
|
||||
@@ -777,7 +777,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 +805,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();
|
||||
|
||||
@@ -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"));
|
||||
|
||||
+5
-143
@@ -11833,7 +11833,7 @@ TEST_F(DBCompactionTest, RoundRobinCleanCutWithSharedBoundary) {
|
||||
// 2. A post-verification step fails (injected here via sync point), setting
|
||||
// compact_->status to error while each subcompaction's status stays OK.
|
||||
// 3. SubcompactionState::Cleanup checks individual status (OK) and skips
|
||||
// ReleaseObsolete -- the cache entries leak.
|
||||
// ReleaseObsolete — the cache entries leak.
|
||||
// 4. FaultInjectionTestFS injects metadata read errors, causing GetChildren
|
||||
// to fail in FindObsoleteFiles.
|
||||
// 5. Close()'s FindObsoleteFiles also fails to find the orphan for the same
|
||||
@@ -11864,7 +11864,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
|
||||
// fail while individual subcompaction statuses stay OK (so Cleanup skips
|
||||
// ReleaseObsolete). The filesystem deactivation makes GetChildren fail
|
||||
// in FindObsoleteFiles, preventing the backstop from evicting the leaked
|
||||
// cache entries -- matching the crash test's metadata read fault injection.
|
||||
// cache entries — matching the crash test's metadata read fault injection.
|
||||
std::atomic<bool> inject_error{true};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::Run():AfterVerifyOutputFiles", [&](void* arg) {
|
||||
@@ -11876,7 +11876,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
|
||||
// Enable metadata read fault injection on the bg compaction thread after
|
||||
// the compaction job finishes but before FindObsoleteFiles runs. This
|
||||
// makes GetChildren fail (metadata read), matching crash test's
|
||||
// --open_metadata_read_fault_one_in=8. Only metadata reads fail --
|
||||
// --open_metadata_read_fault_one_in=8. Only metadata reads fail —
|
||||
// logging and other IO operations continue normally.
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"BackgroundCallCompaction:1", [&](void*) {
|
||||
@@ -11889,7 +11889,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Trigger compaction -- fails after VerifyOutputFiles.
|
||||
// Trigger compaction — fails after VerifyOutputFiles.
|
||||
Status s = dbfull()->TEST_CompactRange(0, nullptr, nullptr);
|
||||
ASSERT_NOK(s);
|
||||
|
||||
@@ -11971,7 +11971,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,144 +11993,6 @@ 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;
|
||||
|
||||
+1
-8
@@ -1852,8 +1852,6 @@ 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.
|
||||
@@ -1874,8 +1872,6 @@ 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);
|
||||
@@ -1895,9 +1891,6 @@ 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();
|
||||
}
|
||||
|
||||
@@ -3890,7 +3883,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);
|
||||
|
||||
|
||||
+23
-229
@@ -30,7 +30,6 @@
|
||||
|
||||
#include "db/arena_wrapped_db_iter.h"
|
||||
#include "db/attribute_group_iterator_impl.h"
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/blob/blob_file_partition_manager.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/builder.h"
|
||||
@@ -175,6 +174,7 @@ DBImpl::DBImpl(const DBOptions& options, const std::string& dbname,
|
||||
const bool seq_per_batch, const bool batch_per_txn,
|
||||
bool read_only)
|
||||
: dbname_(dbname),
|
||||
read_only_(read_only),
|
||||
own_info_log_(options.info_log == nullptr),
|
||||
initial_db_options_(SanitizeOptions(dbname, options, read_only,
|
||||
&init_logger_creation_s_)),
|
||||
@@ -483,18 +483,6 @@ void DBImpl::WaitForBackgroundWork() {
|
||||
}
|
||||
}
|
||||
|
||||
void DBImpl::WaitForAsyncFileOpen() {
|
||||
if (!immutable_db_options_.open_files_async) {
|
||||
return;
|
||||
}
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
TEST_SYNC_POINT("DBImpl::WaitForAsyncFileOpen::BeforeWait");
|
||||
while (bg_async_file_open_state_ == AsyncFileOpenState::kScheduled &&
|
||||
!shutting_down_.load(std::memory_order_acquire)) {
|
||||
bg_cv_.Wait();
|
||||
}
|
||||
}
|
||||
|
||||
// Will lock the mutex_, will wait for completion if wait is true
|
||||
void DBImpl::CancelAllBackgroundWork(bool wait) {
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
@@ -653,7 +641,8 @@ void DBImpl::UnregisterBlobDirectWriteColumnFamily() {
|
||||
Status DBImpl::MaybeWriteWalMarkersToManifestOnClose() {
|
||||
mutex_.AssertHeld();
|
||||
if (!mutable_db_options_.optimize_manifest_for_recovery ||
|
||||
!opened_successfully_ || versions_ == nullptr || logs_.empty()) {
|
||||
!opened_successfully_ || read_only_ || versions_ == nullptr ||
|
||||
logs_.empty()) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -840,7 +829,7 @@ Status DBImpl::CloseHelper() {
|
||||
// manifest file), it is not able to identify live files correctly. As a
|
||||
// result, all "live" files can get deleted by accident. However, corrupted
|
||||
// manifest is recoverable by RepairDB().
|
||||
if (opened_successfully_) {
|
||||
if (opened_successfully_ && !read_only_) {
|
||||
JobContext job_context(next_job_id_.fetch_add(1));
|
||||
FindObsoleteFiles(&job_context, true);
|
||||
|
||||
@@ -2872,148 +2861,6 @@ Status DBImpl::ResolveDirectWriteWideColumns(const ReadOptions& read_options,
|
||||
return status;
|
||||
}
|
||||
|
||||
bool DBImpl::MaybeResolveMemtableBlobValue(const Slice& key,
|
||||
const BlobFetcher* blob_fetcher,
|
||||
PinnableSlice* value,
|
||||
PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index,
|
||||
bool* value_found) {
|
||||
if (!s->ok() || (!value && !columns)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto reset_outputs = [&]() {
|
||||
if (value != nullptr) {
|
||||
value->Reset();
|
||||
}
|
||||
if (columns != nullptr) {
|
||||
columns->Reset();
|
||||
}
|
||||
};
|
||||
auto clear_blob_state = [&]() {
|
||||
if (is_blob_index != nullptr) {
|
||||
*is_blob_index = false;
|
||||
}
|
||||
};
|
||||
|
||||
const bool needs_plain_value_resolution =
|
||||
is_blob_index != nullptr && *is_blob_index;
|
||||
const bool needs_wide_column_resolution =
|
||||
columns != nullptr && !columns->unresolved_blob_column_indices_.empty();
|
||||
if (!needs_plain_value_resolution && !needs_wide_column_resolution) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (blob_fetcher == nullptr) {
|
||||
reset_outputs();
|
||||
*s = Status::NotSupported(
|
||||
"Encountered blob-backed memtable value without blob fetcher.");
|
||||
clear_blob_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (needs_plain_value_resolution) {
|
||||
Slice blob_index_slice;
|
||||
std::string blob_index_storage;
|
||||
if (value != nullptr) {
|
||||
if (value->size() > 0) {
|
||||
blob_index_slice = Slice(value->data(), value->size());
|
||||
} else {
|
||||
blob_index_slice = Slice(*(value->GetSelf()));
|
||||
}
|
||||
} else {
|
||||
assert(columns != nullptr);
|
||||
|
||||
const WideColumns& plain_value_columns = columns->columns();
|
||||
assert(plain_value_columns.size() == 1);
|
||||
assert(plain_value_columns.front().name() == kDefaultWideColumnName);
|
||||
blob_index_slice = plain_value_columns.front().value();
|
||||
}
|
||||
|
||||
PinnableSlice resolved_value;
|
||||
PinnableSlice* target = value != nullptr ? value : &resolved_value;
|
||||
if (value != nullptr) {
|
||||
// BlobIndex::DecodeFrom can retain Slices into the encoded bytes for
|
||||
// inlined blob indices, so take an owned copy before resetting `value`
|
||||
// and reusing the same PinnableSlice as the output target.
|
||||
blob_index_storage.assign(blob_index_slice.data(),
|
||||
blob_index_slice.size());
|
||||
blob_index_slice = Slice(blob_index_storage);
|
||||
value->Reset();
|
||||
}
|
||||
|
||||
*s = blob_fetcher->FetchBlob(key, blob_index_slice,
|
||||
nullptr /* prefetch_buffer */, target,
|
||||
nullptr /* bytes_read */);
|
||||
if (s->ok() && columns != nullptr) {
|
||||
columns->SetPlainValue(std::move(*target));
|
||||
} else if (!s->ok()) {
|
||||
reset_outputs();
|
||||
if (s->IsIncomplete() && value_found != nullptr) {
|
||||
*value_found = false;
|
||||
}
|
||||
}
|
||||
|
||||
clear_blob_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
assert(columns != nullptr);
|
||||
|
||||
std::string resolved_entity;
|
||||
bool resolved = false;
|
||||
*s = WideColumnSerialization::ResolveEntityBlobColumns(
|
||||
columns->value_, key, blob_fetcher, nullptr /* prefetch_buffers */,
|
||||
resolved_entity, resolved, nullptr /* total_bytes_read */,
|
||||
nullptr /* num_blobs_resolved */);
|
||||
if (s->ok()) {
|
||||
assert(resolved);
|
||||
if (resolved) {
|
||||
*s = columns->SetWideColumnValue(std::move(resolved_entity));
|
||||
}
|
||||
}
|
||||
if (!s->ok()) {
|
||||
reset_outputs();
|
||||
if (s->IsIncomplete() && value_found != nullptr) {
|
||||
*value_found = false;
|
||||
}
|
||||
}
|
||||
|
||||
clear_blob_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DBImpl::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) {
|
||||
if (resolve_blob_backed_memtable_value) {
|
||||
std::string blob_lookup_key_storage;
|
||||
const bool value_resolved = MaybeResolveMemtableBlobValue(
|
||||
GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage),
|
||||
memtable_blob_fetcher, value, columns, s, is_blob_index, value_found);
|
||||
if (!value_resolved && value != nullptr && s->ok()) {
|
||||
value->PinSelf();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (s->ok()) {
|
||||
if (value != nullptr) {
|
||||
value->PinSelf();
|
||||
}
|
||||
} else {
|
||||
if (value != nullptr) {
|
||||
value->Reset();
|
||||
}
|
||||
if (columns != nullptr) {
|
||||
columns->Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool DBImpl::MaybeResolveDirectWriteValue(
|
||||
const ReadOptions& read_options, const Slice& key,
|
||||
bool resolve_direct_write_value, const Version* current,
|
||||
@@ -3229,14 +3076,6 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
|
||||
}
|
||||
const bool resolve_direct_write_value =
|
||||
partition_mgr != nullptr && (is_blob_ptr == &is_blob_index);
|
||||
std::optional<BlobFetcher> memtable_blob_fetcher;
|
||||
if (partition_mgr != nullptr) {
|
||||
memtable_blob_fetcher.emplace(sv->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;
|
||||
std::string blob_lookup_key_storage;
|
||||
auto get_blob_lookup_key = [&]() -> Slice {
|
||||
return GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage);
|
||||
@@ -3264,21 +3103,19 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
|
||||
get_impl_options.columns, timestamp, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */,
|
||||
get_impl_options.callback, is_blob_ptr,
|
||||
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
|
||||
get_impl_options.callback, is_blob_ptr)) {
|
||||
done = true;
|
||||
maybe_resolve_memtable_value();
|
||||
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
sv->imm->Get(lkey,
|
||||
get_impl_options.value
|
||||
? get_impl_options.value->GetSelf()
|
||||
: nullptr,
|
||||
get_impl_options.columns, timestamp, &s,
|
||||
&merge_context, &max_covering_tombstone_seq,
|
||||
read_options, get_impl_options.callback,
|
||||
is_blob_ptr, memtable_blob_fetcher_ptr)) {
|
||||
sv->imm->Get(
|
||||
lkey,
|
||||
get_impl_options.value ? get_impl_options.value->GetSelf()
|
||||
: nullptr,
|
||||
get_impl_options.columns, timestamp, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
get_impl_options.callback, is_blob_ptr)) {
|
||||
done = true;
|
||||
maybe_resolve_memtable_value();
|
||||
|
||||
@@ -3290,14 +3127,14 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
|
||||
if (sv->mem->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr,
|
||||
/*timestamp=*/nullptr, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */, nullptr, nullptr, false,
|
||||
memtable_blob_fetcher_ptr)) {
|
||||
false /* immutable_memtable */, nullptr, nullptr,
|
||||
false)) {
|
||||
done = true;
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
sv->imm->GetMergeOperands(
|
||||
lkey, &s, &merge_context, &max_covering_tombstone_seq,
|
||||
read_options, memtable_blob_fetcher_ptr)) {
|
||||
sv->imm->GetMergeOperands(lkey, &s, &merge_context,
|
||||
&max_covering_tombstone_seq,
|
||||
read_options)) {
|
||||
done = true;
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
}
|
||||
@@ -3993,14 +3830,6 @@ Status DBImpl::MultiGetImpl(
|
||||
(*sorted_keys)[start_key]->column_family);
|
||||
ColumnFamilyData* cfd = cfh->cfd();
|
||||
auto* partition_mgr = cfd->blob_partition_manager();
|
||||
std::optional<BlobFetcher> memtable_blob_fetcher;
|
||||
if (partition_mgr != nullptr) {
|
||||
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;
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
for (size_t i = start_key; i < start_key + num_keys; ++i) {
|
||||
@@ -4047,11 +3876,9 @@ Status DBImpl::MultiGetImpl(
|
||||
has_unpersisted_data_.load(std::memory_order_relaxed));
|
||||
if (!skip_memtable) {
|
||||
super_version->mem->MultiGet(read_options, &range, callback,
|
||||
false /* immutable_memtable */,
|
||||
memtable_blob_fetcher_ptr);
|
||||
false /* immutable_memtable */);
|
||||
if (!range.empty()) {
|
||||
super_version->imm->MultiGet(read_options, &range, callback,
|
||||
memtable_blob_fetcher_ptr);
|
||||
super_version->imm->MultiGet(read_options, &range, callback);
|
||||
}
|
||||
if (!range.empty()) {
|
||||
uint64_t left = range.KeysLeft();
|
||||
@@ -4428,20 +4255,8 @@ Status DBImpl::CreateColumnFamilyImpl(const ReadOptions& read_options,
|
||||
const std::string& column_family_name,
|
||||
ColumnFamilyHandle** handle) {
|
||||
options_mutex_.AssertHeld();
|
||||
*handle = nullptr;
|
||||
|
||||
// The empty string is reserved as a "no/unknown column family name" sentinel
|
||||
// in various APIs and serialization formats (e.g.
|
||||
// TablePropertiesCollectorFactory::Context::kUnknownColumnFamily, table
|
||||
// properties), so reject it to avoid ambiguity. Note: prior to this check,
|
||||
// CreateColumnFamily("") would silently "succeed" but the CF was not
|
||||
// persisted in the manifest, causing data loss across DB reopens.
|
||||
if (column_family_name.empty()) {
|
||||
return Status::InvalidArgument(
|
||||
"Column family name cannot be the empty string");
|
||||
}
|
||||
|
||||
Status s;
|
||||
*handle = nullptr;
|
||||
|
||||
DBOptions db_options =
|
||||
BuildDBOptions(immutable_db_options_, mutable_db_options_);
|
||||
@@ -6498,20 +6313,12 @@ Status DBImpl::GetLatestSequenceForKey(
|
||||
|
||||
*seq = kMaxSequenceNumber;
|
||||
*found_record_for_key = false;
|
||||
std::optional<BlobFetcher> memtable_blob_fetcher;
|
||||
if (cfd->blob_partition_manager() != nullptr) {
|
||||
memtable_blob_fetcher.emplace(sv->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;
|
||||
|
||||
// Check if there is a record for this key in the latest memtable
|
||||
sv->mem->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr, timestamp, &s,
|
||||
&merge_context, &max_covering_tombstone_seq, seq, read_options,
|
||||
false /* immutable_memtable */, nullptr /*read_callback*/,
|
||||
is_blob_index, /*do_merge=*/true, memtable_blob_fetcher_ptr);
|
||||
is_blob_index);
|
||||
|
||||
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
|
||||
// unexpected error reading memtable.
|
||||
@@ -6544,8 +6351,7 @@ Status DBImpl::GetLatestSequenceForKey(
|
||||
// Check if there is a record for this key in the immutable memtables
|
||||
sv->imm->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr, timestamp, &s,
|
||||
&merge_context, &max_covering_tombstone_seq, seq, read_options,
|
||||
nullptr /*read_callback*/, is_blob_index,
|
||||
memtable_blob_fetcher_ptr);
|
||||
nullptr /*read_callback*/, is_blob_index);
|
||||
|
||||
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
|
||||
// unexpected error reading memtable.
|
||||
@@ -6578,7 +6384,7 @@ Status DBImpl::GetLatestSequenceForKey(
|
||||
sv->imm->GetFromHistory(lkey, /*value=*/nullptr, /*columns=*/nullptr,
|
||||
timestamp, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, seq, read_options,
|
||||
is_blob_index, memtable_blob_fetcher_ptr);
|
||||
is_blob_index);
|
||||
|
||||
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
|
||||
// unexpected error reading memtable.
|
||||
@@ -7580,7 +7386,6 @@ Status DBImpl::ReserveFileNumbersBeforeIngestion(
|
||||
Status DBImpl::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
|
||||
if (mutable_db_options_.max_open_files == -1) {
|
||||
uint64_t oldest_time = std::numeric_limits<uint64_t>::max();
|
||||
std::once_flag waited_for_async_open;
|
||||
for (auto cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (!cfd->IsDropped()) {
|
||||
uint64_t ctime;
|
||||
@@ -7588,17 +7393,6 @@ Status DBImpl::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
|
||||
SuperVersion* sv = GetAndRefSuperVersion(cfd);
|
||||
Version* version = sv->current;
|
||||
version->GetCreationTimeOfOldestFile(&ctime);
|
||||
// For modern DBs, manifest carries file_creation_time and the
|
||||
// first call returns the real value. We only need to wait for
|
||||
// BGWorkAsyncFileOpen on legacy DBs whose manifest lacks
|
||||
// file_creation_time -- those rely on the pinned reader, which is
|
||||
// null until async file open completes.
|
||||
if (ctime == 0 && immutable_db_options_.open_files_async) {
|
||||
std::call_once(waited_for_async_open, [&]() {
|
||||
WaitForAsyncFileOpen();
|
||||
version->GetCreationTimeOfOldestFile(&ctime);
|
||||
});
|
||||
}
|
||||
ReturnAndCleanupSuperVersion(cfd, sv);
|
||||
}
|
||||
|
||||
|
||||
+1
-37
@@ -1096,7 +1096,6 @@ 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.
|
||||
@@ -1372,6 +1371,7 @@ class DBImpl : public DB {
|
||||
|
||||
protected:
|
||||
const std::string dbname_;
|
||||
const bool read_only_;
|
||||
// TODO(peterd): unify with VersionSet::db_id_
|
||||
std::string db_id_;
|
||||
// db_session_id_ is an identifier that gets reset
|
||||
@@ -1855,11 +1855,6 @@ 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
|
||||
@@ -1875,7 +1870,6 @@ class DBImpl : public DB {
|
||||
|
||||
private:
|
||||
friend class DB;
|
||||
friend class DBImplReadOnly;
|
||||
friend class DBImplSecondary;
|
||||
friend class ErrorHandler;
|
||||
friend class InternalStats;
|
||||
@@ -2953,29 +2947,6 @@ 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>
|
||||
@@ -3380,13 +3351,6 @@ 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,24 +874,6 @@ 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) {
|
||||
@@ -3180,7 +3162,7 @@ void DBImpl::ResumeAllCompactions() {
|
||||
void DBImpl::MaybeScheduleFlushOrCompaction() {
|
||||
mutex_.AssertHeld();
|
||||
TEST_SYNC_POINT("DBImpl::MaybeScheduleFlushOrCompaction:Start");
|
||||
if (!opened_successfully_) {
|
||||
if (!opened_successfully_ || read_only_) {
|
||||
// Compaction may introduce data race to DB open
|
||||
return;
|
||||
}
|
||||
@@ -5132,11 +5114,6 @@ 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
|
||||
@@ -5170,19 +5147,6 @@ 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));
|
||||
|
||||
@@ -1206,7 +1206,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,8 +295,7 @@ 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->db_id_, impl->db_session_id_));
|
||||
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_));
|
||||
impl->column_family_memtables_.reset(
|
||||
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
|
||||
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
|
||||
|
||||
@@ -5,10 +5,7 @@
|
||||
|
||||
#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"
|
||||
@@ -66,25 +63,13 @@ 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_) {
|
||||
@@ -110,18 +95,6 @@ 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(
|
||||
@@ -129,12 +102,11 @@ 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_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);
|
||||
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();
|
||||
}
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else {
|
||||
PERF_TIMER_GUARD(get_from_output_files_time);
|
||||
|
||||
@@ -6,10 +6,8 @@
|
||||
#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"
|
||||
@@ -365,25 +363,13 @@ 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_) {
|
||||
@@ -409,18 +395,6 @@ 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) {
|
||||
@@ -430,14 +404,12 @@ 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_ptr,
|
||||
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
|
||||
false /* immutable_memtable */, &read_cb,
|
||||
/*is_blob_index=*/nullptr, /*do_merge=*/true)) {
|
||||
done = true;
|
||||
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);
|
||||
if (get_impl_options.value) {
|
||||
get_impl_options.value->PinSelf();
|
||||
}
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
super_version->imm->Get(
|
||||
@@ -445,14 +417,11 @@ 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,
|
||||
is_blob_ptr, memtable_blob_fetcher_ptr)) {
|
||||
&max_covering_tombstone_seq, read_options, &read_cb)) {
|
||||
done = true;
|
||||
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);
|
||||
if (get_impl_options.value) {
|
||||
get_impl_options.value->PinSelf();
|
||||
}
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
}
|
||||
} else {
|
||||
@@ -464,14 +433,13 @@ 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,
|
||||
memtable_blob_fetcher_ptr)) {
|
||||
/*is_blob_index=*/nullptr, /*do_merge=*/false)) {
|
||||
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, memtable_blob_fetcher_ptr)) {
|
||||
super_version->imm->GetMergeOperands(lkey, &s, &merge_context,
|
||||
&max_covering_tombstone_seq,
|
||||
read_options)) {
|
||||
done = true;
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
}
|
||||
@@ -489,7 +457,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=*/get_impl_options.get_value);
|
||||
/*do_merge*/ true);
|
||||
RecordTick(stats_, MEMTABLE_MISS);
|
||||
}
|
||||
{
|
||||
@@ -809,8 +777,7 @@ 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->db_id_, impl->db_session_id_));
|
||||
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_));
|
||||
impl->column_family_memtables_.reset(
|
||||
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
|
||||
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
|
||||
@@ -1586,7 +1553,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;
|
||||
|
||||
@@ -509,7 +509,6 @@ 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;
|
||||
@@ -1546,10 +1545,6 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
+52
-47
@@ -76,7 +76,7 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
|
||||
user_comparator_(cmp),
|
||||
merge_operator_(ioptions.merge_operator.get()),
|
||||
iter_(iter),
|
||||
blob_state_(
|
||||
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,
|
||||
@@ -125,6 +125,7 @@ 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_) {
|
||||
@@ -177,7 +178,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 {
|
||||
@@ -307,24 +308,24 @@ bool DBIter::SetValueAndColumnsFromBlobImpl(const Slice& user_key,
|
||||
// has a write-path partition manager.
|
||||
const bool allow_write_path_fallback =
|
||||
cfh_ != nullptr && cfh_->cfd()->blob_partition_manager() != nullptr;
|
||||
const Status s = blob_state_.mut()->reader.RetrieveAndSetBlobValue(
|
||||
const Status s = blob_reader_.RetrieveAndSetBlobValue(
|
||||
user_key, blob_index, allow_write_path_fallback);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
valid_ = false;
|
||||
blob_state_.mut()->is_blob = false;
|
||||
is_blob_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
SetValueAndColumnsFromPlain(blob_state_->reader.GetBlobValue());
|
||||
SetValueAndColumnsFromPlain(blob_reader_.GetBlobValue());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DBIter::SetValueAndColumnsFromBlob(const Slice& user_key,
|
||||
const Slice& blob_index) {
|
||||
assert(!blob_state_->is_blob);
|
||||
blob_state_.mut()->is_blob = true;
|
||||
assert(!is_blob_);
|
||||
is_blob_ = true;
|
||||
|
||||
if (expose_blob_index_) {
|
||||
SetValueAndColumnsFromPlain(blob_index);
|
||||
@@ -332,11 +333,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(blob_state_->lazy_blob_index.empty());
|
||||
blob_state_.mut()->lazy_blob_index = blob_index;
|
||||
assert(lazy_blob_index_.empty());
|
||||
lazy_blob_index_ = blob_index;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -345,10 +346,7 @@ bool DBIter::SetValueAndColumnsFromBlob(const Slice& user_key,
|
||||
}
|
||||
|
||||
bool DBIter::SetValueAndColumnsFromEntity(Slice slice) {
|
||||
// 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();
|
||||
auto& state = value_columns_state_;
|
||||
state.AssertReadyForEntity();
|
||||
|
||||
// Fast path: if no blob columns, use the simpler Deserialize
|
||||
@@ -411,7 +409,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;
|
||||
}
|
||||
@@ -422,7 +420,7 @@ bool DBIter::MaterializeLazyEntityColumns() const {
|
||||
}
|
||||
|
||||
DBIter* const mutable_this = const_cast<DBIter*>(this);
|
||||
auto& mutable_state = *mutable_this->value_columns_state_.mut();
|
||||
auto& mutable_state = mutable_this->value_columns_state_;
|
||||
WideColumns materialized_columns;
|
||||
materialized_columns.reserve(state.lazy_entity_columns().size());
|
||||
for (const auto& col : state.lazy_entity_columns()) {
|
||||
@@ -456,7 +454,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;
|
||||
}
|
||||
@@ -468,7 +466,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;
|
||||
}
|
||||
@@ -476,17 +474,17 @@ bool DBIter::SetValueAndColumnsFromMergeResult(const Status& merge_status,
|
||||
bool DBIter::PrepareValue() {
|
||||
assert(valid_);
|
||||
|
||||
if (blob_state_->lazy_blob_index.empty()) {
|
||||
if (lazy_blob_index_.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
assert(allow_unprepared_value_);
|
||||
assert(blob_state_->is_blob);
|
||||
assert(is_blob_);
|
||||
|
||||
const bool result = SetValueAndColumnsFromBlobImpl(
|
||||
saved_key_.GetUserKey(), blob_state_->lazy_blob_index);
|
||||
const bool result =
|
||||
SetValueAndColumnsFromBlobImpl(saved_key_.GetUserKey(), lazy_blob_index_);
|
||||
|
||||
blob_state_.mut()->lazy_blob_index.clear();
|
||||
lazy_blob_index_.clear();
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -623,7 +621,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 &&
|
||||
@@ -776,18 +774,25 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key) {
|
||||
}
|
||||
} while (iter_.Valid());
|
||||
|
||||
// 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 we accumulated tombstones, insert the range tombstone. Use
|
||||
// iterate_upper_bound_ if within the seek prefix, otherwise fall back to
|
||||
// saved_key_ (the last tracked delete, covering n-1 deletes).
|
||||
if (contiguous_tombstone_count_ > 0 && iter_.status().ok()) {
|
||||
// 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());
|
||||
if (iterate_upper_bound_ != nullptr && PrefixCheck(*iterate_upper_bound_)) {
|
||||
if (timestamp_size_ == 0) {
|
||||
MaybeInsertRangeTombstone(*iterate_upper_bound_);
|
||||
} else {
|
||||
// iterate_upper_bound_ is a plain user key without a timestamp
|
||||
// suffix. Pad with min timestamp so it sorts after all entries with
|
||||
// this user key, preserving the exclusive bound semantics.
|
||||
std::string end_key_with_ts;
|
||||
AppendKeyWithMinTimestamp(&end_key_with_ts, *iterate_upper_bound_,
|
||||
timestamp_size_);
|
||||
MaybeInsertRangeTombstone(end_key_with_ts);
|
||||
}
|
||||
} else if (prefix_.has_value()) {
|
||||
MaybeInsertRangeTombstone(saved_key_.GetUserKey());
|
||||
}
|
||||
}
|
||||
ResetContiguousTombstoneTracking();
|
||||
|
||||
@@ -1097,7 +1102,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_) {
|
||||
@@ -1141,7 +1146,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_.
|
||||
//
|
||||
@@ -1196,7 +1201,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;
|
||||
|
||||
@@ -1587,7 +1592,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_.mut()->saved_value(), &pinned_value_, &result_type);
|
||||
&value_columns_state_.saved_value(), &pinned_value_, &result_type);
|
||||
return SetValueAndColumnsFromMergeResult(s, result_type);
|
||||
}
|
||||
|
||||
@@ -1600,13 +1605,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_.mut()->saved_value(), &pinned_value_, &result_type);
|
||||
&value_columns_state_.saved_value(), &pinned_value_, &result_type);
|
||||
return SetValueAndColumnsFromMergeResult(s, result_type);
|
||||
}
|
||||
|
||||
bool DBIter::MergeWithBlobBaseValue(const Slice& blob_index,
|
||||
const Slice& user_key) {
|
||||
assert(!blob_state_->is_blob);
|
||||
assert(!is_blob_);
|
||||
|
||||
if (expose_blob_index_) {
|
||||
status_ =
|
||||
@@ -1617,7 +1622,7 @@ bool DBIter::MergeWithBlobBaseValue(const Slice& blob_index,
|
||||
|
||||
const bool allow_write_path_fallback =
|
||||
cfh_ != nullptr && cfh_->cfd()->blob_partition_manager() != nullptr;
|
||||
const Status s = blob_state_.mut()->reader.RetrieveAndSetBlobValue(
|
||||
const Status s = blob_reader_.RetrieveAndSetBlobValue(
|
||||
user_key, blob_index, allow_write_path_fallback);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
@@ -1627,11 +1632,11 @@ bool DBIter::MergeWithBlobBaseValue(const Slice& blob_index,
|
||||
|
||||
valid_ = true;
|
||||
|
||||
if (!MergeWithPlainBaseValue(blob_state_->reader.GetBlobValue(), user_key)) {
|
||||
if (!MergeWithPlainBaseValue(blob_reader_.GetBlobValue(), user_key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
blob_state_.Reset();
|
||||
blob_reader_.ResetBlobValue();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1640,7 +1645,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_state_->reader.CreateBlobFetcher();
|
||||
BlobFetcher blob_fetcher = blob_reader_.CreateBlobFetcher();
|
||||
std::string resolved_entity;
|
||||
Slice effective_entity;
|
||||
Status s_resolve = WideColumnSerialization::ResolveEntityForMerge(
|
||||
@@ -1659,7 +1664,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_.mut()->saved_value(), &pinned_value_, &result_type);
|
||||
&value_columns_state_.saved_value(), &pinned_value_, &result_type);
|
||||
return SetValueAndColumnsFromMergeResult(s, result_type);
|
||||
}
|
||||
|
||||
|
||||
+18
-35
@@ -27,7 +27,6 @@
|
||||
#include "rocksdb/wide_columns.h"
|
||||
#include "table/iterator_wrapper.h"
|
||||
#include "util/autovector.h"
|
||||
#include "util/dirty_tracked.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
class BlobFileCache;
|
||||
@@ -196,14 +195,14 @@ 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 {
|
||||
@@ -226,7 +225,7 @@ class DBIter final : public Iterator {
|
||||
}
|
||||
bool IsBlob() const {
|
||||
assert(valid_);
|
||||
return blob_state_->is_blob;
|
||||
return is_blob_;
|
||||
}
|
||||
|
||||
Status GetProperty(std::string prop_name, std::string* prop) override;
|
||||
@@ -297,20 +296,6 @@ 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
|
||||
@@ -498,9 +483,7 @@ class DBIter final : public Iterator {
|
||||
}
|
||||
}
|
||||
|
||||
inline void ClearSavedValue() {
|
||||
value_columns_state_.mut()->ClearSavedValue();
|
||||
}
|
||||
inline void ClearSavedValue() { value_columns_state_.ClearSavedValue(); }
|
||||
|
||||
inline void ResetInternalKeysSkippedCounter() {
|
||||
local_stats_.skip_count_ += num_internal_keys_skipped_;
|
||||
@@ -525,7 +508,7 @@ class DBIter final : public Iterator {
|
||||
}
|
||||
|
||||
void SetValueAndColumnsFromPlain(const Slice& slice) {
|
||||
value_columns_state_.mut()->SetFromPlain(slice);
|
||||
value_columns_state_.SetFromPlain(slice);
|
||||
}
|
||||
|
||||
bool SetValueAndColumnsFromBlobImpl(const Slice& user_key,
|
||||
@@ -541,7 +524,11 @@ class DBIter final : public Iterator {
|
||||
|
||||
void ResetValueAndColumns() { value_columns_state_.Reset(); }
|
||||
|
||||
void ResetBlobData() { blob_state_.Reset(); }
|
||||
void ResetBlobData() {
|
||||
blob_reader_.ResetBlobValue();
|
||||
lazy_blob_index_.clear();
|
||||
is_blob_ = false;
|
||||
}
|
||||
|
||||
// The following methods perform the actual merge operation for the
|
||||
// no/plain/blob/wide-column base value cases.
|
||||
@@ -552,12 +539,6 @@ 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;
|
||||
@@ -567,7 +548,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 (!value_was_prepared && !ParseKey(&ikey_)) {
|
||||
if (!ParseKey(&ikey_)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -589,7 +570,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;
|
||||
@@ -663,7 +644,7 @@ class DBIter final : public Iterator {
|
||||
UserComparatorWrapper user_comparator_;
|
||||
const MergeOperator* const merge_operator_;
|
||||
IteratorWrapper iter_;
|
||||
DirtyTracked<BlobState> blob_state_;
|
||||
BlobReader blob_reader_;
|
||||
ReadCallback* read_callback_;
|
||||
// Max visible sequence number. It is normally the snapshot seq unless we have
|
||||
// uncommitted data in db as in WriteUnCommitted.
|
||||
@@ -680,7 +661,7 @@ class DBIter final : public Iterator {
|
||||
// kTypeValuePreferredSeqno entry, this is the write time specified by the
|
||||
// user.
|
||||
uint64_t saved_write_unix_time_;
|
||||
DirtyTracked<ValueColumnsState> value_columns_state_;
|
||||
ValueColumnsState value_columns_state_;
|
||||
Slice pinned_value_;
|
||||
Statistics* statistics_;
|
||||
uint64_t max_skip_;
|
||||
@@ -701,6 +682,7 @@ class DBIter final : public Iterator {
|
||||
std::optional<IterKey> prefix_;
|
||||
|
||||
Status status_;
|
||||
Slice lazy_blob_index_;
|
||||
|
||||
// List of operands for merge operator.
|
||||
MergeContext merge_context_;
|
||||
@@ -739,6 +721,7 @@ 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_;
|
||||
|
||||
+40
-116
@@ -4151,7 +4151,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.
|
||||
@@ -5820,13 +5820,11 @@ 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 -> 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.
|
||||
// Forward: sees a-d tombstones first → [a, e), then g-j → [g, z).
|
||||
AssertRange(0, "a", "e");
|
||||
AssertRange(1, "g", "j");
|
||||
AssertRange(1, "g", "z");
|
||||
} 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");
|
||||
}
|
||||
@@ -5862,24 +5860,13 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedIteratorNoBounds) {
|
||||
|
||||
VerifyIteration({"e", "f"});
|
||||
|
||||
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);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
TEST_P(ReadPathRangeTombstoneTest, DirectionChange) {
|
||||
@@ -6061,7 +6048,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.
|
||||
@@ -6103,7 +6090,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"));
|
||||
@@ -6231,55 +6218,6 @@ 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
|
||||
@@ -6311,7 +6249,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()) {
|
||||
@@ -6674,8 +6612,8 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedWithUDT) {
|
||||
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
if (Forward()) {
|
||||
// Forward exhaustion past h: saved_key_="h"+ts fallback -> [e+ts, h+ts).
|
||||
AssertRange(0, std::string("e") + ts, std::string("h") + ts);
|
||||
// Forward exhaustion: range is [e+ts, z+min_ts).
|
||||
AssertRange(0, std::string("e") + ts, std::string("z") + min_ts);
|
||||
} else {
|
||||
// Reverse exhaustion: range is [a+ts, e+ts).
|
||||
AssertRange(0, std::string("a") + ts, std::string("e") + ts);
|
||||
@@ -6686,10 +6624,9 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedWithUDT) {
|
||||
}
|
||||
|
||||
TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
|
||||
// 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).
|
||||
// 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).
|
||||
for (bool use_udt : {false, true}) {
|
||||
SCOPED_TRACE(use_udt ? "with UDT" : "without UDT");
|
||||
Options options = CurrentOptions();
|
||||
@@ -6727,7 +6664,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());
|
||||
}
|
||||
@@ -6736,16 +6673,11 @@ 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()) {
|
||||
// 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");
|
||||
}
|
||||
// No upper bound → trailing tombstone run has no end key → dropped.
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 0);
|
||||
} 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);
|
||||
@@ -6755,18 +6687,15 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
|
||||
}
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
1);
|
||||
Forward() ? 0 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(ReadPathRangeTombstoneTest, UpperBoundTombstone) {
|
||||
// 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").
|
||||
// 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).
|
||||
for (bool use_udt : {false, true}) {
|
||||
SCOPED_TRACE(use_udt ? "with UDT" : "without UDT");
|
||||
Options options = CurrentOptions();
|
||||
@@ -6817,17 +6746,13 @@ TEST_P(ReadPathRangeTombstoneTest, UpperBoundTombstone) {
|
||||
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
if (use_udt) {
|
||||
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);
|
||||
}
|
||||
// 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);
|
||||
} else {
|
||||
AssertRange(0, "e", Forward() ? "h" : "i");
|
||||
AssertRange(0, "e", "i");
|
||||
}
|
||||
ASSERT_EQ(
|
||||
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
|
||||
@@ -6837,8 +6762,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();
|
||||
@@ -6975,14 +6900,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"));
|
||||
@@ -6994,7 +6919,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");
|
||||
|
||||
@@ -7097,8 +7022,7 @@ TEST_P(ReadPathRangeTombstoneTest, SeekToLastStaleSavedKey) {
|
||||
|
||||
TEST_P(ReadPathRangeTombstoneTest, SeekToLastTombstones) {
|
||||
if (Forward()) {
|
||||
ROCKSDB_GTEST_BYPASS(
|
||||
"SeekToLast tombstone materialization is reverse-only.");
|
||||
ROCKSDB_GTEST_SKIP("SeekToLast tombstone materialization is reverse-only.");
|
||||
}
|
||||
|
||||
Options options = CurrentOptions();
|
||||
|
||||
+2
-195
@@ -7,12 +7,9 @@
|
||||
// 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 "port/stack_trace.h"
|
||||
#include "rocksdb/utilities/transaction_db.h"
|
||||
#include "test_util/sync_point.h"
|
||||
@@ -360,7 +357,8 @@ TEST_F(DBSecondaryTest, GetMergeOperands) {
|
||||
const Status s = db_secondary_->GetMergeOperands(
|
||||
ReadOptions(), cfh, "k1", values.data(), &merge_operands_info,
|
||||
&number_of_operands);
|
||||
ASSERT_OK(s);
|
||||
ASSERT_NOK(s);
|
||||
ASSERT_TRUE(s.IsMergeInProgress());
|
||||
|
||||
ASSERT_EQ(number_of_operands, 4);
|
||||
ASSERT_EQ(values[0].ToString(), "v1");
|
||||
@@ -369,197 +367,6 @@ 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_;
|
||||
|
||||
@@ -8,17 +8,14 @@
|
||||
// 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"
|
||||
@@ -54,42 +51,6 @@ 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,
|
||||
@@ -851,90 +812,6 @@ 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"));
|
||||
|
||||
+35
-92
@@ -8071,6 +8071,41 @@ INSTANTIATE_TEST_CASE_P(OpenFilesAsync, OpenFilesAsyncTest,
|
||||
::testing::Values(-1, 10),
|
||||
::testing::Bool()));
|
||||
|
||||
TEST_F(DBTest, ReadOnlyCloseDoesNotDeleteWriterFiles) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("before", "value"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::unique_ptr<DB> read_only_db;
|
||||
ASSERT_OK(DB::OpenForReadOnly(options, dbname_, &read_only_db));
|
||||
|
||||
ASSERT_OK(Put("after", "value"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::vector<LiveFileMetaData> live_files;
|
||||
db_->GetLiveFilesMetaData(&live_files);
|
||||
ASSERT_GE(live_files.size(), 2);
|
||||
|
||||
std::string newest_sst_path;
|
||||
uint64_t newest_file_number = 0;
|
||||
for (const auto& live_file : live_files) {
|
||||
if (live_file.file_number > newest_file_number) {
|
||||
newest_file_number = live_file.file_number;
|
||||
newest_sst_path = live_file.directory + "/" + live_file.relative_filename;
|
||||
}
|
||||
}
|
||||
ASSERT_FALSE(newest_sst_path.empty());
|
||||
ASSERT_OK(env_->FileExists(newest_sst_path));
|
||||
|
||||
read_only_db.reset();
|
||||
|
||||
ASSERT_OK(env_->FileExists(newest_sst_path));
|
||||
}
|
||||
|
||||
// Test mix of races with async file open, reads, compactions
|
||||
TEST_P(OpenFilesAsyncTest, ConcurrentFileAccess) {
|
||||
Options options = CurrentOptions();
|
||||
@@ -8203,98 +8238,6 @@ 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));
|
||||
|
||||
+3
-62
@@ -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,65 +7510,6 @@ 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.
|
||||
@@ -8232,7 +8173,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();
|
||||
|
||||
+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);
|
||||
|
||||
@@ -266,9 +266,6 @@ class FaultInjectionTest
|
||||
CloseDB();
|
||||
env_->ResetState();
|
||||
Status s = DB::Open(options_, dbname_, &db_);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
assert(db_ != nullptr);
|
||||
return s;
|
||||
}
|
||||
|
||||
+3
-3
@@ -1696,7 +1696,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 +1714,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,7 +1750,7 @@ 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();
|
||||
|
||||
+4
-1
@@ -48,12 +48,15 @@ Reader::Reader(std::shared_ptr<Logger> info_log,
|
||||
first_record_read_(false),
|
||||
compression_type_(kNoCompression),
|
||||
compression_type_record_read_(false),
|
||||
uncompress_(),
|
||||
uncompress_(nullptr),
|
||||
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_;
|
||||
std::unique_ptr<StreamingUncompress> uncompress_;
|
||||
StreamingUncompress* uncompress_;
|
||||
// Reusable uncompressed output buffer
|
||||
std::unique_ptr<char[]> uncompressed_buffer_;
|
||||
// Reusable uncompressed record
|
||||
|
||||
+6
-3
@@ -1224,11 +1224,11 @@ TEST_P(StreamingCompressionTest, Basic) {
|
||||
}
|
||||
CompressionOptions opts;
|
||||
constexpr uint32_t compression_format_version = 2;
|
||||
auto compress = StreamingCompress::Create(
|
||||
StreamingCompress* compress = StreamingCompress::Create(
|
||||
compression_type, opts, compression_format_version, kBlockSize);
|
||||
auto uncompress = StreamingUncompress::Create(
|
||||
StreamingUncompress* uncompress = StreamingUncompress::Create(
|
||||
compression_type, compression_format_version, kBlockSize);
|
||||
std::unique_ptr<MemoryAllocator> allocator(new DefaultMemoryAllocator());
|
||||
MemoryAllocator* allocator = new DefaultMemoryAllocator();
|
||||
std::string input_buffer = BigString("abc", input_size);
|
||||
std::vector<std::string> compressed_buffers;
|
||||
size_t remaining;
|
||||
@@ -1266,6 +1266,9 @@ 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);
|
||||
|
||||
+4
-1
@@ -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_(),
|
||||
compress_(nullptr),
|
||||
track_and_verify_wals_(track_and_verify_wals),
|
||||
last_seqno_recorded_(0) {
|
||||
for (uint8_t i = 0; i <= kMaxRecordType; i++) {
|
||||
@@ -48,6 +48,9 @@ Writer::~Writer() {
|
||||
if (dest_) {
|
||||
WriteBuffer(WriteOptions()).PermitUncheckedError();
|
||||
}
|
||||
if (compress_) {
|
||||
delete compress_;
|
||||
}
|
||||
ThreadStatusUtil::SetThreadOperation(cur_op_type);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -156,7 +156,7 @@ class Writer {
|
||||
|
||||
// Compression Type
|
||||
CompressionType compression_type_;
|
||||
std::unique_ptr<StreamingCompress> compress_;
|
||||
StreamingCompress* compress_;
|
||||
// Reusable compressed output buffer
|
||||
std::unique_ptr<char[]> compressed_buffer_;
|
||||
|
||||
|
||||
+22
-80
@@ -15,7 +15,6 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/blob/blob_file_partition_manager.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/dbformat.h"
|
||||
@@ -71,67 +70,6 @@ Status GetDefaultColumnBlobIndexSlice(Slice entity, Slice* blob_index_slice) {
|
||||
return status;
|
||||
}
|
||||
|
||||
Status PushWideColumnEntityDefaultOperand(const Slice& user_key,
|
||||
const Slice& entity,
|
||||
MergeContext* merge_context,
|
||||
bool operand_pinned,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
assert(merge_context != nullptr);
|
||||
|
||||
Slice entity_ref = entity;
|
||||
Slice value_of_default;
|
||||
Status status = WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
entity_ref, value_of_default);
|
||||
if (status.ok()) {
|
||||
merge_context->PushOperand(value_of_default, operand_pinned);
|
||||
return status;
|
||||
}
|
||||
if (!status.IsNotSupported()) {
|
||||
return status;
|
||||
}
|
||||
if (blob_fetcher == nullptr) {
|
||||
return Status::Corruption(
|
||||
"Cannot resolve blob-backed default column without a blob fetcher");
|
||||
}
|
||||
|
||||
PinnableSlice resolved_default;
|
||||
bool resolved = false;
|
||||
status = WideColumnSerialization::GetValueOfDefaultColumnResolvingBlobs(
|
||||
entity, user_key, blob_fetcher, resolved_default, resolved);
|
||||
if (status.ok()) {
|
||||
// Resolved blob values are backed by this stack-local PinnableSlice, so
|
||||
// copy them into MergeContext instead of pinning their storage.
|
||||
merge_context->PushOperand(Slice(resolved_default), false);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
Status MergeWithWideColumnEntityBaseValue(
|
||||
const Slice& user_key, const Slice& entity,
|
||||
const MergeOperator* merge_operator, MergeContext* merge_context,
|
||||
Logger* logger, Statistics* statistics, SystemClock* clock,
|
||||
std::string* value, PinnableWideColumns* columns,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
assert(merge_context != nullptr);
|
||||
|
||||
std::string resolved_entity;
|
||||
Slice effective_entity;
|
||||
Status status = WideColumnSerialization::ResolveEntityForMerge(
|
||||
entity, user_key, blob_fetcher, nullptr /* prefetch_buffers */,
|
||||
resolved_entity, effective_entity);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// `op_failure_scope` (an output parameter) is not provided (set to nullptr)
|
||||
// since a failure must be propagated regardless of its value.
|
||||
return MergeHelper::TimedFullMerge(
|
||||
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);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ImmutableMemTableOptions::ImmutableMemTableOptions(
|
||||
@@ -1316,7 +1254,6 @@ struct Saver {
|
||||
bool inplace_update_support;
|
||||
bool do_merge;
|
||||
SystemClock* clock;
|
||||
const BlobFetcher* blob_fetcher;
|
||||
|
||||
ReadCallback* callback_;
|
||||
bool* is_blob_index;
|
||||
@@ -1487,17 +1424,27 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
// Preserve the value with the goal of returning it as part of
|
||||
// raw merge operands to the user
|
||||
|
||||
*(s->status) = PushWideColumnEntityDefaultOperand(
|
||||
s->key->user_key(), v, merge_context,
|
||||
s->inplace_update_support == false /* operand_pinned */,
|
||||
s->blob_fetcher);
|
||||
Slice value_of_default;
|
||||
*(s->status) = WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
v, value_of_default);
|
||||
|
||||
if (s->status->ok()) {
|
||||
merge_context->PushOperand(
|
||||
value_of_default,
|
||||
s->inplace_update_support == false /* operand_pinned */);
|
||||
}
|
||||
} else if (*(s->merge_in_progress)) {
|
||||
assert(s->do_merge);
|
||||
|
||||
if (s->value || s->columns) {
|
||||
*(s->status) = MergeWithWideColumnEntityBaseValue(
|
||||
s->key->user_key(), v, merge_operator, merge_context, s->logger,
|
||||
s->statistics, s->clock, s->value, s->columns, s->blob_fetcher);
|
||||
// `op_failure_scope` (an output parameter) is not provided (set
|
||||
// to nullptr) since a failure must be propagated regardless of
|
||||
// its value.
|
||||
*(s->status) = MergeHelper::TimedFullMerge(
|
||||
merge_operator, s->key->user_key(), MergeHelper::kWideBaseValue,
|
||||
v, merge_context->GetOperands(), s->logger, s->statistics,
|
||||
s->clock, /* update_num_ops_stats */ true,
|
||||
/* op_failure_scope */ nullptr, s->value, s->columns);
|
||||
}
|
||||
} else if (s->value) {
|
||||
Slice value_of_default;
|
||||
@@ -1573,8 +1520,7 @@ bool MemTable::Get(const LookupKey& key, std::string* value,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
SequenceNumber* seq, const ReadOptions& read_opts,
|
||||
bool immutable_memtable, ReadCallback* callback,
|
||||
bool* is_blob_index, bool do_merge,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
bool* is_blob_index, bool do_merge) {
|
||||
// The sequence number is updated synchronously in version_set.h
|
||||
if (IsEmpty()) {
|
||||
// Avoiding recording stats for speed.
|
||||
@@ -1632,7 +1578,7 @@ bool MemTable::Get(const LookupKey& key, std::string* value,
|
||||
}
|
||||
GetFromTable(key, *max_covering_tombstone_seq, do_merge, callback,
|
||||
is_blob_index, value, columns, timestamp, s, merge_context,
|
||||
seq, &found_final_value, &merge_in_progress, blob_fetcher);
|
||||
seq, &found_final_value, &merge_in_progress);
|
||||
}
|
||||
|
||||
// No change to value, since we have not yet found a Put/Delete
|
||||
@@ -1655,8 +1601,7 @@ void MemTable::GetFromTable(const LookupKey& key,
|
||||
PinnableWideColumns* columns,
|
||||
std::string* timestamp, Status* s,
|
||||
MergeContext* merge_context, SequenceNumber* seq,
|
||||
bool* found_final_value, bool* merge_in_progress,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
bool* found_final_value, bool* merge_in_progress) {
|
||||
Saver saver;
|
||||
saver.status = s;
|
||||
saver.found_final_value = found_final_value;
|
||||
@@ -1674,7 +1619,6 @@ void MemTable::GetFromTable(const LookupKey& key,
|
||||
saver.inplace_update_support = moptions_.inplace_update_support;
|
||||
saver.statistics = moptions_.statistics;
|
||||
saver.clock = clock_;
|
||||
saver.blob_fetcher = blob_fetcher;
|
||||
saver.callback_ = callback;
|
||||
saver.is_blob_index = is_blob_index;
|
||||
saver.do_merge = do_merge;
|
||||
@@ -1704,8 +1648,7 @@ Status MemTable::ValidateKey(const char* key, bool allow_data_in_errors) {
|
||||
}
|
||||
|
||||
void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
ReadCallback* callback, bool immutable_memtable,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
ReadCallback* callback, bool immutable_memtable) {
|
||||
// The sequence number is updated synchronously in version_set.h
|
||||
if (IsEmpty()) {
|
||||
// Avoiding recording stats for speed.
|
||||
@@ -1797,7 +1740,6 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
saver.inplace_update_support = moptions_.inplace_update_support;
|
||||
saver.statistics = moptions_.statistics;
|
||||
saver.clock = clock_;
|
||||
saver.blob_fetcher = blob_fetcher;
|
||||
saver.callback_ = callback;
|
||||
saver.is_blob_index = &iter->is_blob_index;
|
||||
saver.do_merge = true;
|
||||
@@ -1889,7 +1831,7 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
*(iter->lkey), iter->max_covering_tombstone_seq, true, callback,
|
||||
&iter->is_blob_index, iter->value ? iter->value->GetSelf() : nullptr,
|
||||
iter->columns, iter->timestamp, iter->s, &(iter->merge_context),
|
||||
&dummy_seq, &found_final_value, &merge_in_progress, blob_fetcher);
|
||||
&dummy_seq, &found_final_value, &merge_in_progress);
|
||||
|
||||
if (!found_final_value && merge_in_progress) {
|
||||
if (iter->s->ok()) {
|
||||
|
||||
+8
-14
@@ -39,7 +39,6 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class BlobFetcher;
|
||||
class BlobFilePartitionManager;
|
||||
struct FlushJobInfo;
|
||||
class Mutex;
|
||||
@@ -237,27 +236,25 @@ class ReadOnlyMemTable {
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
SequenceNumber* seq, const ReadOptions& read_opts,
|
||||
bool immutable_memtable, ReadCallback* callback = nullptr,
|
||||
bool* is_blob_index = nullptr, bool do_merge = true,
|
||||
const BlobFetcher* blob_fetcher = nullptr) = 0;
|
||||
bool* is_blob_index = nullptr, bool do_merge = true) = 0;
|
||||
bool Get(const LookupKey& key, std::string* value,
|
||||
PinnableWideColumns* columns, std::string* timestamp, Status* s,
|
||||
MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
const ReadOptions& read_opts, bool immutable_memtable,
|
||||
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
|
||||
bool do_merge = true, const BlobFetcher* blob_fetcher = nullptr) {
|
||||
bool do_merge = true) {
|
||||
SequenceNumber seq;
|
||||
return Get(key, value, columns, timestamp, s, merge_context,
|
||||
max_covering_tombstone_seq, &seq, read_opts, immutable_memtable,
|
||||
callback, is_blob_index, do_merge, blob_fetcher);
|
||||
callback, is_blob_index, do_merge);
|
||||
}
|
||||
|
||||
// @param immutable_memtable Whether this memtable is immutable. Used
|
||||
// internally by NewRangeTombstoneIterator(). See comment above
|
||||
// NewRangeTombstoneIterator() for more detail.
|
||||
virtual void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
ReadCallback* callback, bool immutable_memtable,
|
||||
const BlobFetcher* blob_fetcher = nullptr) = 0;
|
||||
ReadCallback* callback, bool immutable_memtable) = 0;
|
||||
|
||||
// Get total number of entries in the mem table.
|
||||
// REQUIRES: external synchronization to prevent simultaneous
|
||||
@@ -323,7 +320,7 @@ class ReadOnlyMemTable {
|
||||
virtual uint64_t ApproximateOldestKeyTime() const = 0;
|
||||
|
||||
// Inserts a range tombstone [start_key, end_key) that is logically redundant
|
||||
// -- it is derived from existing point tombstones observed during iteration
|
||||
// — it is derived from existing point tombstones observed during iteration
|
||||
// and does not delete any data that isn't already deleted. This is a
|
||||
// best-effort optimization. It allows future reads to skip iterating over
|
||||
// continuous single deletion tombstones.
|
||||
@@ -691,12 +688,10 @@ class MemTable final : public ReadOnlyMemTable {
|
||||
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
||||
const ReadOptions& read_opts, bool immutable_memtable,
|
||||
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
|
||||
bool do_merge = true,
|
||||
const BlobFetcher* blob_fetcher = nullptr) override;
|
||||
bool do_merge = true) override;
|
||||
|
||||
void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
ReadCallback* callback, bool immutable_memtable,
|
||||
const BlobFetcher* blob_fetcher = nullptr) override;
|
||||
ReadCallback* callback, bool immutable_memtable) override;
|
||||
|
||||
// If `key` exists in current memtable with type value_type and the existing
|
||||
// value is at least as large as the new value, updates it in-place. Otherwise
|
||||
@@ -996,8 +991,7 @@ class MemTable final : public ReadOnlyMemTable {
|
||||
std::string* value, PinnableWideColumns* columns,
|
||||
std::string* timestamp, Status* s,
|
||||
MergeContext* merge_context, SequenceNumber* seq,
|
||||
bool* found_final_value, bool* merge_in_progress,
|
||||
const BlobFetcher* blob_fetcher);
|
||||
bool* found_final_value, bool* merge_in_progress);
|
||||
|
||||
// Always returns non-null and assumes certain pre-checks (e.g.,
|
||||
// is_range_del_table_empty_) are done. This is only valid during the lifetime
|
||||
|
||||
+14
-18
@@ -108,19 +108,18 @@ bool MemTableListVersion::Get(const LookupKey& key, std::string* value,
|
||||
MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
SequenceNumber* seq, const ReadOptions& read_opts,
|
||||
ReadCallback* callback, bool* is_blob_index,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
ReadCallback* callback, bool* is_blob_index) {
|
||||
return GetFromList(&memlist_, key, value, columns, timestamp, s,
|
||||
merge_context, max_covering_tombstone_seq, seq, read_opts,
|
||||
callback, is_blob_index, blob_fetcher);
|
||||
callback, is_blob_index);
|
||||
}
|
||||
|
||||
void MemTableListVersion::MultiGet(const ReadOptions& read_options,
|
||||
MultiGetRange* range, ReadCallback* callback,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
MultiGetRange* range,
|
||||
ReadCallback* callback) {
|
||||
for (auto memtable : memlist_) {
|
||||
memtable->MultiGet(read_options, range, callback,
|
||||
true /* immutable_memtable */, blob_fetcher);
|
||||
true /* immutable_memtable */);
|
||||
if (range->empty()) {
|
||||
return;
|
||||
}
|
||||
@@ -129,13 +128,12 @@ void MemTableListVersion::MultiGet(const ReadOptions& read_options,
|
||||
|
||||
bool MemTableListVersion::GetMergeOperands(
|
||||
const LookupKey& key, Status* s, MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq, const ReadOptions& read_opts,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
SequenceNumber* max_covering_tombstone_seq, const ReadOptions& read_opts) {
|
||||
for (ReadOnlyMemTable* memtable : memlist_) {
|
||||
bool done = memtable->Get(
|
||||
key, /*value=*/nullptr, /*columns=*/nullptr, /*timestamp=*/nullptr, s,
|
||||
merge_context, max_covering_tombstone_seq, read_opts,
|
||||
true /* immutable_memtable */, nullptr, nullptr, false, blob_fetcher);
|
||||
true /* immutable_memtable */, nullptr, nullptr, false);
|
||||
if (done) {
|
||||
return true;
|
||||
}
|
||||
@@ -147,11 +145,10 @@ bool MemTableListVersion::GetFromHistory(
|
||||
const LookupKey& key, std::string* value, PinnableWideColumns* columns,
|
||||
std::string* timestamp, Status* s, MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
||||
const ReadOptions& read_opts, bool* is_blob_index,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
const ReadOptions& read_opts, bool* is_blob_index) {
|
||||
return GetFromList(&memlist_history_, key, value, columns, timestamp, s,
|
||||
merge_context, max_covering_tombstone_seq, seq, read_opts,
|
||||
nullptr /*read_callback*/, is_blob_index, blob_fetcher);
|
||||
nullptr /*read_callback*/, is_blob_index);
|
||||
}
|
||||
|
||||
bool MemTableListVersion::GetFromList(
|
||||
@@ -159,18 +156,17 @@ bool MemTableListVersion::GetFromList(
|
||||
std::string* value, PinnableWideColumns* columns, std::string* timestamp,
|
||||
Status* s, MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
||||
const ReadOptions& read_opts, ReadCallback* callback, bool* is_blob_index,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
const ReadOptions& read_opts, ReadCallback* callback, bool* is_blob_index) {
|
||||
*seq = kMaxSequenceNumber;
|
||||
|
||||
for (auto& memtable : *list) {
|
||||
assert(memtable->IsFragmentedRangeTombstonesConstructed());
|
||||
SequenceNumber current_seq = kMaxSequenceNumber;
|
||||
|
||||
bool done = memtable->Get(key, value, columns, timestamp, s, merge_context,
|
||||
max_covering_tombstone_seq, ¤t_seq,
|
||||
read_opts, true /* immutable_memtable */,
|
||||
callback, is_blob_index, true, blob_fetcher);
|
||||
bool done =
|
||||
memtable->Get(key, value, columns, timestamp, s, merge_context,
|
||||
max_covering_tombstone_seq, ¤t_seq, read_opts,
|
||||
true /* immutable_memtable */, callback, is_blob_index);
|
||||
if (*seq == kMaxSequenceNumber) {
|
||||
// Store the most recent sequence number of any operation on this key.
|
||||
// Since we only care about the most recent change, we only need to
|
||||
|
||||
+9
-16
@@ -61,33 +61,29 @@ class MemTableListVersion {
|
||||
MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
||||
const ReadOptions& read_opts, ReadCallback* callback = nullptr,
|
||||
bool* is_blob_index = nullptr,
|
||||
const BlobFetcher* blob_fetcher = nullptr);
|
||||
bool* is_blob_index = nullptr);
|
||||
|
||||
bool Get(const LookupKey& key, std::string* value,
|
||||
PinnableWideColumns* columns, std::string* timestamp, Status* s,
|
||||
MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
const ReadOptions& read_opts, ReadCallback* callback = nullptr,
|
||||
bool* is_blob_index = nullptr,
|
||||
const BlobFetcher* blob_fetcher = nullptr) {
|
||||
bool* is_blob_index = nullptr) {
|
||||
SequenceNumber seq;
|
||||
return Get(key, value, columns, timestamp, s, merge_context,
|
||||
max_covering_tombstone_seq, &seq, read_opts, callback,
|
||||
is_blob_index, blob_fetcher);
|
||||
is_blob_index);
|
||||
}
|
||||
|
||||
void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
ReadCallback* callback,
|
||||
const BlobFetcher* blob_fetcher = nullptr);
|
||||
ReadCallback* callback);
|
||||
|
||||
// Returns all the merge operands corresponding to the key by searching all
|
||||
// memtables starting from the most recent one.
|
||||
bool GetMergeOperands(const LookupKey& key, Status* s,
|
||||
MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
const ReadOptions& read_opts,
|
||||
const BlobFetcher* blob_fetcher = nullptr);
|
||||
const ReadOptions& read_opts);
|
||||
|
||||
// Similar to Get(), but searches the Memtable history of memtables that
|
||||
// have already been flushed. Should only be used from in-memory only
|
||||
@@ -98,19 +94,17 @@ class MemTableListVersion {
|
||||
Status* s, MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
SequenceNumber* seq, const ReadOptions& read_opts,
|
||||
bool* is_blob_index = nullptr,
|
||||
const BlobFetcher* blob_fetcher = nullptr);
|
||||
bool* is_blob_index = nullptr);
|
||||
bool GetFromHistory(const LookupKey& key, std::string* value,
|
||||
PinnableWideColumns* columns, std::string* timestamp,
|
||||
Status* s, MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
const ReadOptions& read_opts,
|
||||
bool* is_blob_index = nullptr,
|
||||
const BlobFetcher* blob_fetcher = nullptr) {
|
||||
bool* is_blob_index = nullptr) {
|
||||
SequenceNumber seq;
|
||||
return GetFromHistory(key, value, columns, timestamp, s, merge_context,
|
||||
max_covering_tombstone_seq, &seq, read_opts,
|
||||
is_blob_index, blob_fetcher);
|
||||
is_blob_index);
|
||||
}
|
||||
|
||||
Status AddRangeTombstoneIterators(const ReadOptions& read_opts, Arena* arena,
|
||||
@@ -194,8 +188,7 @@ class MemTableListVersion {
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
SequenceNumber* seq, const ReadOptions& read_opts,
|
||||
ReadCallback* callback = nullptr,
|
||||
bool* is_blob_index = nullptr,
|
||||
const BlobFetcher* blob_fetcher = nullptr);
|
||||
bool* is_blob_index = nullptr);
|
||||
|
||||
void AddMemTable(ReadOnlyMemTable* m);
|
||||
|
||||
|
||||
+1
-4
@@ -777,10 +777,7 @@ void TableCache::ReleaseObsolete(Cache* cache, uint64_t file_number,
|
||||
if (table_handle != nullptr) {
|
||||
TableReader* table_reader = typed_cache.Value(table_handle);
|
||||
table_reader->MarkObsolete(uncache_aggressiveness);
|
||||
// Mark the entry Invisible so that if concurrent readers hold references,
|
||||
// the entry will be erased when the last reference is released.
|
||||
cache->Erase(GetSliceForFileNumber(&file_number));
|
||||
typed_cache.Release(table_handle);
|
||||
typed_cache.ReleaseAndEraseIfLastRef(table_handle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ PinnedTableReader& PinnedTableReader::operator=(
|
||||
TableReader* r = other.reader_.load(std::memory_order_acquire);
|
||||
// Only read handle_ when reader_ is non-null. Pin() writes handle_ before
|
||||
// reader_ (with release), so a non-null reader_ guarantees handle_ is stable.
|
||||
// If reader_ is null, Pin() may be in progress -- avoid reading handle_.
|
||||
// If reader_ is null, Pin() may be in progress — avoid reading handle_.
|
||||
handle_ = (r != nullptr) ? other.handle_ : nullptr;
|
||||
reader_.store(r, std::memory_order_release);
|
||||
return *this;
|
||||
|
||||
+10
-15
@@ -2243,10 +2243,8 @@ void Version::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
|
||||
uint64_t oldest_time = std::numeric_limits<uint64_t>::max();
|
||||
for (int level = 0; level < storage_info_.num_non_empty_levels_; level++) {
|
||||
for (FileMetaData* meta : storage_info_.LevelFiles(level)) {
|
||||
assert(meta->fd.pinned_reader.Get() != nullptr);
|
||||
uint64_t file_creation_time = meta->TryGetFileCreationTime();
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"Version::GetCreationTimeOfOldestFile::FileCreationTime",
|
||||
&file_creation_time);
|
||||
if (file_creation_time == kUnknownFileCreationTime) {
|
||||
*creation_time = 0;
|
||||
return;
|
||||
@@ -4219,10 +4217,10 @@ void VersionStorageInfo::ComputeFilesMarkedForReadTriggeredCompaction(
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip files at the last non-empty level -- there is no lower level to
|
||||
// Skip files at the last non-empty level — there is no lower level to
|
||||
// compact into. Exception: L0 files are allowed even when L0 is the last
|
||||
// non-empty level, because in single-level universal or FIFO compaction, L0
|
||||
// files can be compacted together (L0 -> L0).
|
||||
// files can be compacted together (L0 → L0).
|
||||
int last_non_empty = num_non_empty_levels_ - 1;
|
||||
|
||||
for (int level = 0; level < num_levels(); level++) {
|
||||
@@ -5692,7 +5690,7 @@ Status VersionSet::Close(FSDirectory* db_dir, InstrumentedMutex* mu) {
|
||||
content_manifest_name, fs_->OptimizeForManifestRead(file_options_),
|
||||
&manifest_file, nullptr);
|
||||
if (!content_io_s.ok()) {
|
||||
// Surface I/O errors to the caller -- users who call DB::Close() and
|
||||
// Surface I/O errors to the caller — users who call DB::Close() and
|
||||
// check the status should know about filesystem problems.
|
||||
s = content_io_s;
|
||||
ROCKS_LOG_ERROR(db_options_->info_log,
|
||||
@@ -5731,8 +5729,6 @@ Status VersionSet::Close(FSDirectory* db_dir, InstrumentedMutex* mu) {
|
||||
// Manifest is healthy, no need to check again
|
||||
break;
|
||||
}
|
||||
RecordTick(db_options_->statistics.get(),
|
||||
MANIFEST_VALIDATION_FAILURE_COUNT);
|
||||
IOStatus corrupt_io_s =
|
||||
IOStatus::Corruption("MANIFEST content validation failed");
|
||||
IOErrorInfo io_error_info(corrupt_io_s, FileOperationType::kVerify,
|
||||
@@ -5744,7 +5740,7 @@ Status VersionSet::Close(FSDirectory* db_dir, InstrumentedMutex* mu) {
|
||||
corrupt_io_s.PermitUncheckedError();
|
||||
io_error_info.io_status.PermitUncheckedError();
|
||||
if (content_check == 0) {
|
||||
// First check failed -- rewrite and verify again
|
||||
// First check failed — rewrite and verify again
|
||||
ROCKS_LOG_ERROR(db_options_->info_log,
|
||||
"MANIFEST content verification on Close failed, "
|
||||
"filename %s, rewriting manifest\n",
|
||||
@@ -5755,7 +5751,7 @@ Status VersionSet::Close(FSDirectory* db_dir, InstrumentedMutex* mu) {
|
||||
s = LogAndApply(cfd, ReadOptions(), WriteOptions(), &recovery_edit, mu,
|
||||
db_dir);
|
||||
} else {
|
||||
// Rewritten manifest is also corrupt -- likely a recurring filesystem
|
||||
// Rewritten manifest is also corrupt — likely a recurring filesystem
|
||||
// issue. Surface it so DB::Close() callers can detect the problem.
|
||||
ROCKS_LOG_ERROR(db_options_->info_log,
|
||||
"MANIFEST content verification on Close failed again "
|
||||
@@ -6717,7 +6713,7 @@ Status VersionSet::ReopenManifestForAppend(const std::string& manifest_path) {
|
||||
FileOptions opt_file_opts = GetFileOptionsForManifestWrite();
|
||||
|
||||
// Bail if the on-disk size diverges from what Recover's Reader
|
||||
// consumed -- likely a torn tail from a prior crash; mid-block append
|
||||
// consumed — likely a torn tail from a prior crash; mid-block append
|
||||
// would mis-frame the record stream.
|
||||
uint64_t physical_size = 0;
|
||||
IOStatus stat_s = fs_->GetFileSize(manifest_path, IOOptions(), &physical_size,
|
||||
@@ -8163,12 +8159,11 @@ ReactiveVersionSet::ReactiveVersionSet(
|
||||
const MutableDBOptions& mutable_db_options,
|
||||
const FileOptions& _file_options, Cache* table_cache,
|
||||
WriteBufferManager* write_buffer_manager, WriteController* write_controller,
|
||||
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
|
||||
const std::string& db_session_id)
|
||||
const std::shared_ptr<IOTracer>& io_tracer)
|
||||
: VersionSet(dbname, imm_db_options, mutable_db_options, _file_options,
|
||||
table_cache, write_buffer_manager, write_controller,
|
||||
/*block_cache_tracer=*/nullptr, io_tracer, db_id,
|
||||
db_session_id, /*daily_offpeak_time_utc*/ "",
|
||||
/*block_cache_tracer=*/nullptr, io_tracer, /*db_id*/ "",
|
||||
/*db_session_id*/ "", /*daily_offpeak_time_utc*/ "",
|
||||
/*error_handler=*/nullptr, /*unchanging=*/false) {}
|
||||
|
||||
ReactiveVersionSet::~ReactiveVersionSet() = default;
|
||||
|
||||
+2
-4
@@ -1726,7 +1726,7 @@ class VersionSet {
|
||||
// MANIFEST as before) if ReopenWritableFile fails.
|
||||
Status ReopenManifestForAppend(const std::string& manifest_path);
|
||||
|
||||
// FileOptions for MANIFEST writes -- applies the FS's
|
||||
// FileOptions for MANIFEST writes — applies the FS's
|
||||
// OptimizeForManifestWrite tuning, then re-applies the user-configured
|
||||
// temperature so a custom FS can't override it.
|
||||
FileOptions GetFileOptionsForManifestWrite() const;
|
||||
@@ -1898,9 +1898,7 @@ class ReactiveVersionSet : public VersionSet {
|
||||
const FileOptions& _file_options, Cache* table_cache,
|
||||
WriteBufferManager* write_buffer_manager,
|
||||
WriteController* write_controller,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::string& db_id,
|
||||
const std::string& db_session_id);
|
||||
const std::shared_ptr<IOTracer>& io_tracer);
|
||||
|
||||
~ReactiveVersionSet() override;
|
||||
|
||||
|
||||
+2
-34
@@ -20,7 +20,6 @@
|
||||
#include "rocksdb/advanced_options.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "rocksdb/statistics.h"
|
||||
#include "table/block_based/block_based_table_factory.h"
|
||||
#include "table/mock_table.h"
|
||||
#include "table/unique_id_impl.h"
|
||||
@@ -1203,7 +1202,7 @@ class VersionSetTestBase {
|
||||
reactive_versions_ = std::make_shared<ReactiveVersionSet>(
|
||||
dbname_, &imm_db_options_, mutable_db_options_, env_options_,
|
||||
table_cache_.get(), &write_buffer_manager_, &write_controller_,
|
||||
/*io_tracer=*/nullptr, /*db_id=*/"", /*db_session_id=*/"");
|
||||
nullptr);
|
||||
imm_db_options_.db_paths.emplace_back(dbname_,
|
||||
std::numeric_limits<uint64_t>::max());
|
||||
}
|
||||
@@ -2413,8 +2412,6 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseClean) {
|
||||
// Enable content validation, perform normal operations, close.
|
||||
// Verify no manifest rotation (file number unchanged).
|
||||
NewDB();
|
||||
auto stats = CreateDBStatistics();
|
||||
imm_db_options_.statistics = stats;
|
||||
mutable_db_options_.verify_manifest_content_on_close = true;
|
||||
mutex_.Lock();
|
||||
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
|
||||
@@ -2440,9 +2437,6 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseClean) {
|
||||
// Verify content validation actually ran
|
||||
ASSERT_TRUE(content_validation_ran);
|
||||
|
||||
// No corruption -- counter should be zero
|
||||
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
|
||||
|
||||
// Manifest path should be unchanged (no rotation)
|
||||
std::string manifest_path_after;
|
||||
uint64_t manifest_file_number = 0;
|
||||
@@ -2458,8 +2452,6 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseCorruptRecord) {
|
||||
// Enable content validation, corrupt the manifest after closing the writer,
|
||||
// verify manifest rotation occurs and DB reopens cleanly.
|
||||
NewDB();
|
||||
auto stats = CreateDBStatistics();
|
||||
imm_db_options_.statistics = stats;
|
||||
mutable_db_options_.verify_manifest_content_on_close = true;
|
||||
mutex_.Lock();
|
||||
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
|
||||
@@ -2496,9 +2488,6 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseCorruptRecord) {
|
||||
CloseDB();
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
|
||||
// Corruption detected once, rewrite succeeded
|
||||
ASSERT_EQ(1, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
|
||||
|
||||
// Manifest should have been rotated (new file number)
|
||||
std::string manifest_path_after;
|
||||
uint64_t manifest_file_number = 0;
|
||||
@@ -2513,10 +2502,8 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseCorruptRecord) {
|
||||
|
||||
TEST_F(VersionSetTest, ManifestContentValidationOnCloseDisabled) {
|
||||
// Default (option disabled), corrupt manifest after writer close,
|
||||
// verify no rotation occurred -- corrupt manifest persists.
|
||||
// verify no rotation occurred — corrupt manifest persists.
|
||||
NewDB();
|
||||
auto stats = CreateDBStatistics();
|
||||
imm_db_options_.statistics = stats;
|
||||
// verify_manifest_content_on_close defaults to false
|
||||
|
||||
VersionEdit edit;
|
||||
@@ -2538,9 +2525,6 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseDisabled) {
|
||||
|
||||
ASSERT_FALSE(content_validation_ran);
|
||||
|
||||
// Validation disabled -- counter should be zero
|
||||
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
|
||||
|
||||
// Manifest path should be unchanged (no rotation since validation is off)
|
||||
std::string manifest_path_after;
|
||||
uint64_t manifest_file_number = 0;
|
||||
@@ -2555,8 +2539,6 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseSizeCheckFails) {
|
||||
// Verify recovery happens via size-check path. Content validation still
|
||||
// runs afterward on the freshly rewritten manifest.
|
||||
NewDB();
|
||||
auto stats = CreateDBStatistics();
|
||||
imm_db_options_.statistics = stats;
|
||||
mutable_db_options_.verify_manifest_content_on_close = true;
|
||||
mutex_.Lock();
|
||||
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
|
||||
@@ -2584,10 +2566,6 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseSizeCheckFails) {
|
||||
ASSERT_NO_FATAL_FAILURE(CloseDB());
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
|
||||
// Size check caught the issue; content validation on rewritten manifest
|
||||
// should pass -- no content validation failure recorded
|
||||
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
|
||||
|
||||
// Size check should have triggered rotation
|
||||
std::string manifest_path_after;
|
||||
uint64_t manifest_file_number = 0;
|
||||
@@ -2605,8 +2583,6 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseCorruptAfterRewrite) {
|
||||
// The loop should detect corruption twice: once triggering a rewrite, and
|
||||
// once reporting that the rewritten manifest is also corrupt.
|
||||
NewDB();
|
||||
auto stats = CreateDBStatistics();
|
||||
imm_db_options_.statistics = stats;
|
||||
mutable_db_options_.verify_manifest_content_on_close = true;
|
||||
mutex_.Lock();
|
||||
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
|
||||
@@ -2660,17 +2636,12 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseCorruptAfterRewrite) {
|
||||
|
||||
// OnIOError should have fired twice (once per corrupt detection)
|
||||
ASSERT_EQ(io_error_count, 2);
|
||||
|
||||
// Validation failure counter should match
|
||||
ASSERT_EQ(2, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
|
||||
}
|
||||
|
||||
TEST_F(VersionSetTest, ManifestContentValidationOnCloseOpenFails) {
|
||||
// Delete the manifest before content validation so it can't be opened.
|
||||
// Close() should surface the I/O error to the caller.
|
||||
NewDB();
|
||||
auto stats = CreateDBStatistics();
|
||||
imm_db_options_.statistics = stats;
|
||||
mutable_db_options_.verify_manifest_content_on_close = true;
|
||||
mutex_.Lock();
|
||||
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
|
||||
@@ -2696,9 +2667,6 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseOpenFails) {
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_TRUE(close_s.IsIOError()) << close_s.ToString();
|
||||
|
||||
// File couldn't be opened -- no content validation ran
|
||||
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
|
||||
}
|
||||
|
||||
TEST_F(VersionStorageInfoTest, AddRangeDeletionCompensatedFileSize) {
|
||||
|
||||
@@ -30,10 +30,6 @@ class DBWideBasicTest : public DBTestBase {
|
||||
return wide_column_test_util::GetOptionsForBlobTest(GetDefaultOptions());
|
||||
}
|
||||
|
||||
Options GetDirectWriteOptions() {
|
||||
return wide_column_test_util::GetDirectWriteOptions(GetDefaultOptions());
|
||||
}
|
||||
|
||||
// Helper: runs the EntityBlobAfterFlush test logic with the given options.
|
||||
void RunEntityBlobAfterFlush(const Options& options);
|
||||
|
||||
@@ -2163,57 +2159,6 @@ TEST_F(DBWideBasicTest, MultiGetEntityWithBlobResolution) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBWideBasicTest,
|
||||
MultiGetBlobBackedEntityDirectWriteMemtableBatchLookup) {
|
||||
// Goal: force the memtable batch MultiGet optimization to read a direct-write
|
||||
// blob-backed entity from the active memtable. One key exercises merge
|
||||
// resolution against a blob-backed entity base, which requires Saver to carry
|
||||
// the BlobFetcher through the batched callback path. The second key proves
|
||||
// the non-merge default-column path still resolves correctly in the same
|
||||
// batched request.
|
||||
Options options = GetDirectWriteOptions();
|
||||
options.min_blob_size = 50;
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string merge_key = "multiget_direct_write_merge_key";
|
||||
const std::string plain_key = "multiget_direct_write_plain_key";
|
||||
const std::string default_value = GenerateLargeValue(100, 'd');
|
||||
const std::string other_default_value = GenerateLargeValue(110, 'p');
|
||||
const std::string large_value = GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = GenerateSmallValue();
|
||||
const std::string merge_operand = "tail";
|
||||
|
||||
WideColumns merge_columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
WideColumns plain_columns{{kDefaultWideColumnName, other_default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
|
||||
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
|
||||
merge_key, merge_columns));
|
||||
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
|
||||
plain_key, plain_columns));
|
||||
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), merge_key,
|
||||
merge_operand));
|
||||
|
||||
std::array<Slice, 2> keys{{merge_key, plain_key}};
|
||||
std::array<PinnableSlice, 2> values;
|
||||
std::array<Status, 2> statuses;
|
||||
|
||||
db_->MultiGet(ReadOptions(), db_->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0], default_value + "|" + merge_operand);
|
||||
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1], other_default_value);
|
||||
}
|
||||
|
||||
void DBWideBasicTest::RunEntityBlobAfterFlush(const Options& options) {
|
||||
Reopen(options);
|
||||
|
||||
@@ -2831,7 +2776,7 @@ TEST_F(DBWideBasicTest, EntityBlobBlockCacheTierGet) {
|
||||
// Reopen to clear any caches
|
||||
Reopen(options);
|
||||
|
||||
// Read with kBlockCacheTier -- blob is not in cache
|
||||
// Read with kBlockCacheTier — blob is not in cache
|
||||
ReadOptions read_opts;
|
||||
read_opts.read_tier = kBlockCacheTier;
|
||||
PinnableSlice result;
|
||||
@@ -2964,347 +2909,6 @@ TEST_F(DBWideBasicTest, MergeEntityWithBlobColumns) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBWideBasicTest, GetMergeOperandsWithBlobBackedEntityDefaultColumn) {
|
||||
// Goal: cover both GetMergeOperands code paths that read a compacted V2
|
||||
// wide-column entity whose default column was moved to a blob file. The
|
||||
// first read exercises the base-value path directly, then a merge operand is
|
||||
// added so the second read exercises the merge-plus-base path.
|
||||
Options options = GetBlobTestOptions();
|
||||
options.min_blob_size = 50;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string key = "merge_operands_blob_entity";
|
||||
const std::string default_value = GenerateLargeValue(100, 'd');
|
||||
const std::string large_value = GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = GenerateSmallValue();
|
||||
const std::string merge_operand = "suffix";
|
||||
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_FALSE(GetBlobFileNumbers().empty());
|
||||
|
||||
{
|
||||
// The compacted V2 entity becomes the first operand when there are no
|
||||
// newer merge operands above it.
|
||||
GetMergeOperandsOptions get_merge_opts;
|
||||
get_merge_opts.expected_max_number_of_operands = 1;
|
||||
|
||||
std::array<PinnableSlice, 2> merge_operands;
|
||||
int number_of_operands = 0;
|
||||
|
||||
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
key, merge_operands.data(), &get_merge_opts,
|
||||
&number_of_operands));
|
||||
ASSERT_EQ(number_of_operands, 1);
|
||||
ASSERT_EQ(merge_operands[0], default_value);
|
||||
}
|
||||
|
||||
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
|
||||
merge_operand));
|
||||
|
||||
{
|
||||
// After a merge is added, GetMergeOperands must still resolve the blob
|
||||
// backed base default column while traversing the older base entry below
|
||||
// the newer merge operand.
|
||||
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_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
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(DBWideBasicTest,
|
||||
GetMergeOperandsWithBlobBackedEntityDefaultColumnDirectWriteMemtable) {
|
||||
// Goal: cover the pre-flush memtable path when blob direct write stores a
|
||||
// blob-backed V2 entity in memory. The first GetMergeOperands call exercises
|
||||
// the no-merge base-operand path. After adding a merge, DB::Get exercises
|
||||
// merge resolution against the same memtable entity, and a second
|
||||
// GetMergeOperands call exercises the merge-plus-base path.
|
||||
Options options = GetDirectWriteOptions();
|
||||
options.min_blob_size = 50;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string key = "direct_write_merge_operands_blob_entity";
|
||||
const std::string default_value = GenerateLargeValue(100, 'd');
|
||||
const std::string large_value = GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = GenerateSmallValue();
|
||||
const std::string merge_operand = "suffix";
|
||||
const std::string expected_merged = default_value + "|" + merge_operand;
|
||||
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
|
||||
{
|
||||
GetMergeOperandsOptions get_merge_opts;
|
||||
get_merge_opts.expected_max_number_of_operands = 1;
|
||||
|
||||
std::array<PinnableSlice, 2> merge_operands;
|
||||
int number_of_operands = 0;
|
||||
|
||||
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
key, merge_operands.data(), &get_merge_opts,
|
||||
&number_of_operands));
|
||||
ASSERT_EQ(number_of_operands, 1);
|
||||
ASSERT_EQ(merge_operands[0], default_value);
|
||||
}
|
||||
|
||||
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
|
||||
merge_operand));
|
||||
|
||||
{
|
||||
PinnableSlice result;
|
||||
ASSERT_OK(
|
||||
db_->Get(ReadOptions(), db_->DefaultColumnFamily(), 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_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
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(DBWideBasicTest,
|
||||
GetAndGetEntityWithBlobBackedDefaultColumnDirectWriteMemtableReadOnly) {
|
||||
// Goal: cover the read-only reopen path after writing a blob-backed
|
||||
// direct-write entity without manually flushing it first. The test checks
|
||||
// both `Get()`, which must resolve the default-column blob reference, and
|
||||
// `GetEntity()`, which must eagerly resolve the entity's unresolved blob
|
||||
// columns before returning them to the caller.
|
||||
Options options = GetDirectWriteOptions();
|
||||
options.min_blob_size = 50;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string key = "readonly_direct_write_memtable_entity";
|
||||
const std::string default_value = GenerateLargeValue(100, 'd');
|
||||
const std::string large_value = GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = GenerateSmallValue();
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
|
||||
Close();
|
||||
options.avoid_flush_during_recovery = true;
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
|
||||
{
|
||||
PinnableSlice result;
|
||||
ASSERT_OK(
|
||||
db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &result));
|
||||
ASSERT_EQ(result, default_value);
|
||||
}
|
||||
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), key,
|
||||
&result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBWideBasicTest, ReadOnlyDirectWriteMemtableBlobBlockCacheTier) {
|
||||
// Goal: cover the read-only memtable path under kBlockCacheTier. The test
|
||||
// keeps a blob-backed direct-write entity in WAL-backed recovery state so
|
||||
// resolving either Get() or GetEntity() would require blob I/O, which must
|
||||
// surface as Incomplete rather than silently reading the blob file.
|
||||
Options options = GetDirectWriteOptions();
|
||||
options.min_blob_size = 50;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string key = "readonly_direct_write_memtable_block_cache_tier";
|
||||
const std::string default_value = GenerateLargeValue(100, 'd');
|
||||
const std::string large_value = GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = GenerateSmallValue();
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
|
||||
Close();
|
||||
options.avoid_flush_during_recovery = true;
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
|
||||
ReadOptions read_opts;
|
||||
read_opts.read_tier = kBlockCacheTier;
|
||||
|
||||
{
|
||||
PinnableSlice result;
|
||||
const Status s =
|
||||
db_->Get(read_opts, db_->DefaultColumnFamily(), key, &result);
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
ASSERT_TRUE(result.empty());
|
||||
}
|
||||
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
const Status s =
|
||||
db_->GetEntity(read_opts, db_->DefaultColumnFamily(), key, &result);
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
ASSERT_TRUE(result.columns().empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBWideBasicTest,
|
||||
GetMergeOperandsWithBlobBackedEntityDefaultColumnReadOnly) {
|
||||
// Goal: exercise OpenForReadOnly on a blob-backed V2 entity base in SST with
|
||||
// a newer merge operand in a separate SST. The read-only DB must resolve the
|
||||
// blob-backed default column for both Get() and GetMergeOperands() without
|
||||
// relying on mutable memtable state.
|
||||
Options options = GetBlobTestOptions();
|
||||
options.min_blob_size = 50;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string key = "readonly_merge_operands_blob_entity";
|
||||
const std::string default_value = GenerateLargeValue(100, 'd');
|
||||
const std::string large_value = GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = GenerateSmallValue();
|
||||
const std::string merge_operand = "suffix";
|
||||
const std::string expected_merged = default_value + "|" + merge_operand;
|
||||
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_FALSE(GetBlobFileNumbers().empty());
|
||||
|
||||
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
|
||||
merge_operand));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
Close();
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
|
||||
{
|
||||
PinnableSlice result;
|
||||
ASSERT_OK(
|
||||
db_->Get(ReadOptions(), db_->DefaultColumnFamily(), 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_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
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(DBWideBasicTest,
|
||||
GetMergeOperandsWithBlobBackedEntityDefaultColumnBlockCacheTier) {
|
||||
// Goal: prove the SST-backed GetMergeOperands path propagates Incomplete
|
||||
// when resolving a blob-backed default column would require I/O. One key
|
||||
// covers the direct base-entity path, and the other covers the merge-plus-
|
||||
// base path with the merge operand flushed to a newer SST.
|
||||
Options options = GetBlobTestOptions();
|
||||
options.min_blob_size = 50;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string base_key = "merge_operands_blob_entity_cache_base";
|
||||
const std::string merge_key = "merge_operands_blob_entity_cache_merge";
|
||||
const std::string default_value = GenerateLargeValue(100, 'd');
|
||||
const std::string large_value = GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = GenerateSmallValue();
|
||||
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), base_key,
|
||||
columns));
|
||||
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
|
||||
merge_key, columns));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), merge_key,
|
||||
"tail"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Reopen to clear any cached blob readers/values so kBlockCacheTier must
|
||||
// report Incomplete instead of succeeding through a warm cache.
|
||||
Reopen(options);
|
||||
|
||||
ReadOptions read_opts;
|
||||
read_opts.read_tier = kBlockCacheTier;
|
||||
|
||||
{
|
||||
GetMergeOperandsOptions get_merge_opts;
|
||||
get_merge_opts.expected_max_number_of_operands = 1;
|
||||
|
||||
std::array<PinnableSlice, 2> merge_operands;
|
||||
int number_of_operands = 0;
|
||||
|
||||
const Status s = db_->GetMergeOperands(
|
||||
read_opts, db_->DefaultColumnFamily(), base_key, merge_operands.data(),
|
||||
&get_merge_opts, &number_of_operands);
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
}
|
||||
|
||||
{
|
||||
GetMergeOperandsOptions get_merge_opts;
|
||||
get_merge_opts.expected_max_number_of_operands = 2;
|
||||
|
||||
std::array<PinnableSlice, 2> merge_operands;
|
||||
int number_of_operands = 0;
|
||||
|
||||
const Status s = db_->GetMergeOperands(
|
||||
read_opts, db_->DefaultColumnFamily(), merge_key, merge_operands.data(),
|
||||
&get_merge_opts, &number_of_operands);
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBWideBasicTest, MergeEntityWithBlobColumnsNoDefault) {
|
||||
// Test: Merge on a V2 entity without a default column. The merge result
|
||||
// should produce a valid entity with all original columns plus a default
|
||||
@@ -3394,7 +2998,7 @@ TEST_F(DBWideBasicTest, MergeEntityWithBlobColumnsBlockCacheTier) {
|
||||
// Reopen to clear caches
|
||||
Reopen(options);
|
||||
|
||||
// Read with kBlockCacheTier -- blob not in cache, merge needs blob resolution
|
||||
// Read with kBlockCacheTier — blob not in cache, merge needs blob resolution
|
||||
ReadOptions read_opts;
|
||||
read_opts.read_tier = kBlockCacheTier;
|
||||
PinnableSlice result;
|
||||
|
||||
@@ -59,7 +59,7 @@ Status ReadPathBlobResolver::ResolveColumn(size_t column_index,
|
||||
blob_resolver_util::FindBlobColumn(blob_columns_, column_index);
|
||||
|
||||
if (blob_index_ptr == nullptr) {
|
||||
// Inline column -- return the value directly
|
||||
// Inline column — return the value directly
|
||||
*resolved_value = (*columns_)[column_index].value();
|
||||
} else {
|
||||
// Check if already resolved
|
||||
|
||||
@@ -153,8 +153,6 @@ class WideColumnSerialization {
|
||||
// value() is initially the serialized BlobIndex bytes from the entity.
|
||||
// blob_columns: receives (column_index, blob_index) pairs identifying which
|
||||
// entries in `columns` are blob references and their decoded BlobIndex data.
|
||||
// For valid input, this must agree with HasBlobColumns() about whether blob
|
||||
// column references are present.
|
||||
static Status DeserializeV2(
|
||||
Slice& input, std::vector<WideColumn>& columns,
|
||||
std::vector<std::pair<size_t, BlobIndex>>& blob_columns);
|
||||
@@ -162,8 +160,6 @@ class WideColumnSerialization {
|
||||
// Check if the serialized entity has any blob column references.
|
||||
// Sets *has_blob_columns to true if version >= 2 and at least one column
|
||||
// has blob type; false otherwise.
|
||||
// For valid input, this must agree with DeserializeV2() about whether blob
|
||||
// column references are present.
|
||||
// Returns Status::Corruption on decode errors.
|
||||
static Status HasBlobColumns(const Slice& input, bool& has_blob_columns);
|
||||
|
||||
|
||||
@@ -391,7 +391,7 @@ static BlobIndex MakeBlobIndex(uint64_t file_number, uint64_t offset,
|
||||
return bi;
|
||||
}
|
||||
|
||||
// Helper: V2 serialize -> DeserializeV2 round-trip, returning
|
||||
// Helper: V2 serialize → DeserializeV2 round-trip, returning
|
||||
// deserialized columns and blob column info.
|
||||
static void V2SerializeAndDeserialize(
|
||||
const std::vector<std::pair<std::string, std::string>>& columns,
|
||||
@@ -798,7 +798,7 @@ TEST_F(WideColumnSerializationTest, RandomizedSerializeDeserializeRoundTrip) {
|
||||
}
|
||||
}
|
||||
|
||||
// V2 serialize -> DeserializeV2 round-trip
|
||||
// V2 serialize → DeserializeV2 round-trip
|
||||
std::string serialized;
|
||||
std::vector<WideColumn> deserialized;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_out;
|
||||
|
||||
@@ -197,10 +197,10 @@ class DbStressFSWrapper : public FileSystemWrapper {
|
||||
assert(!file_opts.file_checksum_func_name.empty());
|
||||
if (file_opts.file_checksum_func_name == kUnknownFileChecksumFuncName ||
|
||||
file_opts.file_checksum_func_name == kNoFileChecksumFuncName) {
|
||||
// No checksum available -- checksum value must be empty
|
||||
// No checksum available — checksum value must be empty
|
||||
assert(file_opts.file_checksum.empty());
|
||||
} else {
|
||||
// A real checksum function -- checksum value must be present
|
||||
// A real checksum function — checksum value must be present
|
||||
assert(!file_opts.file_checksum.empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2900,7 +2900,7 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
op_logs += "N";
|
||||
}
|
||||
|
||||
// backward scan -- skip when backward iteration is not supported
|
||||
// backward scan — skip when backward iteration is not supported
|
||||
if (FLAGS_test_backward_scan) {
|
||||
key_str = Key(ub - 1);
|
||||
iter->SeekForPrev(key_str);
|
||||
|
||||
@@ -88,7 +88,7 @@ hx235:
|
||||
|
||||
pdillinger:
|
||||
full_name: Peter Dillinger
|
||||
fbid: 593944283
|
||||
fbid: 513108
|
||||
|
||||
alanpaxton:
|
||||
full_name: Alan Paxton
|
||||
@@ -101,7 +101,3 @@ anand1976:
|
||||
|
||||
poojam23:
|
||||
full_name: Pooja Malik
|
||||
|
||||
joshkang97:
|
||||
full_name: Josh Kang
|
||||
fbid: 180489168460795
|
||||
@@ -1,118 +0,0 @@
|
||||
---
|
||||
title: Interpolation search for SST index blocks
|
||||
layout: post
|
||||
author: joshkang97
|
||||
category: blog
|
||||
---
|
||||
|
||||
For workloads with uniformly distributed keys, RocksDB now supports **interpolation search** for SST index blocks as an alternative to the default binary search.
|
||||
|
||||
## The idea
|
||||
|
||||
Binary search always splits the remaining range in half:
|
||||
|
||||
```
|
||||
mid = low + (high - low) / 2
|
||||
```
|
||||
|
||||
That's Θ(log n) probes regardless of the data. Interpolation search instead estimates where the target should land based on its value relative to the current boundaries:
|
||||
|
||||
```
|
||||
probe = low + (target - key[low]) * (high - low) / (key[high] - key[low])
|
||||
```
|
||||
|
||||
On uniformly distributed keys, that's expected O(log log n) probes. The canonical example: for an index block with restart keys `0, 1, 2, ..., 1023` and a seek for `900`, binary search needs about 10 hops; interpolation search lands on it in 1.
|
||||
|
||||
The catch is that pure interpolation search degrades to O(n) on badly skewed data.
|
||||
|
||||
## Turning a key into a number
|
||||
|
||||
The interpolation formula needs numeric values, but index keys are variable-length byte slices. RocksDB extracts a `uint64_t` per key by reading the first 8 bytes after the common prefix shared by the block's boundary keys, in big-endian, and zero-pads to the right if the remaining bytes are too short.
|
||||
|
||||
```cpp
|
||||
inline uint64_t ReadBe64FromKey(Slice s, bool is_user_key, size_t offset) {
|
||||
// ... strip internal seq/type bytes if needed ...
|
||||
if (s.size() - offset >= 8) {
|
||||
uint64_t val;
|
||||
memcpy(&val, s.data() + offset, sizeof(val));
|
||||
return port::kLittleEndian ? EndianSwapValue(val) : val;
|
||||
}
|
||||
// pad short tails with zeros on the right (preserves bytewise order)
|
||||
}
|
||||
```
|
||||
|
||||
Big-endian + zero-pad preserves bytewise ordering, so the linear interpolation formula stays consistent with the comparator. This is also why the feature requires `BytewiseComparator`.
|
||||
|
||||
Two distinct keys can still collapse to the same `uint64_t` once you go past the first 8 non-shared bytes. To avoid a divide-by-zero, we simply fall back to binary search in that case.
|
||||
|
||||
## How to enable it
|
||||
|
||||
To force interpolation search on every index block:
|
||||
|
||||
```cpp
|
||||
rocksdb::BlockBasedTableOptions table_options;
|
||||
table_options.index_block_search_type =
|
||||
rocksdb::BlockBasedTableOptions::kInterpolation;
|
||||
```
|
||||
|
||||
## kAuto: per-block selection
|
||||
|
||||
`kAuto` is the recommended way to use the feature. It chooses the search algorithm for each index block automatically, based on a uniformity hint written into the block footer at SST construction time:
|
||||
|
||||
```cpp
|
||||
table_options.index_block_search_type =
|
||||
rocksdb::BlockBasedTableOptions::kAuto;
|
||||
table_options.uniform_cv_threshold = 0.2;
|
||||
```
|
||||
|
||||
When `uniform_cv_threshold >= 0`, the SST writer scans each index block's restart keys and computes the **coefficient of variation (CV)** of the gaps between consecutive numeric key values:
|
||||
|
||||
```
|
||||
gap[i] = key_value[i + 1] - key_value[i]
|
||||
CV = stddev(gap) / mean(gap)
|
||||
```
|
||||
|
||||
Lower CV means the gaps are more uniform — and the more likely interpolation search will outperform binary search. The CV is computed incrementally with Welford's online algorithm, so the scan is one pass over the restart points.
|
||||
|
||||
If `CV < uniform_cv_threshold`, RocksDB sets an `is_uniform` bit in the block footer. At read time, `kAuto` resolves to `kInterpolation` only when that bit is set *and* the comparator is bytewise; otherwise it uses `kBinary`.
|
||||
|
||||
### Write overhead
|
||||
|
||||
Computing the `is_uniform` bit is a cheap operation as it is only computed for the index blocks in a SST file. CPU profiling of `db_bench -benchmarks=fillseq,compact -compression_type=none -disable_wal=1` attributes only ~0.08% of write-path CPU to `ScanForUniformity`.
|
||||
|
||||
After a few more releases, we plan to enable kAuto and uniform_cv_threshold by default.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Setup — populate a DB and force a single-level shape so all reads hit the same index structure, then measure point-read throughput:
|
||||
|
||||
```
|
||||
# Build a release binary
|
||||
make clean && DEBUG_LEVEL=0 make db_bench
|
||||
|
||||
# Load + compact, varying the index_shortening_mode
|
||||
./db_bench -benchmarks=fillrandom,compact \
|
||||
-index_shortening_mode=1
|
||||
|
||||
# Then point-read against the populated DB
|
||||
./db_bench -use_existing_db=true -benchmarks=readrandom \
|
||||
-index_block_search_type=binary_search # or interpolation_search / auto_search
|
||||
```
|
||||
|
||||
`index_shortening_mode=1` (`kShortenSeparators`) keeps the file's last index key intact, which preserves a roughly uniform numeric distribution for the benchmark.
|
||||
|
||||
Results, averaged over multiple runs:
|
||||
|
||||
| Mode | ops/s | vs `binary_search` |
|
||||
|---|---|---|
|
||||
| `binary_search` | 335,749 | baseline |
|
||||
| `interpolation_search` | 366,598 | **+9.2%** |
|
||||
| `auto_search` | 366,832 | **+9.2%** |
|
||||
|
||||
## Compatibility
|
||||
|
||||
The `is_uniform` bit reuses a previously-reserved bit in the data block footer. SSTs written by older RocksDB never set it and decode as `is_uniform = false`, so they read with binary search under `kAuto`. However, after the bit is set, if read by an older RocksDB version < 11.0.0, it will read it as a corruption error.
|
||||
|
||||
## Future work
|
||||
|
||||
Some future opportunities can involve extending interpolation search to data blocks, as well as supporting other comparators such as `ReverseBytewiseComparator`.
|
||||
Vendored
+2
-2
@@ -1892,7 +1892,7 @@ TEST_F(EnvPosixTest, SupportedOpsNoAsyncIOOnIOUringInitFailure) {
|
||||
int64_t thread_supported_ops2 = 0;
|
||||
std::thread t([&]() {
|
||||
fs->SupportedOps(thread_supported_ops);
|
||||
// Second call on the same thread -- cached failure, no retry.
|
||||
// Second call on the same thread — cached failure, no retry.
|
||||
fs->SupportedOps(thread_supported_ops2);
|
||||
});
|
||||
t.join();
|
||||
@@ -1903,7 +1903,7 @@ TEST_F(EnvPosixTest, SupportedOpsNoAsyncIOOnIOUringInitFailure) {
|
||||
// Second call should also lack kAsyncIO.
|
||||
ASSERT_EQ(thread_supported_ops2 & (1 << FSSupportedOps::kAsyncIO), 0);
|
||||
ASSERT_NE(thread_supported_ops2 & (1 << FSSupportedOps::kFSPrefetch), 0);
|
||||
// CreateIOUring must have been called exactly once -- not retried.
|
||||
// CreateIOUring must have been called exactly once — not retried.
|
||||
ASSERT_EQ(create_count, 1);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
|
||||
+1
-1
@@ -221,7 +221,7 @@ IOStatus GenerateOneFileChecksum(
|
||||
std::unique_ptr<FSRandomAccessFile> r_file;
|
||||
FileOptions fopts = file_options;
|
||||
if (fopts.file_checksum.empty()) {
|
||||
// No expected checksum is known -- this is a from-scratch computation.
|
||||
// No expected checksum is known — this is a from-scratch computation.
|
||||
fopts.file_checksum_func_name = kNoFileChecksumFuncName;
|
||||
}
|
||||
io_s = fs->NewRandomAccessFile(file_path, fopts, &r_file, nullptr);
|
||||
|
||||
@@ -3899,7 +3899,7 @@ TEST_P(FSBufferPrefetchTest, FSBufferPrefetchStatsInternals) {
|
||||
&result, &s, for_compaction);
|
||||
// Platforms that don't have IO uring may not support async IO.
|
||||
// With the ReadAsync sync fallback, s will be OK even when async IO is
|
||||
// unavailable -- detect by checking if the second buffer has an async read
|
||||
// unavailable — detect by checking if the second buffer has an async read
|
||||
// in progress.
|
||||
if (use_async_prefetch && s.IsNotSupported()) {
|
||||
return;
|
||||
|
||||
+70
-42
@@ -1,61 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
# pre-push hook: block pushing on common mistakes.
|
||||
# pre-push hook: auto-format changed code before pushing.
|
||||
#
|
||||
# 1. Checks for untracked source files in tracked directories, which likely
|
||||
# need to be committed or removed before the pushed code will build.
|
||||
# 2. Runs `make check-format` to verify formatting.
|
||||
# Runs `make format-auto` to apply clang-format to files changed relative to
|
||||
# the upstream merge base. If any files are reformatted, they are committed as
|
||||
# a new "format" commit and the push is retried automatically.
|
||||
#
|
||||
# Automatically active via `make all` / `make check` (sets core.hooksPath).
|
||||
# Install with:
|
||||
# make install-hooks
|
||||
#
|
||||
# Skip with:
|
||||
# git push --no-verify
|
||||
|
||||
set -e
|
||||
|
||||
REMOTE="$1"
|
||||
URL="$2"
|
||||
|
||||
# Only act when we have a terminal (skip in CI / automated pushes)
|
||||
if [ ! -t 1 ] && [ -z "$ROCKSDB_FORMAT_HOOK" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
FAILED=0
|
||||
# Read the refs being pushed from stdin
|
||||
PUSH_REFS=()
|
||||
while read local_ref local_oid remote_ref remote_oid; do
|
||||
PUSH_REFS+=("$local_ref:$remote_ref")
|
||||
done
|
||||
|
||||
# --- Check for untracked source files in tracked directories ---
|
||||
SUSPECT_FILES=$(git ls-files --others --exclude-standard \
|
||||
| grep -E '\.(cc|cpp|c|h|hpp|java|py|mk|sh)$' \
|
||||
| grep -v '^third-party/' \
|
||||
| while IFS= read -r f; do
|
||||
top_dir="${f%%/*}"
|
||||
if [ "$top_dir" = "$f" ] || [ -n "$(git ls-files "$top_dir/" | head -1)" ]; then
|
||||
echo "$f"
|
||||
fi
|
||||
done)
|
||||
|
||||
if [ -n "$SUSPECT_FILES" ]; then
|
||||
echo ""
|
||||
echo "==========================================================="
|
||||
echo " pre-push: untracked source files in tracked directories."
|
||||
echo " Push blocked. These may need to be committed, or the"
|
||||
echo " pushed code could fail to build:"
|
||||
echo ""
|
||||
echo "$SUSPECT_FILES" | sed 's/^/ /'
|
||||
echo ""
|
||||
echo " Commit them, add to .gitignore, delete, or --no-verify."
|
||||
echo "==========================================================="
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
# --- Check formatting ---
|
||||
echo "pre-push: running format check..."
|
||||
|
||||
if ! make check-format; then
|
||||
echo ""
|
||||
echo "==========================================================="
|
||||
echo " pre-push: formatting issues detected. Push blocked."
|
||||
echo " Run 'make format-auto' then amend your commit and retry."
|
||||
echo "==========================================================="
|
||||
FAILED=1
|
||||
else
|
||||
echo "pre-push: formatting is clean."
|
||||
# Save any uncommitted work so format-auto only touches committed code
|
||||
NEED_STASH=0
|
||||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||
git stash push -q -m "pre-push-hook-$(date +%s)"
|
||||
NEED_STASH=1
|
||||
fi
|
||||
|
||||
exit $FAILED
|
||||
cleanup() {
|
||||
if [ "$NEED_STASH" -eq 1 ]; then
|
||||
git stash pop -q 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Run format check (check-only mode first to see if anything needs fixing)
|
||||
if make check-format 2>/dev/null; then
|
||||
echo "pre-push: formatting is clean."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "pre-push: formatting issues detected, auto-fixing..."
|
||||
make format-auto
|
||||
|
||||
# Check if format-auto actually changed anything
|
||||
if git diff --quiet; then
|
||||
echo "pre-push: formatting is clean after auto-fix."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create a new format commit
|
||||
git add -A
|
||||
git commit -m "format: auto-format code" --no-verify -q
|
||||
|
||||
# Build the refspecs using the current (post-format) branch HEAD
|
||||
REFSPECS=()
|
||||
for ref_pair in "${PUSH_REFS[@]}"; do
|
||||
remote_ref="${ref_pair#*:}"
|
||||
REFSPECS+=("HEAD:$remote_ref")
|
||||
done
|
||||
|
||||
# Retry push, skipping the hook to avoid recursion
|
||||
if git push --no-verify "$REMOTE" "${REFSPECS[@]}" 2>&1; then
|
||||
echo ""
|
||||
echo "==========================================================="
|
||||
echo " pre-push: automatically formatted with a new commit and"
|
||||
echo " pushed. You can ignore the 'failed to push' message below"
|
||||
echo " — that is git cancelling the stale push; your code is"
|
||||
echo " already on the remote."
|
||||
echo "==========================================================="
|
||||
exit 1
|
||||
else
|
||||
echo ""
|
||||
echo "==========================================================="
|
||||
echo " pre-push: format commit was added but push failed."
|
||||
echo " Please run 'git push' again manually."
|
||||
echo "==========================================================="
|
||||
exit 1
|
||||
fi
|
||||
@@ -152,12 +152,7 @@ class Cache : public Customizable {
|
||||
|
||||
// For helpers without SecondaryCache support
|
||||
explicit CacheItemHelper(CacheEntryRole _role, DeleterFn _del_cb = nullptr)
|
||||
: del_cb(_del_cb),
|
||||
size_cb(nullptr),
|
||||
saveto_cb(nullptr),
|
||||
create_cb(nullptr),
|
||||
role(_role),
|
||||
without_secondary_compat(this) {}
|
||||
: CacheItemHelper(_role, _del_cb, nullptr, nullptr, nullptr, this) {}
|
||||
|
||||
// For helpers with SecondaryCache support
|
||||
explicit CacheItemHelper(CacheEntryRole _role, DeleterFn _del_cb,
|
||||
|
||||
@@ -456,13 +456,6 @@ class CompressionManager
|
||||
// supported)
|
||||
virtual bool SupportsCompressionType(CompressionType type) const = 0;
|
||||
|
||||
// Return a human-readable name for the given compression type within this
|
||||
// CompressionManager's schema. The default implementation returns the
|
||||
// generic built-in name such as "Snappy", "ZSTD", "Reserved4F", or
|
||||
// "Custom8A". Override to provide more specific names for custom
|
||||
// compression types.
|
||||
virtual std::string CompressionTypeToString(CompressionType type) const;
|
||||
|
||||
// TODO: function to check compatibility with or sanitize CompressionOptions
|
||||
|
||||
// ************************* Compressor creation *********************** //
|
||||
@@ -663,10 +656,6 @@ class CompressionManagerWrapper : public CompressionManager {
|
||||
return wrapped_->SupportsCompressionType(type);
|
||||
}
|
||||
|
||||
std::string CompressionTypeToString(CompressionType type) const override {
|
||||
return wrapped_->CompressionTypeToString(type);
|
||||
}
|
||||
|
||||
std::unique_ptr<Compressor> GetCompressorForSST(
|
||||
const FilterBuildingContext& context, const CompressionOptions& opts,
|
||||
CompressionType preferred) override {
|
||||
|
||||
@@ -946,7 +946,7 @@ struct AdvancedColumnFamilyOptions {
|
||||
// (estimated_reads / file_size) exceeds this threshold. This helps reduce
|
||||
// read amplification for hot keys by compacting frequently-read files.
|
||||
//
|
||||
// Only "collapsible" reads are counted -- lookups that return NotFound
|
||||
// Only "collapsible" reads are counted — lookups that return NotFound
|
||||
// (bloom filter false positive), Delete/SingleDeletion (tombstone), or
|
||||
// Merge (partial result). These are reads where the file contributed no
|
||||
// final value and compaction would eliminate the wasted work.
|
||||
@@ -972,15 +972,15 @@ struct AdvancedColumnFamilyOptions {
|
||||
// r * S * B = 2 * (1 + F) * S
|
||||
// r = 2 * (1 + F) / B
|
||||
//
|
||||
// With F = 10, B = 4096: r = 22 / 4096 ~= 0.005.
|
||||
// With F = 10, B = 4096: r = 22 / 4096 ≈ 0.005.
|
||||
//
|
||||
// With a block-cache hit rate h (0 <= h < 1), each collapsible read
|
||||
// With a block-cache hit rate h (0 ≤ h < 1), each collapsible read
|
||||
// only costs (1 - h) * B bytes of actual disk IO, so:
|
||||
// r = 2 * (1 + F) / ((1 - h) * B)
|
||||
//
|
||||
// h = 0 -> r ~= 0.005
|
||||
// h = 0.5 -> r ~= 0.01
|
||||
// h = 0.9 -> r ~= 0.05
|
||||
// h = 0 → r ≈ 0.005
|
||||
// h = 0.5 → r ≈ 0.01
|
||||
// h = 0.9 → r ≈ 0.05
|
||||
//
|
||||
// A recommended starting point is 0.01, which avoids triggering
|
||||
// compactions that cost more IO than they save for most cache-friendly
|
||||
|
||||
@@ -287,10 +287,6 @@ class CompactionFilter : public Customizable {
|
||||
// may not realize there is a write conflict and may allow a Transaction to
|
||||
// Commit that should have failed. Instead, it is better to implement any
|
||||
// Merge filtering inside the MergeOperator.
|
||||
//
|
||||
// Note: Under WritePrepared, WriteUnprepared, and WriteCommitted
|
||||
// TransactionDB, this method is skipped for operands at or below the latest
|
||||
// published sequence, typically every operand in the input.
|
||||
virtual bool FilterMergeOperand(int /*level*/, const Slice& /*key*/,
|
||||
const Slice& /*operand*/) const {
|
||||
return false;
|
||||
@@ -420,7 +416,7 @@ class CompactionFilter : public Customizable {
|
||||
|
||||
// Returns true if the filter overrides FilterV4 and can handle
|
||||
// WideColumnBlobResolver (lazy blob loading). When false (default),
|
||||
// FilterV4 delegates to FilterV3 with fully resolved column values --
|
||||
// FilterV4 delegates to FilterV3 with fully resolved column values —
|
||||
// blob columns are eagerly fetched before the filter is called, ensuring
|
||||
// backward compatibility with FilterV3-only filters.
|
||||
//
|
||||
|
||||
@@ -1868,13 +1868,6 @@ class DB {
|
||||
// and may not have file_creation_time property. In both the cases
|
||||
// file_creation_time is considered 0 which means this API will return
|
||||
// creation_time = 0 as there wouldn't be a timestamp lower than 0.
|
||||
//
|
||||
// NOTE: When the DB was opened with open_files_async = true, this API may
|
||||
// block to wait for background SST file loading to complete, but only when
|
||||
// necessary (i.e., when a file's manifest does not carry file_creation_time
|
||||
// and the value must come from the SST reader). Modern DBs return the real
|
||||
// value immediately. It is possible to have an incomplete result if the DB
|
||||
// is closed while files are still being opened in the background.
|
||||
virtual Status GetCreationTimeOfOldestFile(uint64_t* creation_time) = 0;
|
||||
|
||||
// Note: this API is not yet consistent with WritePrepared transactions.
|
||||
|
||||
@@ -235,21 +235,18 @@ struct ExternalTableBuilderOptions {
|
||||
const std::string db_id;
|
||||
const std::string db_session_id;
|
||||
const TableFileCreationReason reason;
|
||||
const std::shared_ptr<FileSystem>& fs;
|
||||
|
||||
ExternalTableBuilderOptions(
|
||||
const ReadOptions& _read_options, const WriteOptions& _write_options,
|
||||
const std::shared_ptr<const SliceTransform>& _prefix_extractor,
|
||||
const Comparator* _comparator, const std::string& _column_family_name,
|
||||
const TableFileCreationReason _reason,
|
||||
const std::shared_ptr<FileSystem>& _fs)
|
||||
const TableFileCreationReason _reason)
|
||||
: read_options(_read_options),
|
||||
write_options(_write_options),
|
||||
prefix_extractor(_prefix_extractor),
|
||||
comparator(_comparator),
|
||||
column_family_name(_column_family_name),
|
||||
reason(_reason),
|
||||
fs(_fs) {}
|
||||
reason(_reason) {}
|
||||
};
|
||||
|
||||
class ExternalTableFactory : public Customizable {
|
||||
|
||||
@@ -556,7 +556,7 @@ struct IOErrorInfo {
|
||||
uint64_t offset;
|
||||
};
|
||||
|
||||
// EXPERIMENTAL -- under active development, fields may change.
|
||||
// EXPERIMENTAL — under active development, fields may change.
|
||||
// Point-in-time snapshot of background job pressure for one DB: how busy
|
||||
// compaction and flush are, and how close the DB is to write-stalling.
|
||||
struct BackgroundJobPressure {
|
||||
|
||||
@@ -2952,7 +2952,7 @@ struct SizeApproximationOptions {
|
||||
// blob file data in the key range. When enabled, the total blob file size
|
||||
// is prorated by the ratio of SST data in the range to the total SST data:
|
||||
//
|
||||
// blob_size_in_range ~= total_blob_size * (sst_in_range / total_sst)
|
||||
// blob_size_in_range ≈ total_blob_size * (sst_in_range / total_sst)
|
||||
//
|
||||
// Limitations of this approximation:
|
||||
// - Assumes blob data is distributed proportionally to SST data, which
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/table_properties.h"
|
||||
#include "rocksdb/types.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -57,12 +56,6 @@ class SstFileReader {
|
||||
// capabilities like filling cache, read ahead are disabled.
|
||||
std::unique_ptr<Iterator> NewTableIterator();
|
||||
|
||||
// Parses a raw key returned by NewTableIterator().
|
||||
// The Slice fields in parsed_key point into raw_table_key and are valid only
|
||||
// while raw_table_key remains valid.
|
||||
Status ParseTableIteratorKey(const Slice& raw_table_key,
|
||||
ParsedEntryInfo* parsed_key) const;
|
||||
|
||||
std::shared_ptr<const TableProperties> GetTableProperties() const;
|
||||
|
||||
// Verifies whether there is corruption in this table.
|
||||
|
||||
@@ -600,9 +600,6 @@ enum Tickers : uint32_t {
|
||||
// via FileOptions::file_metadata (on DB open / table cache miss)
|
||||
FILE_OPEN_METADATA_PASSED,
|
||||
|
||||
// # of times MANIFEST content validation detected corruption on DB close
|
||||
MANIFEST_VALIDATION_FAILURE_COUNT,
|
||||
|
||||
TICKER_ENUM_MAX
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class InternalTblPropColl;
|
||||
class CompressionManager;
|
||||
|
||||
// -- Table Properties
|
||||
// Other than basic table properties, each table may also have the user
|
||||
@@ -446,28 +445,6 @@ struct TableProperties {
|
||||
std::string* mismatch) const;
|
||||
};
|
||||
|
||||
// Parse TableProperties::compression_name into human-readable format.
|
||||
// Thread-safe, but not purely local: the single-argument overload may consult
|
||||
// globally registered CompressionManagers via ObjectLibrary using the stored
|
||||
// compatibility name, and the two-argument overload consults the supplied
|
||||
// CompressionManager first before falling back to that lookup.
|
||||
// For format_version >= 7: "<compatibility_name>;<hex_codes>;" -> "ZSTD",
|
||||
// "LZ4", etc. For older versions (no semicolon): returns as-is (e.g., "ZSTD",
|
||||
// "Snappy", "kZSTD" are preserved with their original names). This means the
|
||||
// single-argument overload can now return manager-specific names for custom
|
||||
// compression types when a compatible CompressionManager is registered
|
||||
// globally; otherwise, reserved/custom values fall back to generic names such
|
||||
// as "Reserved7F" or "Custom8A". Returns "NoCompression" for an empty input,
|
||||
// an empty hex field such as "BuiltinV2;;", or when all parsed entries are
|
||||
// filtered out as NoCompression/DisableOption. Returns "Unknown" for malformed
|
||||
// format_version >= 7 metadata, including a missing second semicolon,
|
||||
// odd-length hex payload, or invalid hex characters. If multiple compression
|
||||
// types are present, returns a comma-separated list in input order.
|
||||
std::string ParseCompressionNameForDisplay(const std::string& compression_name);
|
||||
std::string ParseCompressionNameForDisplay(
|
||||
const std::string& compression_name,
|
||||
std::shared_ptr<CompressionManager> compression_manager);
|
||||
|
||||
// Extra properties
|
||||
// Below is a list of non-basic properties that are collected by database
|
||||
// itself. Especially some properties regarding to the internal keys (which
|
||||
|
||||
@@ -43,7 +43,7 @@ class UserDefinedIndexBuilder {
|
||||
kOther = 3, // Other types (e.g., blob reference, wide-column entity).
|
||||
// The value format is type-specific and may not be the
|
||||
// actual user data.
|
||||
kTypeMax, // Sentinel -- must be last. Value may change across releases.
|
||||
kTypeMax, // Sentinel — must be last. Value may change across releases.
|
||||
};
|
||||
|
||||
// File offset and size of the data block
|
||||
@@ -106,7 +106,7 @@ class UserDefinedIndexBuilder {
|
||||
// (e.g., trie-based indexes) can leave this as a no-op.
|
||||
//
|
||||
// @key: The user key (without sequence number or type suffix).
|
||||
// @type: The entry type -- kValue (Put), kDelete, kMerge, or kOther.
|
||||
// @type: The entry type — kValue (Put), kDelete, kMerge, or kOther.
|
||||
// For kDelete entries, the value may be empty. For kOther, the
|
||||
// value format is type-specific and may not be actual user data.
|
||||
// @value: The associated value (may be empty for deletions).
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
// NOTE: in 'main' development branch, this should be the *next*
|
||||
// minor or major version number planned for release.
|
||||
#define ROCKSDB_MAJOR 11
|
||||
#define ROCKSDB_MINOR 3
|
||||
#define ROCKSDB_PATCH 0
|
||||
#define ROCKSDB_MINOR 2
|
||||
#define ROCKSDB_PATCH 4
|
||||
|
||||
// Make it easy to do conditional compilation based on version checks, i.e.
|
||||
// #if ROCKSDB_VERSION_GE(4, 5, 6)
|
||||
|
||||
@@ -5311,8 +5311,6 @@ class TickerTypeJni {
|
||||
return -0x69;
|
||||
case ROCKSDB_NAMESPACE::Tickers::READ_PATH_RANGE_TOMBSTONES_DISCARDED:
|
||||
return -0x6A;
|
||||
case ROCKSDB_NAMESPACE::Tickers::MANIFEST_VALIDATION_FAILURE_COUNT:
|
||||
return -0x6B;
|
||||
case ROCKSDB_NAMESPACE::Tickers::TICKER_ENUM_MAX:
|
||||
// -0x54 is the max value at this time. Since these values are exposed
|
||||
// directly to Java clients, we'll keep the value the same till the next
|
||||
@@ -5814,8 +5812,6 @@ class TickerTypeJni {
|
||||
return ROCKSDB_NAMESPACE::Tickers::READ_PATH_RANGE_TOMBSTONES_INSERTED;
|
||||
case -0x6A:
|
||||
return ROCKSDB_NAMESPACE::Tickers::READ_PATH_RANGE_TOMBSTONES_DISCARDED;
|
||||
case -0x6B:
|
||||
return ROCKSDB_NAMESPACE::Tickers::MANIFEST_VALIDATION_FAILURE_COUNT;
|
||||
case -0x54:
|
||||
// -0x54 is the max value at this time. Since these values are exposed
|
||||
// directly to Java clients, we'll keep the value the same till the next
|
||||
|
||||
@@ -967,11 +967,6 @@ public enum TickerType {
|
||||
*/
|
||||
READ_PATH_RANGE_TOMBSTONES_DISCARDED((byte) -0x6A),
|
||||
|
||||
/**
|
||||
* # of times MANIFEST content validation detected corruption on DB close
|
||||
*/
|
||||
MANIFEST_VALIDATION_FAILURE_COUNT((byte) -0x6B),
|
||||
|
||||
TICKER_ENUM_MAX((byte) -0x54);
|
||||
|
||||
private final byte value;
|
||||
|
||||
@@ -559,7 +559,7 @@ TEST_F(InlineSkipTest, MultiGetDuplicateKeysWithCallbackWalk) {
|
||||
}
|
||||
|
||||
// Callback that walks forward through multiple entries before stopping.
|
||||
// This simulates the Merge operand accumulation in SaveValue -- the callback
|
||||
// This simulates the Merge operand accumulation in SaveValue — the callback
|
||||
// returns true for entries until it reaches one >= stop_at, simulating
|
||||
// walking through merge chain entries.
|
||||
struct WalkingCallbackArg {
|
||||
@@ -1068,11 +1068,11 @@ static void ConcurrentMultiGetWorker(void* arg) {
|
||||
// Validate all results: each found result must equal its query key
|
||||
// (since all queried keys were inserted, an exact match is expected).
|
||||
// However, due to concurrent inserts, a key sampled from the ring
|
||||
// might not yet be visible -- so we only check that if a result is
|
||||
// might not yet be visible — so we only check that if a result is
|
||||
// found, it equals the query key (i.e., no wrong key returned).
|
||||
for (size_t j = 0; j < batch_size; j++) {
|
||||
if (results[j] != 0 && results[j] != query_keys[j]) {
|
||||
// Got a result that doesn't match the query -- either a bug or
|
||||
// Got a result that doesn't match the query — either a bug or
|
||||
// the exact key wasn't inserted and we got the next one. Since
|
||||
// all our query keys are inserted, this means the result should
|
||||
// be >= query. For the just-inserted key we already checked
|
||||
|
||||
@@ -48,18 +48,13 @@ bool WBWIMemTable::Get(const LookupKey& key, std::string* value,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
SequenceNumber* out_seq, const ReadOptions&,
|
||||
bool immutable_memtable, ReadCallback* callback,
|
||||
bool* is_blob_index, bool do_merge,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
bool* is_blob_index, bool do_merge) {
|
||||
assert(s->ok() || s->IsMergeInProgress());
|
||||
(void)immutable_memtable;
|
||||
(void)timestamp;
|
||||
(void)columns;
|
||||
(void)blob_fetcher;
|
||||
assert(immutable_memtable);
|
||||
assert(!timestamp); // TODO: support UDT
|
||||
// IngestWriteBatchWithIndex() is rejected while any live column family has
|
||||
// blob direct write enabled, so WBWI should never need blob resolution.
|
||||
assert(blob_fetcher == nullptr);
|
||||
assert(assigned_seqno_.upper_bound != kMaxSequenceNumber);
|
||||
assert(assigned_seqno_.lower_bound != kMaxSequenceNumber);
|
||||
// WBWI does not support DeleteRange yet.
|
||||
@@ -159,15 +154,10 @@ bool WBWIMemTable::Get(const LookupKey& key, std::string* value,
|
||||
|
||||
void WBWIMemTable::MultiGet(const ReadOptions& read_options,
|
||||
MultiGetRange* range, ReadCallback* callback,
|
||||
bool immutable_memtable,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
bool immutable_memtable) {
|
||||
(void)immutable_memtable;
|
||||
(void)blob_fetcher;
|
||||
// Should only be used as immutable memtable.
|
||||
assert(immutable_memtable);
|
||||
// IngestWriteBatchWithIndex() is rejected while any live column family has
|
||||
// blob direct write enabled, so WBWI should never need blob resolution.
|
||||
assert(blob_fetcher == nullptr);
|
||||
// TODO: reuse the InternalIterator created in Get().
|
||||
for (auto iter = range->begin(); iter != range->end(); ++iter) {
|
||||
SequenceNumber dummy_seq = 0;
|
||||
|
||||
@@ -134,12 +134,10 @@ class WBWIMemTable final : public ReadOnlyMemTable {
|
||||
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
||||
const ReadOptions& read_opts, bool immutable_memtable,
|
||||
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
|
||||
bool do_merge = true,
|
||||
const BlobFetcher* blob_fetcher = nullptr) override;
|
||||
bool do_merge = true) override;
|
||||
|
||||
void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
ReadCallback* callback, bool immutable_memtable,
|
||||
const BlobFetcher* blob_fetcher = nullptr) override;
|
||||
ReadCallback* callback, bool immutable_memtable) override;
|
||||
|
||||
uint64_t NumEntries() const override { return num_entries_; }
|
||||
|
||||
|
||||
@@ -302,8 +302,6 @@ const std::vector<std::pair<Tickers, std::string>> TickersNameMap = {
|
||||
"rocksdb.read.path.range.tombstones.discarded"},
|
||||
{FILE_OPEN_METADATA_RETRIEVED, "rocksdb.file.open.metadata.retrieved"},
|
||||
{FILE_OPEN_METADATA_PASSED, "rocksdb.file.open.metadata.passed"},
|
||||
{MANIFEST_VALIDATION_FAILURE_COUNT,
|
||||
"rocksdb.manifest.validation.failure.count"},
|
||||
};
|
||||
|
||||
const std::vector<std::pair<Histograms, std::string>> HistogramsNameMap = {
|
||||
|
||||
+2
-2
@@ -434,14 +434,14 @@ void InstallStackTraceHandler() {
|
||||
// Ignore SIGPIPE so that broken-pipe writes (e.g. to a closed stdout)
|
||||
// return EPIPE instead of killing the process.
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
// Crash signals -- invoke full stack trace + ring buffer
|
||||
// Crash signals — invoke full stack trace + ring buffer
|
||||
signal(SIGILL, StackTraceHandler);
|
||||
signal(SIGSEGV, StackTraceHandler);
|
||||
signal(SIGBUS, StackTraceHandler);
|
||||
signal(SIGABRT, StackTraceHandler);
|
||||
signal(SIGFPE, StackTraceHandler);
|
||||
signal(SIGQUIT, StackTraceHandler);
|
||||
// Termination signals -- print ring buffer only, no stack trace
|
||||
// Termination signals — print ring buffer only, no stack trace
|
||||
signal(SIGTERM, TerminationHandler);
|
||||
signal(SIGINT, TerminationHandler);
|
||||
atexit(AtExit);
|
||||
|
||||
@@ -41,7 +41,7 @@ void BlockBasedTableIterator::SeekImpl(const Slice* target,
|
||||
return;
|
||||
}
|
||||
|
||||
// MultiScan requires an explicit seek key -- SeekToFirst() is not supported
|
||||
// MultiScan requires an explicit seek key — SeekToFirst() is not supported
|
||||
if (multi_scan_read_set_ && !target) {
|
||||
multi_scan_status_ = Status::InvalidArgument("No seek key for MultiScan");
|
||||
RecordTick(table_->GetStatistics(), MULTISCAN_SEEK_ERRORS);
|
||||
@@ -683,7 +683,7 @@ void BlockBasedTableIterator::FindBlockForward() {
|
||||
if (multi_scan_index_iter_ &&
|
||||
multi_scan_index_iter_->IsScanRangeExhausted()) {
|
||||
if (multi_scan_index_iter_->HasMoreScanRanges()) {
|
||||
// More ranges remain -- signal out-of-bound so DBIter/LevelIter
|
||||
// More ranges remain — signal out-of-bound so DBIter/LevelIter
|
||||
// will trigger the next Seek for the next scan range.
|
||||
is_out_of_bound_ = true;
|
||||
}
|
||||
|
||||
@@ -1087,17 +1087,13 @@ class BlockBasedTableReaderMultiScanAsyncIOTest
|
||||
: public BlockBasedTableReaderMultiScanTest {};
|
||||
|
||||
// TODO: test no block cache case
|
||||
TEST_P(BlockBasedTableReaderMultiScanAsyncIOTest, DISABLED_MultiScanPrepare) {
|
||||
TEST_P(BlockBasedTableReaderMultiScanAsyncIOTest, MultiScanPrepare) {
|
||||
auto param = GetParam();
|
||||
auto fill_cache = param.fill_cache;
|
||||
auto use_async_io = param.use_async_io;
|
||||
|
||||
options_.statistics = CreateDBStatistics();
|
||||
std::shared_ptr<FileSystem> fs = options_.env->GetFileSystem();
|
||||
int64_t supported_ops = 0;
|
||||
fs->SupportedOps(supported_ops);
|
||||
const bool async_supported =
|
||||
use_async_io && ((supported_ops & (1 << FSSupportedOps::kAsyncIO)) != 0);
|
||||
ReadOptions read_opts;
|
||||
read_opts.fill_cache = fill_cache;
|
||||
size_t ts_sz = options_.comparator->timestamp_size();
|
||||
@@ -1213,12 +1209,17 @@ TEST_P(BlockBasedTableReaderMultiScanAsyncIOTest, DISABLED_MultiScanPrepare) {
|
||||
iter->Prepare(&scan_options);
|
||||
read_count_after =
|
||||
options_.statistics->getTickerCount(NON_LAST_LEVEL_READ_COUNT);
|
||||
// When async IO is available on this thread, Prepare() only queues the reads
|
||||
// and stats are updated later during Poll(). Otherwise it falls back to the
|
||||
// synchronous path and records the reads immediately.
|
||||
const uint64_t expected_prepare_reads =
|
||||
async_supported ? 0 : (fill_cache ? 2 : 1);
|
||||
ASSERT_EQ(read_count_before + expected_prepare_reads, read_count_after);
|
||||
if (!use_async_io) {
|
||||
if (!fill_cache) {
|
||||
ASSERT_EQ(read_count_before + 1, read_count_after);
|
||||
} else {
|
||||
ASSERT_EQ(read_count_before + 2, read_count_after);
|
||||
}
|
||||
} else {
|
||||
// stat is recorded in async callback which happens in Poll(), and
|
||||
// Poll() happens during scanning.
|
||||
ASSERT_EQ(read_count_before, read_count_after);
|
||||
}
|
||||
|
||||
iter->Seek(kv[40 * kEntriesPerBlock].first);
|
||||
for (size_t i = 40 * kEntriesPerBlock; i < 80 * kEntriesPerBlock; ++i) {
|
||||
|
||||
Loaded 100 of 148 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user