Compare commits

..
Author SHA1 Message Date
Jay Huh 937bcacfe9 Update version and HISTORY.md for 11.2.4 release 2026-06-25 13:23:54 -07:00
Josh Kang f051e35ce8 Do not allow read only DBs to delete obsolete files (#14881)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14881

Read-only DB instances can share a directory with a live writer, so their view of live files is only a snapshot from the time they opened. After read-only DB open began setting `opened_successfully_` for its intended open-success semantics, that state had an unintended side effect: the common close path treated a successful read-only open like a read-write open and ran obsolete-file cleanup. If the writer created or made files live after the read-only handle opened, that cleanup could use the stale read-only live set and delete files still needed by the writer.

This change records whether a `DBImpl` was opened read-only and skips close-time obsolete-file cleanup for read-only DBs instead of simply keeping `opened_successfully_=false`, which could be confusing and easily mistaken in the future again.

For completeness I also updated other callsites of `opened_successfully_`, but those should not be real bugs as ReadOnly DBs do not run flushes/compactions or write to WALs.

Reviewed By: xingbowang

Differential Revision: D109622445

fbshipit-source-id: d7be4b20fce86ccb218a63ac6f5b707b316aac79
2026-06-25 13:22:12 -07:00
anand76 9ed9d29162 Update release history and version to 11.2.3 2026-05-07 16:41:38 -07:00
Anand Ananthabhotla 336e4f9db1 Add reuse_manifest_on_open DBOption (#14704)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14704

Add an immutable DBOption `reuse_manifest_on_open` (default false). When enabled, `DB::Open` can keep using the recovered MANIFEST for the first post-open metadata update instead of rebuilding a fresh MANIFEST, which can reduce warm-open latency for DBs whose MANIFEST is expensive to regenerate.

Reuse is still best-effort. If RocksDB cannot safely resume appending to the recovered MANIFEST, it falls back to the existing fresh-MANIFEST path. The option is also disabled under `best_efforts_recovery`.

This diff also teaches the reopened MANIFEST writer to adopt the existing file size before appending, documents the small-`max_manifest_file_size` caveat for the reused path, and keeps the full warm-reopen composition working with `optimize_manifest_for_recovery`.

Reviewed By: hx235, pdillinger

Differential Revision: D103568447

fbshipit-source-id: f4f5c35ea3ef0b80a0d52d94be40c6bd11505999
2026-05-07 16:37:38 -07:00
Anand Ananthabhotla b8672a8101 Extend optimize_manifest_for_recovery through DB::Close (#14703)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14703

Extend `optimize_manifest_for_recovery` so a clean `DB::Close` can persist up-to-date WAL recovery markers when that can be done safely. Combined with the recovery-side optimization in the previous diff, a clean close/reopen can avoid recovery-time MANIFEST appends.

This remains best-effort: if the close-time write is disabled, skipped, or fails, RocksDB falls back to the standard recovery path on the next open. The option stays mutable so it can be turned off before close to suppress the optimization without restarting the DB.

The close-time path respects the existing recovery constraints for 2PC, non-empty column families, dropped column families, and WAL tracking, and preserves the existing file-number invariants.

Reviewed By: pdillinger, hx235

Differential Revision: D103568449

fbshipit-source-id: ae62867507a8a87640a2c140bea852b7c608cb66
2026-05-07 16:37:25 -07:00
Anand Ananthabhotla 53f215f6d4 Add optimize_manifest_for_recovery DBOption (#14702)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14702

Add a mutable DBOption `optimize_manifest_for_recovery` (default false) as a temporary rollout / kill switch for warm-reopen MANIFEST optimizations.

In this diff, enabling the option lets recovery skip MANIFEST updates during `DB::Open` when the recovered state is already reflected on disk, which reduces metadata appends after a clean shutdown and can lower warm-reopen latency on storage where MANIFEST appends are expensive.

If the option is disabled, RocksDB follows the existing recovery path unchanged. The optimization is disabled under `best_efforts_recovery`, where recovery intentionally rewrites metadata as part of salvage, and the option is mutable so later diffs in this stack can share the same rollout knob.

Reviewed By: pdillinger, hx235

Differential Revision: D103568448

fbshipit-source-id: 9ec930343e434f1bee6130bcdbd7738dddd92b6d
2026-05-07 16:37:11 -07:00
Xingbo Wang 75da341340 Update HISTORY.md for 11.2.2 2026-05-07 09:03:06 -07:00
Xingbo Wang e603f55e5f Update version for 11.2.2 2026-05-07 09:03:06 -07:00
xingbowang 1c634ade4a Keep remote compaction stats serialization compatible with 11.1 (#14712)
Summary:
- Reapply c4941760c9 so remote compaction serializes InternalStats::CompactionStats::counts with the pre-11.2 compaction-reason count.
- Keep remote compaction result metadata compatible with 11.1 readers and writers until the stats serialization path has version-aware array handling.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14712

Test Plan:
- make format-auto
- make -j$(sysctl -n hw.ncpu) compaction_job_test compaction_service_test
- /usr/local/bin/timeout 60 ./compaction_job_test --gtest_filter=CompactionJobTest.ResultSerialization
- /usr/local/bin/timeout 60 ./compaction_service_test --gtest_filter=CompactionServiceTest.VerifyStats

Reviewed By: joshkang97, jaykorean

Differential Revision: D104130456

Pulled By: xingbowang

fbshipit-source-id: e9e92cb044253edbec0fc79b92c3efff53f01d8b
2026-05-07 09:03:06 -07:00
Peter Dillinger 81ee59210c Update buckifier to use folly/coro instead of folly/experimental/coro (#14685)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14685

D100235293 and D102823090 migrated the generated RocksDB BUCK file from
`//folly/experimental/coro:*` to `//folly/coro:*`, but did not update
the buckifier script that generates it in internal_repo_rocksdb. The next release would revert the change.

Update buckify_rocksdb.py to match, so the generated BUCK file stays consistent with the folly coro migration.

Reviewed By: nmk70

Differential Revision: D103096688

fbshipit-source-id: 9055769ed5e9893397c7504ada22e21980f59dd2
2026-05-07 09:03:06 -07:00
anand76 08d8122809 Update HISTORY.md for 11.2.1 2026-04-29 12:55:26 -07:00
Josh Kang 7be9ffc81a Fix range tombstone conversion + ingest sst and re-enable crash tests (#14654)
Summary:
Fixes a correctness bug in read-path range-tombstone synthesis when it races with `IngestExternalFile`. The synthesis path could insert a tombstone into the active memtable at a snapshot's sequence number, while ingestion installed an L0 SST at `LastSequence + 1` — a higher seqno than the synthesized tombstone. This breaks the main assumption of range tombstone reads that all lower levels have lower seqno.

The fix introduces a per-CF `port::RWMutex` (`ColumnFamilyData::ingest_sst_lock_`) plus a per-memtable `ingest_seqno_barrier_`. Ingestion takes the read lock and range tombstone synthesis **tries** to take a write lock.

If iterator lock is successful, then we have a new updated barrier seqno that we can validate the iterator seqno against. An added benefit is we no longer need to gate against empty memtable. This was originally added as an easy fix to prevent memtables from being inserted into while ingestion was happening.

## The bug, by example

`ReadPathRangeTombstoneTest.NewerPointInOlderFileStillVisible` (`db/db_iterator_test.cc:6929`):

1. L0 file `b@1, c@2, d@3`, then L0 file `Delete(b)4, Delete(c)5`.
2. Active memtable: `Put(z)6`. Snapshot taken at seq 6.
3. `IngestExternalFile({c → "vc_live"})` → installed at L0 with seq 7.
4. Iterator at snap 6 walks the deletion run and synthesizes `[b, d) @ seq 6` into the active memtable via `MemTable::AddLogicallyRedundantRangeTombstone`.
5. `Get("c")` at the latest snapshot: memtable returns covering tombstone (seq 6), `Version::Get` short-circuits, **never reads `c@7`** — returns `NotFound` instead of `"vc_live"`.

The invariant `Version::Get` relies on (memtable seqs ≥ any L0 seq for the same key) is broken because synthesis writes at the *snapshot's* seq while ingestion writes at `LastSequence + 1`.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14654

Test Plan:
- Updated regression test
- Re-enable crashtests and manually run

| Flavor | Jobs |
|---|---:|
| `fbcode_blackbox_crash_test` | 200 |
| `fbcode_whitebox_crash_test` | 30 |
| `fbcode_asan_blackbox_crash_test` | 30 |
| `fbcode_tsan_blackbox_crash_test` | 30 |
| `fbcode_crash_test_with_atomic_flush` | 30 |
| `fbcode_crash_test_with_wc_txn` | 30 |
| `fbcode_crash_test_with_ts` | 30 |
| **Total** | **380** |

Reviewed By: xingbowang

Differential Revision: D102044512

Pulled By: joshkang97

fbshipit-source-id: 0c69187595edc5a5fa80be24bffdba710a92e56e
2026-04-29 12:40:45 -07:00
anand76 1d37a88852 Update version for 11.2.1 2026-04-28 22:16:41 -07:00
Anand Ananthabhotla 7b0019a1b8 Gate file_open_metadata consumption on fast_sst_open option (#14676)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14676

When fast_sst_open is disabled, RocksDB was still passing previously-persisted
file_open_metadata from the MANIFEST to NewRandomAccessFile. This could cause
failures when the metadata becomes stale (e.g. expired filesystem credentials).

This change gates the consumption of file_open_metadata in
TableCache::GetTableReader on the fast_sst_open option. When fast_sst_open is
false, previously persisted metadata is ignored and not passed to the filesystem
via FileOptions::file_metadata.

The fast_sst_open flag is threaded from MutableDBOptions through VersionSet ->
ColumnFamilySet -> ColumnFamilyData -> TableCache at construction time, ensuring
the gate is active before any table readers are opened during recovery. Dynamic
changes via SetDBOptions are also propagated to all existing TableCache instances.

Reviewed By: mszeszko-meta, xingbowang

Differential Revision: D102735581

fbshipit-source-id: 9a2c4dc0644a2f65c36b2468605df57779e127cd
2026-04-28 22:11:41 -07:00
Xingbo Wang d3a28293a5 Revert temporary 11.1 remote compaction stats serialization workaround (#14663)
Summary:
There is a separate way to handle incompatibility. Revert this.

- revert the temporary workaround from https://github.com/facebook/rocksdb/issues/14656 that serialized `InternalStats::CompactionStats::counts` with the pre-11.2 compaction-reason count
- restore remote compaction stats serialization to use the full `CompactionReason::kNumOfReasons` array size again

## Context
https://github.com/facebook/rocksdb/issues/14656 temporarily shrank the serialized `counts` array to keep remote compaction metadata readable across the 11.1/11.2 boundary. This follow-up removes that special case because the compatibility issue is being handled separately.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14663

Test Plan: - Not run in this metadata-prep task

Reviewed By: jaykorean

Differential Revision: D102376258

Pulled By: xingbowang

fbshipit-source-id: 55693ea25a97b70de9d53b0d6eebcbd88a325b1c
2026-04-24 12:44:18 -07:00
Xingbo Wang 2b3cdc5764 Keep remote compaction stats serialization compatible with 11.1
Summary

Keep remote compaction stats serialization compatible with RocksDB 11.1.
Serialize InternalStats::CompactionStats::counts using the pre-11.2
compaction-reason count so remote compaction metadata remains readable
across the version boundary.
Preserve the existing formatting-only follow-up commit on the branch.

Needs a better mechanism to make OptionTypeInfo::Array ser/des friendly.
Will follow up after 11.2 release.
2026-04-22 16:31:17 -07:00
xingbowang 6c65d8046d Revert "Migrate fbcode coro references from folly/experimental to folly/coro (5/6 - fbcode a-m)"
This reverts commit bad2d5b0af.

This change broke internal build.
2026-04-22 06:08:53 -07:00
anand1976 2126b6e3ba Update HISTORY.md for 11.2.0 release 2026-04-20 11:54:02 -07:00
148 changed files with 1696 additions and 7045 deletions

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');
};
+30 -33
View File
@@ -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');
};
-98
View File
@@ -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',
],
});
};
+19 -166
View File
@@ -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');
}
}
};
-347
View File
@@ -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
-185
View File
@@ -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.'
);
+131 -10
View File
@@ -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.');
+561 -38
View File
@@ -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
-68
View File
@@ -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
View File
@@ -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.
-7
View File
@@ -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 \
+1 -1
View File
@@ -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
View File
@@ -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
-61
View File
@@ -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.
+12 -52
View File
@@ -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.
-55
View File
@@ -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
View File
@@ -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
+16 -27
View File
@@ -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,
+3 -3
View File
@@ -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);
+3 -3
View File
@@ -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);
}
+1 -3
View File
@@ -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.
-41
View File
@@ -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();
-106
View File
@@ -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;
+1 -60
View File
@@ -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
-36
View File
@@ -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;
+1 -1
View File
@@ -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_;
+9 -9
View File
@@ -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;
}
}
+1 -1
View File
@@ -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
View File
@@ -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();
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -37
View File
@@ -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));
+1 -1
View File
@@ -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;
+1 -2
View File
@@ -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();
+7 -35
View File
@@ -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 ? &timestamp_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);
+18 -51
View File
@@ -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 ? &timestamp_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;
-5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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_;
-123
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
-3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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, &current_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, &current_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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+2 -398
View File
@@ -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;
+1 -1
View File
@@ -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
-4
View File
@@ -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);
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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());
}
}
+1 -1
View File
@@ -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);
+1 -5
View File
@@ -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`.
+2 -2
View File
@@ -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
View File
@@ -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);
+1 -1
View File
@@ -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
View File
@@ -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
+1 -6
View File
@@ -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,
-11
View File
@@ -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 {
+6 -6
View File
@@ -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
+1 -5
View File
@@ -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.
//
-7
View File
@@ -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.
+2 -5
View File
@@ -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 {
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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
-7
View File
@@ -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.
-3
View File
@@ -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
};
-23
View File
@@ -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
+2 -2
View File
@@ -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).
+2 -2
View File
@@ -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)
-4
View File
@@ -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;
+3 -3
View File
@@ -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
+2 -12
View File
@@ -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;
+2 -4
View File
@@ -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_; }
-2
View File
@@ -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
View File
@@ -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