Compare commits

..
18 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
176 changed files with 1732 additions and 8758 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
-35
View File
@@ -217,41 +217,6 @@ The following patterns emerged as frequent sources of review feedback:
* Use `make dbg` command to build all of the unit test in debug mode.
* For -j in make command, use the number of CPU cores to decide it.
### When to run `make clean` (avoid mixing build modes)
The Makefile does **not** track build mode, so object files from a prior
build are silently reused even when compiled with different flags, leading
to confusing linker errors, sanitizer false negatives, ODR violations, or
"phantom" bugs.
Run `make clean` before switching any of these:
* **`ASSERT_STATUS_CHECKED=1` ↔ unset** — changes the `Status` class layout (ABI break).
* **Sanitizer builds** — toggling any of `COMPILE_WITH_ASAN=1`,
`COMPILE_WITH_UBSAN=1`, `COMPILE_WITH_TSAN=1` on/off.
* `DEBUG_LEVEL=0` (release) ↔ `DEBUG_LEVEL=1` (debug, default for `make dbg`).
* Different compilers, `OPT` levels, or other flags affecting codegen/ABI.
**Notable exception:** `DEBUG_LEVEL=2` can be safely mixed with
`DEBUG_LEVEL=1` — rebuild a subset of files with `DEBUG_LEVEL=2` to get
extra/more accurate runtime checks for those files without a full clean.
When in doubt, `make clean` is cheap insurance compared to chasing a
phantom bug.
### Source checks
* Run `make check-sources` before committing. This catches non-ASCII
characters in source files and other source-level issues that CI will
reject. In particular, **do not use Unicode characters** (em dashes,
smart quotes, etc.) in comments or strings -- use ASCII equivalents
(`--` instead of em dash, `'` instead of smart quote, etc.).
### RTTI and dynamic_cast
* Production code and `db_stress` must build in **release mode
(`-fno-rtti`)**. Do not use `dynamic_cast` anywhere except unit tests.
Use `static_cast_with_check` from `util/cast_util.h` (validates with
`dynamic_cast` in debug builds, plain `static_cast` in release).
* Unit tests (`*_test.cc`) are built in debug mode with RTTI enabled.
### Unit Test
* After all of the unit tests are added, review them and try to extract common
reusable utility functions to reduce code duplication due to copy past between
+6 -22
View File
@@ -1,34 +1,19 @@
# Rocksdb Change Log
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
## 11.3.2 (06/24/2026)
## 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.3.1 (06/04/2026)
* Don't call `EventListener::OnCompactionPreCommit` during DB shutdown, like other compaction callbacks
## 11.3.0 (05/15/2026)
## 11.2.3 (05/07/2026)
### New Features
* Add experimental DB option `async_wal_precreate` to precreate the next WAL file in a background thread and reduce foreground WAL rotation latency. The option is sanitized to false when WAL recycling is enabled.
* Added a new `EventListener::OnCompactionPreCommit` callback that fires after a compaction job finishes but before its input files are released (i.e. while `FileMetaData::being_compacted` is still true). Listeners that maintain bookkeeping of which files are currently being compacted can clean up such state in this new callback to avoid races with concurrent compaction picking, where another thread might pick up the same files for a new compaction immediately after `being_compacted` is flipped back to false but before `OnCompactionCompleted` fires. The default implementation is a no-op so this is not a breaking change.
* Add mutable DBOption `optimize_manifest_for_recovery` (default false). When enabled, RocksDB can reduce recovery work after a clean shutdown, which may lower DB::Open latency on warm reopens.
* Added public utility APIs `ParseCompressionNameForDisplay()` to convert `TableProperties::compression_name` into a human-readable compression name for both legacy and format_version 7+ SST metadata, including custom `CompressionManager`-provided display names for custom compression types.
* Add `reuse_manifest_on_open` DBOption (default false). When enabled, DB::Open reuses the existing MANIFEST file for append instead of creating a fresh one, avoiding the cost of serializing the entire database state into a new MANIFEST on the first post-open write. To prevent this feature from interfering with manifest file size auto-tuning, an extra forward-compatible field is now always added to the MANIFEST (to track the last "compacted" size).
### Behavior Changes
* Read-only open with `error_if_wal_file_exists=true` now tolerates empty WAL files so empty precreated WALs do not prevent inspection.
* WriteCommitted TransactionDB now matches WritePrepared and WriteUnprepared compaction filtering in both single- and two-write-queue modes: a compaction filter's FilterMergeOperand will not be invoked on merge operands at or below the latest published sequence number.
* 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
* Fixed blob-backed wide-column merge reads to preserve correct status
propagation and resolution across memtable, read-only, and secondary DB
paths.
* Fixed a bug where `DB::GetCreationTimeOfOldestFile()` could return inaccurate results instead of the real creation time when called shortly after opening a legacy DB (one whose manifest lacks `file_creation_time`) with `open_files_async = true`. The API now waits for background SST file loading to complete only when needed; modern DBs are unaffected.
* Fixed merge reads against wide-column/blob-backed base values to preserve precise failure statuses, including `GetMergeOperands()` and direct-write memtable reads.
* Fix bug in range tombstone synthesis that covers live keys added during an IngestExternalFile
* Reject the empty string as a column family name in `DB::CreateColumnFamily` / `DB::CreateColumnFamilies`. Previously such calls returned OK and a usable handle, but the column family was not persisted in the manifest, so any data written to it was silently lost on DB reopen.
* Fixed a bug where a WriteCommitted TransactionDB using commit-bypass WBWI ingestion could drop an entry that is still visible at the published sequence boundary.
## 11.2.0 (04/18/2026)
### New Features
@@ -50,7 +35,6 @@ paths.
* 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.
+11 -24
View File
@@ -327,9 +327,6 @@ missing_make_config_paths := $(shell \
$(foreach path, $(missing_make_config_paths), \
$(warning Warning: $(path) does not exist))
# This (the first rule) must depend on "all".
default: all
ifeq ($(PLATFORM), OS_AIX)
# no debug info
else ifneq ($(PLATFORM), IOS)
@@ -387,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
@@ -488,6 +481,9 @@ ifdef ROCKSDB_MODIFY_NPHASH
PLATFORM_CXXFLAGS += -DROCKSDB_MODIFY_NPHASH=1
endif
# This (the first rule) must depend on "all".
default: all
WARNING_FLAGS = -W -Wextra -Wall -Wsign-compare -Wshadow \
-Wunused-parameter
@@ -990,7 +986,7 @@ check_0:
awk -v s=$(CI_SHARD_INDEX) -v n=$(CI_TOTAL_SHARDS) '(NR-1)%n==s'; \
else cat; fi; \
fi; \
$(FIND) t -name 'run-*' -print; \
find t -name 'run-*' -print; \
} \
| $(prioritize_long_running_tests) \
| grep -E '$(tests-regexp)' \
@@ -1012,7 +1008,7 @@ valgrind_check_0:
' run "make watch-log" in a separate window' ''; \
{ \
printf './%s\n' $(filter-out $(PARALLEL_TEST) %skiplist_test options_settable_test, $(TESTS)); \
$(FIND) t -name 'run-*' -print; \
find t -name 'run-*' -print; \
} \
| $(prioritize_long_running_tests) \
| grep -E '$(tests-regexp)' \
@@ -1230,17 +1226,11 @@ clean-not-downloaded: clean-ext-libraries-bin clean-rocks clean-not-downloaded-r
clean-rocks:
# Not practical to exactly match all versions/variants in naming (e.g. debug or not)
@echo Removing link targets
@rm -f ${LIBNAME}*.so* ${LIBNAME}*.a $(BENCHMARKS) $(TOOLS) $(TESTS) $(PARALLEL_TEST) $(MICROBENCHS)
@echo Finding and cleaning other files
@rm -rf $(CLEAN_FILES) ios-x86 ios-arm scan_build_report
@if $(FIND) Makefile -regextype awk &> /dev/null; then \
$(FIND) . -regextype awk \
-regex '.*/([.].*|third-party)' -prune -o \
-type f -regex '.*[.]([oda]|gcda|gcno)' -exec rm -f {} \; ; \
else \
$(FIND) . -name "*.[oda]" -exec rm -f {} \; ; \
fi
rm -f ${LIBNAME}*.so* ${LIBNAME}*.a
rm -f $(BENCHMARKS) $(TOOLS) $(TESTS) $(PARALLEL_TEST) $(MICROBENCHS)
rm -rf $(CLEAN_FILES) ios-x86 ios-arm scan_build_report
$(FIND) . -name "*.[oda]" -exec rm -f {} \;
$(FIND) . -type f \( -name "*.gcda" -o -name "*.gcno" \) -exec rm -f {} \;
clean-rocksjava: clean-rocks
rm -rf jl jls
@@ -1253,7 +1243,7 @@ clean-ext-libraries-all:
rm -rf bzip2* snappy* zlib* lz4* zstd*
clean-ext-libraries-bin:
$(FIND) . -maxdepth 1 -type d \( -name bzip2\* -or -name snappy\* -or -name zlib\* -or -name lz4\* -or -name zstd\* \) -prune -exec rm -rf {} \;
find . -maxdepth 1 -type d \( -name bzip2\* -or -name snappy\* -or -name zlib\* -or -name lz4\* -or -name zstd\* \) -prune -exec rm -rf {} \;
tags:
ctags -R .
@@ -1278,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 \
@@ -1295,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
-4
View File
@@ -1,7 +1,3 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# This source code is licensed under both the GPLv2 (found in the COPYING file in the root directory)
# and the Apache 2.0 License (found in the LICENSE.Apache file in the root directory).
#!/usr/bin/env python3
"""
Pre-download packages with unreliable mirrors using fallback mirrors.
-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
-9
View File
@@ -5286,15 +5286,6 @@ size_t rocksdb_options_get_recycle_log_file_num(rocksdb_options_t* opt) {
return opt->rep.recycle_log_file_num;
}
void rocksdb_options_set_async_wal_precreate(rocksdb_options_t* opt,
unsigned char v) {
opt->rep.async_wal_precreate = v;
}
unsigned char rocksdb_options_get_async_wal_precreate(rocksdb_options_t* opt) {
return opt->rep.async_wal_precreate;
}
void rocksdb_options_set_soft_pending_compaction_bytes_limit(
rocksdb_options_t* opt, size_t v) {
opt->rep.soft_pending_compaction_bytes_limit = v;
-3
View File
@@ -2869,9 +2869,6 @@ int main(int argc, char** argv) {
rocksdb_options_set_track_and_verify_wals_in_manifest(o, 42);
CheckCondition(1 ==
rocksdb_options_get_track_and_verify_wals_in_manifest(o));
CheckCondition(0 == rocksdb_options_get_async_wal_precreate(o));
rocksdb_options_set_async_wal_precreate(o, 1);
CheckCondition(1 == rocksdb_options_get_async_wal_precreate(o));
/* Blob Options */
rocksdb_options_set_enable_blob_files(o, 1);
-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;
-18
View File
@@ -454,18 +454,6 @@ class Compaction {
return notify_on_compaction_completion_;
}
// Called by DBImpl::NotifyOnCompactionPreCommit to ensure the
// OnCompactionPreCommit listener callback fires at most once per
// compaction lifecycle, even though the notify helper is invoked at multiple
// potential ReleaseCompactionFiles() sites for safety.
void SetNotifyOnCompactionPreCommitCalled() {
notify_on_compaction_pre_commit_called_ = true;
}
bool WasNotifyOnCompactionPreCommitCalled() const {
return notify_on_compaction_pre_commit_called_;
}
static constexpr int kInvalidLevel = -1;
// Evaluate proximal output level. If the compaction supports
@@ -638,12 +626,6 @@ class Compaction {
// begin.
bool notify_on_compaction_completion_;
// Tracks whether OnCompactionPreCommit has already fired for this
// compaction. The notify helper is called from multiple potential
// ReleaseCompactionFiles() call sites; this flag guarantees that listeners
// observe the callback exactly once per compaction.
bool notify_on_compaction_pre_commit_called_ = false;
// Enable/disable GC collection for blobs during compaction.
bool enable_blob_garbage_collection_;
+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;
-60
View File
@@ -1,8 +1,3 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
@@ -150,61 +145,6 @@ TEST_F(DBEtc3Test, AutoTuneManifestSize) {
AddCfFn();
ASSERT_LT(prev_manifest_num, cur_manifest_num);
// ---- Verify persisted compacted manifest size survives close/reopen ----
// Close with CFs still live. Reopen with reuse_manifest_on_open so the
// manifest is NOT rewritten from scratch. The persisted compacted size
// should be loaded and used for auto-tuning.
// At this point we have 7 CF handles plus default.
ASSERT_EQ(handles.size(), 7U);
// Collect CF names for reopen, then release handles (Close needs this)
std::vector<std::string> cf_names = {"default"};
for (auto* h : handles) {
cf_names.push_back(h->GetName());
}
for (auto* h : handles) {
ASSERT_OK(db_->DestroyColumnFamilyHandle(h));
}
handles.clear();
Close();
// Use a large max_manifest_file_size so the reused manifest (which is
// already ~10KB) does NOT trigger rotation on the first few writes.
// Auto-tuning with the persisted compacted size (~5KB) at 200% amp
// gives a tuned threshold of ~15KB. Without persistence, the threshold
// would be max(max_manifest_file_size, 0 * anything) =
// max_manifest_file_size.
//
// We set max_manifest_file_size to two values to distinguish:
// - 3000: if persisted compacted size is NOT loaded, tuned = 3000,
// and the first AddCf will rotate (manifest is already ~10KB > 3000)
// - With persisted compacted size loaded, tuned = max(3000, 5000*3) = 15000,
// so no rotation until we exceed 15KB
options.max_manifest_file_size = 3000;
options.max_manifest_space_amp_pct = 200;
options.reuse_manifest_on_open = true;
uint64_t manifest_num_before_reopen = cur_manifest_num;
ReopenWithColumnFamilies(cf_names, options);
cur_manifest_num = dbfull()->TEST_Current_Manifest_FileNo();
// With persistence: the manifest file number should NOT have changed
// during reopen, because the persisted compacted size keeps the tuned
// threshold high enough. Without persistence, last_compacted = 0, so
// tuned = max_manifest_file_size = 3000, and the first LogAndApply
// during Open rotates the manifest because it's already ~10KB > 3000.
ASSERT_EQ(manifest_num_before_reopen, cur_manifest_num);
// Adding CFs should still not trigger rotation because the tuned
// threshold (~15KB) exceeds the current manifest size (~10KB + adds).
for (int i = 1; i <= 4; ++i) {
AddCfFn();
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
}
for (int i = 1; i <= 4; ++i) {
AddCfFn();
ASSERT_EQ(prev_manifest_num, cur_manifest_num);
}
// Wrap up
while (!handles.empty()) {
DropCfFn();
+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);
+19 -242
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"
@@ -484,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,
@@ -791,27 +778,12 @@ Status DBImpl::CloseHelper() {
bg_flush_scheduled_ || bg_purge_scheduled_ ||
bg_pressure_callback_in_progress_ ||
bg_async_file_open_state_ == AsyncFileOpenState::kScheduled ||
async_wal_precreate_state_ == AsyncWALPrecreateState::kScheduled ||
pending_purge_obsolete_files_ ||
error_handler_.IsRecoveryInProgress()) {
TEST_SYNC_POINT("DBImpl::~DBImpl:WaitJob");
bg_cv_.Wait();
}
// Release any opened-but-unpublished WAL writer after the in-flight worker
// has published its result. Clear the DB-owned async slot while holding
// mutex_, but destroy the detached writer after dropping mutex_ because
// log::Writer / WritableFileWriter destruction can flush and close the file.
// The file itself can be left behind as an empty future WAL; recovery already
// tolerates it and marks its file number used if observed.
UnpublishedWAL unused_async_wal = std::move(async_wal_precreate_wal_);
async_wal_precreate_state_ = AsyncWALPrecreateState::kNotScheduled;
if (unused_async_wal.writer) {
mutex_.Unlock();
unused_async_wal.Reset();
mutex_.Lock();
}
// Ensure subclasses don't forget to schedule async file opening
assert(!immutable_db_options_.open_files_async || !opened_successfully_ ||
bg_async_file_open_state_ != AsyncFileOpenState::kNotScheduled);
@@ -2889,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,
@@ -3246,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);
@@ -3281,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();
@@ -3307,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);
}
@@ -4010,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) {
@@ -4064,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();
@@ -4445,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_);
@@ -6515,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.
@@ -6561,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.
@@ -6595,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.
@@ -7597,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;
@@ -7605,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);
}
+5 -141
View File
@@ -15,8 +15,6 @@
#include <limits>
#include <list>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <unordered_map>
@@ -1098,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.
@@ -1530,15 +1527,6 @@ class DBImpl : public DB {
const Status& st,
const CompactionJobStats& job_stats,
int job_id);
// Fires the OnCompactionPreCommit listener callback. Mirrors
// NotifyOnCompactionCompleted but is invoked before ReleaseCompactionFiles()
// (i.e. while input files still have being_compacted == true). Idempotent:
// safe to call from multiple potential release sites; only fires once per
// compaction, and only if NotifyOnCompactionBegin previously fired.
void NotifyOnCompactionPreCommit(ColumnFamilyData* cfd, Compaction* c,
const Status& st,
const CompactionJobStats& job_stats,
int job_id);
void NotifyOnMemTableSealed(ColumnFamilyData* cfd,
const MemTableInfo& mem_table_info);
@@ -1867,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
@@ -1887,7 +1870,6 @@ class DBImpl : public DB {
private:
friend class DB;
friend class DBImplReadOnly;
friend class DBImplSecondary;
friend class ErrorHandler;
friend class InternalStats;
@@ -2586,15 +2568,10 @@ class DBImpl : public DB {
// Helper function to perform trivial move by updating manifest metadata
// without rewriting data files. This is called when IsTrivialMove() is true.
// REQUIRES: mutex held
// Populates the Compaction's edit with DeleteFile/AddFile entries for
// a trivial move (no data rewriting). Does not commit the edit.
void PrepareTrivialMoveEdit(Compaction& c, LogBuffer* log_buffer,
size_t& moved_files, size_t& moved_bytes);
// REQUIRES: mutex held, PrepareTrivialMoveEdit already called
// Commits the trivial move by calling LogAndApply on the prepared edit.
// Returns: Status of the manifest write
Status CommitTrivialMove(Compaction& c, bool& compaction_released);
// Returns: Status of the trivial move operation
Status PerformTrivialMove(Compaction& c, LogBuffer* log_buffer,
bool& compaction_released, size_t& moved_files,
size_t& moved_bytes);
// REQUIRES: mutex unlocked
void TrackOrUntrackFiles(const std::vector<std::string>& existing_data_files,
@@ -2827,68 +2804,7 @@ class DBImpl : public DB {
size_t GetWalPreallocateBlockSize(uint64_t write_buffer_size) const;
Env::WriteLifeTimeHint CalculateWALWriteHint() { return Env::WLTH_SHORT; }
// Returns true when async WAL precreation is enabled and compatible with the
// active WAL strategy. WAL recycling already avoids file creation latency, so
// precreation is disabled when recycle_log_file_num is non-zero.
bool AsyncWALPrecreateEnabled() const;
// A WAL file that has a reserved file number and may have an opened writer,
// but has not been added to DBImpl's in-memory logical WAL tracking lists
// (logs_ and alive_wal_files_).
struct UnpublishedWAL {
uint64_t log_number = 0;
std::unique_ptr<log::Writer> writer;
UnpublishedWAL() = default;
UnpublishedWAL(const UnpublishedWAL&) = delete;
UnpublishedWAL& operator=(const UnpublishedWAL&) = delete;
UnpublishedWAL(UnpublishedWAL&& other) noexcept {
*this = std::move(other);
}
UnpublishedWAL& operator=(UnpublishedWAL&& other) noexcept {
if (this != &other) {
log_number = other.log_number;
writer = std::move(other.writer);
other.Reset();
}
return *this;
}
void Reset() {
log_number = 0;
writer.reset();
}
};
// Reserves the next WAL file number and schedules a HIGH-priority background
// task to precreate that WAL file. A precreated WAL is not a logical WAL
// until a foreground WAL rotation consumes it.
void MaybeScheduleAsyncWALPrecreate(size_t preallocate_block_size);
// Background task for opening the reserved future WAL and publishing the
// result under mutex_.
static void BGWorkAsyncWALPrecreate(void* arg);
// Waits for an in-flight async WAL precreation and returns a prepared WAL if
// one is available. If precreation failed, returns an empty WAL and lets the
// foreground rotation create the WAL synchronously. Caller must hold mutex_.
UnpublishedWAL WaitForAsyncWALPrecreate();
// Opens and preallocates a WAL writer without writing logical WAL records.
// Used by async WAL precreation and by synchronous WAL creation.
IOStatus CreateWALWriter(const DBOptions& db_options, uint64_t log_file_num,
uint64_t recycle_log_number,
size_t preallocate_block_size,
UnpublishedWAL* new_wal);
// Starts an opened WAL file by writing the initial records required before it
// can be installed as the current WAL for foreground writes.
IOStatus StartWALFile(const WriteOptions& write_options,
const PredecessorWALInfo& predecessor_wal_info,
log::Writer* new_log);
IOStatus CreateWAL(const DBOptions& db_options,
const WriteOptions& write_options, uint64_t log_file_num,
IOStatus CreateWAL(const WriteOptions& write_options, uint64_t log_file_num,
uint64_t recycle_log_number, size_t preallocate_block_size,
const PredecessorWALInfo& predecessor_wal_info,
log::Writer** new_log);
@@ -3031,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>
@@ -3384,28 +3277,6 @@ class DBImpl : public DB {
AsyncFileOpenState bg_async_file_open_state_ =
AsyncFileOpenState::kNotScheduled;
// State machine for the single async WAL precreation slot protected by
// mutex_. Background precreation failure returns to kNotScheduled; foreground
// rotation handles it the same as no prepared WAL and creates one
// synchronously. kScheduled owns a reserved file number; kReady owns an
// opened writer that has not been started or added to logical WAL tracking.
enum class AsyncWALPrecreateState : uint8_t {
kNotScheduled = 0, // No WAL precreate work is in-flight or ready.
kScheduled, // Background task owns creation of the reserved WAL.
kReady, // Reserved WAL writer is open but not logically live.
};
// Protected by mutex_. Tracks at most one background precreated WAL. A
// precreated WAL is only reserved empty storage until SwitchMemtable()
// consumes it and installs it in DBImpl's in-memory logical WAL tracking
// lists (logs_ and alive_wal_files_).
AsyncWALPrecreateState async_wal_precreate_state_ =
AsyncWALPrecreateState::kNotScheduled;
// Reserved in-flight/ready precreated WAL. The writer is populated only while
// state is kReady.
UnpublishedWAL async_wal_precreate_wal_;
std::deque<ManualCompactionState*> manual_compaction_dequeue_;
// shall we disable deletion of obsolete files
@@ -3480,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
+7 -106
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) {
@@ -1581,8 +1563,9 @@ Status DBImpl::CompactFiles(const CompactionOptions& compact_options,
return s;
}
void DBImpl::PrepareTrivialMoveEdit(Compaction& c, LogBuffer* log_buffer,
size_t& moved_files, size_t& moved_bytes) {
Status DBImpl::PerformTrivialMove(Compaction& c, LogBuffer* log_buffer,
bool& compaction_released,
size_t& moved_files, size_t& moved_bytes) {
mutex_.AssertHeld();
ROCKS_LOG_BUFFER(log_buffer, "[%s] Moving %d files to level-%d\n",
@@ -1615,10 +1598,6 @@ void DBImpl::PrepareTrivialMoveEdit(Compaction& c, LogBuffer* log_buffer,
}
moved_files += c.num_input_files(l);
}
}
Status DBImpl::CommitTrivialMove(Compaction& c, bool& compaction_released) {
mutex_.AssertHeld();
// Install the new version
const ReadOptions read_options(Env::IOActivity::kCompaction);
@@ -1743,8 +1722,8 @@ Status DBImpl::CompactFilesImpl(
bool compaction_released = false;
size_t moved_files = 0;
size_t moved_bytes = 0;
PrepareTrivialMoveEdit(*c.get(), log_buffer, moved_files, moved_bytes);
Status status = CommitTrivialMove(*c.get(), compaction_released);
Status status = PerformTrivialMove(
*c.get(), log_buffer, compaction_released, moved_files, moved_bytes);
if (status.ok()) {
InstallSuperVersionAndScheduleWork(
@@ -2012,46 +1991,6 @@ void DBImpl::NotifyOnCompactionCompleted(
// flush process.
}
void DBImpl::NotifyOnCompactionPreCommit(
ColumnFamilyData* cfd, Compaction* c, const Status& st,
const CompactionJobStats& compaction_job_stats, const int job_id) {
if (immutable_db_options_.listeners.size() == 0U) {
return;
}
mutex_.AssertHeld();
if (shutting_down_.load(std::memory_order_acquire)) {
return;
}
// Only fire if OnCompactionBegin has fired for this compaction.
if (c->ShouldNotifyOnCompactionCompleted() == false) {
return;
}
// Idempotency: ensure listeners observe at most one
// OnCompactionPreCommit per compaction lifecycle, even though the
// notify helper is invoked at multiple potential ReleaseCompactionFiles()
// sites for safety.
if (c->WasNotifyOnCompactionPreCommitCalled()) {
return;
}
c->SetNotifyOnCompactionPreCommitCalled();
int num_l0_files = cfd->current()->storage_info()->NumLevelFiles(0);
// release lock while notifying events
mutex_.Unlock();
TEST_SYNC_POINT("DBImpl::NotifyOnCompactionPreCommit::UnlockMutex");
{
CompactionJobInfo info{};
info.num_l0_files = num_l0_files;
BuildCompactionJobInfo(cfd, c, st, compaction_job_stats, job_id, &info);
for (const auto& listener : immutable_db_options_.listeners) {
listener->OnCompactionPreCommit(this, info);
}
info.status.PermitUncheckedError();
}
mutex_.Lock();
}
// REQUIREMENT: block all background work by calling PauseBackgroundWork()
// before calling this function
// TODO (hx235): Replace Status::NotSupported() with Status::Aborted() for
@@ -4298,8 +4237,6 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
for (const auto& f : *c->inputs(0)) {
c->edit()->DeleteFile(c->level(), f->fd.GetNumber());
}
NotifyOnCompactionPreCommit(c->column_family_data(), c.get(), status,
compaction_job_stats, job_context->job_id);
status = versions_->LogAndApply(
c->column_family_data(), read_options, write_options, c->edit(),
&mutex_, directories_.GetDbDir(),
@@ -4518,8 +4455,6 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
++out_file_metadata_it;
}
NotifyOnCompactionPreCommit(c->column_family_data(), c.get(), status,
compaction_job_stats, job_context->job_id);
status = versions_->LogAndApply(
c->column_family_data(), read_options, write_options, c->edit(),
&mutex_, directories_.GetDbDir(),
@@ -4596,12 +4531,8 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
// Perform the trivial move
size_t moved_files = 0;
size_t moved_bytes = 0;
// Populate the edit first so that CompactionJobInfo has output file
// info when OnCompactionPreCommit fires.
PrepareTrivialMoveEdit(*c.get(), log_buffer, moved_files, moved_bytes);
NotifyOnCompactionPreCommit(c->column_family_data(), c.get(), status,
compaction_job_stats, job_context->job_id);
status = CommitTrivialMove(*c.get(), compaction_released);
status = PerformTrivialMove(*c.get(), log_buffer, compaction_released,
moved_files, moved_bytes);
io_s = versions_->io_status();
InstallSuperVersionAndScheduleWork(
c->column_family_data(), job_context->superversion_contexts.data());
@@ -4724,10 +4655,6 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
ReleaseOptionsFileNumber(min_options_file_number_elem);
}
// Fire OnCompactionPreCommit before compaction_job.Install runs
// ReleaseCompactionFiles in its manifest write callback.
NotifyOnCompactionPreCommit(c->column_family_data(), c.get(), status,
compaction_job_stats, job_context->job_id);
status = compaction_job.Install(&compaction_released);
io_s = compaction_job.io_status();
if (status.ok()) {
@@ -4747,14 +4674,6 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
if (c != nullptr) {
if (!compaction_released) {
// Safety net: if any of the paths above did not fire the
// OnCompactionPreCommit notification (e.g. status was not OK and
// an early-out skipped the LogAndApply / Install), still fire the
// notify here while files are about to be released. The notify is
// idempotent and is a no-op if it has already fired (or if Begin never
// fired).
NotifyOnCompactionPreCommit(c->column_family_data(), c.get(), status,
compaction_job_stats, job_context->job_id);
c->ReleaseCompactionFiles(status);
} else {
#ifndef NDEBUG
@@ -5195,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
@@ -5233,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();
+22 -189
View File
@@ -21,7 +21,6 @@
#include "file/writable_file_writer.h"
#include "logging/logging.h"
#include "monitoring/persistent_stats_history.h"
#include "monitoring/statistics_impl.h"
#include "monitoring/thread_status_util.h"
#include "options/options_helper.h"
#include "rocksdb/options.h"
@@ -123,16 +122,6 @@ DBOptions SanitizeOptions(const std::string& dbname, const DBOptions& src,
result.recycle_log_file_num = 0;
}
if (result.async_wal_precreate && result.recycle_log_file_num != 0) {
// Async WAL precreation reserves a future log number, while WAL recycling
// chooses from old WAL files. Keep the recycling behavior and disable only
// the async optimization.
result.async_wal_precreate = false;
ROCKS_LOG_WARN(result.info_log,
"async_wal_precreate is disabled since "
"recycle_log_file_num is non-zero");
}
if (result.db_paths.size() == 0) {
result.db_paths.emplace_back(dbname, std::numeric_limits<uint64_t>::max());
} else if (result.wal_dir.empty()) {
@@ -810,19 +799,9 @@ Status DBImpl::Recover(
if (!wal_files.empty()) {
if (error_if_wal_file_exists) {
for (const auto& wal_file : wal_files) {
uint64_t bytes;
s = env_->GetFileSize(wal_file.second, &bytes);
if (!s.ok()) {
return s;
}
if (bytes > 0) {
return Status::Corruption(
"The db was opened in readonly mode with "
"error_if_wal_file_exists flag but a non-empty WAL file "
"already exists");
}
}
return Status::Corruption(
"The db was opened in readonly mode with error_if_wal_file_exists"
"flag but a WAL file already exists");
} else if (error_if_data_exists_in_wals) {
for (auto& wal_file : wal_files) {
uint64_t bytes;
@@ -2398,17 +2377,16 @@ Status DB::OpenAndTrimHistory(
return s;
}
IOStatus DBImpl::CreateWALWriter(const DBOptions& db_options,
uint64_t log_file_num,
uint64_t recycle_log_number,
size_t preallocate_block_size,
UnpublishedWAL* new_wal) {
assert(new_wal);
assert(new_wal->log_number == 0);
assert(new_wal->writer == nullptr);
IOStatus DBImpl::CreateWAL(const WriteOptions& write_options,
uint64_t log_file_num, uint64_t recycle_log_number,
size_t preallocate_block_size,
const PredecessorWALInfo& predecessor_wal_info,
log::Writer** new_log) {
IOStatus io_s;
std::unique_ptr<FSWritableFile> lfile;
DBOptions db_options =
BuildDBOptions(immutable_db_options_, mutable_db_options_);
FileOptions opt_file_options =
fs_->OptimizeForLogWrite(file_options_, db_options);
opt_file_options.write_hint = CalculateWALWriteHint();
@@ -2446,160 +2424,19 @@ IOStatus DBImpl::CreateWALWriter(const DBOptions& db_options,
Histograms::HISTOGRAM_ENUM_MAX /* hist_type */, listeners, nullptr,
tmp_set.Contains(FileType::kWalFile),
tmp_set.Contains(FileType::kWalFile)));
new_wal->log_number = log_file_num;
new_wal->writer = std::make_unique<log::Writer>(
std::move(file_writer), log_file_num,
immutable_db_options_.recycle_log_file_num > 0,
immutable_db_options_.manual_wal_flush,
immutable_db_options_.wal_compression,
immutable_db_options_.track_and_verify_wals);
}
return io_s;
}
IOStatus DBImpl::StartWALFile(const WriteOptions& write_options,
const PredecessorWALInfo& predecessor_wal_info,
log::Writer* new_log) {
assert(new_log);
IOStatus io_s = new_log->AddCompressionTypeRecord(write_options);
TEST_SYNC_POINT_CALLBACK("DBImpl::StartWALFile:AfterCompressionTypeRecord",
&io_s);
if (io_s.ok()) {
io_s = new_log->MaybeAddPredecessorWALInfo(write_options,
predecessor_wal_info);
}
return io_s;
}
IOStatus DBImpl::CreateWAL(const DBOptions& db_options,
const WriteOptions& write_options,
uint64_t log_file_num, uint64_t recycle_log_number,
size_t preallocate_block_size,
const PredecessorWALInfo& predecessor_wal_info,
log::Writer** new_log) {
assert(new_log);
assert(*new_log == nullptr);
UnpublishedWAL wal;
IOStatus io_s = CreateWALWriter(db_options, log_file_num, recycle_log_number,
preallocate_block_size, &wal);
if (io_s.ok()) {
io_s = StartWALFile(write_options, predecessor_wal_info, wal.writer.get());
}
if (io_s.ok()) {
*new_log = wal.writer.release();
}
return io_s;
}
bool DBImpl::AsyncWALPrecreateEnabled() const {
return immutable_db_options_.async_wal_precreate &&
immutable_db_options_.recycle_log_file_num == 0;
}
struct AsyncWALPrecreateContext {
DBImpl* db = nullptr;
DBOptions db_options;
uint64_t log_number = 0;
size_t preallocate_block_size = 0;
};
void DBImpl::MaybeScheduleAsyncWALPrecreate(size_t preallocate_block_size) {
mutex_.AssertHeld();
if (!AsyncWALPrecreateEnabled() ||
shutting_down_.load(std::memory_order_acquire) ||
error_handler_.IsBGWorkStopped() ||
async_wal_precreate_state_ != AsyncWALPrecreateState::kNotScheduled) {
return;
}
async_wal_precreate_wal_.Reset();
async_wal_precreate_wal_.log_number = versions_->NewFileNumber();
async_wal_precreate_state_ = AsyncWALPrecreateState::kScheduled;
auto* ctx = new AsyncWALPrecreateContext();
ctx->db = this;
ctx->db_options = BuildDBOptions(immutable_db_options_, mutable_db_options_);
ctx->log_number = async_wal_precreate_wal_.log_number;
ctx->preallocate_block_size = preallocate_block_size;
env_->Schedule(&DBImpl::BGWorkAsyncWALPrecreate, ctx, Env::Priority::HIGH,
nullptr);
}
DBImpl::UnpublishedWAL DBImpl::WaitForAsyncWALPrecreate() {
mutex_.AssertHeld();
UnpublishedWAL result;
if (!AsyncWALPrecreateEnabled()) {
return result;
}
TEST_SYNC_POINT("DBImpl::WaitForAsyncWALPrecreate:Begin");
bool waited = false;
uint64_t wait_start_micros = 0;
while (async_wal_precreate_state_ == AsyncWALPrecreateState::kScheduled) {
TEST_SYNC_POINT("DBImpl::WaitForAsyncWALPrecreate:BeforeWait");
if (!waited) {
waited = true;
wait_start_micros = immutable_db_options_.clock->NowMicros();
RecordTick(stats_, WAL_PRECREATE_WAITED);
*new_log = new log::Writer(std::move(file_writer), log_file_num,
immutable_db_options_.recycle_log_file_num > 0,
immutable_db_options_.manual_wal_flush,
immutable_db_options_.wal_compression,
immutable_db_options_.track_and_verify_wals);
io_s = (*new_log)->AddCompressionTypeRecord(write_options);
if (io_s.ok()) {
io_s = (*new_log)->MaybeAddPredecessorWALInfo(write_options,
predecessor_wal_info);
}
bg_cv_.Wait();
}
if (waited) {
RecordTick(stats_, WAL_PRECREATE_WAIT_MICROS,
immutable_db_options_.clock->NowMicros() - wait_start_micros);
}
if (async_wal_precreate_state_ == AsyncWALPrecreateState::kReady) {
result = std::move(async_wal_precreate_wal_);
async_wal_precreate_state_ = AsyncWALPrecreateState::kNotScheduled;
RecordTick(stats_, WAL_PRECREATE_HIT);
return result;
}
assert(async_wal_precreate_state_ == AsyncWALPrecreateState::kNotScheduled);
RecordTick(stats_, WAL_PRECREATE_MISS);
return result;
}
void DBImpl::BGWorkAsyncWALPrecreate(void* arg) {
TEST_SYNC_POINT("DBImpl::BGWorkAsyncWALPrecreate:Start");
std::unique_ptr<AsyncWALPrecreateContext> ctx(
static_cast<AsyncWALPrecreateContext*>(arg));
DBImpl* db = ctx->db;
UnpublishedWAL new_wal;
IOStatus io_s = db->CreateWALWriter(ctx->db_options, ctx->log_number,
0 /* recycle_log_number */,
ctx->preallocate_block_size, &new_wal);
if (!io_s.ok()) {
RecordTick(db->stats_, WAL_PRECREATE_FAILED);
ROCKS_LOG_WARN(db->immutable_db_options_.info_log,
"Async WAL precreate failed for #%" PRIu64 ": %s",
ctx->log_number, io_s.ToString().c_str());
}
TEST_SYNC_POINT("DBImpl::BGWorkAsyncWALPrecreate:BeforePublish");
TEST_SYNC_POINT("DBImpl::BGWorkAsyncWALPrecreate:Publish");
{
InstrumentedMutexLock l(&db->mutex_);
if (db->async_wal_precreate_state_ == AsyncWALPrecreateState::kScheduled &&
db->async_wal_precreate_wal_.log_number == ctx->log_number) {
if (io_s.ok()) {
db->async_wal_precreate_wal_ = std::move(new_wal);
db->async_wal_precreate_state_ = AsyncWALPrecreateState::kReady;
} else {
// Async WAL precreation is best-effort. Clear the slot so foreground
// rotation falls back to normal synchronous WAL creation.
db->async_wal_precreate_wal_.Reset();
db->async_wal_precreate_state_ = AsyncWALPrecreateState::kNotScheduled;
}
}
db->bg_cv_.SignalAll();
}
TEST_SYNC_POINT("DBImpl::BGWorkAsyncWALPrecreate:Done");
return io_s;
}
void DBImpl::TrackExistingDataFiles(
@@ -2697,10 +2534,8 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
// created during DB open with predecessor WALs from previous DB session due
// to `avoid_flush_during_recovery == true`. This can protect the last WAL
// recovered.
const DBOptions db_options_snapshot =
BuildDBOptions(impl->immutable_db_options_, impl->mutable_db_options_);
s = impl->CreateWAL(db_options_snapshot, write_options, new_log_number,
0 /*recycle_log_number*/, preallocate_block_size,
s = impl->CreateWAL(write_options, new_log_number, 0 /*recycle_log_number*/,
preallocate_block_size,
PredecessorWALInfo() /* predecessor_wal_info */,
&new_log);
if (s.ok()) {
@@ -2905,8 +2740,6 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
if (impl->immutable_db_options_.open_files_async) {
impl->ScheduleAsyncFileOpening();
}
impl->MaybeScheduleAsyncWALPrecreate(
impl->GetWalPreallocateBlockSize(max_write_buffer_size));
impl->mutex_.Unlock();
}
+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;
+4 -44
View File
@@ -16,7 +16,6 @@
#include "db/error_handler.h"
#include "db/event_helpers.h"
#include "db/wide/wide_columns_helper.h"
#include "file/filename.h"
#include "logging/logging.h"
#include "memtable/wbwi_memtable.h"
#include "monitoring/perf_context_imp.h"
@@ -510,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;
@@ -1547,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);
}
}
@@ -3108,7 +3102,6 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
const WriteOptions write_options;
log::Writer* new_log = nullptr;
UnpublishedWAL prepared_wal;
MemTable* new_mem = nullptr;
IOStatus io_s;
@@ -3136,16 +3129,8 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
!wal_recycle_files_.empty() && IsFileDeletionsEnabled()) {
recycle_log_number = wal_recycle_files_.front();
}
uint64_t new_log_number = cur_wal_number_;
if (creating_new_log) {
prepared_wal = WaitForAsyncWALPrecreate();
if (prepared_wal.writer) {
new_log_number = prepared_wal.log_number;
} else {
assert(prepared_wal.log_number == 0);
new_log_number = versions_->NewFileNumber();
}
}
uint64_t new_log_number =
creating_new_log ? versions_->NewFileNumber() : cur_wal_number_;
// For use outside of holding DB mutex
const MutableCFOptions mutable_cf_options_copy =
cfd->GetLatestMutableCFOptions();
@@ -3166,8 +3151,6 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
// Log this later after lock release. It may be outdated, e.g., if background
// flush happens before logging, but that should be ok.
int num_imm_unflushed = cfd->imm()->NumNotFlushed();
const DBOptions db_options_snapshot =
BuildDBOptions(immutable_db_options_, mutable_db_options_);
const auto preallocate_block_size =
GetWalPreallocateBlockSize(mutable_cf_options_copy.write_buffer_size);
mutex_.Unlock();
@@ -3183,35 +3166,13 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
wal_write_mutex_.Unlock();
// TODO: Write buffer size passed in should be max of all CF's instead
// of mutable_cf_options.write_buffer_size.
if (prepared_wal.writer) {
io_s = StartWALFile(write_options, info, prepared_wal.writer.get());
if (io_s.ok()) {
new_log = prepared_wal.writer.release();
}
} else {
io_s =
CreateWAL(db_options_snapshot, write_options, new_log_number,
recycle_log_number, preallocate_block_size, info, &new_log);
}
io_s = CreateWAL(write_options, new_log_number, recycle_log_number,
preallocate_block_size, info, &new_log);
TEST_SYNC_POINT_CALLBACK("DBImpl::SwitchMemtable:AfterCreateWAL", &io_s);
if (s.ok()) {
s = io_s;
}
}
if (!s.ok() && prepared_wal.writer) {
const uint64_t prepared_wal_number = prepared_wal.log_number;
const std::string prepared_wal_fname =
LogFileName(immutable_db_options_.GetWalDir(), prepared_wal_number);
prepared_wal.Reset();
Status delete_s = env_->DeleteFile(prepared_wal_fname);
if (!delete_s.ok() && !delete_s.IsNotFound()) {
ROCKS_LOG_WARN(immutable_db_options_.info_log,
"Failed to delete unstarted async precreated WAL #%" PRIu64
": %s",
prepared_wal_number, delete_s.ToString().c_str());
}
delete_s.PermitUncheckedError();
}
if (s.ok()) {
// FIXME: from the comment for GetEarliestSequenceNumber(), any key with
// seqno >= earliest_seqno should be in this or later memtable. This means
@@ -3393,7 +3354,6 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
new_mem->Ref();
cfd->SetMemtable(new_mem);
InstallSuperVersionAndScheduleWork(cfd, &context->superversion_context);
MaybeScheduleAsyncWALPrecreate(preallocate_block_size);
// Notify client that memtable is sealed, now that we have successfully
// installed a new memtable
+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();
-34
View File
@@ -102,40 +102,6 @@ TEST_F(DBOptionsTest, ImmutableTrackAndVerifyWalsInManifest) {
ASSERT_FALSE(s.ok());
}
TEST_F(DBOptionsTest, ImmutableAsyncWalPrecreate) {
Options options;
options.env = env_;
options.async_wal_precreate = true;
ImmutableDBOptions db_options(options);
ASSERT_TRUE(db_options.async_wal_precreate);
Reopen(options);
ASSERT_TRUE(dbfull()->GetDBOptions().async_wal_precreate);
Status s = dbfull()->SetDBOptions({{"async_wal_precreate", "false"}});
ASSERT_FALSE(s.ok());
}
TEST_F(DBOptionsTest, SanitizeAsyncWalPrecreateWithWalRecycle) {
// This checks the option-level contract for the incompatible WAL paths:
// SanitizeOptions() preserves WAL recycling and disables only the
// opportunistic async precreation path, so the immutable options reflect
// effective behavior.
Options options;
options.env = env_;
options.async_wal_precreate = true;
options.recycle_log_file_num = 1;
Options sanitized_options = SanitizeOptions(dbname_, options);
ASSERT_FALSE(sanitized_options.async_wal_precreate);
ASSERT_EQ(1U, sanitized_options.recycle_log_file_num);
options.recycle_log_file_num = 0;
sanitized_options = SanitizeOptions(dbname_, options);
ASSERT_TRUE(sanitized_options.async_wal_precreate);
}
TEST_F(DBOptionsTest, ImmutableVerifySstUniqueIdInManifest) {
Options options;
options.env = env_;
+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"));
-92
View File
@@ -8238,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();
+14 -443
View File
@@ -7,8 +7,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include <algorithm>
#include "db/db_test_util.h"
#include "db/db_with_timestamp_test_util.h"
#include "options/options_helper.h"
@@ -21,62 +19,12 @@
#include "utilities/fault_injection_env.h"
#include "utilities/fault_injection_fs.h"
#if defined(OS_LINUX)
#include <sys/statfs.h>
#endif
#if defined(OS_LINUX) && !defined(BTRFS_SUPER_MAGIC)
#define BTRFS_SUPER_MAGIC 0x9123683E
#endif
#if defined(OS_LINUX) && !defined(TMPFS_MAGIC)
#define TMPFS_MAGIC 0x01021994
#endif
#if defined(OS_LINUX) && !defined(OVERLAYFS_SUPER_MAGIC)
#define OVERLAYFS_SUPER_MAGIC 0x794c7630
#endif
#if defined(OS_LINUX) && !defined(ZFS_SUPER_MAGIC)
#define ZFS_SUPER_MAGIC 0x2fc12fc1
#endif
namespace ROCKSDB_NAMESPACE {
class DBWALTestBase : public DBTestBase {
protected:
explicit DBWALTestBase(const std::string& dir_name)
: DBTestBase(dir_name, /*env_do_fsync=*/true) {}
std::vector<uint64_t> ListWalNumbers() {
std::vector<uint64_t> wal_numbers;
std::vector<std::string> files;
EXPECT_OK(env_->GetChildren(dbname_, &files));
for (const auto& file : files) {
uint64_t number = 0;
FileType type = kWalFile;
if (ParseFileName(file, &number, &type) && type == kWalFile) {
wal_numbers.push_back(number);
}
}
std::sort(wal_numbers.begin(), wal_numbers.end());
return wal_numbers;
}
void CreateEmptyWal(uint64_t wal_number) {
std::unique_ptr<WritableFile> file;
ASSERT_OK(env_->NewWritableFile(LogFileName(dbname_, wal_number), &file,
EnvOptions()));
ASSERT_OK(file->Close());
}
void CreateWalWithContents(uint64_t wal_number, const Slice& contents) {
std::unique_ptr<WritableFile> file;
ASSERT_OK(env_->NewWritableFile(LogFileName(dbname_, wal_number), &file,
EnvOptions()));
ASSERT_OK(file->Append(contents));
ASSERT_OK(file->Close());
}
#if defined(ROCKSDB_PLATFORM_POSIX)
public:
#if defined(ROCKSDB_FALLOCATE_PRESENT)
@@ -109,31 +57,6 @@ class DBWALTestBase : public DBTestBase {
assert(err == 0);
return sbuf.st_blocks * 512;
}
#if defined(ROCKSDB_FALLOCATE_PRESENT)
bool ShouldSkipAllocationCheck(const std::string& file_name) {
(void)file_name;
#if defined(OS_LINUX)
struct statfs fs_stat;
if (statfs(file_name.c_str(), &fs_stat) == 0) {
if (fs_stat.f_type ==
static_cast<decltype(fs_stat.f_type)>(BTRFS_SUPER_MAGIC)) {
return true;
} else if (fs_stat.f_type ==
static_cast<decltype(fs_stat.f_type)>(ZFS_SUPER_MAGIC)) {
return true;
} else if (fs_stat.f_type ==
static_cast<decltype(fs_stat.f_type)>(TMPFS_MAGIC)) {
return true;
} else if (fs_stat.f_type ==
static_cast<decltype(fs_stat.f_type)>(OVERLAYFS_SUPER_MAGIC)) {
return true;
}
}
#endif
return false;
}
#endif // ROCKSDB_FALLOCATE_PRESENT
#endif // ROCKSDB_PLATFORM_POSIX
};
@@ -2829,35 +2752,16 @@ TEST_F(DBWALTest, TruncateLastLogAfterRecoverWithoutFlush) {
auto& file_before = log_files_before[0];
ASSERT_LT(file_before->SizeFileBytes(), 1 * kKB);
// The log file has preallocated space.
{
std::string fname = dbname_ + file_before->PathName();
uint64_t allocated = GetAllocatedFileSize(fname);
if (ShouldSkipAllocationCheck(fname)) {
fprintf(stderr,
"Skipping preallocation check on this filesystem for %s\n",
fname.c_str());
} else if (allocated < preallocated_size) {
fprintf(stderr,
"Warning: allocated size (%lu) less than preallocated size (%lu) "
"for %s, skipping check. This may indicate the filesystem does "
"not support preallocation or does not report it.\n",
allocated, preallocated_size, fname.c_str());
} else {
ASSERT_GE(allocated, preallocated_size);
}
}
ASSERT_GE(GetAllocatedFileSize(dbname_ + file_before->PathName()),
preallocated_size);
Reopen(options);
VectorLogPtr log_files_after;
ASSERT_OK(dbfull()->GetSortedWalFiles(log_files_after));
ASSERT_EQ(1, log_files_after.size());
ASSERT_LT(log_files_after[0]->SizeFileBytes(), 1 * kKB);
// The preallocated space should be truncated.
{
std::string fname = dbname_ + file_before->PathName();
if (!ShouldSkipAllocationCheck(fname)) {
ASSERT_LT(GetAllocatedFileSize(fname), preallocated_size);
}
}
ASSERT_LT(GetAllocatedFileSize(dbname_ + file_before->PathName()),
preallocated_size);
}
// Tests that we will truncate the preallocated space of the last log from
// previous.
@@ -2884,23 +2788,8 @@ TEST_F(DBWALTest, TruncateLastLogAfterRecoverWithFlush) {
ASSERT_EQ(1, log_files_before.size());
auto& file_before = log_files_before[0];
ASSERT_LT(file_before->SizeFileBytes(), 1 * kKB);
{
std::string fname = dbname_ + file_before->PathName();
uint64_t allocated = GetAllocatedFileSize(fname);
if (ShouldSkipAllocationCheck(fname)) {
fprintf(stderr,
"Skipping preallocation check on this filesystem for %s\n",
fname.c_str());
} else if (allocated < preallocated_size) {
fprintf(stderr,
"Warning: allocated size (%lu) less than preallocated size (%lu) "
"for %s, skipping check. This may indicate the filesystem does "
"not support preallocation or does not report it.\n",
allocated, preallocated_size, fname.c_str());
} else {
ASSERT_GE(allocated, preallocated_size);
}
}
ASSERT_GE(GetAllocatedFileSize(dbname_ + file_before->PathName()),
preallocated_size);
// The log file has preallocated space.
Close();
@@ -2918,12 +2807,8 @@ TEST_F(DBWALTest, TruncateLastLogAfterRecoverWithFlush) {
// if the process is in a crash loop, the log file may not get
// deleted and thte preallocated space will keep accumulating. So we need
// to ensure it gets trtuncated.
{
std::string fname = dbname_ + file_before->PathName();
if (!ShouldSkipAllocationCheck(fname)) {
EXPECT_LT(GetAllocatedFileSize(fname), preallocated_size);
}
}
EXPECT_LT(GetAllocatedFileSize(dbname_ + file_before->PathName()),
preallocated_size);
TEST_SYNC_POINT(
"DBWALTest::TruncateLastLogAfterRecoverWithFlush:AfterTruncate");
reopen_thread.join();
@@ -2978,31 +2863,14 @@ TEST_F(DBWALTest, TruncateLastLogAfterRecoverWALEmpty) {
log_file->PrepareWrite(0, 4096);
log_file.reset();
{
uint64_t allocated = GetAllocatedFileSize(last_log);
if (ShouldSkipAllocationCheck(last_log)) {
fprintf(stderr,
"Skipping preallocation check on this filesystem for %s\n",
last_log.c_str());
} else if (allocated < preallocated_size) {
fprintf(stderr,
"Warning: allocated size (%lu) less than preallocated size (%lu) "
"for %s, skipping check. This may indicate the filesystem does "
"not support preallocation or does not report it.\n",
allocated, preallocated_size, last_log.c_str());
} else {
ASSERT_GE(allocated, preallocated_size);
}
}
ASSERT_GE(GetAllocatedFileSize(last_log), preallocated_size);
port::Thread reopen_thread([&]() { Reopen(options); });
TEST_SYNC_POINT(
"DBWALTest::TruncateLastLogAfterRecoverWithFlush:AfterRecover");
// The preallocated space should be truncated.
if (!ShouldSkipAllocationCheck(last_log)) {
EXPECT_LT(GetAllocatedFileSize(last_log), preallocated_size);
}
EXPECT_LT(GetAllocatedFileSize(last_log), preallocated_size);
TEST_SYNC_POINT(
"DBWALTest::TruncateLastLogAfterRecoverWithFlush:AfterTruncate");
reopen_thread.join();
@@ -3044,23 +2912,8 @@ TEST_F(DBWALTest, ReadOnlyRecoveryNoTruncate) {
auto& file_before = log_files_before[0];
ASSERT_LT(file_before->SizeFileBytes(), 1 * kKB);
// The log file has preallocated space.
std::string fname = dbname_ + file_before->PathName();
auto db_size = GetAllocatedFileSize(fname);
{
if (ShouldSkipAllocationCheck(fname)) {
fprintf(stderr,
"Skipping preallocation check on this filesystem for %s\n",
fname.c_str());
} else if (db_size < preallocated_size) {
fprintf(stderr,
"Warning: allocated size (%lu) less than preallocated size (%lu) "
"for %s, skipping check. This may indicate the filesystem does "
"not support preallocation or does not report it.\n",
db_size, preallocated_size, fname.c_str());
} else {
ASSERT_GE(db_size, preallocated_size);
}
}
auto db_size = GetAllocatedFileSize(dbname_ + file_before->PathName());
ASSERT_GE(db_size, preallocated_size);
Close();
// enable truncate and open DB as readonly, the file should not be truncated
@@ -3074,12 +2927,8 @@ TEST_F(DBWALTest, ReadOnlyRecoveryNoTruncate) {
ASSERT_EQ(log_files_after[0]->PathName(), file_before->PathName());
// The preallocated space should NOT be truncated.
// the DB size is almost the same.
{
std::string fname2 = dbname_ + file_before->PathName();
if (!ShouldSkipAllocationCheck(fname2)) {
ASSERT_NEAR(GetAllocatedFileSize(fname2), db_size, db_size / 100);
}
}
ASSERT_NEAR(GetAllocatedFileSize(dbname_ + file_before->PathName()), db_size,
db_size / 100);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
@@ -3222,284 +3071,6 @@ TEST_F(DBWALTest, EmptyWalReopenTest) {
}
}
TEST_F(DBWALTest, AsyncWalPrecreateWaitsForPendingFile) {
Options options = CurrentOptions();
options.env = env_;
options.statistics = CreateDBStatistics();
options.async_wal_precreate = true;
options.recycle_log_file_num = 0;
options.write_buffer_size = 1024;
options.max_write_buffer_number = 4;
// Goal: cover the race where foreground WAL rotation reaches the switch
// before the background precreate publishes its result. The sync points hold
// the async worker after it has created the file but before it marks it
// ready, then verify the foreground writer waits instead of allocating a
// higher WAL number.
SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::BGWorkAsyncWALPrecreate:BeforePublish",
"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:WorkerBlocked"},
{"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:AllowPublish",
"DBImpl::BGWorkAsyncWALPrecreate:Publish"},
{"DBImpl::WaitForAsyncWALPrecreate:BeforeWait",
"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:ForegroundWaiting"},
});
SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_point([] {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
});
Reopen(options);
TEST_SYNC_POINT(
"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:WorkerBlocked");
ASSERT_OK(Put("fill", "value"));
Status writer_status;
port::Thread writer(
[&]() { writer_status = dbfull()->TEST_SwitchMemtable(); });
TEST_SYNC_POINT(
"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:ForegroundWaiting");
TEST_SYNC_POINT(
"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:AllowPublish");
writer.join();
ASSERT_OK(writer_status);
ASSERT_OK(Put("trigger", "value"));
ASSERT_EQ("value", Get("trigger"));
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_HIT));
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_WAITED));
ASSERT_EQ(0, options.statistics->getTickerCount(WAL_PRECREATE_MISS));
}
TEST_F(DBWALTest, AsyncWalPrecreateConsumesReadyFile) {
Options options = CurrentOptions();
options.env = env_;
options.statistics = CreateDBStatistics();
options.async_wal_precreate = true;
options.recycle_log_file_num = 0;
options.write_buffer_size = 1024;
options.max_write_buffer_number = 4;
// Goal: exercise the no-wait consumption path. The background task publishes
// a prepared future WAL before the foreground memtable switch, and the switch
// must install exactly that reserved WAL number instead of allocating
// another.
SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::BGWorkAsyncWALPrecreate:Done",
"DBWALTest::AsyncWalPrecreateConsumesReadyFile:PrecreateDone"},
});
SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_point([] {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
});
Reopen(options);
TEST_SYNC_POINT(
"DBWALTest::AsyncWalPrecreateConsumesReadyFile:PrecreateDone");
const std::vector<uint64_t> wal_numbers = ListWalNumbers();
ASSERT_GE(wal_numbers.size(), 2);
const uint64_t prepared_wal_number = wal_numbers.back();
ASSERT_NE(prepared_wal_number, dbfull()->TEST_GetCurrentLogNumber());
ASSERT_OK(Put("fill", "value"));
ASSERT_OK(dbfull()->TEST_SwitchMemtable());
ASSERT_EQ(prepared_wal_number, dbfull()->TEST_GetCurrentLogNumber());
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_HIT));
ASSERT_EQ(0, options.statistics->getTickerCount(WAL_PRECREATE_MISS));
ASSERT_EQ(0, options.statistics->getTickerCount(WAL_PRECREATE_WAITED));
}
TEST_F(DBWALTest, AsyncWalPrecreateRecoveryToleratesActualFutureWal) {
Options options = CurrentOptions();
options.env = env_;
options.async_wal_precreate = true;
options.recycle_log_file_num = 0;
options.track_and_verify_wals = true;
options.track_and_verify_wals_in_manifest = true;
options.avoid_flush_during_shutdown = true;
options.avoid_flush_during_recovery = true;
// Goal: cover the actual async precreation path, not a hand-forged empty WAL.
// The background task opens the future WAL, clean close releases the
// unpublished writer and leaves the empty future WAL behind, and recovery
// must tolerate that file while replaying the preceding WAL with real user
// data.
SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::BGWorkAsyncWALPrecreate:Done",
"DBWALTest::AsyncWalPrecreateRecoveryToleratesActualFutureWal:"
"PrecreateDone"},
});
SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_point([] {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
});
DestroyAndReopen(options);
TEST_SYNC_POINT(
"DBWALTest::AsyncWalPrecreateRecoveryToleratesActualFutureWal:"
"PrecreateDone");
const std::vector<uint64_t> wal_numbers = ListWalNumbers();
ASSERT_GE(wal_numbers.size(), 2);
const uint64_t future_wal_number = wal_numbers.back();
uint64_t future_wal_size = 0;
ASSERT_OK(env_->GetFileSize(LogFileName(dbname_, future_wal_number),
&future_wal_size));
ASSERT_EQ(0, future_wal_size);
ASSERT_OK(Put("key", "value"));
Close();
ASSERT_OK(TryReopen(options));
ASSERT_EQ("value", Get("key"));
ASSERT_OK(Put("after-recovery", "value2"));
ASSERT_EQ("value2", Get("after-recovery"));
}
TEST_F(DBWALTest, AsyncWalPrecreateDeletesFileOnStartFailure) {
Options options = CurrentOptions();
options.env = env_;
options.statistics = CreateDBStatistics();
options.async_wal_precreate = true;
options.recycle_log_file_num = 0;
options.write_buffer_size = 1024;
options.max_write_buffer_number = 4;
// Goal: force failure after consuming a prepared WAL but before it becomes a
// logical WAL. The failed, unpublished file must be deleted and its writer
// destroyed outside mutex_.
SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::BGWorkAsyncWALPrecreate:Done",
"DBWALTest::AsyncWalPrecreateDeletesFileOnStartFailure:PrecreateDone"},
});
SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_point([] {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
});
Reopen(options);
TEST_SYNC_POINT(
"DBWALTest::AsyncWalPrecreateDeletesFileOnStartFailure:PrecreateDone");
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::StartWALFile:AfterCompressionTypeRecord", [](void* arg) {
*static_cast<IOStatus*>(arg) =
IOStatus::IOError("injected async WAL start failure");
});
const std::vector<uint64_t> wal_numbers = ListWalNumbers();
ASSERT_GE(wal_numbers.size(), 2);
const uint64_t prepared_wal_number = wal_numbers.back();
ASSERT_OK(Put("fill", "value"));
ASSERT_NOK(dbfull()->TEST_SwitchMemtable());
ASSERT_TRUE(
env_->FileExists(LogFileName(dbname_, prepared_wal_number)).IsNotFound());
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_HIT));
}
TEST_F(DBWALTest, AsyncWalPrecreateFailureFallsBackToSyncCreation) {
Options options = CurrentOptions();
options.env = env_;
options.statistics = CreateDBStatistics();
options.async_wal_precreate = true;
options.recycle_log_file_num = 0;
options.write_buffer_size = 1024;
options.max_write_buffer_number = 4;
// Goal: fail the background precreate file open after normal DB open has
// created the current WAL. The foreground switch should observe no prepared
// WAL, create one synchronously, and expose both failure and miss counters.
SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::BGWorkAsyncWALPrecreate:Done",
"DBWALTest::AsyncWalPrecreateFailureFallsBackToSyncCreation:"
"PrecreateDone"},
});
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::Open:AfterDeleteFiles",
[&](void*) { env_->non_writable_count_ = 1; });
SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_point([&] {
env_->non_writable_count_ = 0;
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
});
Reopen(options);
TEST_SYNC_POINT(
"DBWALTest::AsyncWalPrecreateFailureFallsBackToSyncCreation:"
"PrecreateDone");
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_FAILED));
ASSERT_OK(Put("fill", "value"));
ASSERT_OK(dbfull()->TEST_SwitchMemtable());
ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_MISS));
}
TEST_F(DBWALTest, ReadOnlyErrorIfWalFileExistsIgnoresEmptyWals) {
Options options = CurrentOptions();
options.env = env_;
DestroyAndReopen(options);
Close();
// Goal: preserve error_if_wal_file_exists for WALs containing data while
// allowing empty future WALs left by async precreation. This creates empty
// WAL files directly so the test isolates the read-only open contract.
const std::vector<uint64_t> existing_wal_numbers = ListWalNumbers();
const uint64_t empty_wal_number =
existing_wal_numbers.empty() ? 1 : existing_wal_numbers.back() + 1;
CreateEmptyWal(empty_wal_number);
DBOptions db_options(options);
std::vector<ColumnFamilyDescriptor> column_families;
column_families.emplace_back(kDefaultColumnFamilyName,
ColumnFamilyOptions(options));
std::vector<ColumnFamilyHandle*> handles;
std::unique_ptr<DB> read_only_db;
ASSERT_OK(DB::OpenForReadOnly(db_options, dbname_, column_families, &handles,
&read_only_db,
true /* error_if_wal_file_exists */));
for (ColumnFamilyHandle* handle : handles) {
ASSERT_OK(read_only_db->DestroyColumnFamilyHandle(handle));
}
read_only_db.reset();
handles.clear();
CreateWalWithContents(empty_wal_number + 1, Slice("not empty"));
ASSERT_NOK(DB::OpenForReadOnly(db_options, dbname_, column_families, &handles,
&read_only_db,
true /* error_if_wal_file_exists */));
}
TEST_F(DBWALTest, RecoveryToleratesEmptyFutureWal) {
Options options = CurrentOptions();
options.env = env_;
options.track_and_verify_wals = true;
options.track_and_verify_wals_in_manifest = true;
options.avoid_flush_during_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("key", "value"));
Close();
const std::vector<uint64_t> wal_numbers = ListWalNumbers();
ASSERT_FALSE(wal_numbers.empty());
const uint64_t future_wal_number = wal_numbers.back() + 1;
CreateEmptyWal(future_wal_number);
// Goal: model a crash after async WAL precreation but before consumption.
// Recovery should process the zero-byte future WAL, mark its file number
// used, and keep replaying the real preceding WAL without reporting a WAL
// chain or MANIFEST-tracking error.
ASSERT_OK(TryReopen(options));
ASSERT_EQ("value", Get("key"));
ASSERT_OK(Put("after-recovery", "value2"));
ASSERT_EQ("value2", Get("after-recovery"));
}
TEST_F(DBWALTest, RecoveryFlushSwitchWALOnEmptyMemtable) {
Options options = CurrentOptions();
auto fault_fs = std::make_shared<FaultInjectionTestFS>(FileSystem::Default());
+2 -2
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 -118
View File
@@ -201,121 +201,6 @@ TEST_F(EventListenerTest, OnSingleDBCompactionTest) {
}
}
// Listener that asserts OnCompactionPreCommit fires strictly between
// OnCompactionBegin and OnCompactionCompleted, and that input files'
// being_compacted flag is still true at that point.
class TestCompactionPreCommitListener : public EventListener {
public:
explicit TestCompactionPreCommitListener(EventListenerTest* test)
: test_(test) {}
void OnCompactionBegin(DB* /*db*/, const CompactionJobInfo& ci) override {
std::lock_guard<std::mutex> lock(mutex_);
++begin_count_;
last_begin_job_id_ = ci.job_id;
EXPECT_EQ(begin_count_, pre_commit_count_ + 1);
EXPECT_EQ(begin_count_, completed_count_ + 1);
}
void OnCompactionPreCommit(DB* db, const CompactionJobInfo& ci) override {
std::lock_guard<std::mutex> lock(mutex_);
++pre_commit_count_;
// Must fire after Begin and before Completed for this compaction.
EXPECT_EQ(pre_commit_count_, begin_count_);
EXPECT_EQ(pre_commit_count_, completed_count_ + 1);
EXPECT_EQ(ci.job_id, last_begin_job_id_);
EXPECT_GT(ci.input_files.size(), 0U);
// Verify input files are still marked being_compacted.
std::vector<std::vector<FileMetaData>> files_by_level;
test_->dbfull()->TEST_GetFilesMetaData(test_->handles_[ci.cf_id],
&files_by_level);
EXPECT_EQ(test_->db_.get(), db);
for (const auto& info : ci.input_file_infos) {
bool found = false;
for (const auto& level_files : files_by_level) {
for (const auto& meta : level_files) {
if (meta.fd.GetNumber() == info.file_number) {
found = true;
EXPECT_TRUE(meta.being_compacted)
<< "input file " << info.file_number
<< " should still be being_compacted in "
"OnCompactionPreCommit";
}
}
}
EXPECT_TRUE(found) << "input file " << info.file_number
<< " not found in DB";
}
}
void OnCompactionCompleted(DB* /*db*/, const CompactionJobInfo& ci) override {
std::lock_guard<std::mutex> lock(mutex_);
++completed_count_;
// Must fire after the matching PreCommit.
EXPECT_EQ(completed_count_, pre_commit_count_);
EXPECT_EQ(completed_count_, begin_count_);
EXPECT_EQ(ci.job_id, last_begin_job_id_);
}
size_t BeginCount() {
std::lock_guard<std::mutex> lock(mutex_);
return begin_count_;
}
size_t PreCommitCount() {
std::lock_guard<std::mutex> lock(mutex_);
return pre_commit_count_;
}
size_t CompletedCount() {
std::lock_guard<std::mutex> lock(mutex_);
return completed_count_;
}
private:
EventListenerTest* test_;
std::mutex mutex_;
size_t begin_count_ = 0;
size_t pre_commit_count_ = 0;
size_t completed_count_ = 0;
int last_begin_job_id_ = -1;
};
TEST_F(EventListenerTest, OnCompactionPreCommitOrdering) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.compaction_style = kCompactionStyleLevel;
options.compression = kNoCompression;
options.level0_file_num_compaction_trigger = 4;
auto* listener = new TestCompactionPreCommitListener(this);
options.listeners.emplace_back(listener);
// Verify sync-point ordering: Begin -> PreCommit -> Completed.
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"DBImpl::NotifyOnCompactionBegin::UnlockMutex",
"DBImpl::NotifyOnCompactionPreCommit::UnlockMutex"},
{"DBImpl::NotifyOnCompactionPreCommit::UnlockMutex",
"DBImpl::NotifyOnCompactionCompleted::UnlockMutex"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
CreateAndReopenWithCF({"pikachu"}, options);
Random rnd(301);
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 10; j++) {
ASSERT_OK(Put(1, rnd.RandomString(10), rnd.RandomString(10)));
}
ASSERT_OK(Flush(1));
}
ASSERT_OK(dbfull()->TEST_WaitForCompact());
EXPECT_GT(listener->BeginCount(), 0U);
EXPECT_EQ(listener->PreCommitCount(), listener->BeginCount());
EXPECT_EQ(listener->CompletedCount(), listener->BeginCount());
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
}
// This simple Listener can only handle one flush at a time.
class TestFlushListener : public EventListener {
public:
@@ -1811,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());
@@ -1829,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();
{
@@ -1865,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 -27
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;
@@ -240,13 +240,6 @@ bool VersionEdit::EncodeTo(std::string* dst,
PutLengthPrefixedSlice(dst, progress_data);
}
if (has_last_compacted_manifest_file_size_) {
PutVarint32(dst, kLastCompactedManifestFileSize);
std::string varint_size;
PutVarint64(&varint_size, last_compacted_manifest_file_size_);
PutLengthPrefixedSlice(dst, Slice(varint_size));
}
return true;
}
@@ -897,17 +890,6 @@ Status VersionEdit::DecodeFrom(const Slice& src) {
break;
}
case kLastCompactedManifestFileSize: {
Slice encoded;
if (GetLengthPrefixedSlice(&input, &encoded) &&
GetVarint64(&encoded, &last_compacted_manifest_file_size_)) {
has_last_compacted_manifest_file_size_ = true;
} else {
msg = "last compacted manifest file size";
}
break;
}
default:
if (tag & kTagSafeIgnoreMask) {
// Tag from future which can be safely ignored.
@@ -1082,10 +1064,6 @@ std::string VersionEdit::DebugString(bool hex_key) const {
r.append("\n SubcompactionProgress: ");
r.append(subcompaction_progress_.ToString());
}
if (has_last_compacted_manifest_file_size_) {
r.append("\n LastCompactedManifestFileSize: ");
AppendNumberTo(&r, last_compacted_manifest_file_size_);
}
r.append("\n}\n");
return r;
}
@@ -1242,10 +1220,6 @@ std::string VersionEdit::DebugJSON(int edit_num, bool hex_key) const {
jw << "SubcompactionProgress" << subcompaction_progress_.ToString();
}
if (has_last_compacted_manifest_file_size_) {
jw << "LastCompactedManifestFileSize" << last_compacted_manifest_file_size_;
}
jw.EndObject();
return jw.Get();
-15
View File
@@ -74,7 +74,6 @@ enum Tag : uint32_t {
kWalDeletion2,
kPersistUserDefinedTimestamps,
kSubcompactionProgress,
kLastCompactedManifestFileSize,
};
enum SubcompactionProgressPerLevelCustomTag : uint32_t {
@@ -1017,17 +1016,6 @@ class VersionEdit {
subcompaction_progress_.Clear();
}
void SetLastCompactedManifestFileSize(uint64_t size) {
has_last_compacted_manifest_file_size_ = true;
last_compacted_manifest_file_size_ = size;
}
bool HasLastCompactedManifestFileSize() const {
return has_last_compacted_manifest_file_size_;
}
uint64_t GetLastCompactedManifestFileSize() const {
return last_compacted_manifest_file_size_;
}
// Recovery-time per-column-family edits only need to be written when they
// advance the CF's log number or carry some other manifest state.
bool ShouldEmitPerColumnFamilyRecoveryEdit(uint64_t current_log_number) const;
@@ -1123,9 +1111,6 @@ class VersionEdit {
bool has_subcompaction_progress_ = false;
SubcompactionProgress subcompaction_progress_;
bool has_last_compacted_manifest_file_size_ = false;
uint64_t last_compacted_manifest_file_size_ = 0;
// Newly created table files and blob files are eligible for deletion if they
// are not registered as live files after the background jobs creating them
// have finished. In case committing the VersionEdit containing such changes
-5
View File
@@ -642,11 +642,6 @@ Status VersionEditHandler::ExtractInfoFromVersionEdit(ColumnFamilyData* cfd,
version_edit_params_.GetLastSequence() <= edit.GetLastSequence());
version_edit_params_.SetLastSequence(edit.GetLastSequence());
}
if (edit.HasLastCompactedManifestFileSize()) {
version_set_->last_compacted_manifest_file_size_ =
edit.GetLastCompactedManifestFileSize();
version_set_->TuneMaxManifestFileSize();
}
if (!version_edit_params_.HasPrevLogNumber()) {
version_edit_params_.SetPrevLogNumber(0);
}
+10 -37
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,
@@ -7496,28 +7492,6 @@ Status VersionSet::WriteCurrentStateToManifest(
}
}
}
// Record the approximate compacted manifest size so it's available at
// recovery time for TuneMaxManifestFileSize(). This record must come last
// in WriteCurrentStateToManifest for an accurate size estimate.
{
// Include a rough estimate of this record's own size (~15 bytes for the
// VersionEdit payload + log record header).
constexpr uint64_t kEstimatedRecordOverhead = 15;
VersionEdit edit;
edit.SetLastCompactedManifestFileSize(log->file()->GetFileSize() +
kEstimatedRecordOverhead);
std::string record;
if (!edit.EncodeTo(&record)) {
return Status::Corruption("Unable to Encode VersionEdit:" +
edit.DebugString(true));
}
io_s = log->AddRecord(write_options, record);
if (!io_s.ok()) {
return io_s;
}
}
return Status::OK();
}
@@ -8185,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;
-3
View File
@@ -114,7 +114,6 @@ DECLARE_double(memtable_prefix_bloom_size_ratio);
DECLARE_bool(memtable_whole_key_filtering);
DECLARE_int32(open_files);
DECLARE_bool(open_files_async);
DECLARE_bool(async_wal_precreate);
DECLARE_uint64(compressed_secondary_cache_size);
DECLARE_int32(compressed_secondary_cache_numshardbits);
DECLARE_int32(secondary_cache_update_interval);
@@ -473,8 +472,6 @@ DECLARE_int32(compaction_on_deletion_window_size);
DECLARE_double(compaction_on_deletion_ratio);
DECLARE_double(read_triggered_compaction_threshold);
DECLARE_string(listener_uri);
constexpr long KB = 1024;
constexpr int kRandomValueMaxFactor = 3;
constexpr int kValueMaxLen = 100;
@@ -1,8 +1,3 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
+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());
}
}
-8
View File
@@ -206,10 +206,6 @@ DEFINE_int32(open_files, ROCKSDB_NAMESPACE::Options().max_open_files,
DEFINE_bool(open_files_async, ROCKSDB_NAMESPACE::Options().open_files_async,
"Options.open_files_async");
DEFINE_bool(async_wal_precreate,
ROCKSDB_NAMESPACE::Options().async_wal_precreate,
"Options.async_wal_precreate");
DEFINE_uint64(compressed_secondary_cache_size, 0,
"Number of bytes to use as a cache of compressed data."
" 0 means use default settings.");
@@ -1668,10 +1664,6 @@ DEFINE_double(read_triggered_compaction_threshold,
ROCKSDB_NAMESPACE::Options().read_triggered_compaction_threshold,
"Sets CF option read_triggered_compaction_threshold.");
DEFINE_string(listener_uri, "",
"URI for an additional EventListener to attach (e.g. "
"auto_tuner://). Empty means none.");
DEFINE_bool(
auto_refresh_iterator_with_snapshot,
ROCKSDB_NAMESPACE::ReadOptions().auto_refresh_iterator_with_snapshot,
+2 -145
View File
@@ -6,9 +6,7 @@
#ifdef GFLAGS
#pragma once
#include <cinttypes>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
#include "db_stress_tool/db_stress_compaction_service.h"
@@ -21,7 +19,6 @@
#include "rocksdb/listener.h"
#include "rocksdb/table_properties.h"
#include "rocksdb/unique_id.h"
#include "util/atomic.h"
#include "util/gflags_compat.h"
#include "util/random.h"
#include "utilities/fault_injection_fs.h"
@@ -72,13 +69,6 @@ class DbStressListener : public EventListener {
static const char* kClassName() { return "DBStressListener"; }
~DbStressListener() override { assert(num_pending_file_creations_ == 0); }
// Signal that the DB is about to shut down. Must be called before DB::Close()
// or CancelAllBackgroundWork(). Why? Listener notifications, especially for
// compaction, have different (mostly relaxed) contracts during shutdown, and
// not all callbacks take a DB object. Ideally, public API improvements would
// make this unnecessary in the future.
void NotifyShuttingDown() { shutting_down_.Store(true); }
void OnFlushCompleted(DB* /*db*/, const FlushJobInfo& info) override {
assert(IsValidColumnFamilyName(info.cf_name));
VerifyFilePath(info.file_path);
@@ -128,83 +118,11 @@ class DbStressListener : public EventListener {
}
}
void OnTableFileDeleted(const TableFileDeletionInfo& info) override {
// A file must not be deleted while it is still between
// OnCompactionBegin and OnCompactionPreCommit (i.e. actively being
// compacted with being_compacted == true). (Perhaps more realistically,
// this is checking for failure to call OnCompactionPreCommit.)
//
// During shutdown, compaction notifications are skipped so tracking
// state may be stale -- skip the check.
if (!shutting_down_.Load()) {
std::lock_guard<std::mutex> lock(compacting_files_mu_);
uint64_t file_number = FileNumberFromPath(info.file_path);
if (file_number != 0 &&
compacting_files_.find(file_number) != compacting_files_.end()) {
fprintf(stderr,
"OnTableFileDeleted for file tracked as being compacted "
"(between Begin and PreCommit): file_number=%" PRIu64 "\n",
file_number);
fflush(stderr);
std::abort();
}
}
void OnTableFileDeleted(const TableFileDeletionInfo& /*info*/) override {
RandomSleep();
}
void OnCompactionBegin(DB* /*db*/, const CompactionJobInfo& ci) override {
// Sanity check inspired by a Meta-internal check: the same input file must
// not be in two concurrent compactions. Inserting input file numbers into a
// shared set on Begin and removing them on PreCommit (rather than on
// Completed) is what exercises the new OnCompactionPreCommit
// callback. Removing on Completed would race with another picker that
// grabs the same file as soon as ReleaseCompactionFiles flips
// FileMetaData::being_compacted back to false; PreCommit runs
// while being_compacted is still true and so closes that race.
{
std::lock_guard<std::mutex> lock(compacting_files_mu_);
for (const auto& info : ci.input_file_infos) {
auto [_, inserted] = compacting_files_.insert(info.file_number);
if (!inserted) {
fprintf(stderr,
"Concurrent compaction of SST file detected: cf=%s "
"file_number=%" PRIu64 "\n",
ci.cf_name.c_str(), info.file_number);
fflush(stderr);
std::abort();
}
}
}
RandomSleep();
}
void OnCompactionPreCommit(DB* /*db*/, const CompactionJobInfo& ci) override {
// Pair with OnCompactionBegin's bookkeeping: move files from
// compacting_files_ and record the job for Completed verification.
{
std::lock_guard<std::mutex> lock(compacting_files_mu_);
std::unordered_set<uint64_t> job_files;
for (const auto& info : ci.input_file_infos) {
size_t erased = compacting_files_.erase(info.file_number);
if (erased != 1) {
fprintf(stderr,
"OnCompactionPreCommit for file not in tracking set: "
"cf=%s file_number=%" PRIu64 "\n",
ci.cf_name.c_str(), info.file_number);
fflush(stderr);
std::abort();
}
job_files.insert(info.file_number);
}
auto [_, inserted] =
precommitted_jobs_.emplace(ci.job_id, std::move(job_files));
if (!inserted) {
fprintf(stderr, "OnCompactionPreCommit: duplicate job_id %d\n",
ci.job_id);
fflush(stderr);
std::abort();
}
}
void OnCompactionBegin(DB* /*db*/, const CompactionJobInfo& /*ci*/) override {
RandomSleep();
}
@@ -217,34 +135,6 @@ class DbStressListener : public EventListener {
for (const auto& file_path : ci.output_files) {
VerifyFilePath(file_path);
}
// Verify that OnCompactionPreCommit fired before OnCompactionCompleted
// for the same job, with matching input files.
{
std::lock_guard<std::mutex> lock(compacting_files_mu_);
auto it = precommitted_jobs_.find(ci.job_id);
if (it == precommitted_jobs_.end()) {
fprintf(stderr,
"OnCompactionCompleted without prior OnCompactionPreCommit: "
"cf=%s job_id=%d\n",
ci.cf_name.c_str(), ci.job_id);
fflush(stderr);
std::abort();
}
// Verify input file sets match between PreCommit and Completed.
for (const auto& info : ci.input_file_infos) {
if (it->second.find(info.file_number) == it->second.end()) {
fprintf(stderr,
"OnCompactionCompleted: input file %" PRIu64
" not in PreCommit set for job_id=%d\n",
info.file_number, ci.job_id);
fflush(stderr);
std::abort();
}
}
precommitted_jobs_.erase(it);
}
// pretending doing some work here
RandomSleep();
}
@@ -487,39 +377,6 @@ class DbStressListener : public EventListener {
SharedState* shared_;
mutable std::mutex bg_pressure_mu_;
BackgroundJobPressure last_bg_pressure_;
// Files (by file_number) currently in flight from OnCompactionBegin to
// OnCompactionPreCommit. Used to detect concurrent compaction of
// the same SST file -- the bug fixed by the OnCompactionPreCommit
// callback. Protected by compacting_files_mu_.
std::mutex compacting_files_mu_;
std::unordered_set<uint64_t> compacting_files_;
// Set before DB close to suppress false positives from stale tracking.
Atomic<bool> shutting_down_{false};
// Jobs that have passed OnCompactionPreCommit but not yet
// OnCompactionCompleted. Maps job_id -> input file numbers.
// Used to verify Begin -> PreCommit -> Completed ordering per job.
// Protected by compacting_files_mu_.
std::unordered_map<int, std::unordered_set<uint64_t>> precommitted_jobs_;
// Extract file number from a file path like "/path/to/000123.sst".
// Returns 0 if the path cannot be parsed.
static uint64_t FileNumberFromPath(const std::string& file_path) {
size_t pos = file_path.find_last_of('/');
// Avoid copying file_path when no '/' separator is found
std::string file_name_buf;
if (pos != std::string::npos) {
file_name_buf = file_path.substr(pos + 1);
}
const std::string& file_name =
(pos == std::string::npos) ? file_path : file_name_buf;
uint64_t file_number = 0;
FileType file_type;
if (ParseFileName(file_name, &file_number, &file_type) &&
file_type == kTableFile) {
return file_number;
}
return 0;
}
};
} // namespace ROCKSDB_NAMESPACE
#endif // GFLAGS
-30
View File
@@ -95,9 +95,6 @@ StressTest::StressTest()
}
void StressTest::CleanUp() {
// Notify listener before DB close so it can tolerate stale tracking
// from skipped notifications during shutdown.
NotifyListenerShuttingDown();
CleanUpColumnFamilies();
if (db_) {
db_->Close();
@@ -108,15 +105,6 @@ void StressTest::CleanUp() {
secondary_db_.reset();
}
void StressTest::NotifyListenerShuttingDown() {
for (auto& listener : options_.listeners) {
if (strcmp(listener->Name(), DbStressListener::kClassName()) == 0) {
static_cast_with_check<DbStressListener>(listener.get())
->NotifyShuttingDown();
}
}
}
void StressTest::CleanUpColumnFamilies() {
for (auto cf : column_families_) {
delete cf;
@@ -3974,19 +3962,6 @@ void StressTest::Open(SharedState* shared, bool reopen) {
FLAGS_db, options_.db_paths, cf_descriptors, shared));
RegisterAdditionalListeners();
if (!FLAGS_listener_uri.empty()) {
std::shared_ptr<EventListener> listener;
Status listener_status =
ObjectRegistry::Default()->NewSharedObject<EventListener>(
FLAGS_listener_uri, &listener);
if (!listener_status.ok()) {
fprintf(stderr, "Failed to create listener from URI '%s': %s\n",
FLAGS_listener_uri.c_str(), listener_status.ToString().c_str());
exit(1);
}
options_.listeners.emplace_back(std::move(listener));
}
// If this is for DB reopen, error injection may have been enabled.
// Disable it here in case there is no open fault injection.
if (fault_fs_guard) {
@@ -4254,10 +4229,6 @@ void StressTest::Open(SharedState* shared, bool reopen) {
}
void StressTest::Reopen(ThreadState* thread) {
// Notify listener before DB close so it can tolerate stale tracking
// from skipped notifications during shutdown.
NotifyListenerShuttingDown();
// BG jobs in WritePrepared must be canceled first because i) they can access
// the db via a callbac ii) they hold on to a snapshot and the upcoming
// ::Close would complain about it.
@@ -4618,7 +4589,6 @@ void InitializeOptionsFromFlags(
}
options.max_open_files = FLAGS_open_files;
options.open_files_async = FLAGS_open_files_async;
options.async_wal_precreate = FLAGS_async_wal_precreate;
if (FLAGS_open_files_async && !FLAGS_skip_stats_update_on_db_open) {
FLAGS_skip_stats_update_on_db_open = true;
fprintf(stderr,
-3
View File
@@ -71,9 +71,6 @@ class StressTest {
Options GetOptions(int cf_id);
void CleanUp();
private:
void NotifyListenerShuttingDown();
protected:
static int GetMinInjectedErrorCount(int error_count_1, int error_count_2) {
if (error_count_1 > 0 && error_count_2 > 0) {
+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`.
+1 -13
View File
@@ -94,23 +94,11 @@ Step 3 - **Synced**: `fsync()` is called when `WriteOptions::sync` is true, or w
Step 4 - **Obsolete**: After all memtables referencing this WAL are flushed, the WAL becomes obsolete and is either recycled or deleted.
## Async WAL Precreation
When `DBOptions::async_wal_precreate` is enabled, RocksDB keeps at most one future WAL file precreated in the background. RocksDB reserves a file number before scheduling the background task; the task opens the file/writer, but deliberately leaves the file as reserved empty storage:
- It does not write compression metadata.
- It does not write predecessor WAL info.
- It does not add the file to `logs_`, `alive_wal_files_`, or MANIFEST WAL tracking.
The file becomes a logical WAL only when foreground `SwitchMemtable()` consumes it. At that point RocksDB writes the normal WAL metadata, flushes the previous WAL writer buffer, and installs the new WAL in the same order as synchronous WAL creation. If foreground rotation reaches the switch while the background task is still running, the writer waits for the reserved file number instead of allocating a higher numbered WAL. This avoids a late lower-numbered WAL appearing after a newer WAL has become live. If background precreation fails, the background task logs the failure and foreground rotation falls back to normal synchronous WAL creation.
Unused precreated WALs are safe across crashes and clean shutdowns because they are zero-record future WAL files. Recovery processes WAL files by log number, marks every observed WAL number as used, and treats an empty future WAL as EOF. On clean close, RocksDB releases the unpublished WAL writer but does not need to delete the empty file. The option is sanitized to false when `recycle_log_file_num > 0`.
## WAL Tracking in MANIFEST
`WalSet` (see `db/wal_edit.h`, tracked in `VersionSet`) manages WAL metadata in the MANIFEST:
- `WalAddition` records are written when a closed/inactive WAL's synced size is finalized
- `WalAddition` records are written when a WAL is created or when a closed/inactive WAL's synced size is finalized
- `WalDeletion` records are written when a WAL becomes obsolete after flush
- Live-WAL syncs (via `DB::SyncWAL()` or `WriteOptions::sync`) are intentionally not tracked in MANIFEST
@@ -103,7 +103,7 @@ When a flush is triggered, `DBImpl::SwitchMemtable()` (see `db/db_impl/db_impl_w
Step 1 - Writes any recoverable state to the current memtable.
Step 2 - Creates a new WAL file (unless the current WAL is empty) with the next file number from `VersionSet::NewFileNumber()`. If `DBOptions::async_wal_precreate` has prepared a future WAL, this step consumes that reserved file number and finalizes the WAL metadata instead of creating the file synchronously.
Step 2 - Creates a new WAL file (unless the current WAL is empty) with the next file number from `VersionSet::NewFileNumber()`.
Step 3 - Constructs a new `MemTable` with `earliest_seq` set to the current last sequence.
+11 -53
View File
@@ -38,18 +38,6 @@
#define BTRFS_SUPER_MAGIC 0x9123683E
#endif
#if !defined(TMPFS_MAGIC)
#define TMPFS_MAGIC 0x01021994
#endif
#if !defined(OVERLAYFS_SUPER_MAGIC)
#define OVERLAYFS_SUPER_MAGIC 0x794c7630
#endif
#if !defined(ZFS_SUPER_MAGIC)
#define ZFS_SUPER_MAGIC 0x2fc12fc1
#endif
#ifdef ROCKSDB_FALLOCATE_PRESENT
#include <cerrno>
#endif
@@ -1396,34 +1384,16 @@ TEST_P(EnvPosixTestWithParam, AllocateTest) {
struct stat f_stat;
ASSERT_EQ(stat(fname.c_str(), &f_stat), 0);
ASSERT_EQ((unsigned int)kDataSize, f_stat.st_size);
// btrfs (and other CoW filesystems) accept fallocate but use
// copy-on-write, so preallocated extents are not reliably reflected in
// st_blocks (especially under load). Skip block-count verification on
// those filesystems.
// Also skip on tmpfs and overlayfs, which may not report preallocated
// blocks in st_blocks reliably.
// btrfs accepts fallocate but uses copy-on-write, so preallocated extents
// are not reflected in st_blocks. Skip block-count verification there.
bool skip_block_checks = false;
#ifdef OS_LINUX
struct statfs fs_stat;
if (statfs(fname.c_str(), &fs_stat) == 0) {
if (fs_stat.f_type ==
static_cast<decltype(fs_stat.f_type)>(BTRFS_SUPER_MAGIC)) {
fprintf(stderr, "Skipping preallocation block count checks on btrfs\n");
skip_block_checks = true;
} else if (fs_stat.f_type ==
static_cast<decltype(fs_stat.f_type)>(ZFS_SUPER_MAGIC)) {
fprintf(stderr, "Skipping preallocation block count checks on zfs\n");
skip_block_checks = true;
} else if (fs_stat.f_type ==
static_cast<decltype(fs_stat.f_type)>(TMPFS_MAGIC)) {
fprintf(stderr, "Skipping preallocation block count checks on tmpfs\n");
skip_block_checks = true;
} else if (fs_stat.f_type ==
static_cast<decltype(fs_stat.f_type)>(OVERLAYFS_SUPER_MAGIC)) {
fprintf(stderr,
"Skipping preallocation block count checks on overlayfs\n");
skip_block_checks = true;
}
if (statfs(fname.c_str(), &fs_stat) == 0 &&
fs_stat.f_type ==
static_cast<decltype(fs_stat.f_type)>(BTRFS_SUPER_MAGIC)) {
fprintf(stderr, "Skipping preallocation block count checks on btrfs\n");
skip_block_checks = true;
}
#endif
if (!skip_block_checks) {
@@ -1433,20 +1403,8 @@ TEST_P(EnvPosixTestWithParam, AllocateTest) {
// expect.
// It looks like some FS give us more blocks that we asked for. That's
// fine. It might be worth investigating further.
if ((unsigned int)(kPreallocateSize / kBlockSize) > f_stat.st_blocks) {
// Preallocation may not be supported or reflected in st_blocks on this
// filesystem. Print a warning and skip the check rather than failing.
fprintf(stderr,
"Warning: preallocated blocks (%u) less than expected (%u), "
"skipping block count check. This may indicate the filesystem "
"does not support preallocation or does not report it in "
"st_blocks.\n",
(unsigned int)f_stat.st_blocks,
(unsigned int)(kPreallocateSize / kBlockSize));
} else {
ASSERT_LE((unsigned int)(kPreallocateSize / kBlockSize),
f_stat.st_blocks);
}
ASSERT_LE((unsigned int)(kPreallocateSize / kBlockSize),
f_stat.st_blocks);
}
// close the file, should deallocate the blocks
@@ -1934,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();
@@ -1945,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
Loaded 100 of 176 files, more files were not shown because too many files have changed in this diff. Show more