Compare commits

..
Author SHA1 Message Date
Peter Dillinger c8600f85f6 [ci] Add OPENAI_API_KEY check to Codex steps
Add early exit checks to Codex steps so they skip gracefully when
OPENAI_API_KEY is not configured, instead of failing with exit code 1.

This prevents noise from Codex workflows when the API key is not set up.
2026-05-01 15:22:17 -07:00
Peter Dillinger 1f910525a7 [ci] Fix invalid secrets reference in workflow conditions
The secrets context cannot be used directly in if conditions.
Remove the invalid 'secrets.OPENAI_API_KEY != ''' checks that were
added in the previous commit. This was causing workflow dispatch to fail
with 'Looks like something went wrong!' error.

The Codex workflows will now run but may fail if OPENAI_API_KEY is not
configured. This is preferable to having an invalid workflow file.
2026-05-01 15:21:42 -07:00
Peter Dillinger 5239c4caeb Merge remote-tracking branch 'pd/blog_format_no_wal' into fix_agent_review2 2026-05-01 14:10:57 -07:00
Peter Dillinger 2e21612c3e Fix AI review workflows broken by cc8d9ea04
Summary:
This commit fixes several potential issues introduced by cc8d9ea04 (CI: AI review workflow improvements) that broke the agent review jobs in GitHub CI.

1. Codex workflows failing due to missing OPENAI_API_KEY:
   - The commit added Codex review workflows, but they fail when OPENAI_API_KEY is not configured
   - Added 'secrets.OPENAI_API_KEY != ''' condition to all 13 Codex steps to skip gracefully when the API key is unavailable

2. Missing pull-requests: read permission in manual-review job:
   - The manual-review job calls github.rest.pulls.get() but lacked the required permission
   - Added 'pull-requests: read' to the manual-review job permissions

3. Potential crash when headSha is undefined:
   - parse-claude-review.js and parse-codex-review.js accessed meta.headSha.substring() without null check
   - Added defensive checks to handle undefined headSha gracefully

4. Claude model claude-opus-4-7 incompatible with thinking API:
   - The commit upgraded default model to claude-opus-4-7, but this model doesn't support the 'thinking.type.enabled' API used by the Claude Code action
   - Error: 'thinking.type.enabled' is not supported for this model. Use 'thinking.type.adaptive' and 'output_config.effort' to control thinking behavior
   - Reverted default model to claude-opus-4-6 and removed claude-opus-4-7 from options

Test Plan:
GitHub CI
2026-05-01 13:32:27 -07:00
Peter Dillinger 5ba6eb5497 Blog file format: unified WAL and blob file format (1/n)
Summary:
Introduces the "blog" file format (portmanteau of "blob" + "log"), a new
unified file format for WAL and blob files in RocksDB. This change makes
the new format an opt-in option for blob files ONLY. An immediate
follow-up will add WAL support. This format is intended to be the future
default for both WAL and blob files, and likely also manifest files.

The impetus for this new file format was an apparent convergence in
requirements for interesting and useful future directions for RocksDB,
along with some tech debt:
* Supporting blob "direct write" (key-value separation in the memtable)
  with WAL enabled and at least the option to have all the WAL+blob data
  go into one file to reduce overheads in some cases like WAL sync write
  with blob direct write. (In other cases, separating WAL WriteBatches
  and blobs into distinct files would likely be the better choice.)
  The "preamble start" marker record is intended to support this case so
  that WriteBatches can carry external values in a "preamble" in memory
  and the WriteBatch doesn't need to be rewritten on storage to a single
  blog file serving both WAL and blob functions. (Details in later
  work.)
* Preserve the continuity of each blob value for efficient reads (NOTE:
  WAL/Manifest format often breaks up payloads), and extend this
  continuity to WriteBatches so that keys/values with known checksums
  could be carried and extended to the WriteBatch and its contiguous
  encoding in the blog-as-WAL file. (The goal is to leverage checksums
  across layers as much as possible rather than computing new ones at
  each layer; only CRC checksums are "extendable.")
* Support some "linear log" workloads with monotonically increasing keys
  and FIFO pruning of old data. A CF could be configured to use its own
  blog-as-WAL files writing this data, and those files could get
  indexing information written to them as each file is sealed. This
  would enable moderately efficient read queries that process WriteBatch
  records for results, and no WAL->SST write amplification.
* Modernize blog and WAL formats with features like explicit versioning
  and extensibility, configurable and context-aware checksums, debugging
  and statistical information, customizable compression
  (CompressionManager aware), and more.

New DB options: use_blog_format_for_blobs, blog_checksum.
Other public API changes:
* ChecksumType moved to include/rocksdb/checksum_type.h.
* kStreamingCompressionSentinel (0x7F) added to CompressionType enum.
Some included refactoring:
* BlobLogWriter::log_number_ removed (was unused).
* BlobLogWriter::AppendFooter renamed to LegacyAppendFooterAndClose.

Test Plan:
TODO
2026-04-27 18:13:43 -07:00
155 changed files with 4403 additions and 4915 deletions

No files matched your search

+14 -50
View File
@@ -531,9 +531,7 @@ jobs:
core.warning(
`Could not parse classifier output: ${error.message}`);
}
// Keep Claude's thinking budget below the action's effective
// max_tokens so the API has room for the final review text.
const budget = label === 'simple' ? '16000' : '24000';
const budget = label === 'simple' ? '16000' : '32000';
core.info(
`Complexity classified as: ${label} ` +
`-> MAX_THINKING_TOKENS=${budget}`
@@ -553,28 +551,15 @@ jobs:
run: |
if [ -z "${OPENAI_API_KEY}" ]; then
echo "OPENAI_API_KEY not configured, skipping Codex review"
printf 'complex\n' > codex-complexity-output.txt
exit 0
fi
mkdir -p "${CODEX_HOME}"
set +e
codex exec \
-m "${CODEX_MODEL}" \
-c model_reasoning_effort="medium" \
--dangerously-bypass-approvals-and-sandbox \
--output-last-message codex-complexity-output.txt \
- < /tmp/complexity_prompt.txt > codex-complexity.log 2>&1
exit_code=$?
set -e
if [ "${exit_code}" -ne 0 ]; then
echo "Codex complexity classification failed with exit code ${exit_code}"
echo "Defaulting to complex review budget"
if [ -s codex-complexity.log ]; then
echo "Last 100 lines of codex-complexity.log:"
tail -100 codex-complexity.log
fi
printf 'complex\n' > codex-complexity-output.txt
fi
- name: Pick thinking budget (Codex)
if: >-
@@ -714,6 +699,8 @@ jobs:
CODEX_HOME: /tmp/codex-home
CODEX_MODEL: ${{ inputs.default_model }}
CODEX_REASONING_EFFORT: ${{ steps.budget_codex.outputs.effort }}
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
run: |
if [ -z "${OPENAI_API_KEY}" ]; then
echo "OPENAI_API_KEY not configured, skipping Codex review"
@@ -721,7 +708,9 @@ jobs:
fi
mkdir -p "${CODEX_HOME}"
set +e
codex exec \
codex exec review \
--base "${BASE_SHA}" \
--title "${PR_TITLE}" \
-m "${CODEX_MODEL}" \
-c model_reasoning_effort="${CODEX_REASONING_EFFORT}" \
--dangerously-bypass-approvals-and-sandbox \
@@ -1113,19 +1102,9 @@ jobs:
}
const override = (process.env.OVERRIDE || '').trim();
if (override && /^\d+$/.test(override)) {
const maxSafeBudget = 24000;
const requested = parseInt(override, 10);
const budget = Math.min(requested, maxSafeBudget).toString();
if (budget !== override) {
core.info(
`Clamping thinking_budget override from ${override} ` +
`to ${budget} so max_tokens can exceed thinking tokens.`
);
} else {
core.info(`Using thinking_budget override: ${budget}`);
}
core.info(`Using thinking_budget override: ${override}`);
core.setOutput('label', 'override');
core.setOutput('tokens', budget);
core.setOutput('tokens', override);
return;
}
let label = 'complex';
@@ -1143,9 +1122,7 @@ jobs:
core.warning(
`Could not parse classifier output: ${error.message}`);
}
// Keep Claude's thinking budget below the action's effective
// max_tokens so the API has room for the final review text.
const budget = label === 'simple' ? '16000' : '24000';
const budget = label === 'simple' ? '16000' : '32000';
core.info(
`Complexity classified as: ${label} ` +
`-> MAX_THINKING_TOKENS=${budget}`
@@ -1163,30 +1140,13 @@ jobs:
CODEX_HOME: /tmp/codex-home
CODEX_MODEL: ${{ inputs.selected_model != '' && inputs.selected_model || inputs.default_model }}
run: |
if [ -z "${OPENAI_API_KEY}" ]; then
echo "OPENAI_API_KEY not configured, skipping Codex review"
printf 'complex\n' > codex-complexity-output.txt
exit 0
fi
mkdir -p "${CODEX_HOME}"
set +e
codex exec \
-m "${CODEX_MODEL}" \
-c model_reasoning_effort="medium" \
--dangerously-bypass-approvals-and-sandbox \
--output-last-message codex-complexity-output.txt \
- < /tmp/complexity_prompt.txt > codex-complexity.log 2>&1
exit_code=$?
set -e
if [ "${exit_code}" -ne 0 ]; then
echo "Codex complexity classification failed with exit code ${exit_code}"
echo "Defaulting to complex review budget"
if [ -s codex-complexity.log ]; then
echo "Last 100 lines of codex-complexity.log:"
tail -100 codex-complexity.log
fi
printf 'complex\n' > codex-complexity-output.txt
fi
- name: Pick thinking budget (Codex)
if: steps.request.outputs.skip != 'true' && inputs.provider == 'codex'
@@ -1336,6 +1296,8 @@ jobs:
CODEX_HOME: /tmp/codex-home
CODEX_MODEL: ${{ inputs.selected_model != '' && inputs.selected_model || inputs.default_model }}
CODEX_REASONING_EFFORT: ${{ steps.manual_budget_codex.outputs.effort }}
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
COMMAND_TYPE: ${{ steps.request.outputs.type }}
run: |
mkdir -p "${CODEX_HOME}"
@@ -1348,7 +1310,9 @@ jobs:
--output-last-message codex-review-output.md \
- < /tmp/prompt.txt > codex-review-execution.log 2>&1
else
codex exec \
codex exec review \
--base "${BASE_SHA}" \
--title "${PR_TITLE}" \
-m "${CODEX_MODEL}" \
-c model_reasoning_effort="${CODEX_REASONING_EFFORT}" \
--dangerously-bypass-approvals-and-sandbox \
+1 -1
View File
@@ -34,7 +34,7 @@ on:
- claude-sonnet-4-6
default: claude-opus-4-6
thinking_budget:
description: Override MAX_THINKING_TOKENS (blank = auto-classify, capped at 24000)
description: Override MAX_THINKING_TOKENS (blank = auto-classify)
required: false
type: string
default: ""
+15
View File
@@ -41,6 +41,9 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"db/blob/blob_source.cc",
"db/blob/blob_write_batch_transformer.cc",
"db/blob/prefetch_buffer_collection.cc",
"db/blog/blog_format.cc",
"db/blog/blog_reader.cc",
"db/blog/blog_writer.cc",
"db/builder.cc",
"db/c.cc",
"db/coalescing_iterator.cc",
@@ -4592,6 +4595,18 @@ cpp_unittest_wrapper(name="block_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="blog_format_test",
srcs=["db/blog/blog_format_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="blog_writer_test",
srcs=["db/blog/blog_writer_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="bloom_test",
srcs=["util/bloom_test.cc"],
deps=[":rocksdb_test_lib"],
+5
View File
@@ -743,6 +743,9 @@ set(SOURCES
db/blob/blob_source.cc
db/blob/blob_write_batch_transformer.cc
db/blob/prefetch_buffer_collection.cc
db/blog/blog_format.cc
db/blog/blog_reader.cc
db/blog/blog_writer.cc
db/builder.cc
db/c.cc
db/coalescing_iterator.cc
@@ -1431,6 +1434,8 @@ if(WITH_TESTS)
db/blob/db_blob_compaction_test.cc
db/blob/db_blob_corruption_test.cc
db/blob/db_blob_index_test.cc
db/blog/blog_format_test.cc
db/blog/blog_writer_test.cc
db/column_family_test.cc
db/compact_files_test.cc
db/compaction/clipping_iterator_test.cc
+6 -4
View File
@@ -384,10 +384,6 @@ endif
# TSAN doesn't work well with jemalloc. If we're compiling with TSAN, we should use regular malloc.
ifdef COMPILE_WITH_TSAN
DISABLE_JEMALLOC=1
# Use a suppressions file instead of the process-wide TSAN default
# suppressions hook, which belongs to the final application.
TSAN_OPTIONS?=suppressions=$(CURDIR)/tools/tsan_suppressions.txt
export TSAN_OPTIONS
EXEC_LDFLAGS += -fsanitize=thread
PLATFORM_CCFLAGS += -fsanitize=thread -fPIC -DFOLLY_SANITIZE_THREAD
PLATFORM_CXXFLAGS += -fsanitize=thread -fPIC -DFOLLY_SANITIZE_THREAD
@@ -1540,6 +1536,12 @@ db_logical_block_size_cache_test: $(OBJ_DIR)/db/db_logical_block_size_cache_test
db_blob_index_test: $(OBJ_DIR)/db/blob/db_blob_index_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
blog_format_test: $(OBJ_DIR)/db/blog/blog_format_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
blog_writer_test: $(OBJ_DIR)/db/blog/blog_writer_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_block_cache_test: $(OBJ_DIR)/db/db_block_cache_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
+318
View File
@@ -0,0 +1,318 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Blog File Format Specification - RocksDB</title>
<style>
body { font-family: 'Segoe UI', Arial, sans-serif; max-width: 960px; margin: 40px auto; padding: 0 20px; color: #222; line-height: 1.6; }
h1 { border-bottom: 3px solid #333; padding-bottom: 8px; }
h2 { border-bottom: 2px solid #999; padding-bottom: 4px; margin-top: 40px; }
h3 { margin-top: 30px; }
pre { background: #f4f4f4; border: 1px solid #ddd; border-radius: 4px; padding: 12px 16px; overflow-x: auto; font-size: 13px; line-height: 1.5; }
code { background: #f0f0f0; padding: 1px 5px; border-radius: 3px; font-size: 13px; }
table { border-collapse: collapse; width: 100%; margin: 16px 0; }
th, td { border: 1px solid #bbb; padding: 8px 12px; text-align: left; vertical-align: top; }
th { background: #e8e8e8; font-weight: 600; }
.diagram { background: #fafafa; border: 1px solid #ccc; border-radius: 4px; padding: 16px; margin: 16px 0; font-family: 'Courier New', monospace; font-size: 13px; white-space: pre; overflow-x: auto; line-height: 1.4; }
.note { background: #fff8e1; border-left: 4px solid #ffc107; padding: 10px 14px; margin: 14px 0; }
.decision { background: #e8f5e9; border-left: 4px solid #4caf50; padding: 10px 14px; margin: 14px 0; }
</style>
</head>
<body>
<h1>"Blog" File Format Specification</h1>
<p><em>A unified file format for WAL, Blob, and related data in RocksDB</em></p>
<p>Status: Implementation in progress &nbsp;|&nbsp; Format name: "blog" (portmanteau of "blob" + "log")</p>
<!-- ======================================================================= -->
<h2>Vocabulary</h2>
<table>
<tr><th>Term</th><th>Scope</th><th>Definition</th></tr>
<tr><td><strong>Record</strong></td><td>Blog file</td><td>A top-level unit in the blog file body: blob record, WriteBatch record, preamble-start record, footer record.</td></tr>
<tr><td><strong>Operation (op)</strong></td><td>WriteBatch</td><td>An entry within a WriteBatch: Put op, Delete op, Merge op, etc.</td></tr>
<tr><td><strong>Compact format</strong></td><td>Record</td><td>A record with varint length &le; 3 bytes. Omits the type byte (type from header).</td></tr>
<tr><td><strong>Full format</strong></td><td>Record</td><td>A record with varint length &gt; 3 bytes. Includes explicit type, pre-payload compression type, and prefix checksum.</td></tr>
</table>
<!-- ======================================================================= -->
<h2 id="s1">File-Level Structure</h2>
<p>A blog file consists of three regions: a <strong>fixed header</strong> with a <strong>property section</strong>, a <strong>body</strong> of records, and an optional <strong>footer</strong> composed of footer records.</p>
<div class="diagram">
+---------------------------------------------------------------------+
| FILE HEADER (40 bytes fixed) |
| 96-bit magic, schema version, checksum type, compact record type, |
| flags, escape sequence, reserved, property section size, checksum |
+---------------------------------------------------------------------+
| HEADER PROPERTY SECTION |
| Named key-value pairs + 5-byte trailer (compression + checksum) |
| Followed by arbitrarily long padding (0x00 or 0xFF) |
+---------------------------------------------------------------------+
| FILE BODY |
| Sequence of records, each starting with the escape sequence. |
| Record types: blob, WriteBatch, preamble-start, footer records. |
+---------------------------------------------------------------------+
| FILE FOOTER (optional) |
| Footer index records (0xFD), footer properties record (0xFE), |
| footer locator record (0xFF) -- must be last. |
+---------------------------------------------------------------------+</div>
<h3>Fixed Header (40 bytes)</h3>
<table>
<tr><th>Offset</th><th>Size</th><th>Field</th></tr>
<tr><td>0</td><td>12</td><td>File magic: <code>kBlogFileMagic</code> -- opaque 12 bytes ("Blog" ASCII + XXH3_64bits("Blog"))</td></tr>
<tr><td>12</td><td>1</td><td>Schema version (currently 0). Newer versions are rejected by older readers.</td></tr>
<tr><td>13</td><td>1</td><td>Checksum type (<code>ChecksumType</code> enum: 0=none, 1=CRC32c, 4=XXH3). Configured via <code>DBOptions::blog_checksum</code>.</td></tr>
<tr><td>14</td><td>1</td><td>Compact record type (<code>BlogRecordType</code> -- type for all compact-format records)</td></tr>
<tr><td>15</td><td>1</td><td>Flags: bit 0 = <code>kBlogFileRecycled</code> (file may contain trailing stale data)</td></tr>
<tr><td>16</td><td>10</td><td>Escape sequence: bytes [0-5] random (byte[0] not 0x00/0xFF), bytes [6-9] = lower32(XXH3_64bits_withSeed(bytes[0-5], kBlogEscapeSeqSeed)). First 4 bytes also serve as incarnation ID (little-endian via DecodeFixed32).</td></tr>
<tr><td>26</td><td>6</td><td>Reserved (zero)</td></tr>
<tr><td>32</td><td>4</td><td>Property section size (uint32 LE) -- includes 5-byte trailer</td></tr>
<tr><td>36</td><td>4</td><td>Fixed header checksum -- covers bytes [0, 36)</td></tr>
</table>
<h3>Header Property Section</h3>
<p>Immediately follows the fixed header. Contains named key-value pairs encoded as:</p>
<pre>[name_len: varint32] [name: name_len bytes] [value_len: varint32] [value: value_len bytes] ...</pre>
<p>Followed by a 5-byte trailer: <code>[compression_type: 1B][checksum: 4B]</code> (same as block-based table). The checksum covers the property data + compression_type, with a context checksum modifier using the incarnation ID at offset <code>kBlogFileFixedHeaderSize</code>.</p>
<p>After the property section, arbitrarily long padding (0x00 or 0xFF, differing from the last meaningful byte) aligns the first record's escape sequence. This supports stronger alignment requirements (flush/sync boundaries) or recycling files of all zeros.</p>
<h3>Property Name Convention</h3>
<table>
<tr><th>First letter</th><th>Meaning</th></tr>
<tr><td>Uppercase (e.g. <code>"CompressionCompatibilityName"</code>)</td><td><strong>Required</strong> -- reader must recognize or reject the file</td></tr>
<tr><td>Lowercase (e.g. <code>"role"</code>)</td><td><strong>Ignorable</strong> -- reader may skip unrecognized properties</td></tr>
</table>
<h3>Known Header Properties</h3>
<table>
<tr><th>Name</th><th>Required?</th><th>Description</th></tr>
<tr><td><code>"CompressionCompatibilityName"</code></td><td>Required (if any compression)</td><td>Compression manager compatibility name (e.g. "builtin_v2")</td></tr>
<tr><td><code>"WriteBatchStreamingCompressionType"</code></td><td>Required (if streaming active)</td><td>2-digit hex of CompressionType for streaming WriteBatch compression (e.g. "07" for ZSTD). Presence means WriteBatch records with <code>kStreamingCompressionSentinel</code> (0x7F) trailer are part of a cross-record streaming context.</td></tr>
<tr><td><code>"TimestampSize"</code></td><td>Required (if &gt;0)</td><td>Size of user timestamps</td></tr>
<tr><td><code>"PredecessorWalInfo"</code></td><td>Required (if tracking)</td><td>Predecessor WAL metadata for verification</td></tr>
<tr><td><code>"role"</code></td><td>Ignorable</td><td>File purpose hint: "wal", "blob", "rlog"</td></tr>
<tr><td><code>"compressionSettings"</code></td><td>Ignorable</td><td>Human-readable compression settings for debugging</td></tr>
<tr><td><code>"creationTime"</code></td><td>Ignorable</td><td>Unix timestamp of file creation</td></tr>
<tr><td><code>"creator"</code></td><td>Ignorable</td><td>Host/process identifier for debugging</td></tr>
</table>
<!-- ======================================================================= -->
<h2 id="s2">Escape Sequences</h2>
<div class="decision">
<strong>Decision:</strong> 10-byte escape sequence: 6 random bytes + 4 derived bytes. First byte not 0x00 nor 0xFF (distinguishable from padding). Bytes [6-9] = lower32(XXH3_64bits_withSeed(bytes[0-5], kBlogEscapeSeqSeed)), enabling heuristic detection without knowing the header.
</div>
<p>Escape sequences must be <strong>32-bit aligned</strong> in the file. The first 4 bytes (little-endian) serve as the <strong>incarnation ID</strong> for context checksums.</p>
<h3>Primary Roles</h3>
<ul>
<li><strong>WAL recycling detection:</strong> When recovery encounters undecodable data, scan for our escape sequence. If not found, the data is stale/recycled (different random sequence). The <code>kBlogFileRecycled</code> flag in the header indicates recycling is expected.</li>
<li><strong>Non-linear recovery:</strong> After corruption, scan forward for the next escape sequence to resume recovery (<code>kSkipAnyCorruptedRecords</code>).</li>
<li><strong>Record boundary discovery:</strong> Find record boundaries from any file position.</li>
</ul>
<!-- ======================================================================= -->
<h2 id="s3">Record Types and Formats</h2>
<h3>Record Type Values</h3>
<table>
<tr><th>Value</th><th>Name</th><th>Usage</th></tr>
<tr><td colspan="3"><em>Body record types (0x01-0x7F)</em></td></tr>
<tr><td>0x01</td><td><code>kBlogBlobRecord</code></td><td>Blob data (typically a separated value)</td></tr>
<tr><td>0x02</td><td><code>kBlogPreambleStartRecord</code></td><td>Preamble marker (stub)</td></tr>
<tr><td>0x03</td><td><code>kBlogWriteBatchRecord</code></td><td>Serialized WriteBatch</td></tr>
<tr><td>0x04-0x7F</td><td></td><td><em>Reserved for future body record types</em></td></tr>
<tr><td colspan="3"><em>Footer record types (0xF0-0xFF, in expected order of occurrence with gaps)</em></td></tr>
<tr><td>0xF0</td><td><code>kBlogFooterIndexRecord</code></td><td>Large footer metadata (sparse index)</td></tr>
<tr><td>0xF1-0xF3</td><td></td><td><em>Reserved for future footer data records</em></td></tr>
<tr><td>0xF4-0xF7</td><td></td><td><em>Reserved for future footer data records</em></td></tr>
<tr><td>0xF8</td><td><code>kBlogFooterPropertiesRecord</code></td><td>Small footer metadata (named properties)</td></tr>
<tr><td>0xF9-0xFB</td><td></td><td><em>Reserved</em></td></tr>
<tr><td>0xFC</td><td><code>kBlogFooterFileChecksumInfo</code></td><td>Full file checksum state dump. Allows inferring the file checksum from just reading the tail. Placeholder for future use.</td></tr>
<tr><td>0xFD-0xFE</td><td></td><td><em>Reserved</em></td></tr>
<tr><td>0xFF</td><td><code>kBlogFooterLocatorRecord</code></td><td>Footer locator with relative offsets. Must be the last record in the file.</td></tr>
</table>
<h3>Length Semantics</h3>
<ul>
<li><code>length = 0</code>: No payload and no 5-byte trailer. Record is escape_seq + prefix fields + padding.</li>
<li><code>length &gt; 0</code>: Payload of <code>length</code> bytes followed by 5-byte trailer.</li>
<li><code>length = 2^63 - 1</code>: Unspecified size. Reader scans forward for next escape sequence.</li>
</ul>
<h3>Compact Record Format (varint length &le; 2 bytes, default record type)</h3>
<div class="diagram">
[escape_seq: 10B] [varint length: 1-2B]
[payload: length bytes]
[compression_type: 1B] [checksum: 4B]
[padding: 0+ B]</div>
<p>No type byte -- type comes from the header's <code>compact_record_type</code> field. Used for payloads &lt; 16 KiB.</p>
<h3>Full Record Format (varint length &gt; 2 bytes, or forced)</h3>
<div class="diagram">
[escape_seq: 10B] [varint length: 3+B] [type: 1B] [compression_type: 1B]
[prefix_checksum: 4B]
[payload: length bytes] (if length &gt; 0)
[compression_type: 1B] [checksum: 4B] (if length &gt; 0)
[padding: 0+ B]</div>
<p>The <code>prefix_checksum</code> covers (varint + type + compression_type), with a context checksum modifier for defense-in-depth. Validates the length before attempting a potentially large read. The pre-payload <code>compression_type</code> enables streaming decompression without seeking to the trailer.</p>
<p>Records that don't match the compact_record_type (e.g. footer records, preamble-start) always use full format by encoding the varint with &ge; 3 bytes (irregular encoding).</p>
<p>All checksums in blog files (fixed header, property section trailer, record prefix, record trailer) use context checksum modifiers incorporating the incarnation ID, providing defense-in-depth for misplaced data detection even with <code>kNoChecksum</code>.</p>
<!-- ======================================================================= -->
<h2 id="s4">Compression</h2>
<h3>Per-Record Compression</h3>
<p>Each record's trailer <code>compression_type</code> byte indicates the compression algorithm used for that record's payload. The actual type comes from <code>Compressor::CompressBlock</code> output, not the configured type. This is the same approach used by SST block-based table builders.</p>
<h3>Streaming Compression (WriteBatch records only)</h3>
<p>When the header property <code>WriteBatchStreamingCompressionType</code> is set, WriteBatch record payloads may use streaming compression across records. Such records use <code>kStreamingCompressionSentinel</code> (0x7F) as their trailer's compression_type byte, indicating "compressed using the streaming algorithm declared in the header."</p>
<p>A record with <code>kNoCompression</code> (0x00) in the trailer remains individually uncompressed even when streaming is active, enabling dynamic on/off switching based on compression effectiveness.</p>
<p>Non-WriteBatch records (blob records, footer records) are always outside the streaming context and use per-record compression, keeping blob data randomly readable.</p>
<div class="note">
Currently only ZSTD is supported for streaming compression. The <code>WriteBatchStreamingCompressionType</code> property is required (uppercase first letter), so older readers that don't understand it will reject the file rather than misinterpret streaming-compressed payloads.
</div>
<!-- ======================================================================= -->
<h2 id="s5">Padding Scheme</h2>
<p>Padding fills the gap between a record's trailer (or the header property section) and the next escape sequence. The padding byte value is chosen to <strong>differ from the last meaningful byte</strong>, enabling unambiguous scanning.</p>
<table>
<tr><th>Last meaningful byte</th><th>Pad with</th><th>Mandatory?</th></tr>
<tr><td><code>0x00</code></td><td><code>0xFF</code></td><td>Yes -- at least 1 byte required</td></tr>
<tr><td><code>0xFF</code></td><td><code>0x00</code></td><td>Yes -- at least 1 byte required</td></tr>
<tr><td>Anything else</td><td><code>0x00</code> (writer's choice)</td><td>Only if needed for alignment</td></tr>
</table>
<p>Padding may be arbitrarily long for stronger alignment requirements (e.g. flush/sync boundaries) or for recycling a file of all zeros. The reader skips padding by scanning forward over 0x00/0xFF bytes in 4-byte-aligned chunks until finding a byte that is neither, which is the first byte of the next escape sequence (guaranteed to be neither 0x00 nor 0xFF).</p>
<!-- ======================================================================= -->
<h2 id="s6">Context Checksums</h2>
<div class="decision">
<strong>Decision (adopted from SST format_version=6):</strong> Each record's checksum incorporates a 32-bit <strong>incarnation ID</strong> (first 4 bytes of the escape sequence). Uses <code>ChecksumModifierForContext(incarnation_id, record_offset)</code> -- the same mechanism as block-based SST files.
</div>
<p>Context checksums provide defense-in-depth for recycling detection and general data integrity verification with zero per-record overhead.</p>
<p>The checksum type is configured from the CF's <code>BlockBasedTableOptions::checksum</code> setting (for blob files) or the default CF's setting (for WAL files). This is a reasonable but hopefully temporary way of configuring blog file internal checksums; a dedicated option may be added later.</p>
<!-- ======================================================================= -->
<h2 id="s7">Footer</h2>
<p>The footer is composed of <strong>regular records</strong> using the full record format, maintaining simple forward parsability. A file is cleanly sealed when a valid footer locator record is the last record. Footer records appear in the order listed below.</p>
<ol>
<li><strong>Footer index records</strong> (0xF0): Large metadata like sparse indexes for range queries. Zero or more per file.</li>
<li><strong>Footer properties record</strong> (0xF8): Named properties (same encoding as header properties, but the full record format's own trailer provides integrity). At most one per file.</li>
<li><strong>Footer file checksum info</strong> (0xFC): Placeholder for future use. Would contain a dump of the full file checksum state, allowing the file checksum to be inferred from just reading the tail of the file.</li>
<li><strong>Footer locator record</strong> (0xFF): Must be the <strong>last record</strong>. Contains relative offsets (in 4-byte units) to other footer records, sequential relative to each other in reverse order.</li>
</ol>
<h3>Footer Locator Payload</h3>
<pre>[record_type: 1B] [relative_offset_in_4B_units: uint32 LE] ...</pre>
<p>First entry: distance from locator's escape_seq backward to the preceding footer record. Subsequent: from the previous entry backward to the next.</p>
<h3>Known Footer Properties</h3>
<table>
<tr><th>Name</th><th>Description</th></tr>
<tr><td><code>"blobCount"</code></td><td>Number of blob records</td></tr>
<tr><td><code>"totalBlobBytes"</code></td><td>Total uncompressed blob payload bytes</td></tr>
<tr><td><code>"totalBlobBytesCompressed"</code></td><td>Total compressed blob payload bytes on disk</td></tr>
<tr><td><code>"totalWriteBatchBytes"</code></td><td>Total uncompressed WriteBatch payload bytes</td></tr>
<tr><td><code>"totalWriteBatchBytesCompressed"</code></td><td>Total compressed WriteBatch payload bytes on disk</td></tr>
<tr><td><code>"minSequence"</code></td><td>Minimum sequence number</td></tr>
<tr><td><code>"maxSequence"</code></td><td>Maximum sequence number</td></tr>
</table>
<!-- ======================================================================= -->
<h2 id="s8">WAL Recovery Modes</h2>
<p>The escape sequence is the <strong>primary mechanism</strong> for detecting WAL recycling and for non-linear data recovery. The <code>kBlogFileRecycled</code> flag in the header indicates the file may contain trailing stale data from a previous incarnation.</p>
<table>
<tr>
<th style="width:22%">Mode</th>
<th style="width:48%">Behavior on Undecodable Data</th>
<th style="width:30%">Escape Sequence Role</th>
</tr>
<tr>
<td><code>kAbsoluteConsistency</code></td>
<td>Error on any failure, including incomplete trailing records. If recycled and no escape sequence found in trailing data, accept as clean end.</td>
<td>Distinguishes stale recycled data from corruption.</td>
</tr>
<tr>
<td><code>kTolerateCorruptedTailRecords</code></td>
<td>Incomplete trailing record (Incomplete status): tolerate, stop at last complete record. Corruption: scan for escape sequence. Not found = clean end (recycled or preallocated). Found with valid record beyond = mid-file corruption (error).</td>
<td>Distinguishes tail damage from mid-file corruption.</td>
</tr>
<tr>
<td><code>kPointInTimeRecovery</code> (default)</td>
<td>Stop at first inconsistency. Incomplete trailing records tolerated. Recycled trailing data accepted.</td>
<td>Diagnostic improvement.</td>
</tr>
<tr>
<td><code>kSkipAnyCorruptedRecords</code></td>
<td>On corruption, scan forward for next escape sequence. If found, try to decode and verify; resume if valid. If not found, stop.</td>
<td>Essential for record discovery after corruption.</td>
</tr>
</table>
<!-- ======================================================================= -->
<h2 id="s9">WriteBatch Blob Preamble</h2>
<p>A <strong>blob preamble</strong> is a sequence of blob records preceded by a preamble-start record (type 0x02, full format, length=0) and followed by the associated WriteBatch record. The blob records contain separated values referenced by <code>file_number = 0</code> blob references in the WriteBatch.</p>
<p>This functionality is stubbed in the current implementation: the preamble-start record type is defined and can be written/read, but the full preamble flow (in-memory serialization, blob reference encoding) is deferred.</p>
<!-- ======================================================================= -->
<h2 id="s10">Replication Log Column Family (ShallowL0Store)</h2>
<p>Deferred. The blog format supports a "rlog" role hint for replication log storage, with sparse indexes in footer records and in-memory index maintenance. See design notes in the original proposal.</p>
<!-- ======================================================================= -->
<h2 id="s11">Implementation Status</h2>
<h3>Resolved (from original Open Questions)</h3>
<table>
<tr><th>Question</th><th>Resolution</th></tr>
<tr><td>Escape sequence size</td><td>10 bytes: 6 random + 4 derived. First byte not 0x00/0xFF.</td></tr>
<tr><td>Header/footer encoding</td><td>40-byte fixed header + named property section with 5-byte trailer. Footer uses regular full-format records: index (0xF0), properties (0xF8), file checksum info (0xFC, placeholder), locator (0xFF). Numbered in expected occurrence order with gaps for future types.</td></tr>
<tr><td>Preamble-start record format</td><td>Full format, length=0, type 0x02. Minimal -- just the type tag.</td></tr>
<tr><td>WriteBatch record framing</td><td>Same compact/full format as blob records, type 0x03.</td></tr>
<tr><td>Compact vs full format</td><td>Compact: varint &le; 2B (payload &lt; 16 KiB), matches compact_record_type, no type byte. Full: varint &gt; 2B (possibly irregular), includes type + pre-payload compression_type + prefix_checksum with context modifier.</td></tr>
<tr><td>Checksum context</td><td>All checksums (fixed header, property section, record prefix, record trailer) use context checksum modifiers with the incarnation ID, ensuring nonzero stored values even with <code>kNoChecksum</code>.</td></tr>
<tr><td>Schema version</td><td>Starts at 0. Newer versions are rejected by older readers (returns <code>NotSupported</code>). Checksum is verified before schema version is checked.</td></tr>
<tr><td>Streaming compression sentinel</td><td><code>kStreamingCompressionSentinel = 0x7F</code> defined in <code>CompressionType</code> enum (compression_type.h). Used in blog record trailers for streaming-compressed WriteBatch payloads.</td></tr>
</table>
<h3>DB Options</h3>
<ul>
<li><code>use_blog_format_for_wals</code> (bool, default false): New WAL files use blog format.</li>
<li><code>use_blog_format_for_blobs</code> (bool, default false): New blob files use blog format.</li>
<li><code>blog_checksum</code> (<code>ChecksumType</code>, default <code>kXXH3</code>): Checksum algorithm for blog format files. <code>ChecksumType</code> enum moved to <code>include/rocksdb/checksum_type.h</code> (from <code>table.h</code>) for use by both table and DB options.</li>
</ul>
<p>All are immutable <code>DBOptions</code>. Existing legacy WAL and blob files are always readable regardless of these settings.</p>
<h3>Compression Manager Integration</h3>
<ul>
<li><strong>Blog blob files</strong> use the CF's configured compression manager (via <code>MutableCFOptions::compression_manager</code>). Legacy blob files continue to use the builtin manager for backward compatibility.</li>
<li><strong>Blog WAL files</strong> use per-record compression via <code>Compressor::CompressBlock</code>. The actual <code>CompressionType</code> from <code>CompressBlock</code> output is stored in the record trailer.</li>
<li>Streaming compression infrastructure exists (<code>WriteBatchStreamingCompressionType</code> property, <code>kStreamingCompressionSentinel</code> 0x7F trailer value) but is not yet enabled by default.</li>
</ul>
</body>
</html>
+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 @@ 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
+169 -44
View File
@@ -14,6 +14,8 @@
#include "db/blob/blob_log_format.h"
#include "db/blob/blob_log_writer.h"
#include "db/blob/blob_source.h"
#include "db/blog/blog_format.h"
#include "db/blog/blog_writer.h"
#include "db/event_helpers.h"
#include "db/version_set.h"
#include "file/filename.h"
@@ -67,10 +69,13 @@ BlobFileBuilder::BlobFileBuilder(
min_blob_size_(mutable_cf_options->min_blob_size),
blob_file_size_(mutable_cf_options->blob_file_size),
blob_compression_type_(mutable_cf_options->blob_compression_type),
// TODO with schema change: support custom compression manager and options
// such as max_compressed_bytes_per_kb
// NOTE: returns nullptr for kNoCompression
blob_compressor_(GetBuiltinV2CompressionManager()->GetCompressor(
// Blog format uses the CF's compression manager; legacy format must
// use the builtin for compatibility.
blob_compression_manager_(immutable_options->use_blog_format_for_blobs &&
mutable_cf_options->compression_manager
? mutable_cf_options->compression_manager
: GetBuiltinV2CompressionManager()),
blob_compressor_(blob_compression_manager_->GetCompressor(
mutable_cf_options->blob_compression_opts, blob_compression_type_)),
blob_compressor_wa_(blob_compressor_
? blob_compressor_->ObtainWorkingArea()
@@ -123,8 +128,10 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
Slice blob = value;
GrowableBuffer compressed_blob;
{
const Status s = CompressBlobIfNeeded(&blob, &compressed_blob);
if (!blog_writer_) {
// Legacy format: compress before writing (compression type is implicit
// from the configured blob_compression_type_).
const Status s = LegacyCompressBlob(&blob, &compressed_blob);
if (!s.ok()) {
return s;
}
@@ -132,10 +139,13 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
uint64_t blob_file_number = 0;
uint64_t blob_offset = 0;
uint64_t blob_on_disk_size = 0;
CompressionType actual_compression_type = blob_compression_type_;
{
const Status s =
WriteBlobToFile(key, blob, &blob_file_number, &blob_offset);
WriteBlobToFile(key, blob, &blob_file_number, &blob_offset,
&blob_on_disk_size, &actual_compression_type);
if (!s.ok()) {
return s;
}
@@ -158,8 +168,8 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
}
}
BlobIndex::EncodeBlob(blob_index, blob_file_number, blob_offset, blob.size(),
blob_compression_type_);
BlobIndex::EncodeBlob(blob_index, blob_file_number, blob_offset,
blob_on_disk_size, actual_compression_type);
return Status::OK();
}
@@ -172,7 +182,9 @@ Status BlobFileBuilder::Finish() {
return CloseBlobFile();
}
bool BlobFileBuilder::IsBlobFileOpen() const { return !!writer_; }
bool BlobFileBuilder::IsBlobFileOpen() const {
return !!writer_ || !!blog_writer_;
}
Status BlobFileBuilder::OpenBlobFileIfNeeded() {
if (IsBlobFileOpen()) {
@@ -231,37 +243,73 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
immutable_options_->file_checksum_gen_factory.get(),
tmp_set.Contains(FileType::kBlobFile), false));
constexpr bool do_flush = false;
blob_file_number_ = blob_file_number;
std::unique_ptr<BlobLogWriter> blob_log_writer(new BlobLogWriter(
std::move(file_writer), immutable_options_->clock, statistics,
blob_file_number, immutable_options_->use_fsync, do_flush));
if (immutable_options_->use_blog_format_for_blobs) {
// Blog format path
BlogFileHeader blog_header;
blog_header.checksum_type = immutable_options_->blog_checksum;
blog_header.compact_record_type = kBlogBlobRecord;
blog_header.GenerateFromUniqueId(db_id_, db_session_id_, blob_file_number);
blog_header.SetProperty(kBlogPropRole, "blob");
if (!immutable_options_->db_host_id.empty()) {
blog_header.SetProperty(kBlogPropDbHostId,
immutable_options_->db_host_id);
}
blog_header.SetUint32Property(kBlogPropColumnFamilyId, column_family_id_);
if (!column_family_name_.empty()) {
blog_header.SetProperty(kBlogPropColumnFamilyName, column_family_name_);
}
if (blob_compressor_) {
blog_header.SetProperty(kBlogPropCompressionCompatibilityName,
blob_compression_manager_->CompatibilityName());
}
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range;
blog_writer_ =
std::make_unique<BlogFileWriter>(std::move(file_writer), blog_header);
BlobLogHeader header(column_family_id_, blob_compression_type_, has_ttl,
expiration_range);
{
Status s = blob_log_writer->WriteHeader(*write_options_, header);
Status s = blog_writer_->WriteHeader(*write_options_);
TEST_SYNC_POINT_CALLBACK(
"BlobFileBuilder::OpenBlobFileIfNeeded:WriteHeader", &s);
"BlobFileBuilder::OpenBlobFileIfNeeded:WriteBlogHeader", &s);
if (!s.ok()) {
return s;
}
}
} else {
// Legacy blob log format path
constexpr bool do_flush = false;
writer_ = std::move(blob_log_writer);
std::unique_ptr<BlobLogWriter> blob_log_writer(
new BlobLogWriter(std::move(file_writer), immutable_options_->clock,
statistics, immutable_options_->use_fsync, do_flush));
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range;
BlobLogHeader header(column_family_id_, blob_compression_type_, has_ttl,
expiration_range);
{
Status s = blob_log_writer->WriteHeader(*write_options_, header);
TEST_SYNC_POINT_CALLBACK(
"BlobFileBuilder::OpenBlobFileIfNeeded:WriteHeader", &s);
if (!s.ok()) {
return s;
}
}
writer_ = std::move(blob_log_writer);
}
assert(IsBlobFileOpen());
return Status::OK();
}
Status BlobFileBuilder::CompressBlobIfNeeded(
Status BlobFileBuilder::LegacyCompressBlob(
Slice* blob, GrowableBuffer* compressed_blob) const {
assert(blob);
assert(compressed_blob);
@@ -290,17 +338,48 @@ Status BlobFileBuilder::CompressBlobIfNeeded(
return Status::OK();
}
Status BlobFileBuilder::WriteBlobToFile(const Slice& key, const Slice& blob,
uint64_t* blob_file_number,
uint64_t* blob_offset) {
Status BlobFileBuilder::WriteBlobToFile(
const Slice& key, const Slice& blob, uint64_t* blob_file_number,
uint64_t* blob_offset, uint64_t* blob_on_disk_size,
CompressionType* actual_compression_type) {
assert(IsBlobFileOpen());
assert(blob_file_number);
assert(blob_offset);
assert(blob_on_disk_size);
assert(actual_compression_type);
uint64_t key_offset = 0;
Status s;
Status s =
writer_->AddRecord(*write_options_, key, blob, &key_offset, blob_offset);
if (blog_writer_) {
// Blog format: compress here using CompressBlock (like SST) so we
// capture the actual compression type used in the record trailer.
CompressionType actual_type = kNoCompression;
GrowableBuffer compressed;
if (blob_compressor_) {
StopWatch stop_watch(immutable_options_->clock, immutable_options_->stats,
BLOB_DB_COMPRESSION_MICROS);
size_t max_compressed_size = blob.size();
compressed.ResetForSize(max_compressed_size);
s = blob_compressor_->CompressBlock(blob, compressed.data(),
&compressed.MutableSize(),
&actual_type, &blob_compressor_wa_);
if (!s.ok()) {
return s;
}
}
Slice write_data =
(actual_type != kNoCompression) ? Slice(compressed) : blob;
s = blog_writer_->AddBlobRecord(*write_options_, write_data, actual_type,
blob_offset);
*blob_on_disk_size = write_data.size();
*actual_compression_type = actual_type;
} else {
uint64_t key_offset = 0;
s = writer_->AddRecord(*write_options_, key, blob, &key_offset,
blob_offset);
*blob_on_disk_size = blob.size(); // legacy: already compressed by caller
*actual_compression_type = blob_compression_type_;
}
TEST_SYNC_POINT_CALLBACK("BlobFileBuilder::WriteBlobToFile:AddRecord", &s);
@@ -308,10 +387,16 @@ Status BlobFileBuilder::WriteBlobToFile(const Slice& key, const Slice& blob,
return s;
}
*blob_file_number = writer_->get_log_number();
*blob_file_number = blob_file_number_;
++blob_count_;
blob_bytes_ += BlobLogRecord::kHeaderSize + key.size() + blob.size();
if (blog_writer_) {
// Blog format: blob_bytes_ tracks uncompressed payload size only
// (no legacy record header, no key).
blob_bytes_ += blob.size();
} else {
blob_bytes_ += BlobLogRecord::kHeaderSize + key.size() + blob.size();
}
return Status::OK();
}
@@ -319,22 +404,60 @@ Status BlobFileBuilder::WriteBlobToFile(const Slice& key, const Slice& blob,
Status BlobFileBuilder::CloseBlobFile() {
assert(IsBlobFileOpen());
BlobLogFooter footer;
footer.blob_count = blob_count_;
std::string checksum_method;
std::string checksum_value;
Status s;
Status s = writer_->AppendFooter(*write_options_, footer, &checksum_method,
&checksum_value);
if (blog_writer_) {
// Blog format: write footer properties and locator records.
BlogFileFooterProperties footer_props;
footer_props.SetBlobCount(blob_count_);
footer_props.SetTotalBlobBytes(blob_bytes_);
TEST_SYNC_POINT_CALLBACK("BlobFileBuilder::WriteBlobToFile:AppendFooter", &s);
uint64_t props_offset = blog_writer_->current_offset();
s = blog_writer_->AddFooterPropertiesRecord(*write_options_, footer_props);
if (s.ok()) {
BlogFileFooterLocator locator;
uint64_t locator_offset = blog_writer_->current_offset();
locator.entries.push_back(
{kBlogFooterPropertiesRecord,
static_cast<uint32_t>((locator_offset - props_offset) / 4)});
s = blog_writer_->AddFooterLocatorRecord(*write_options_, locator);
}
if (s.ok()) {
s = blog_writer_->Sync(*write_options_);
}
if (s.ok()) {
s = blog_writer_->Close(*write_options_);
}
if (s.ok()) {
std::string method = blog_writer_->file()->GetFileChecksumFuncName();
if (method != kUnknownFileChecksumFuncName) {
checksum_method = std::move(method);
}
std::string value = blog_writer_->file()->GetFileChecksum();
if (value != kUnknownFileChecksum) {
checksum_value = std::move(value);
}
}
} else {
BlobLogFooter footer;
footer.blob_count = blob_count_;
s = writer_->LegacyAppendFooterAndClose(*write_options_, footer,
&checksum_method, &checksum_value);
}
TEST_SYNC_POINT_CALLBACK(
"BlobFileBuilder::WriteBlobToFile:LegacyAppendFooterAndClose", &s);
if (!s.ok()) {
return s;
}
const uint64_t blob_file_number = writer_->get_log_number();
const uint64_t blob_file_number = blob_file_number_;
if (blob_callback_) {
s = blob_callback_->OnBlobFileCompleted(
@@ -356,6 +479,7 @@ Status BlobFileBuilder::CloseBlobFile() {
blob_count_, blob_bytes_);
writer_.reset();
blog_writer_.reset();
blob_count_ = 0;
blob_bytes_ = 0;
@@ -365,7 +489,8 @@ Status BlobFileBuilder::CloseBlobFile() {
Status BlobFileBuilder::CloseBlobFileIfNeeded() {
assert(IsBlobFileOpen());
const WritableFileWriter* const file_writer = writer_->file();
const WritableFileWriter* const file_writer =
blog_writer_ ? blog_writer_->file() : writer_->file();
assert(file_writer);
if (file_writer->GetFileSize() < blob_file_size_) {
@@ -384,13 +509,13 @@ void BlobFileBuilder::Abandon(const Status& s) {
// Blob files. So we can ignore the below error.
blob_callback_
->OnBlobFileCompleted(blob_file_paths_->back(), column_family_name_,
job_id_, writer_->get_log_number(),
creation_reason_, s, "", "", blob_count_,
blob_bytes_)
job_id_, blob_file_number_, creation_reason_, s,
"", "", blob_count_, blob_bytes_)
.PermitUncheckedError();
}
writer_.reset();
blog_writer_.reset();
blob_count_ = 0;
blob_bytes_ = 0;
}
+9 -3
View File
@@ -34,6 +34,8 @@ class BlobLogWriter;
class IOTracer;
class BlobFileCompletionCallback;
class BlogFileWriter;
class BlobFileBuilder {
public:
BlobFileBuilder(VersionSet* versions, FileSystem* fs,
@@ -78,10 +80,11 @@ class BlobFileBuilder {
private:
bool IsBlobFileOpen() const;
Status OpenBlobFileIfNeeded();
Status CompressBlobIfNeeded(Slice* blob,
GrowableBuffer* compressed_blob) const;
Status LegacyCompressBlob(Slice* blob, GrowableBuffer* compressed_blob) const;
Status WriteBlobToFile(const Slice& key, const Slice& blob,
uint64_t* blob_file_number, uint64_t* blob_offset);
uint64_t* blob_file_number, uint64_t* blob_offset,
uint64_t* blob_on_disk_size,
CompressionType* actual_compression_type);
Status CloseBlobFile();
Status CloseBlobFileIfNeeded();
@@ -94,6 +97,7 @@ class BlobFileBuilder {
uint64_t min_blob_size_;
uint64_t blob_file_size_;
CompressionType blob_compression_type_;
std::shared_ptr<CompressionManager> blob_compression_manager_;
std::unique_ptr<Compressor> blob_compressor_;
mutable Compressor::ManagedWorkingArea blob_compressor_wa_;
PrepopulateBlobCache prepopulate_blob_cache_;
@@ -111,6 +115,8 @@ class BlobFileBuilder {
std::vector<std::string>* blob_file_paths_;
std::vector<BlobFileAddition>* blob_file_additions_;
std::unique_ptr<BlobLogWriter> writer_;
std::unique_ptr<BlogFileWriter> blog_writer_;
uint64_t blob_file_number_ = 0;
uint64_t blob_count_;
uint64_t blob_bytes_;
};
+1 -1
View File
@@ -598,7 +598,7 @@ INSTANTIATE_TEST_CASE_P(
"BlobFileBuilder::OpenBlobFileIfNeeded:NewWritableFile",
"BlobFileBuilder::OpenBlobFileIfNeeded:WriteHeader",
"BlobFileBuilder::WriteBlobToFile:AddRecord",
"BlobFileBuilder::WriteBlobToFile:AppendFooter"}));
"BlobFileBuilder::WriteBlobToFile:LegacyAppendFooterAndClose"}));
TEST_P(BlobFileBuilderIOErrorTest, IOError) {
// Simulate an I/O error during the specified step of Add()
+4 -6
View File
@@ -48,8 +48,7 @@ void WriteBlobFile(uint32_t column_family_id,
constexpr bool do_flush = false;
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
statistics, blob_file_number, use_fsync,
do_flush);
statistics, use_fsync, do_flush);
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range;
@@ -77,8 +76,8 @@ void WriteBlobFile(uint32_t column_family_id,
std::string checksum_method;
std::string checksum_value;
ASSERT_OK(blob_log_writer.AppendFooter(WriteOptions(), footer,
&checksum_method, &checksum_value));
ASSERT_OK(blob_log_writer.LegacyAppendFooterAndClose(
WriteOptions(), footer, &checksum_method, &checksum_value));
}
} // anonymous namespace
@@ -222,8 +221,7 @@ TEST_F(BlobFileCacheTest, RefreshBlobFileReaderPrefersLargestObservedFileSize) {
constexpr bool use_fsync = false;
constexpr bool do_flush = false;
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
statistics, blob_file_number, use_fsync,
do_flush);
statistics, use_fsync, do_flush);
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range;
+8 -14
View File
@@ -214,10 +214,10 @@ Status BlobFilePartitionManager::OpenNewBlobFile(Partition* partition,
// This only drains WritableFileWriter's buffered bytes so readers can see
// each appended record promptly. Durability still comes from SyncAllOpenFiles
// or AppendFooter(), both of which call Sync().
// or LegacyAppendFooterAndClose(), both of which call Sync().
constexpr bool kDoFlushEachRecord = true;
auto blob_log_writer = std::make_unique<BlobLogWriter>(
std::move(file_writer), clock_, statistics_, blob_file_number, use_fsync_,
std::move(file_writer), clock_, statistics_, use_fsync_,
kDoFlushEachRecord);
constexpr bool has_ttl = false;
@@ -300,8 +300,8 @@ Status BlobFilePartitionManager::FinalizeBlobFile(
std::string checksum_method;
std::string checksum_value;
Status s = writer->AppendFooter(write_options, footer, &checksum_method,
&checksum_value);
Status s = writer->LegacyAppendFooterAndClose(
write_options, footer, &checksum_method, &checksum_value);
if (!s.ok()) {
return s;
}
@@ -786,19 +786,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,
+179 -64
View File
@@ -5,11 +5,14 @@
#include "db/blob/blob_file_reader.h"
#include <algorithm>
#include <cassert>
#include <cstring>
#include <string>
#include "db/blob/blob_contents.h"
#include "db/blob/blob_log_format.h"
#include "db/blog/blog_format.h"
#include "file/file_prefetch_buffer.h"
#include "file/filename.h"
#include "monitoring/statistics_impl.h"
@@ -51,37 +54,30 @@ Status BlobFileReader::Create(
Statistics* const statistics = immutable_options.stats;
CompressionType compression_type = kNoCompression;
// Construct with defaults; ReadHeader will detect blog vs legacy format
// and populate the appropriate fields.
blob_file_reader->reset(
new BlobFileReader(std::move(file_reader), file_size, kNoCompression,
/*decompressor=*/nullptr, immutable_options.clock,
statistics, /*has_footer=*/false));
{
const Status s =
ReadHeader(file_reader.get(), read_options, column_family_id,
statistics, &compression_type);
(*blob_file_reader)->ReadHeader(read_options, column_family_id);
if (!s.ok()) {
blob_file_reader->reset();
return s;
}
}
if (!skip_footer_validation) {
const Status s =
ReadFooter(file_reader.get(), read_options, file_size, statistics);
const Status s = (*blob_file_reader)->ReadFooter(read_options);
if (!s.ok()) {
blob_file_reader->reset();
return s;
}
}
std::shared_ptr<Decompressor> decompressor;
if (compression_type != kNoCompression) {
// The blob format has always used compression format 2
decompressor = GetBuiltinV2CompressionManager()->GetDecompressorOptimizeFor(
compression_type);
}
blob_file_reader->reset(
new BlobFileReader(std::move(file_reader), file_size, compression_type,
std::move(decompressor), immutable_options.clock,
statistics, !skip_footer_validation));
return Status::OK();
}
@@ -155,13 +151,17 @@ Status BlobFileReader::OpenFile(
return Status::OK();
}
Status BlobFileReader::ReadHeader(const RandomAccessFileReader* file_reader,
const ReadOptions& read_options,
uint32_t column_family_id,
Statistics* statistics,
CompressionType* compression_type) {
assert(file_reader);
assert(compression_type);
Status BlobFileReader::ReadHeader(const ReadOptions& read_options,
uint32_t column_family_id) {
assert(file_reader_);
// Read ~4 KiB from the start to cover both legacy (30B) and blog (40B
// fixed + property section) headers in a single I/O. This is enough for
// typical blog headers; only if the property section exceeds ~4 KiB do
// we need a second read.
constexpr size_t kInitialReadSize = 4096;
const size_t read_size =
std::min(static_cast<uint64_t>(kInitialReadSize), file_size_);
Slice header_slice;
Buffer buf;
@@ -170,12 +170,9 @@ Status BlobFileReader::ReadHeader(const RandomAccessFileReader* file_reader,
{
TEST_SYNC_POINT("BlobFileReader::ReadHeader:ReadFromFile");
constexpr uint64_t read_offset = 0;
constexpr size_t read_size = BlobLogHeader::kSize;
const Status s =
ReadFromFile(file_reader, read_options, read_offset, read_size,
statistics, &header_slice, &buf, &aligned_buf);
ReadFromFile(file_reader_.get(), read_options, 0, read_size,
statistics_, &header_slice, &buf, &aligned_buf);
if (!s.ok()) {
return s;
}
@@ -184,10 +181,58 @@ Status BlobFileReader::ReadHeader(const RandomAccessFileReader* file_reader,
&header_slice);
}
// Detect blog format by checking the 12-byte magic.
if (BlogFileHeader::IsBlogFormat(header_slice.data(), header_slice.size())) {
// Try to decode the blog header from the buffer we already read.
// DecodeFromBuffer handles the case where the property section
// extends beyond the buffer.
BlogFileHeader blog_header;
Slice buf_slice = header_slice;
size_t additional_needed = 0;
Status s = blog_header.DecodeFromBuffer(&buf_slice, &additional_needed);
if (s.IsIncomplete()) {
// Need more data. Read the rest and retry.
Slice extra_slice;
Buffer extra_buf;
AlignedBuf extra_aligned;
s = ReadFromFile(file_reader_.get(), read_options, header_slice.size(),
additional_needed, statistics_, &extra_slice, &extra_buf,
&extra_aligned);
if (!s.ok()) {
return s;
}
std::string extended;
extended.append(header_slice.data(), header_slice.size());
extended.append(extra_slice.data(), extra_slice.size());
Slice extended_slice(extended);
s = blog_header.DecodeFromBuffer(&extended_slice, nullptr);
}
if (!s.ok()) {
return s;
}
is_blog_format_ = true;
blog_checksum_type_ = blog_header.checksum_type;
blog_incarnation_id_ = blog_header.incarnation_id();
memcpy(blog_escape_sequence_, blog_header.escape_sequence,
kBlogEscapeSequenceSize);
compression_type_ = kNoCompression;
decompressor_ = GetBuiltinV2CompressionManager()->GetDecompressor();
return Status::OK();
}
// Legacy blob format. The initial read already covers the 30-byte header.
if (header_slice.size() < BlobLogHeader::kSize) {
return Status::Corruption("Blob file header too small");
}
Slice legacy_slice(header_slice.data(), BlobLogHeader::kSize);
BlobLogHeader header;
{
const Status s = header.DecodeFrom(header_slice);
const Status s = header.DecodeFrom(legacy_slice);
if (!s.ok()) {
return s;
}
@@ -203,16 +248,62 @@ Status BlobFileReader::ReadHeader(const RandomAccessFileReader* file_reader,
return Status::Corruption("Column family ID mismatch");
}
*compression_type = header.compression;
compression_type_ = header.compression;
if (compression_type_ != kNoCompression) {
decompressor_ =
GetBuiltinV2CompressionManager()->GetDecompressorOptimizeFor(
compression_type_);
}
return Status::OK();
}
Status BlobFileReader::ReadFooter(const RandomAccessFileReader* file_reader,
const ReadOptions& read_options,
uint64_t file_size, Statistics* statistics) {
assert(file_size >= BlobLogHeader::kSize + BlobLogFooter::kSize);
assert(file_reader);
Status BlobFileReader::ReadFooter(const ReadOptions& read_options) {
assert(file_reader_);
if (is_blog_format_) {
// Blog format: verify the file was cleanly sealed by scanning backward
// from EOF for the last record's escape sequence. Read the trailing
// ~4 KiB in a single I/O and scan within the buffer.
if (file_size_ < kBlogFileFixedHeaderSize + kBlogEscapeSequenceSize) {
return Status::Corruption("Blog blob file too small for footer");
}
constexpr size_t kTrailingReadSize = 4096;
const uint64_t min_offset = kBlogFileFixedHeaderSize;
// Round trailing_offset up to 4-byte alignment so buffer positions
// and file positions share the same alignment grid.
const uint64_t trailing_offset =
(file_size_ -
std::min(static_cast<uint64_t>(kTrailingReadSize),
file_size_ - min_offset) +
3) &
~uint64_t{3};
const size_t trailing_size =
static_cast<size_t>(file_size_ - trailing_offset);
Slice trailing_slice;
Buffer trailing_buf;
AlignedBuf trailing_aligned;
Status s = ReadFromFile(file_reader_.get(), read_options, trailing_offset,
trailing_size, statistics_, &trailing_slice,
&trailing_buf, &trailing_aligned);
if (!s.ok()) {
return s;
}
if (VerifyBlogFooterLocator(trailing_slice.data(), trailing_size,
trailing_offset, blog_escape_sequence_,
blog_checksum_type_, blog_incarnation_id_)) {
has_footer_ = true;
return Status::OK();
}
return Status::Corruption("Blog blob file: no footer locator record found");
}
// Legacy blob format.
assert(file_size_ >= BlobLogHeader::kSize + BlobLogFooter::kSize);
Slice footer_slice;
Buffer buf;
@@ -221,12 +312,12 @@ Status BlobFileReader::ReadFooter(const RandomAccessFileReader* file_reader,
{
TEST_SYNC_POINT("BlobFileReader::ReadFooter:ReadFromFile");
const uint64_t read_offset = file_size - BlobLogFooter::kSize;
const uint64_t read_offset = file_size_ - BlobLogFooter::kSize;
constexpr size_t read_size = BlobLogFooter::kSize;
const Status s =
ReadFromFile(file_reader, read_options, read_offset, read_size,
statistics, &footer_slice, &buf, &aligned_buf);
ReadFromFile(file_reader_.get(), read_options, read_offset, read_size,
statistics_, &footer_slice, &buf, &aligned_buf);
if (!s.ok()) {
return s;
}
@@ -250,6 +341,7 @@ Status BlobFileReader::ReadFooter(const RandomAccessFileReader* file_reader,
return Status::Corruption("Unexpected TTL blob file");
}
has_footer_ = true;
return Status::OK();
}
@@ -323,30 +415,40 @@ Status BlobFileReader::GetBlob(
std::unique_ptr<BlobContents>* result, uint64_t* bytes_read) const {
assert(result);
const uint64_t key_size = user_key.size();
// Compute the read region. Both formats store the blob value at `offset`
// with size `value_size`. The difference:
// - Legacy: if verifying checksums, also read the record header (before
// the value) which contains key + CRC.
// - Blog: if verifying checksums, also read the 5-byte trailer (after
// the value) which contains compression_type + checksum.
uint64_t adjustment_before = 0; // bytes before the value to include
uint64_t adjustment_after = 0; // bytes after the value to include
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
has_footer_)) {
return Status::Corruption("Invalid blob offset");
if (is_blog_format_) {
if (read_options.verify_checksums) {
adjustment_after = kBlogBlockTrailerSize;
}
} else {
const uint64_t key_size = user_key.size();
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
has_footer_)) {
return Status::Corruption("Invalid blob offset");
}
if (compression_type != compression_type_) {
return Status::Corruption("Compression type mismatch when reading blob");
}
if (read_options.verify_checksums) {
adjustment_before =
BlobLogRecord::CalculateAdjustmentForRecordHeader(key_size);
}
}
if (compression_type != compression_type_) {
return Status::Corruption("Compression type mismatch when reading blob");
}
// Note: if verify_checksum is set, we read the entire blob record to be able
// to perform the verification; otherwise, we just read the blob itself. Since
// the offset in BlobIndex actually points to the blob value, we need to make
// an adjustment in the former case.
const uint64_t adjustment =
read_options.verify_checksums
? BlobLogRecord::CalculateAdjustmentForRecordHeader(key_size)
: 0;
assert(offset >= adjustment);
const uint64_t record_offset = offset - adjustment;
const uint64_t record_size = value_size + adjustment;
assert(offset >= adjustment_before);
const uint64_t record_offset = offset - adjustment_before;
const uint64_t record_size =
adjustment_before + value_size + adjustment_after;
// Read the data: try prefetch buffer first, then fall back to file read.
Slice record_slice;
Buffer buf;
AlignedBuf aligned_buf;
@@ -388,17 +490,30 @@ Status BlobFileReader::GetBlob(
TEST_SYNC_POINT_CALLBACK("BlobFileReader::GetBlob:TamperWithResult",
&record_slice);
// Verify checksum (format-specific).
CompressionType actual_comp_type = compression_type;
if (read_options.verify_checksums) {
const Status s = VerifyBlob(record_slice, user_key, value_size);
if (!s.ok()) {
return s;
if (is_blog_format_) {
const Status s = VerifyBlogRecordTrailer(
blog_checksum_type_, record_slice.data(),
static_cast<size_t>(value_size), blog_incarnation_id_, offset,
&actual_comp_type);
if (!s.ok()) {
return s;
}
} else {
const Status s = VerifyBlob(record_slice, user_key, value_size);
if (!s.ok()) {
return s;
}
}
}
const Slice value_slice(record_slice.data() + adjustment, value_size);
// Extract the value slice (format-specific offset within the read region).
const Slice value_slice(record_slice.data() + adjustment_before, value_size);
{
const Status s = UncompressBlobIfNeeded(value_slice, compression_type,
const Status s = UncompressBlobIfNeeded(value_slice, actual_comp_type,
decompressor_.get(), allocator,
clock_, statistics_, result);
if (!s.ok()) {
+15 -7
View File
@@ -93,14 +93,15 @@ class BlobFileReader {
std::unique_ptr<RandomAccessFileReader>* file_reader,
bool skip_footer_size_check);
static Status ReadHeader(const RandomAccessFileReader* file_reader,
const ReadOptions& read_options,
uint32_t column_family_id, Statistics* statistics,
CompressionType* compression_type);
// Read and validate the file header. Detects blog format vs legacy format,
// setting is_blog_format_ and related fields accordingly. For legacy format,
// validates column_family_id and sets compression_type_.
Status ReadHeader(const ReadOptions& read_options, uint32_t column_family_id);
static Status ReadFooter(const RandomAccessFileReader* file_reader,
const ReadOptions& read_options, uint64_t file_size,
Statistics* statistics);
// Validate the file footer. For legacy format, reads and validates the
// BlobLogFooter. For blog format, scans backward for the footer locator
// record to verify the file was cleanly sealed.
Status ReadFooter(const ReadOptions& read_options);
using Buffer = std::unique_ptr<char[]>;
@@ -129,6 +130,13 @@ class BlobFileReader {
Statistics* statistics_;
// False when the reader was opened before the blob file footer was written.
bool has_footer_;
// True when the file uses blog format instead of legacy blob log format.
bool is_blog_format_ = false;
// For blog format: fields from the file header used for record checksum
// verification and footer validation.
uint32_t blog_incarnation_id_ = 0;
ChecksumType blog_checksum_type_ = kNoChecksum;
char blog_escape_sequence_[10] = {}; // kBlogEscapeSequenceSize
};
} // namespace ROCKSDB_NAMESPACE
+4 -5
View File
@@ -57,8 +57,7 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
constexpr bool do_flush = false;
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
statistics, blob_file_number, use_fsync,
do_flush);
statistics, use_fsync, do_flush);
BlobLogHeader header(column_family_id, compression, has_ttl,
expiration_range_header);
@@ -98,8 +97,8 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
std::string checksum_method;
std::string checksum_value;
ASSERT_OK(blob_log_writer.AppendFooter(WriteOptions(), footer,
&checksum_method, &checksum_value));
ASSERT_OK(blob_log_writer.LegacyAppendFooterAndClose(
WriteOptions(), footer, &checksum_method, &checksum_value));
}
// Creates a test blob file with a single blob in it. Note: this method
@@ -465,7 +464,7 @@ TEST_F(BlobFileReaderTest, Malformed) {
BlobLogWriter blob_log_writer(std::move(file_writer),
immutable_options.clock, statistics,
blob_file_number, use_fsync, do_flush);
use_fsync, do_flush);
BlobLogHeader header(column_family_id, kNoCompression, has_ttl,
expiration_range);
+4 -7
View File
@@ -20,12 +20,10 @@ namespace ROCKSDB_NAMESPACE {
BlobLogWriter::BlobLogWriter(std::unique_ptr<WritableFileWriter>&& dest,
SystemClock* clock, Statistics* statistics,
uint64_t log_number, bool use_fs, bool do_flush,
uint64_t boffset)
bool use_fs, bool do_flush, uint64_t boffset)
: dest_(std::move(dest)),
clock_(clock),
statistics_(statistics),
log_number_(log_number),
block_offset_(boffset),
use_fsync_(use_fs),
do_flush_(do_flush),
@@ -74,10 +72,9 @@ Status BlobLogWriter::WriteHeader(const WriteOptions& write_options,
return s;
}
Status BlobLogWriter::AppendFooter(const WriteOptions& write_options,
BlobLogFooter& footer,
std::string* checksum_method,
std::string* checksum_value) {
Status BlobLogWriter::LegacyAppendFooterAndClose(
const WriteOptions& write_options, BlobLogFooter& footer,
std::string* checksum_method, std::string* checksum_value) {
assert(block_offset_ != 0);
assert(last_elem_type_ == kEtFileHdr || last_elem_type_ == kEtRecord);
+6 -8
View File
@@ -32,8 +32,8 @@ class BlobLogWriter {
// "*dest" must be initially empty.
// "*dest" must remain live while this BlobLogWriter is in use.
BlobLogWriter(std::unique_ptr<WritableFileWriter>&& dest, SystemClock* clock,
Statistics* statistics, uint64_t log_number, bool use_fsync,
bool do_flush, uint64_t boffset = 0);
Statistics* statistics, bool use_fsync, bool do_flush,
uint64_t boffset = 0);
// No copying allowed
BlobLogWriter(const BlobLogWriter&) = delete;
BlobLogWriter& operator=(const BlobLogWriter&) = delete;
@@ -56,9 +56,10 @@ class BlobLogWriter {
const Slice& val, uint64_t* key_offset,
uint64_t* blob_offset);
Status AppendFooter(const WriteOptions& write_options, BlobLogFooter& footer,
std::string* checksum_method,
std::string* checksum_value);
Status LegacyAppendFooterAndClose(const WriteOptions& write_options,
BlobLogFooter& footer,
std::string* checksum_method,
std::string* checksum_value);
Status WriteHeader(const WriteOptions& write_options, BlobLogHeader& header);
@@ -66,15 +67,12 @@ class BlobLogWriter {
const WritableFileWriter* file() const { return dest_.get(); }
uint64_t get_log_number() const { return log_number_; }
Status Sync(const WriteOptions& write_options);
private:
std::unique_ptr<WritableFileWriter> dest_;
SystemClock* clock_;
Statistics* statistics_;
uint64_t log_number_;
uint64_t block_offset_; // Current offset in block
bool use_fsync_;
bool do_flush_;
+3 -4
View File
@@ -59,8 +59,7 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
constexpr bool do_flush = false;
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
statistics, blob_file_number, use_fsync,
do_flush);
statistics, use_fsync, do_flush);
BlobLogHeader header(column_family_id, compression, has_ttl,
expiration_range_header);
@@ -100,8 +99,8 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
std::string checksum_method;
std::string checksum_value;
ASSERT_OK(blob_log_writer.AppendFooter(WriteOptions(), footer,
&checksum_method, &checksum_value));
ASSERT_OK(blob_log_writer.LegacyAppendFooterAndClose(
WriteOptions(), footer, &checksum_method, &checksum_value));
}
} // anonymous namespace
+1 -1
View File
@@ -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 -1
View File
@@ -35,7 +35,7 @@ struct BlobDirectWriteSettings {
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();
+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
+621
View File
@@ -0,0 +1,621 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "db/blog/blog_format.h"
#include <cassert>
#include <cstring>
#include "env/unique_id_gen.h"
#include "table/format.h"
#include "table/unique_id_impl.h"
#include "util/coding.h"
#include "util/fastrange.h"
#include "util/xxhash.h"
namespace ROCKSDB_NAMESPACE {
// --- Property encoding/decoding ---
void EncodeBlogProperties(const BlogPropertyMap& props, std::string* dst) {
for (const auto& [name, value] : props) {
PutLengthPrefixedSlice(dst, name);
PutLengthPrefixedSlice(dst, value);
}
}
Status DecodeBlogProperties(Slice* input, BlogPropertyMap* props) {
while (input->size() > 0) {
Slice name_slice;
Slice value_slice;
if (!GetLengthPrefixedSlice(input, &name_slice)) {
return Status::Corruption("Blog properties: truncated property name");
}
if (!GetLengthPrefixedSlice(input, &value_slice)) {
return Status::Corruption("Blog properties: truncated property value");
}
props->emplace_back(name_slice.ToString(), value_slice.ToString());
}
return Status::OK();
}
// --- BlogFileHeader ---
uint32_t BlogFileHeader::incarnation_id() const {
return DecodeFixed32(escape_sequence);
}
void BlogFileHeader::EncodeTo(std::string* dst) const {
// --- Fixed header (40 bytes) ---
// Bytes [0-11]: File magic
dst->append(kBlogFileMagic, kBlogFileMagicSize);
// Byte [12]: Schema version
dst->push_back(static_cast<char>(schema_version));
// Byte [13]: Checksum type
dst->push_back(static_cast<char>(checksum_type));
// Byte [14]: Compact record type
dst->push_back(static_cast<char>(compact_record_type));
// Byte [15]: Flags
dst->push_back(static_cast<char>(flags));
// Bytes [16-25]: Escape sequence
dst->append(escape_sequence, kBlogEscapeSequenceSize);
// Bytes [26-31]: Reserved
for (int i = 0; i < 6; ++i) {
dst->push_back(0);
}
// Encode properties into a temporary buffer to measure size.
std::string prop_buf;
EncodeBlogProperties(properties, &prop_buf);
char prop_comp_type = static_cast<char>(kNoCompression);
// Build property section: [properties][compression_type][checksum]
uint32_t prop_checksum = ComputeBuiltinChecksumWithLastByte(
checksum_type, prop_buf.data(), prop_buf.size(), prop_comp_type);
prop_checksum +=
ChecksumModifierForContext(incarnation_id(), kBlogFileFixedHeaderSize);
std::string prop_section = std::move(prop_buf);
prop_section.push_back(prop_comp_type);
PutFixed32(&prop_section, prop_checksum);
// Bytes [32-35]: Property section size (trailer included, no padding)
PutFixed32(dst, static_cast<uint32_t>(prop_section.size()));
// Bytes [36-39]: Fixed header checksum (covers bytes [0, 36))
// Uses context checksum with incarnation_id at offset 0 for
// defense-in-depth (misplaced data detection).
assert(dst->size() == 36);
uint32_t header_checksum =
ComputeBuiltinChecksum(checksum_type, dst->data(), dst->size()) +
ChecksumModifierForContext(incarnation_id(), 0);
PutFixed32(dst, header_checksum);
// Property section follows the fixed header
assert(dst->size() == kBlogFileFixedHeaderSize);
dst->append(prop_section);
}
Status BlogFileHeader::DecodeFrom(Slice* input) {
if (input->size() < kBlogFileFixedHeaderSize) {
return Status::Corruption("Blog header too short: have " +
std::to_string(input->size()) + " bytes, need " +
std::to_string(kBlogFileFixedHeaderSize));
}
const char* p = input->data();
// Verify magic
if (memcmp(p, kBlogFileMagic, kBlogFileMagicSize) != 0) {
return Status::Corruption("Blog header: bad magic (first 4 bytes: 0x" +
Slice(p, 4).ToString(true) + ")");
}
// Parse fields needed for checksum computation before verifying.
checksum_type = static_cast<ChecksumType>(p[13]);
memcpy(escape_sequence, p + 16, kBlogEscapeSequenceSize);
// Verify fixed header checksum FIRST, before trusting any other fields.
uint32_t prop_section_size = DecodeFixed32(p + 32);
uint32_t stored_header_checksum = DecodeFixed32(p + 36);
uint32_t computed_header_checksum =
ComputeBuiltinChecksum(checksum_type, p, 36) +
ChecksumModifierForContext(incarnation_id(), 0);
if (stored_header_checksum != computed_header_checksum) {
return Status::Corruption(
"Blog header: fixed header checksum mismatch (stored: 0x" +
Slice(p + 36, 4).ToString(true) + ", computed: 0x" +
Slice(reinterpret_cast<const char*>(&computed_header_checksum), 4)
.ToString(true) +
")");
}
// Now that checksum is verified, validate the remaining fields.
schema_version = static_cast<uint8_t>(p[12]);
if (schema_version > kBlogCurrentSchemaVersion) {
return Status::NotSupported("Blog header: schema version " +
std::to_string(schema_version) +
" is newer than supported version " +
std::to_string(kBlogCurrentSchemaVersion));
}
compact_record_type = static_cast<BlogRecordType>(p[14]);
flags = static_cast<uint8_t>(p[15]);
if (!VerifyEscapeSequence(escape_sequence)) {
return Status::Corruption(
"Blog header: escape sequence derived bytes mismatch (first byte: 0x" +
Slice(escape_sequence, 1).ToString(true) + ")");
}
// Advance past fixed header
input->remove_prefix(kBlogFileFixedHeaderSize);
// Read property section
if (input->size() < prop_section_size) {
return Status::Corruption("Blog header: truncated property section (have " +
std::to_string(input->size()) + " bytes, need " +
std::to_string(prop_section_size) + ")");
}
if (prop_section_size < kBlogBlockTrailerSize) {
return Status::Corruption("Blog header: property section too small (" +
std::to_string(prop_section_size) +
" bytes, min " +
std::to_string(kBlogBlockTrailerSize) + ")");
}
// Property section layout: [properties][comp_type:1B][checksum:4B]
const char* prop_data = input->data();
size_t prop_data_size = prop_section_size - kBlogBlockTrailerSize;
char prop_comp_type = prop_data[prop_data_size];
uint32_t stored_prop_checksum = DecodeFixed32(prop_data + prop_data_size + 1);
uint32_t computed_prop_checksum = ComputeBuiltinChecksumWithLastByte(
checksum_type, prop_data, prop_data_size, prop_comp_type);
computed_prop_checksum +=
ChecksumModifierForContext(incarnation_id(), kBlogFileFixedHeaderSize);
if (stored_prop_checksum != computed_prop_checksum) {
return Status::Corruption(
"Blog header: property section checksum mismatch (prop_size=" +
std::to_string(prop_data_size) + ", comp_type=0x" +
Slice(&prop_comp_type, 1).ToString(true) + ")");
}
// Decode properties
Slice prop_slice(prop_data, prop_data_size);
properties.clear();
Status s = DecodeBlogProperties(&prop_slice, &properties);
if (!s.ok()) {
return s;
}
// Check for unrecognized required properties
for (const auto& [name, value] : properties) {
if (IsBlogRequiredProperty(name)) {
if (name != kBlogPropCompressionCompatibilityName &&
name != kBlogPropTimestampSize &&
name != kBlogPropPredecessorWalInfo &&
name != kBlogPropWriteBatchStreamingCompressionType) {
return Status::NotSupported(
"Blog header: unrecognized required property: \"" + name + "\"");
}
}
}
input->remove_prefix(prop_section_size);
return Status::OK();
}
void BlogFileHeader::SetEscapeSequenceFromU64(uint64_t v) {
// Byte 0: derived from top 4 bytes via FastRange32 into [1, 254],
// avoiding 0x00 and 0xFF (which are padding bytes).
uint32_t top = static_cast<uint32_t>(v >> 32);
escape_sequence[0] = static_cast<char>(FastRange32(top, 254) + 1);
assert(escape_sequence[0] != '\0');
assert(escape_sequence[0] != '\xFF');
// Bytes 1-5: lowest 5 bytes as-is (little-endian).
EncodeFixed32(escape_sequence + 1, static_cast<uint32_t>(v));
escape_sequence[5] = static_cast<char>(v >> 32);
// (byte at index 5 overlaps with top byte used for FastRange32, but
// that's fine -- it's just input entropy, not the derived byte 0)
// Bytes 6-9: XXH3-derived suffix for heuristic detection. This would enable
// us to scan through a file missing its header and, with some hashing effort,
// detect escape sequences with high accuracy (expect 1 false positive per
// ~16GB; further validation/pruning can be done trying to parse records
// and/or picking the most common candidate escape sequence).
uint64_t derived = XXH3_64bits_withSeed(
escape_sequence, kBlogEscapeSeqRandomPartSize, kBlogEscapeSeqSeed);
EncodeFixed32(escape_sequence + kBlogEscapeSeqRandomPartSize,
static_cast<uint32_t>(derived));
}
void BlogFileHeader::GenerateFromUniqueId(const std::string& db_id,
const std::string& db_session_id,
uint64_t file_number) {
if (db_id.empty() || db_session_id.empty() || file_number == 0) {
GenerateRandomFields();
return;
}
// Derive escape sequence from the same unique ID scheme as SST files.
UniqueId64x2 unique_id;
Status s =
GetSstInternalUniqueId(db_id, db_session_id, file_number, &unique_id);
if (!s.ok()) {
GenerateRandomFields();
return;
}
// The internal unique ID is intentionally not fully random/mixed. Mix it
// so that it is.
InternalUniqueIdToExternal(&unique_id);
SetEscapeSequenceFromU64(unique_id[0]);
// Store the inputs as ignorable header properties for debugging/tracking.
SetProperty(kBlogPropDbId, db_id);
SetProperty(kBlogPropDbSessionId, db_session_id);
SetUint64Property(kBlogPropFileNumber, file_number);
}
void BlogFileHeader::GenerateRandomFields() {
uint64_t rand_a;
uint64_t rand_b;
GenerateRawUniqueId(&rand_a, &rand_b);
// Use rand_a for escape sequence, rand_b is unused extra entropy.
(void)rand_b;
SetEscapeSequenceFromU64(rand_a);
}
bool BlogFileHeader::VerifyEscapeSequence(const char* seq) {
uint8_t first = static_cast<uint8_t>(seq[0]);
if (first == 0x00 || first == 0xFF) {
return false;
}
// Verify derived part
uint64_t derived = XXH3_64bits_withSeed(seq, kBlogEscapeSeqRandomPartSize,
kBlogEscapeSeqSeed);
uint32_t expected = static_cast<uint32_t>(derived);
uint32_t actual = DecodeFixed32(seq + kBlogEscapeSeqRandomPartSize);
return expected == actual;
}
void BlogFileHeader::SetProperty(const std::string& name,
const std::string& value) {
for (auto& [k, v] : properties) {
if (k == name) {
v = value;
return;
}
}
properties.emplace_back(name, value);
}
std::string BlogFileHeader::GetProperty(const std::string& name) const {
for (const auto& [k, v] : properties) {
if (k == name) {
return v;
}
}
return "";
}
bool BlogFileHeader::HasProperty(const std::string& name) const {
for (const auto& [k, v] : properties) {
if (k == name) {
return true;
}
}
return false;
}
void BlogFileHeader::SetUint64Property(const std::string& name,
uint64_t value) {
std::string encoded;
PutVarint64(&encoded, value);
SetProperty(name, encoded);
}
void BlogFileHeader::SetUint32Property(const std::string& name,
uint32_t value) {
std::string encoded;
PutVarint32(&encoded, value);
SetProperty(name, encoded);
}
bool BlogFileHeader::GetUint64Property(const std::string& name,
uint64_t* value) const {
std::string raw = GetProperty(name);
if (raw.empty()) {
return false;
}
Slice s(raw);
return GetVarint64(&s, value);
}
bool BlogFileHeader::GetUint32Property(const std::string& name,
uint32_t* value) const {
std::string raw = GetProperty(name);
if (raw.empty()) {
return false;
}
Slice s(raw);
return GetVarint32(&s, value);
}
Status BlogFileHeader::DecodeFromBuffer(Slice* buffer,
size_t* additional_bytes_needed) {
if (buffer->size() < kBlogFileFixedHeaderSize) {
return Status::Corruption("Blog header: buffer too small for fixed header");
}
// Check if the full header (fixed + property section) fits in the buffer.
uint32_t prop_section_size = DecodeFixed32(buffer->data() + 32);
size_t total_header_size = kBlogFileFixedHeaderSize + prop_section_size;
if (buffer->size() < total_header_size) {
if (additional_bytes_needed) {
*additional_bytes_needed = total_header_size - buffer->size();
}
return Status::Incomplete(
"Blog header: need more data for property section",
std::to_string(total_header_size));
}
// Full header is available. Decode it.
Slice header_slice(buffer->data(), total_header_size);
Status s = DecodeFrom(&header_slice);
if (s.ok()) {
buffer->remove_prefix(total_header_size);
}
return s;
}
// --- Footer locator verification ---
bool VerifyBlogFooterLocator(const char* buffer, size_t buffer_size,
uint64_t buffer_file_offset,
const char* expected_escape_seq,
ChecksumType checksum_type,
uint32_t incarnation_id) {
// Scan backward in 4-byte steps for the escape sequence.
// Buffer must start at a 4-byte-aligned file offset.
for (size_t pos = buffer_size; pos >= 4; pos -= 4) {
size_t esc_start = pos - 4;
uint8_t first_byte = static_cast<uint8_t>(buffer[esc_start]);
if (first_byte == 0x00 || first_byte == 0xFF) {
continue; // padding byte
}
// Check if this is our escape sequence
if (esc_start + kBlogEscapeSequenceSize > buffer_size ||
memcmp(buffer + esc_start, expected_escape_seq,
kBlogEscapeSequenceSize) != 0) {
continue; // not our escape sequence
}
// Found escape sequence. Parse the full-format record that follows.
// Full format: [escape_seq:10B] [varint length:4+B] [type:1B]
// [compression_type:1B] [prefix_checksum:4B]
// [payload:length B] [comp:1B] [checksum:4B]
const char* rec = buffer + esc_start + kBlogEscapeSequenceSize;
size_t remaining = buffer_size - esc_start - kBlogEscapeSequenceSize;
// Parse varint length (must be >= 4 bytes for full format)
Slice varint_slice(rec, remaining);
uint64_t length = 0;
const char* varint_end = GetVarint64Ptr(rec, rec + remaining, &length);
if (!varint_end) {
continue; // truncated varint
}
size_t varint_len = static_cast<size_t>(varint_end - rec);
if (varint_len <= kBlogCompactVarintMaxBytes) {
continue; // not full format
}
// Need type(1) + comp(1) + prefix_checksum(4) after varint
size_t after_varint = remaining - varint_len;
if (after_varint < 6) {
continue; // truncated prefix
}
BlogRecordType type =
static_cast<BlogRecordType>(static_cast<uint8_t>(varint_end[0]));
if (type != kBlogFooterLocatorRecord) {
continue; // not a footer locator
}
// Verify prefix checksum over (varint + type + compression_type)
size_t prefix_data_size = varint_len + 2; // varint + type + comp
uint64_t esc_file_offset = buffer_file_offset + esc_start;
uint32_t stored_prefix_checksum = DecodeFixed32(varint_end + 2);
uint32_t computed_prefix_checksum =
ComputeBuiltinChecksum(checksum_type, rec, prefix_data_size) +
ChecksumModifierForContext(incarnation_id, esc_file_offset);
if (stored_prefix_checksum != computed_prefix_checksum) {
continue; // prefix checksum mismatch
}
// If length > 0, verify the payload trailer checksum
if (length > 0) {
size_t after_prefix = after_varint - 6; // after type+comp+prefix_cksum
if (after_prefix < length + kBlogBlockTrailerSize) {
continue; // truncated payload or trailer
}
const char* payload = varint_end + 6;
uint64_t payload_file_offset = buffer_file_offset + esc_start +
kBlogEscapeSequenceSize + varint_len + 6;
CompressionType actual_comp;
Status s = VerifyBlogRecordTrailer(
checksum_type, payload, static_cast<size_t>(length), incarnation_id,
payload_file_offset, &actual_comp);
if (!s.ok()) {
continue; // trailer checksum mismatch
}
}
// Valid footer locator record found.
return true;
}
return false;
}
// --- BlogFileFooterLocator ---
void BlogFileFooterLocator::EncodeTo(std::string* dst) const {
for (const auto& entry : entries) {
dst->push_back(static_cast<char>(entry.record_type));
PutFixed32(dst, entry.relative_offset_4B);
}
}
Status BlogFileFooterLocator::DecodeFrom(const Slice& input) {
Slice s = input;
entries.clear();
while (s.size() >= 5) { // 1 byte type + 4 bytes offset
entries.push_back(
{.record_type = static_cast<BlogRecordType>(static_cast<uint8_t>(s[0])),
.relative_offset_4B = DecodeFixed32(s.data() + 1)});
s.remove_prefix(5);
}
if (s.size() != 0) {
return Status::Corruption(
"Blog footer locator: trailing bytes after entries");
}
return Status::OK();
}
// --- BlogFileFooterProperties ---
void BlogFileFooterProperties::SetBlobCount(uint64_t count) {
std::string val;
PutVarint64(&val, count);
properties.emplace_back("blobCount", std::move(val));
}
void BlogFileFooterProperties::SetTotalBlobBytes(uint64_t bytes) {
std::string val;
PutVarint64(&val, bytes);
properties.emplace_back("totalBlobBytes", std::move(val));
}
void BlogFileFooterProperties::SetSequenceRange(uint64_t min_seq,
uint64_t max_seq) {
std::string min_val;
PutVarint64(&min_val, min_seq);
properties.emplace_back("minSequence", std::move(min_val));
std::string max_val;
PutVarint64(&max_val, max_seq);
properties.emplace_back("maxSequence", std::move(max_val));
}
void BlogFileFooterProperties::EncodeTo(std::string* dst) const {
EncodeBlogProperties(properties, dst);
}
Status BlogFileFooterProperties::DecodeFrom(const Slice& input) {
Slice s = input;
properties.clear();
return DecodeBlogProperties(&s, &properties);
}
// --- Padding ---
void ComputeBlogPaddingParams(uint8_t last_meaningful_byte,
size_t current_offset, uint8_t* pad_byte,
size_t* pad_count) {
// Determine how many bytes needed to reach next 4-byte alignment
size_t remainder = current_offset % 4;
size_t needed = (remainder == 0) ? 0 : (4 - remainder);
// Choose pad byte to differ from last_meaningful_byte
if (last_meaningful_byte == 0x00) {
*pad_byte = 0xFF;
// Must have at least 1 byte of padding to distinguish from checksum
if (needed == 0) {
needed = 4;
}
} else if (last_meaningful_byte == 0xFF) {
*pad_byte = 0x00;
if (needed == 0) {
needed = 4;
}
} else {
*pad_byte = 0x00; // Writer's choice; 0x00 is conventional
}
*pad_count = needed;
}
void ComputeBlogPadding(uint8_t last_meaningful_byte, size_t current_offset,
std::string* dst) {
uint8_t pad_byte;
size_t pad_count;
ComputeBlogPaddingParams(last_meaningful_byte, current_offset, &pad_byte,
&pad_count);
dst->append(pad_count, static_cast<char>(pad_byte));
}
// --- Checksum ---
uint32_t ComputeBlogRecordChecksum(ChecksumType type, const char* data,
size_t data_size, char compression_type_byte,
uint32_t base_context_checksum,
uint64_t record_offset) {
uint32_t checksum = ComputeBuiltinChecksumWithLastByte(type, data, data_size,
compression_type_byte);
checksum += ChecksumModifierForContext(base_context_checksum, record_offset);
return checksum;
}
// --- Record trailer verification ---
Status VerifyBlogRecordTrailer(ChecksumType checksum_type, const char* payload,
size_t payload_size, uint32_t incarnation_id,
uint64_t payload_file_offset,
CompressionType* actual_compression_type) {
// Trailer is the 5 bytes immediately after the payload:
// [compression_type: 1B][checksum: 4B]
const char* trailer = payload + payload_size;
char comp_byte = trailer[0];
uint32_t stored_checksum = DecodeFixed32(trailer + 1);
uint32_t computed_checksum =
ComputeBlogRecordChecksum(checksum_type, payload, payload_size, comp_byte,
incarnation_id, payload_file_offset);
if (stored_checksum != computed_checksum) {
return Status::Corruption("Blog record checksum mismatch");
}
if (actual_compression_type) {
*actual_compression_type =
static_cast<CompressionType>(static_cast<uint8_t>(comp_byte));
}
return Status::OK();
}
// --- Irregular varint ---
void PutBlogIrregularVarint64(std::string* dst, uint64_t value,
size_t min_encoded_bytes) {
// Encode as standard varint, then pad with trailing zero-data bytes
// to reach min_encoded_bytes. The result decodes to the same value.
char buf[10];
char* end = EncodeVarint64(buf, value);
size_t natural_len = static_cast<size_t>(end - buf);
if (natural_len >= min_encoded_bytes) {
dst->append(buf, natural_len);
return;
}
// Set the continuation bit on what was the terminal byte, then append
// (extra - 1) continuation bytes (0x80) and one terminal byte (0x00).
size_t extra = min_encoded_bytes - natural_len;
buf[natural_len - 1] |= static_cast<char>(0x80);
dst->append(buf, natural_len);
for (size_t i = 0; i + 1 < extra; ++i) {
dst->push_back(static_cast<char>(0x80));
}
dst->push_back(static_cast<char>(0x00));
}
} // namespace ROCKSDB_NAMESPACE
+299
View File
@@ -0,0 +1,299 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#pragma once
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
#include "rocksdb/rocksdb_namespace.h"
#include "rocksdb/slice.h"
#include "rocksdb/status.h"
#include "rocksdb/table.h"
namespace ROCKSDB_NAMESPACE {
// Record types used in blog file body and footer.
// Types 0x01-0x7F are for body records.
// Types 0xF0-0xFF are for footer records (in expected order of occurrence,
// with gaps for future additions).
enum BlogRecordType : uint8_t {
kBlogBlobRecord = 0x01,
kBlogPreambleStartRecord = 0x02,
kBlogWriteBatchRecord = 0x03,
// 0x04-0x7F: reserved for future body record types
kBlogFooterIndexRecord = 0xF0, // sparse index or similar large metadata
// 0xF1-0xF3: reserved for future footer data records
kBlogFooterPropertiesRecord = 0xF8, // small metadata (named properties)
// 0xF9-0xFB: reserved
kBlogFooterFileChecksumInfo = 0xFC, // full file checksum state dump, so
// the file checksum can be inferred
// from just reading the tail
// 0xFD-0xFE: reserved
kBlogFooterLocatorRecord = 0xFF, // relative offsets to other footer
// records; must be last
};
// --- Constants ---
// 12-byte file magic: "Blog" (ASCII) followed by XXH3_64bits("Blog", 4).
// Used as an opaque identifier for blog format files.
static constexpr char kBlogFileMagic[12] = {
'\x42', '\x6C', '\x6F', '\x67', // "Blog"
'\x9F', '\xFC', '\x41', '\x52', '\xF7', '\xF2', '\x2C', '\x49' // XXH3_64
};
static constexpr size_t kBlogFileMagicSize = 12;
// Fixed header size in bytes (before property section).
static constexpr size_t kBlogFileFixedHeaderSize = 40;
// Escape sequence: 6 random bytes + 4 derived bytes.
static constexpr size_t kBlogEscapeSequenceSize = 10;
static constexpr size_t kBlogEscapeSeqRandomPartSize = 6;
// Seed for deriving bytes [6-9] of escape sequence via
// XXH3_64bits_withSeed(random_6_bytes, 6, kBlogEscapeSeqSeed).
// Chosen arbitrarily; must never change once the format is released.
static constexpr uint64_t kBlogEscapeSeqSeed =
0x52636B73426C6F67ULL; // "RcksBlg\0"
// Sentinel for unspecified-size records. Encodes to a 9-byte varint,
// which always triggers the full record format.
static constexpr uint64_t kBlogUnspecifiedSize = (uint64_t{1} << 63) - 1;
// Maximum payload size that encodes to a compact-format varint (<= 2 bytes).
// varint of 16383 (0x3FFF) uses 2 bytes; 16384 uses 3 bytes.
// Records larger than this get full format with prefix checksum protection.
static constexpr uint64_t kBlogMaxCompactPayloadSize = (1u << 14) - 1;
// Compact format varint threshold: varints of this many bytes or fewer
// use compact format (no type byte, no prefix checksum).
static constexpr size_t kBlogCompactVarintMaxBytes = 2;
// Block trailer size: 1 byte compression_type + 4 bytes checksum.
static constexpr size_t kBlogBlockTrailerSize = 5;
// kStreamingCompressionSentinel (0x7F) is defined in compression_type.h
// and used in blog record trailers to indicate streaming compression.
// A record with kNoCompression in the trailer remains individually
// uncompressed even when streaming is active (dynamic on/off).
// --- Named Properties ---
// Named property: string key -> string value.
// Case convention: Uppercase first letter = required (reader must recognize
// or reject the file). Lowercase first letter = ignorable.
using BlogPropertyMap = std::vector<std::pair<std::string, std::string>>;
// Encode properties as a sequence of length-prefixed name/value pairs.
// No count prefix; section is terminated by reaching the end of the slice.
void EncodeBlogProperties(const BlogPropertyMap& props, std::string* dst);
// Decode properties from a slice, consuming all bytes.
Status DecodeBlogProperties(Slice* input, BlogPropertyMap* props);
// Returns true if a property name starts with an uppercase letter,
// meaning the reader must recognize it or reject the file.
inline bool IsBlogRequiredProperty(const Slice& name) {
return name.size() > 0 && name[0] >= 'A' && name[0] <= 'Z';
}
// Known header property names
static constexpr const char* kBlogPropCompressionCompatibilityName =
"CompressionCompatibilityName";
static constexpr const char* kBlogPropTimestampSize = "TimestampSize";
static constexpr const char* kBlogPropPredecessorWalInfo = "PredecessorWalInfo";
// Required (uppercase) when streaming compression is active for WriteBatch
// records. Value is a 2-digit hex encoding of the CompressionType used
// (e.g. "07" for kZSTD). The presence of this property means WriteBatch
// record payloads with kStreamingCompressionSentinel in their trailer
// are part of a streaming compression context that spans across records.
static constexpr const char* kBlogPropWriteBatchStreamingCompressionType =
"WriteBatchStreamingCompressionType";
static constexpr const char* kBlogPropRole = "role";
static constexpr const char* kBlogPropCompressionSettings =
"compressionSettings";
static constexpr const char* kBlogPropCreationTime = "creationTime";
static constexpr const char* kBlogPropCreator = "creator";
// Ignorable properties for file identity and diagnostics.
static constexpr const char* kBlogPropDbId = "dbId";
static constexpr const char* kBlogPropDbSessionId = "dbSessionId";
static constexpr const char* kBlogPropFileNumber = "fileNumber";
static constexpr const char* kBlogPropDbHostId = "dbHostId";
static constexpr const char* kBlogPropColumnFamilyId = "columnFamilyId";
static constexpr const char* kBlogPropColumnFamilyName = "columnFamilyName";
// --- BlogFileHeader ---
// Bit flags for BlogFileHeader::flags (byte [15] in the fixed header).
enum BlogFileHeaderFlags : uint8_t {
kBlogFileRecycled = 0x01, // File may contain trailing stale data from a
// previous incarnation. Recovery should tolerate
// trailing data that doesn't match our escape
// sequence (recycled/stale, not corruption).
};
static constexpr uint8_t kBlogCurrentSchemaVersion = 0;
struct BlogFileHeader {
uint8_t schema_version = kBlogCurrentSchemaVersion;
ChecksumType checksum_type = kXXH3;
BlogRecordType compact_record_type = kBlogBlobRecord;
uint8_t flags = 0; // BlogFileHeaderFlags, byte [15] in fixed header
// 10-byte escape sequence: bytes [0-5] random (byte[0] not 0x00/0xFF),
// bytes [6-9] = lower32(XXH3_64bits_withSeed(bytes[0-5],
// kBlogEscapeSeqSeed)).
char escape_sequence[kBlogEscapeSequenceSize] = {};
bool is_recycled() const { return flags & kBlogFileRecycled; }
BlogPropertyMap properties;
// Incarnation ID = first 4 bytes of escape_sequence (little-endian via
// DecodeFixed32). Guaranteed nonzero since byte[0] is not 0x00.
uint32_t incarnation_id() const;
// Encode the full header (fixed prefix + property section + checksums)
// into dst.
void EncodeTo(std::string* dst) const;
// Decode from input. Consumes the bytes read.
Status DecodeFrom(Slice* input);
// Generate escape_sequence from the file's unique ID (db_id, db_session_id,
// file_number), using the same scheme as SST internal unique IDs. Falls back
// to GenerateRandomFields() if any input is empty/zero. Also sets the
// ignorable dbId/dbSessionId/fileNumber header properties.
void GenerateFromUniqueId(const std::string& db_id,
const std::string& db_session_id,
uint64_t file_number);
// Generate escape_sequence from random bytes. Appropriate when unique ID
// inputs are not available.
void GenerateRandomFields();
private:
// Populate escape_sequence from a 64 bits of entropy.
void SetEscapeSequenceFromU64(uint64_t v);
public:
// Verify that the escape sequence's derived bytes match.
static bool VerifyEscapeSequence(const char* seq);
// Check if a buffer starts with the blog file magic number.
static bool IsBlogFormat(const char* data, size_t size) {
return size >= kBlogFileMagicSize &&
memcmp(data, kBlogFileMagic, kBlogFileMagicSize) == 0;
}
// Decode a blog file header from a buffer that may or may not contain
// the full header. Returns OK and consumes the header bytes on success.
// If the buffer contains the fixed header but not the full property
// section, returns the number of additional bytes needed in
// *additional_bytes_needed and returns Status::Incomplete().
// This enables single-read header parsing from a ~4 KiB buffer.
Status DecodeFromBuffer(Slice* buffer, size_t* additional_bytes_needed);
// Raw property helpers (string values)
void SetProperty(const std::string& name, const std::string& value);
std::string GetProperty(const std::string& name) const;
// Typed property setters/getters for properties that aren't natively strings.
void SetUint64Property(const std::string& name, uint64_t value);
void SetUint32Property(const std::string& name, uint32_t value);
bool GetUint64Property(const std::string& name, uint64_t* value) const;
bool GetUint32Property(const std::string& name, uint32_t* value) const;
bool HasProperty(const std::string& name) const;
};
// --- Footer types ---
// Entry in a footer locator record. Offsets are in units of 4 bytes,
// sequential relative to each other in reverse order of footer records.
// First entry: offset from locator's escape_seq to the preceding footer
// record's escape_seq. Subsequent entries: offset from the previous
// entry's escape_seq to the next earlier footer record's escape_seq.
// Verify that a blog file's trailing data contains a valid footer locator
// record. Scans backward from the end of the buffer for the escape sequence,
// then parses and verifies the full record (varint length, type, prefix
// checksum, payload, trailer checksum). The buffer must start at a 4-byte-
// aligned file offset. Returns true if a valid footer locator record is found.
bool VerifyBlogFooterLocator(const char* buffer, size_t buffer_size,
uint64_t buffer_file_offset,
const char* expected_escape_seq,
ChecksumType checksum_type,
uint32_t incarnation_id);
struct BlogFooterLocatorEntry {
BlogRecordType record_type;
uint32_t relative_offset_4B;
};
struct BlogFileFooterLocator {
std::vector<BlogFooterLocatorEntry> entries;
// Encode as the payload of a footer locator record.
void EncodeTo(std::string* dst) const;
Status DecodeFrom(const Slice& input);
};
struct BlogFileFooterProperties {
BlogPropertyMap properties;
// Convenience setters that encode values into the property map.
void SetBlobCount(uint64_t count);
void SetTotalBlobBytes(uint64_t bytes);
void SetSequenceRange(uint64_t min_seq, uint64_t max_seq);
// Encode as named properties (no 5-byte trailer; that comes from
// the enclosing full record format).
void EncodeTo(std::string* dst) const;
Status DecodeFrom(const Slice& input);
};
// --- Padding helpers ---
// Compute padding needed to reach the next 4-byte-aligned offset,
// choosing the pad byte value to differ from last_meaningful_byte.
// Appends padding bytes to dst.
void ComputeBlogPadding(uint8_t last_meaningful_byte, size_t current_offset,
std::string* dst);
// Same as above, but returns the pad byte value and count without
// allocating. For use on hot paths.
void ComputeBlogPaddingParams(uint8_t last_meaningful_byte,
size_t current_offset, uint8_t* pad_byte,
size_t* pad_count);
// --- Checksum helpers ---
// Compute a context-aware checksum over data + compression_type byte,
// suitable for a blog record trailer. Uses the same algorithm and context
// checksum approach as block-based SST files.
uint32_t ComputeBlogRecordChecksum(ChecksumType type, const char* data,
size_t data_size, char compression_type_byte,
uint32_t base_context_checksum,
uint64_t record_offset);
// Verify a blog record's 5-byte trailer (compression_type + checksum).
// payload points to the record payload of payload_size bytes; the trailer
// immediately follows. Returns OK if checksum matches, Corruption otherwise.
// On success, *actual_compression_type is set from the trailer.
Status VerifyBlogRecordTrailer(ChecksumType checksum_type, const char* payload,
size_t payload_size, uint32_t incarnation_id,
uint64_t payload_file_offset,
CompressionType* actual_compression_type);
// Encode an irregular varint: a value encoded with more bytes than the
// minimum required, ensuring varint length > 3 bytes to trigger full
// record format. The value is encoded correctly and will decode to the
// same value via standard GetVarint64.
void PutBlogIrregularVarint64(std::string* dst, uint64_t value,
size_t min_encoded_bytes);
} // namespace ROCKSDB_NAMESPACE
+539
View File
@@ -0,0 +1,539 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "db/blog/blog_format.h"
#include <cstdint>
#include <cstring>
#include <string>
#include "table/format.h"
#include "test_util/testharness.h"
#include "util/coding.h"
namespace ROCKSDB_NAMESPACE {
class BlogFormatTest : public testing::Test {};
// --- Property encoding/decoding ---
TEST_F(BlogFormatTest, EncodeDecodePropertiesEmpty) {
BlogPropertyMap props;
std::string encoded;
EncodeBlogProperties(props, &encoded);
ASSERT_EQ(encoded.size(), 0u);
BlogPropertyMap decoded;
Slice input(encoded);
ASSERT_OK(DecodeBlogProperties(&input, &decoded));
ASSERT_TRUE(decoded.empty());
}
TEST_F(BlogFormatTest, EncodeDecodePropertiesMultiple) {
BlogPropertyMap props = {
{"CompressionCompatibilityName", "zstd"},
{"role", "wal"},
{"compressionSettings", "level=3"},
};
std::string encoded;
EncodeBlogProperties(props, &encoded);
ASSERT_GT(encoded.size(), 0u);
BlogPropertyMap decoded;
Slice input(encoded);
ASSERT_OK(DecodeBlogProperties(&input, &decoded));
ASSERT_EQ(decoded.size(), 3u);
ASSERT_EQ(decoded[0].first, "CompressionCompatibilityName");
ASSERT_EQ(decoded[0].second, "zstd");
ASSERT_EQ(decoded[1].first, "role");
ASSERT_EQ(decoded[1].second, "wal");
ASSERT_EQ(decoded[2].first, "compressionSettings");
ASSERT_EQ(decoded[2].second, "level=3");
}
TEST_F(BlogFormatTest, DecodePropertiesTruncated) {
// Write a name but no value
std::string encoded;
PutLengthPrefixedSlice(&encoded, "key");
// Truncate before value
BlogPropertyMap decoded;
Slice input(encoded);
ASSERT_TRUE(DecodeBlogProperties(&input, &decoded).IsCorruption());
}
// --- IsBlogRequiredProperty ---
TEST_F(BlogFormatTest, IsBlogRequiredProperty) {
ASSERT_TRUE(IsBlogRequiredProperty("CompressionCompatibilityName"));
ASSERT_TRUE(IsBlogRequiredProperty("TimestampSize"));
ASSERT_TRUE(IsBlogRequiredProperty("A"));
ASSERT_FALSE(IsBlogRequiredProperty("role"));
ASSERT_FALSE(IsBlogRequiredProperty("compressionSettings"));
ASSERT_FALSE(IsBlogRequiredProperty("a"));
ASSERT_FALSE(IsBlogRequiredProperty(""));
}
// --- Escape sequence generation ---
TEST_F(BlogFormatTest, GenerateRandomFieldsConstraints) {
// Generate many escape sequences and verify constraints
for (int i = 0; i < 100; ++i) {
BlogFileHeader header;
header.GenerateRandomFields();
// First byte must not be 0x00 or 0xFF
uint8_t first = static_cast<uint8_t>(header.escape_sequence[0]);
ASSERT_NE(first, 0x00u) << "iteration " << i;
ASSERT_NE(first, 0xFFu) << "iteration " << i;
// Derived bytes must match
ASSERT_TRUE(BlogFileHeader::VerifyEscapeSequence(header.escape_sequence))
<< "iteration " << i;
}
}
TEST_F(BlogFormatTest, IncarnationIdNonzero) {
BlogFileHeader header;
header.GenerateRandomFields();
// incarnation_id is first 4 bytes little-endian; since byte[0] is not 0,
// the incarnation_id must be nonzero.
ASSERT_NE(header.incarnation_id(), 0u);
}
TEST_F(BlogFormatTest, VerifyEscapeSequenceRejectsInvalid) {
char seq[kBlogEscapeSequenceSize] = {};
// All zeros: first byte is 0x00 -> invalid
ASSERT_FALSE(BlogFileHeader::VerifyEscapeSequence(seq));
// Valid first byte but wrong derived part
seq[0] = 0x42;
seq[1] = 0x13;
// Derived part is random garbage, likely won't match
ASSERT_FALSE(BlogFileHeader::VerifyEscapeSequence(seq));
}
TEST_F(BlogFormatTest, HeuristicEscapeSequenceDetection) {
// Generate a valid escape sequence and verify we can detect it
// heuristically (by checking derived bytes against random part).
BlogFileHeader header;
header.GenerateRandomFields();
// VerifyEscapeSequence does exactly this heuristic check
ASSERT_TRUE(BlogFileHeader::VerifyEscapeSequence(header.escape_sequence));
// Corrupt one random byte -> should fail verification
char corrupted[kBlogEscapeSequenceSize];
memcpy(corrupted, header.escape_sequence, kBlogEscapeSequenceSize);
corrupted[2] ^= 0x42;
// Might still pass if the corruption happens to produce a valid derived
// part, but astronomically unlikely
// (We don't ASSERT_FALSE here because of the tiny probability)
}
// --- BlogFileHeader encode/decode ---
TEST_F(BlogFormatTest, HeaderEncodeDecodeMinimal) {
BlogFileHeader header;
header.checksum_type = kXXH3;
header.compact_record_type = kBlogWriteBatchRecord;
header.GenerateRandomFields();
std::string encoded;
header.EncodeTo(&encoded);
ASSERT_GE(encoded.size(), kBlogFileFixedHeaderSize);
BlogFileHeader decoded;
Slice input(encoded);
ASSERT_OK(decoded.DecodeFrom(&input));
ASSERT_EQ(decoded.schema_version, header.schema_version);
ASSERT_EQ(decoded.checksum_type, header.checksum_type);
ASSERT_EQ(decoded.compact_record_type, header.compact_record_type);
ASSERT_EQ(memcmp(decoded.escape_sequence, header.escape_sequence,
kBlogEscapeSequenceSize),
0);
ASSERT_TRUE(decoded.properties.empty());
ASSERT_EQ(input.size(), 0u); // all bytes consumed
}
TEST_F(BlogFormatTest, HeaderEncodeDecodeWithProperties) {
BlogFileHeader header;
header.checksum_type = kCRC32c;
header.compact_record_type = kBlogBlobRecord;
header.GenerateRandomFields();
header.SetProperty("CompressionCompatibilityName", "lz4");
header.SetProperty("role", "blob");
header.SetProperty("compressionSettings", "level=1");
std::string encoded;
header.EncodeTo(&encoded);
BlogFileHeader decoded;
Slice input(encoded);
ASSERT_OK(decoded.DecodeFrom(&input));
ASSERT_EQ(decoded.GetProperty("CompressionCompatibilityName"), "lz4");
ASSERT_EQ(decoded.GetProperty("role"), "blob");
ASSERT_EQ(decoded.GetProperty("compressionSettings"), "level=1");
ASSERT_EQ(input.size(), 0u);
}
TEST_F(BlogFormatTest, HeaderRejectsUnknownRequiredProperty) {
BlogFileHeader header;
header.GenerateRandomFields();
header.SetProperty("FutureRequired", "something");
std::string encoded;
header.EncodeTo(&encoded);
BlogFileHeader decoded;
Slice input(encoded);
Status s = decoded.DecodeFrom(&input);
ASSERT_TRUE(s.IsNotSupported()) << s.ToString();
}
TEST_F(BlogFormatTest, HeaderRejectsNewerSchemaVersion) {
BlogFileHeader header;
header.GenerateRandomFields();
std::string encoded;
header.EncodeTo(&encoded);
// Tamper with schema version byte (offset 12) to a future version.
// Must also recompute the fixed header checksum (bytes [36-39]) with
// context modifier using the incarnation_id.
encoded[12] = static_cast<char>(kBlogCurrentSchemaVersion + 1);
uint32_t new_checksum =
ComputeBuiltinChecksum(header.checksum_type, encoded.data(), 36) +
ChecksumModifierForContext(header.incarnation_id(), 0);
EncodeFixed32(&encoded[36], new_checksum);
BlogFileHeader decoded;
Slice input(encoded);
Status s = decoded.DecodeFrom(&input);
ASSERT_TRUE(s.IsNotSupported()) << s.ToString();
ASSERT_NE(s.ToString().find("newer than supported"), std::string::npos)
<< s.ToString();
}
TEST_F(BlogFormatTest, HeaderCurrentSchemaVersionAccepted) {
BlogFileHeader header;
header.GenerateRandomFields();
ASSERT_EQ(header.schema_version, kBlogCurrentSchemaVersion);
std::string encoded;
header.EncodeTo(&encoded);
BlogFileHeader decoded;
Slice input(encoded);
ASSERT_OK(decoded.DecodeFrom(&input));
ASSERT_EQ(decoded.schema_version, kBlogCurrentSchemaVersion);
}
TEST_F(BlogFormatTest, TypedPropertyRoundTrip) {
BlogFileHeader header;
header.GenerateRandomFields();
header.SetUint64Property("fileNumber", 12345678ULL);
header.SetUint32Property("columnFamilyId", 42);
std::string encoded;
header.EncodeTo(&encoded);
BlogFileHeader decoded;
Slice input(encoded);
ASSERT_OK(decoded.DecodeFrom(&input));
uint64_t fn = 0;
ASSERT_TRUE(decoded.GetUint64Property("fileNumber", &fn));
ASSERT_EQ(fn, 12345678ULL);
uint32_t cf_id = 0;
ASSERT_TRUE(decoded.GetUint32Property("columnFamilyId", &cf_id));
ASSERT_EQ(cf_id, 42u);
// Missing property returns false
uint64_t missing = 0;
ASSERT_FALSE(decoded.GetUint64Property("nonexistent", &missing));
}
TEST_F(BlogFormatTest, HeaderAcceptsUnknownIgnorableProperty) {
BlogFileHeader header;
header.GenerateRandomFields();
header.SetProperty("futureIgnorable", "something");
std::string encoded;
header.EncodeTo(&encoded);
BlogFileHeader decoded;
Slice input(encoded);
ASSERT_OK(decoded.DecodeFrom(&input));
ASSERT_EQ(decoded.GetProperty("futureIgnorable"), "something");
}
TEST_F(BlogFormatTest, HeaderChecksumCorruption) {
BlogFileHeader header;
header.GenerateRandomFields();
std::string encoded;
header.EncodeTo(&encoded);
// Corrupt a byte in the fixed header
encoded[14] ^= 0xFF;
BlogFileHeader decoded;
Slice input(encoded);
ASSERT_TRUE(decoded.DecodeFrom(&input).IsCorruption());
}
TEST_F(BlogFormatTest, HeaderPropertyChecksumCorruption) {
BlogFileHeader header;
header.GenerateRandomFields();
header.SetProperty("role", "wal");
std::string encoded;
header.EncodeTo(&encoded);
// Corrupt a byte in the property section (after fixed header)
if (encoded.size() > kBlogFileFixedHeaderSize + 2) {
encoded[kBlogFileFixedHeaderSize + 2] ^= 0xFF;
}
BlogFileHeader decoded;
Slice input(encoded);
Status s = decoded.DecodeFrom(&input);
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
}
TEST_F(BlogFormatTest, HeaderSetPropertyOverwrites) {
BlogFileHeader header;
header.SetProperty("role", "wal");
ASSERT_EQ(header.GetProperty("role"), "wal");
header.SetProperty("role", "blob");
ASSERT_EQ(header.GetProperty("role"), "blob");
// Should not have duplicate entries
int count = 0;
for (const auto& [k, v] : header.properties) {
if (k == "role") {
++count;
}
}
ASSERT_EQ(count, 1);
}
TEST_F(BlogFormatTest, HeaderMagicValues) {
BlogFileHeader header;
header.GenerateRandomFields();
std::string encoded;
header.EncodeTo(&encoded);
// Verify magic bytes
ASSERT_GE(encoded.size(), kBlogFileMagicSize);
ASSERT_EQ(memcmp(encoded.data(), kBlogFileMagic, kBlogFileMagicSize), 0);
}
// --- Footer locator ---
TEST_F(BlogFormatTest, FooterLocatorEncodeDecodeEmpty) {
BlogFileFooterLocator locator;
std::string encoded;
locator.EncodeTo(&encoded);
ASSERT_EQ(encoded.size(), 0u);
BlogFileFooterLocator decoded;
ASSERT_OK(decoded.DecodeFrom(Slice(encoded)));
ASSERT_TRUE(decoded.entries.empty());
}
TEST_F(BlogFormatTest, FooterLocatorEncodeDecodeMultiple) {
BlogFileFooterLocator locator;
locator.entries.push_back({kBlogFooterPropertiesRecord, 50});
locator.entries.push_back({kBlogFooterIndexRecord, 85});
std::string encoded;
locator.EncodeTo(&encoded);
ASSERT_EQ(encoded.size(), 10u); // 2 * (1 + 4)
BlogFileFooterLocator decoded;
ASSERT_OK(decoded.DecodeFrom(Slice(encoded)));
ASSERT_EQ(decoded.entries.size(), 2u);
ASSERT_EQ(decoded.entries[0].record_type, kBlogFooterPropertiesRecord);
ASSERT_EQ(decoded.entries[0].relative_offset_4B, 50u);
ASSERT_EQ(decoded.entries[1].record_type, kBlogFooterIndexRecord);
ASSERT_EQ(decoded.entries[1].relative_offset_4B, 85u);
}
// --- Footer properties ---
TEST_F(BlogFormatTest, FooterPropertiesEncodeDecodeRoundTrip) {
BlogFileFooterProperties props;
props.SetBlobCount(42);
props.SetTotalBlobBytes(123456);
props.SetSequenceRange(100, 200);
std::string encoded;
props.EncodeTo(&encoded);
BlogFileFooterProperties decoded;
ASSERT_OK(decoded.DecodeFrom(Slice(encoded)));
ASSERT_EQ(decoded.properties.size(), 4u);
}
// --- Padding ---
TEST_F(BlogFormatTest, PaddingLastByteZero) {
// When last byte is 0x00, must pad with 0xFF, at least 1 byte
std::string pad;
// At offset 0 (already aligned), still need at least 1 byte
ComputeBlogPadding(0x00, 0, &pad);
ASSERT_GE(pad.size(), 1u);
for (char c : pad) {
ASSERT_EQ(static_cast<uint8_t>(c), 0xFFu);
}
// Result must be 4-byte aligned
ASSERT_EQ(pad.size() % 4, 0u);
}
TEST_F(BlogFormatTest, PaddingLastByteFF) {
// When last byte is 0xFF, must pad with 0x00, at least 1 byte
std::string pad;
ComputeBlogPadding(0xFF, 0, &pad);
ASSERT_GE(pad.size(), 1u);
for (char c : pad) {
ASSERT_EQ(static_cast<uint8_t>(c), 0x00u);
}
ASSERT_EQ(pad.size() % 4, 0u);
}
TEST_F(BlogFormatTest, PaddingLastByteOther) {
// When last byte is something else, pad only if needed for alignment
std::string pad;
ComputeBlogPadding(0x42, 0, &pad);
// Already aligned, no padding needed
ASSERT_EQ(pad.size(), 0u);
pad.clear();
ComputeBlogPadding(0x42, 1, &pad);
// Need 3 bytes to reach alignment
ASSERT_EQ(pad.size(), 3u);
for (char c : pad) {
ASSERT_EQ(static_cast<uint8_t>(c), 0x00u);
}
}
TEST_F(BlogFormatTest, PaddingAlignment) {
for (size_t offset = 0; offset < 8; ++offset) {
std::string pad;
ComputeBlogPadding(0x42, offset, &pad);
size_t after = offset + pad.size();
ASSERT_EQ(after % 4, 0u) << "offset=" << offset;
}
}
// --- Irregular varint ---
TEST_F(BlogFormatTest, IrregularVarintZero) {
std::string encoded;
PutBlogIrregularVarint64(&encoded, 0, 4);
ASSERT_GE(encoded.size(), 4u);
// Must decode back to 0
Slice input(encoded);
uint64_t value;
ASSERT_TRUE(GetVarint64(&input, &value));
ASSERT_EQ(value, 0u);
}
TEST_F(BlogFormatTest, IrregularVarintSmallValue) {
std::string encoded;
PutBlogIrregularVarint64(&encoded, 42, 4);
ASSERT_GE(encoded.size(), 4u);
Slice input(encoded);
uint64_t value;
ASSERT_TRUE(GetVarint64(&input, &value));
ASSERT_EQ(value, 42u);
}
TEST_F(BlogFormatTest, IrregularVarintLargeValue) {
// A value that naturally needs 3 bytes, padded to 5
uint64_t original = 100000;
std::string encoded;
PutBlogIrregularVarint64(&encoded, original, 5);
ASSERT_GE(encoded.size(), 5u);
Slice input(encoded);
uint64_t value;
ASSERT_TRUE(GetVarint64(&input, &value));
ASSERT_EQ(value, original);
}
TEST_F(BlogFormatTest, IrregularVarintNaturallyLargeEnough) {
// A value that naturally needs 5+ bytes should not be padded further
uint64_t original = 1ULL << 35;
std::string encoded;
PutBlogIrregularVarint64(&encoded, original, 4);
// Natural encoding is 6 bytes, already >= 4
ASSERT_EQ(encoded.size(), 6u);
Slice input(encoded);
uint64_t value;
ASSERT_TRUE(GetVarint64(&input, &value));
ASSERT_EQ(value, original);
}
TEST_F(BlogFormatTest, UnspecifiedSizeVarint) {
std::string encoded;
PutVarint64(&encoded, kBlogUnspecifiedSize);
// 2^63 - 1 should encode to 9 bytes (63 bits / 7 = 9)
ASSERT_EQ(encoded.size(), 9u);
Slice input(encoded);
uint64_t value;
ASSERT_TRUE(GetVarint64(&input, &value));
ASSERT_EQ(value, kBlogUnspecifiedSize);
}
// --- Checksum ---
TEST_F(BlogFormatTest, ComputeBlogRecordChecksumBasic) {
const char* data = "hello world";
size_t size = strlen(data);
// With no context (incarnation_id = 0), should match standard checksum
uint32_t cksum = ComputeBlogRecordChecksum(
kXXH3, data, size, static_cast<char>(kNoCompression), 0, 0);
uint32_t expected = ComputeBuiltinChecksumWithLastByte(
kXXH3, data, size, static_cast<char>(kNoCompression));
ASSERT_EQ(cksum, expected);
}
TEST_F(BlogFormatTest, ComputeBlogRecordChecksumWithContext) {
const char* data = "hello world";
size_t size = strlen(data);
uint32_t cksum1 = ComputeBlogRecordChecksum(
kXXH3, data, size, static_cast<char>(kNoCompression), 0x12345678, 100);
uint32_t cksum2 = ComputeBlogRecordChecksum(
kXXH3, data, size, static_cast<char>(kNoCompression), 0x12345678, 200);
// Different offsets should produce different checksums
ASSERT_NE(cksum1, cksum2);
// Different incarnation IDs should produce different checksums
uint32_t cksum3 = ComputeBlogRecordChecksum(
kXXH3, data, size, static_cast<char>(kNoCompression), 0xABCDEF01, 100);
ASSERT_NE(cksum1, cksum3);
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
+388
View File
@@ -0,0 +1,388 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "db/blog/blog_reader.h"
#include <cassert>
#include <cstring>
#include "table/format.h"
#include "util/coding.h"
namespace ROCKSDB_NAMESPACE {
BlogFileReader::BlogFileReader(std::unique_ptr<SequentialFileReader>&& file,
Reporter* reporter, bool verify_checksums)
: seq_file_(std::move(file)),
reporter_(reporter),
verify_checksums_(verify_checksums) {}
Status BlogFileReader::ReadHeader(BlogFileHeader* header) {
assert(!header_read_);
// Read the fixed header to get property_section_size
std::string fixed_buf;
Slice fixed_slice;
Status s = ReadBytes(kBlogFileFixedHeaderSize, &fixed_slice, &fixed_buf);
if (!s.ok()) {
return s;
}
// Peek at property_section_size from bytes [32-35]
uint32_t prop_section_size = DecodeFixed32(fixed_slice.data() + 32);
// Read the property section
std::string prop_buf;
Slice prop_slice;
s = ReadBytes(prop_section_size, &prop_slice, &prop_buf);
if (!s.ok()) {
return s;
}
// Combine into a single buffer for decoding
std::string full_header;
full_header.append(fixed_slice.data(), fixed_slice.size());
full_header.append(prop_slice.data(), prop_slice.size());
Slice input(full_header);
s = header->DecodeFrom(&input);
if (!s.ok()) {
return s;
}
header_ = *header;
header_read_ = true;
// Skip padding after header to reach the first record's escape sequence.
// Padding may be arbitrarily long (for stronger alignment or recycled
// files of all zeros). EOF here is fine (header-only file, no records).
s = SkipPadding();
if (!s.ok() && !s.IsNotFound()) {
return s;
}
return Status::OK();
}
Status BlogFileReader::ReadRecord(BlogRecordType* type, Slice* payload,
std::string* scratch, uint64_t* record_offset,
CompressionType* record_compression_type) {
if (!header_read_) {
return Status::Corruption("Blog reader: header not read");
}
if (eof_) {
return Status::NotFound();
}
Status s;
scratch->clear();
// Read escape sequence. If we have a pending prefix from header padding
// skip or inter-record padding skip, use it as the first 4 bytes.
std::string esc_buf;
if (!pending_escape_prefix_.empty()) {
esc_buf = std::move(pending_escape_prefix_);
pending_escape_prefix_.clear();
Slice rest_slice;
std::string rest_buf;
s = ReadBytes(kBlogEscapeSequenceSize - 4, &rest_slice, &rest_buf);
if (!s.ok()) {
eof_ = true;
// Short read after we had a pending prefix = incomplete trailing record
return Status::Incomplete("Blog reader: incomplete escape sequence");
}
esc_buf.append(rest_slice.data(), rest_slice.size());
} else {
Slice esc_slice;
s = ReadBytes(kBlogEscapeSequenceSize, &esc_slice, &esc_buf);
if (!s.ok()) {
eof_ = true;
return Status::NotFound(); // clean EOF
}
if (esc_slice.data() != esc_buf.data()) {
esc_buf.assign(esc_slice.data(), esc_slice.size());
}
}
// Verify escape sequence matches
if (memcmp(esc_buf.data(), header_.escape_sequence,
kBlogEscapeSequenceSize) != 0) {
return ReportCorruption(kBlogEscapeSequenceSize,
"escape sequence mismatch");
}
uint64_t rec_offset = offset_ - kBlogEscapeSequenceSize;
if (record_offset) {
*record_offset = rec_offset;
}
last_record_offset_ = rec_offset;
// Read varint length. scratch receives the raw varint bytes for use
// in prefix checksum verification (avoids fragile re-encoding).
uint64_t length = 0;
s = ReadVarint64(&length, scratch);
if (!s.ok()) {
eof_ = true;
return Status::Incomplete("Blog reader: incomplete varint");
}
size_t varint_len = scratch->size();
bool is_full_format = (varint_len > kBlogCompactVarintMaxBytes);
BlogRecordType rec_type;
std::string prefix_rest_buf;
if (is_full_format) {
// Full format: read type + compression_type + prefix_checksum
Slice prefix_rest_slice;
s = ReadBytes(6, &prefix_rest_slice, &prefix_rest_buf); // 1+1+4
if (!s.ok()) {
eof_ = true;
return Status::Incomplete("Blog reader: incomplete full record prefix");
}
rec_type =
static_cast<BlogRecordType>(static_cast<uint8_t>(prefix_rest_slice[0]));
if (verify_checksums_) {
// Build the prefix data from the original varint bytes (in scratch)
// plus type and compression_type bytes.
scratch->push_back(prefix_rest_slice[0]); // type
scratch->push_back(prefix_rest_slice[1]); // comp_type
uint32_t stored_prefix_checksum =
DecodeFixed32(prefix_rest_slice.data() + 2);
uint32_t computed_prefix_checksum =
ComputeBuiltinChecksum(header_.checksum_type, scratch->data(),
scratch->size()) +
ChecksumModifierForContext(header_.incarnation_id(), rec_offset);
if (stored_prefix_checksum != computed_prefix_checksum) {
return ReportCorruption(varint_len + 6, "prefix checksum mismatch");
}
}
} else {
rec_type = header_.compact_record_type;
}
*type = rec_type;
if (length == 0) {
payload->clear();
if (record_compression_type) {
*record_compression_type = kNoCompression;
}
s = SkipPadding();
if (!s.ok() && !s.IsNotFound()) {
return s;
}
return Status::OK();
}
if (length == kBlogUnspecifiedSize) {
return ReportCorruption(0, "unspecified-size records not yet implemented");
}
// Read payload
scratch->clear();
Slice payload_slice;
s = ReadBytes(static_cast<size_t>(length), &payload_slice, scratch);
if (!s.ok()) {
eof_ = true;
return Status::Incomplete("Blog reader: incomplete payload");
}
if (payload_slice.data() != scratch->data()) {
scratch->assign(payload_slice.data(), payload_slice.size());
}
// Read 5-byte trailer
Slice trailer_slice;
std::string trailer_buf;
s = ReadBytes(kBlogBlockTrailerSize, &trailer_slice, &trailer_buf);
if (!s.ok()) {
eof_ = true;
return Status::Incomplete("Blog reader: incomplete trailer");
}
char trailer_comp_type = trailer_slice[0];
uint32_t stored_checksum = DecodeFixed32(trailer_slice.data() + 1);
if (verify_checksums_) {
uint32_t computed_checksum = ComputeBlogRecordChecksum(
header_.checksum_type, scratch->data(), scratch->size(),
trailer_comp_type, header_.incarnation_id(),
offset_ - kBlogBlockTrailerSize - length);
if (stored_checksum != computed_checksum) {
return ReportCorruption(
static_cast<size_t>(length) + kBlogBlockTrailerSize,
"record checksum mismatch");
}
}
*payload = Slice(*scratch);
if (record_compression_type) {
*record_compression_type =
static_cast<CompressionType>(static_cast<uint8_t>(trailer_comp_type));
}
// Skip padding to next record.
s = SkipPadding();
if (!s.ok() && !s.IsNotFound()) {
return s;
}
return Status::OK();
}
Status BlogFileReader::ScanForEscapeSequence(uint64_t* found_offset) {
// Scan forward 4 bytes at a time (escape sequences are 32-bit aligned).
// Check first 4 bytes of escape sequence, then verify remaining 6 bytes.
uint32_t first4;
memcpy(&first4, header_.escape_sequence, 4);
std::string buf;
// Align to 4-byte boundary first
size_t remainder = offset_ % 4;
if (remainder != 0) {
size_t skip = 4 - remainder;
Slice skip_slice;
Status s = ReadBytes(skip, &skip_slice, &buf);
if (!s.ok()) {
return s;
}
}
while (!eof_) {
Slice chunk_slice;
Status s = ReadBytes(4, &chunk_slice, &buf);
if (!s.ok()) {
if (eof_) {
return Status::NotFound("escape sequence not found before EOF");
}
return s;
}
uint32_t candidate;
memcpy(&candidate, chunk_slice.data(), 4);
if (candidate == first4) {
// Potential match - verify remaining 6 bytes
uint64_t candidate_offset = offset_ - 4;
Slice rest_slice;
std::string rest_buf;
s = ReadBytes(kBlogEscapeSequenceSize - 4, &rest_slice, &rest_buf);
if (!s.ok()) {
return s;
}
if (memcmp(rest_slice.data(), header_.escape_sequence + 4,
kBlogEscapeSequenceSize - 4) == 0) {
*found_offset = candidate_offset;
return Status::OK();
}
// False match, continue scanning. But we consumed 6 extra bytes that
// might not be aligned. Re-align.
remainder = offset_ % 4;
if (remainder != 0) {
size_t skip = 4 - remainder;
Slice skip_s;
s = ReadBytes(skip, &skip_s, &buf);
if (!s.ok()) {
return s;
}
}
}
}
return Status::NotFound("escape sequence not found before EOF");
}
Status BlogFileReader::SkipPadding() {
// Skip arbitrarily long padding (0x00 or 0xFF bytes). Padding is
// 32-bit aligned. Since the escape sequence's first byte is guaranteed
// to be neither 0x00 nor 0xFF, we align to 4 bytes then scan 4-byte
// chunks until we find one that starts with a non-padding byte.
size_t remainder = offset_ % 4;
if (remainder != 0) {
Slice skip_slice;
std::string skip_buf;
Status s = ReadBytes(4 - remainder, &skip_slice, &skip_buf);
if (!s.ok()) {
if (eof_) {
return Status::NotFound();
}
return s;
}
}
while (true) {
Slice chunk_slice;
std::string chunk_buf;
Status s = ReadBytes(4, &chunk_slice, &chunk_buf);
if (!s.ok()) {
if (eof_) {
return Status::NotFound();
}
return s;
}
uint8_t first = static_cast<uint8_t>(chunk_slice[0]);
if (first != 0x00 && first != 0xFF) {
// Found the start of the next escape sequence.
pending_escape_prefix_.assign(chunk_slice.data(), 4);
return Status::OK();
}
}
}
Status BlogFileReader::ReadBytes(size_t n, Slice* result,
std::string* scratch) {
scratch->resize(n);
Slice read_result;
IOStatus ios =
seq_file_->Read(n, &read_result, scratch->data(), Env::IO_TOTAL);
if (!ios.ok()) {
return static_cast<Status>(ios);
}
if (read_result.size() < n) {
eof_ = true;
// Return Incomplete -- the caller (ReadRecord) determines whether
// this is an acceptable incomplete tail vs. genuine corruption.
return Status::Incomplete("Blog reader", "unexpected EOF");
}
offset_ += n;
*result = Slice(scratch->data(), n);
return Status::OK();
}
// FIXME? Consider a better encoding that could be processed more efficiently
Status BlogFileReader::ReadVarint64(uint64_t* value, std::string* scratch) {
// Varints can be up to 10 bytes. Read one byte at a time.
// The raw varint bytes are appended to scratch so the caller can use
// them for checksum verification without re-encoding.
scratch->clear();
std::string byte_buf;
for (int i = 0; i < 10; ++i) {
Slice byte_slice;
Status s = ReadBytes(1, &byte_slice, &byte_buf);
if (!s.ok()) {
return s;
}
scratch->push_back(byte_slice[0]);
// Check if this is the terminal byte (high bit not set)
if ((static_cast<uint8_t>(byte_slice[0]) & 0x80) == 0) {
Slice varint_slice(*scratch);
if (!GetVarint64(&varint_slice, value)) {
return ReportCorruption(scratch->size(), "invalid varint");
}
return Status::OK();
}
}
return ReportCorruption(10, "varint too long");
}
Status BlogFileReader::ReportCorruption(size_t bytes, const std::string& msg) {
Status s = Status::Corruption("Blog reader", msg);
if (reporter_) {
reporter_->Corruption(bytes, s);
}
return s;
}
} // namespace ROCKSDB_NAMESPACE
+101
View File
@@ -0,0 +1,101 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include "db/blog/blog_format.h"
#include "file/random_access_file_reader.h"
#include "file/sequence_file_reader.h"
#include "rocksdb/slice.h"
#include "rocksdb/status.h"
namespace ROCKSDB_NAMESPACE {
// Reads blog-format files sequentially (WAL replay) or at random offsets
// (blob lookups). Supports footer reading by backward scanning.
class BlogFileReader {
public:
// Error reporter, same pattern as log::Reader::Reporter.
class Reporter {
public:
virtual ~Reporter() = default;
virtual void Corruption(size_t bytes, const Status& status) = 0;
};
// Construct for sequential reading (WAL replay, forward scan).
BlogFileReader(std::unique_ptr<SequentialFileReader>&& file,
Reporter* reporter, bool verify_checksums);
~BlogFileReader() = default;
// Take ownership of a reporter (e.g., an adapter wrapping another type).
void SetOwnedReporter(std::unique_ptr<Reporter> reporter) {
owned_reporter_ = std::move(reporter);
reporter_ = owned_reporter_.get();
}
// Read and validate the file header. Must be called first.
Status ReadHeader(BlogFileHeader* header);
// Read the next record sequentially. Returns:
// OK - record read successfully
// NotFound - clean EOF (no more records, only padding/zeros remain)
// Incomplete - partial record at tail (crash mid-write)
// Corruption - data integrity error
// On OK, *type is the record type and *payload contains the payload.
// *scratch is used as backing storage for payload.
Status ReadRecord(BlogRecordType* type, Slice* payload, std::string* scratch,
uint64_t* record_offset = nullptr,
CompressionType* record_compression_type = nullptr);
// Scan forward from the current position for the next occurrence of
// the file's escape sequence. Used for WAL recovery and unspecified-
// size record parsing.
// On success, *found_offset is the file offset of the escape sequence.
Status ScanForEscapeSequence(uint64_t* found_offset);
bool IsEOF() const { return eof_; }
uint64_t LastRecordOffset() const { return last_record_offset_; }
uint64_t current_offset() const { return offset_; }
const BlogFileHeader& header() const { return header_; }
private:
// Skip arbitrarily long padding (0x00 or 0xFF bytes) after a record or
// header. Aligns to 4-byte boundary first, then scans 4-byte chunks
// until finding one whose first byte is not 0x00/0xFF. Stashes that
// chunk in pending_escape_prefix_ for the next ReadRecord call.
// Returns OK on success, NotFound on clean EOF (no more records),
// or an error Status on I/O failure.
Status SkipPadding();
// Read exactly n bytes sequentially. Returns Corruption on short read.
Status ReadBytes(size_t n, Slice* result, std::string* scratch);
// Read a varint64 from the sequential stream.
Status ReadVarint64(uint64_t* value, std::string* scratch);
// Report corruption and return a Corruption status.
Status ReportCorruption(size_t bytes, const std::string& msg);
std::unique_ptr<SequentialFileReader> seq_file_;
Reporter* reporter_;
std::unique_ptr<Reporter> owned_reporter_; // optional, for adapter lifetime
bool verify_checksums_;
BlogFileHeader header_;
bool header_read_ = false;
bool eof_ = false;
uint64_t offset_ = 0;
uint64_t last_record_offset_ = 0;
// First 4 bytes of escape sequence consumed during header padding skip.
// ReadRecord uses these as the beginning of the first escape sequence.
std::string pending_escape_prefix_;
};
} // namespace ROCKSDB_NAMESPACE
+320
View File
@@ -0,0 +1,320 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "db/blog/blog_writer.h"
#include <cassert>
#include "table/format.h"
#include "util/coding.h"
#include "util/string_util.h"
namespace ROCKSDB_NAMESPACE {
BlogFileWriter::BlogFileWriter(std::unique_ptr<WritableFileWriter>&& dest,
const BlogFileHeader& header, bool manual_flush)
: dest_(std::move(dest)), header_(header), manual_flush_(manual_flush) {}
BlogFileWriter::~BlogFileWriter() = default;
IOStatus BlogFileWriter::WriteHeader(const WriteOptions& wo) {
assert(!header_written_);
std::string buf;
header_.EncodeTo(&buf);
// Pad after header properties using the same padding scheme as records.
uint8_t last_byte = static_cast<uint8_t>(buf.back());
ComputeBlogPadding(last_byte, buf.size(), &buf);
IOStatus s = EmitBytes(wo, buf);
if (s.ok()) {
header_written_ = true;
}
if (s.ok() && !manual_flush_) {
IOOptions opts;
s = WritableFileWriter::PrepareIOOptions(wo, opts);
if (s.ok()) {
s = dest_->Flush(opts);
}
}
return s;
}
IOStatus BlogFileWriter::AddBlobRecord(const WriteOptions& wo,
const Slice& payload,
CompressionType comp_type,
uint64_t* blob_offset) {
return AddRecord(wo, kBlogBlobRecord, payload, comp_type, blob_offset);
}
IOStatus BlogFileWriter::AddWriteBatchRecord(const WriteOptions& wo,
const Slice& wb_data,
CompressionType comp_type) {
// Stub: WriteBatch record writing is not yet integrated (WAL integration
// deferred). The record format is the same as blob records with type 0x03.
return AddRecord(wo, kBlogWriteBatchRecord, wb_data, comp_type, nullptr);
}
IOStatus BlogFileWriter::AddPreambleStartRecord(const WriteOptions& wo) {
return AddRecord(wo, kBlogPreambleStartRecord, Slice(), kNoCompression,
nullptr, /*force_full=*/true);
}
IOStatus BlogFileWriter::AddFooterIndexRecord(const WriteOptions& wo,
const Slice& index_data) {
return AddRecord(wo, kBlogFooterIndexRecord, index_data, kNoCompression,
nullptr, /*force_full=*/true);
}
IOStatus BlogFileWriter::AddFooterPropertiesRecord(
const WriteOptions& wo, const BlogFileFooterProperties& props) {
std::string payload;
props.EncodeTo(&payload);
return AddRecord(wo, kBlogFooterPropertiesRecord, payload, kNoCompression,
nullptr, /*force_full=*/true);
}
IOStatus BlogFileWriter::AddFooterLocatorRecord(
const WriteOptions& wo, const BlogFileFooterLocator& locator) {
std::string payload;
locator.EncodeTo(&payload);
return AddRecord(wo, kBlogFooterLocatorRecord, payload, kNoCompression,
nullptr, /*force_full=*/true);
}
IOStatus BlogFileWriter::WriteBuffer(const WriteOptions& wo) {
IOOptions opts;
IOStatus s = WritableFileWriter::PrepareIOOptions(wo, opts);
if (s.ok()) {
s = dest_->Flush(opts);
}
return s;
}
IOStatus BlogFileWriter::Sync(const WriteOptions& wo, bool use_fsync) {
IOOptions opts;
IOStatus s = WritableFileWriter::PrepareIOOptions(wo, opts);
if (s.ok()) {
s = dest_->Sync(opts, use_fsync);
}
return s;
}
IOStatus BlogFileWriter::Close(const WriteOptions& wo) {
IOOptions opts;
IOStatus s = WritableFileWriter::PrepareIOOptions(wo, opts);
if (s.ok()) {
s = dest_->Close(opts);
}
return s;
}
IOStatus BlogFileWriter::AddRecord(const WriteOptions& wo, BlogRecordType type,
const Slice& payload,
CompressionType comp_type,
uint64_t* payload_offset, bool force_full) {
assert(header_written_);
// Determine varint length of payload size
uint16_t varint_len = VarintLength(payload.size());
// Use compact format when:
// 1. varint fits in <= kBlogCompactVarintMaxBytes (payload < ~16 KiB)
// 2. record type matches the header's compact_record_type
// 3. not forced to full format (e.g. footer/preamble records)
bool use_compact =
!force_full && (varint_len <= kBlogCompactVarintMaxBytes) &&
(type == header_.compact_record_type) && (payload.size() > 0);
IOStatus s;
if (use_compact) {
s = EmitCompactRecord(wo, payload, comp_type, payload_offset);
} else {
s = EmitFullRecord(wo, type, payload, comp_type, payload_offset);
}
if (s.ok() && !manual_flush_) {
IOOptions opts;
s = WritableFileWriter::PrepareIOOptions(wo, opts);
if (s.ok()) {
s = dest_->Flush(opts);
}
}
return s;
}
IOStatus BlogFileWriter::EmitCompactRecord(const WriteOptions& wo,
const Slice& payload,
CompressionType comp_type,
uint64_t* payload_offset) {
// Compact format:
// [escape_seq: 10B] [varint length: 1-3B]
// [payload: length bytes]
// [compression_type: 1B] [checksum: 4B]
// [padding: 0+B]
assert(payload.size() > 0); // length=0 compact records not expected
std::string prefix;
// Escape sequence
prefix.append(header_.escape_sequence, kBlogEscapeSequenceSize);
// Varint length
PutVarint64(&prefix, payload.size());
IOStatus s = EmitBytes(wo, prefix);
if (!s.ok()) {
return s;
}
// Record payload offset for caller
if (payload_offset) {
*payload_offset = offset_;
}
// Payload
s = EmitBytes(wo, payload);
if (!s.ok()) {
return s;
}
// 5-byte trailer: compression_type + checksum
char trailer[kBlogBlockTrailerSize];
trailer[0] = static_cast<char>(comp_type);
uint32_t checksum = ComputeBlogRecordChecksum(
header_.checksum_type, payload.data(), payload.size(), trailer[0],
header_.incarnation_id(),
offset_ - payload.size()); // record offset = start of payload
EncodeFixed32(trailer + 1, checksum);
s = EmitBytes(wo, trailer, kBlogBlockTrailerSize);
if (!s.ok()) {
return s;
}
// Padding (stack-allocated, no heap allocation)
uint8_t last_byte = static_cast<uint8_t>(trailer[kBlogBlockTrailerSize - 1]);
uint8_t pad_byte;
size_t pad_count;
ComputeBlogPaddingParams(last_byte, offset_, &pad_byte, &pad_count);
if (pad_count > 0) {
char pad_buf[4];
memset(pad_buf, pad_byte, pad_count);
s = EmitBytes(wo, pad_buf, pad_count);
}
return s;
}
IOStatus BlogFileWriter::EmitFullRecord(const WriteOptions& wo,
BlogRecordType type,
const Slice& payload,
CompressionType comp_type,
uint64_t* payload_offset) {
// Full format:
// [escape_seq: 10B] [varint length: 4+B] [type: 1B] [compression_type: 1B]
// [prefix_checksum: 4B]
// (if length > 0):
// [payload: length bytes]
// [compression_type: 1B] [checksum: 4B]
// [padding: 0+B]
std::string prefix;
// Escape sequence
prefix.append(header_.escape_sequence, kBlogEscapeSequenceSize);
// Varint length (must be > kBlogCompactVarintMaxBytes to trigger full format)
constexpr size_t kFullVarintMinBytes = kBlogCompactVarintMaxBytes + 1;
size_t varint_start = prefix.size();
if (payload.size() == 0) {
PutBlogIrregularVarint64(&prefix, 0, kFullVarintMinBytes);
} else {
uint16_t natural_len = VarintLength(payload.size());
if (natural_len >= kFullVarintMinBytes) {
PutVarint64(&prefix, payload.size());
} else {
PutBlogIrregularVarint64(&prefix, payload.size(), kFullVarintMinBytes);
}
}
// Type byte
prefix.push_back(static_cast<char>(type));
// Pre-payload compression type
prefix.push_back(static_cast<char>(comp_type));
// Prefix checksum covers (varint length + type + compression_type),
// with context modifier for defense-in-depth.
const char* prefix_data = prefix.data() + varint_start;
size_t prefix_data_size = prefix.size() - varint_start;
uint32_t prefix_checksum =
ComputeBuiltinChecksum(header_.checksum_type, prefix_data,
prefix_data_size) +
ChecksumModifierForContext(header_.incarnation_id(), offset_);
PutFixed32(&prefix, prefix_checksum);
IOStatus s = EmitBytes(wo, prefix);
if (!s.ok()) {
return s;
}
// Track the last meaningful byte for padding computation.
// EncodeFixed32 is little-endian: last byte written = (value >> 24) & 0xFF.
uint8_t last_byte;
if (payload.size() > 0) {
// Record payload offset for caller
if (payload_offset) {
*payload_offset = offset_;
}
// Payload
s = EmitBytes(wo, payload);
if (!s.ok()) {
return s;
}
// 5-byte trailer: compression_type + checksum
char trailer[kBlogBlockTrailerSize];
trailer[0] = static_cast<char>(comp_type);
uint32_t checksum = ComputeBlogRecordChecksum(
header_.checksum_type, payload.data(), payload.size(), trailer[0],
header_.incarnation_id(), offset_ - payload.size());
EncodeFixed32(trailer + 1, checksum);
s = EmitBytes(wo, trailer, kBlogBlockTrailerSize);
if (!s.ok()) {
return s;
}
last_byte = static_cast<uint8_t>(trailer[kBlogBlockTrailerSize - 1]);
} else {
// No payload, no trailer. Last meaningful byte is the last byte of
// prefix_checksum (little-endian).
last_byte = static_cast<uint8_t>(prefix_checksum >> 24);
}
uint8_t pad_byte;
size_t pad_count;
ComputeBlogPaddingParams(last_byte, offset_, &pad_byte, &pad_count);
if (pad_count > 0) {
char pad_buf[4];
memset(pad_buf, pad_byte, pad_count);
s = EmitBytes(wo, pad_buf, pad_count);
}
return s;
}
IOStatus BlogFileWriter::EmitBytes(const WriteOptions& wo, const Slice& data) {
return EmitBytes(wo, data.data(), data.size());
}
IOStatus BlogFileWriter::EmitBytes(const WriteOptions& wo, const char* data,
size_t len) {
IOOptions opts;
IOStatus s = WritableFileWriter::PrepareIOOptions(wo, opts);
if (!s.ok()) {
return s;
}
s = dest_->Append(opts, Slice(data, len));
if (s.ok()) {
offset_ += len;
}
return s;
}
} // namespace ROCKSDB_NAMESPACE
+95
View File
@@ -0,0 +1,95 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include "db/blog/blog_format.h"
#include "file/writable_file_writer.h"
#include "rocksdb/io_status.h"
#include "rocksdb/options.h"
namespace ROCKSDB_NAMESPACE {
// Writes blog-format files (WAL or blob). The caller constructs a
// BlogFileHeader, calls WriteHeader(), then adds records via
// AddBlobRecord/AddWriteBatchRecord, and optionally appends footer records
// before closing.
//
// Format selection (compact vs full) is automatic based on payload size and
// record type. See blog_format.h for the wire format details.
class BlogFileWriter {
public:
BlogFileWriter(std::unique_ptr<WritableFileWriter>&& dest,
const BlogFileHeader& header, bool manual_flush = false);
~BlogFileWriter();
// Write the file header. Must be called exactly once, before any records.
IOStatus WriteHeader(const WriteOptions& wo);
// Write a blob record. Selects compact or full format automatically.
// On success, *blob_offset is the file offset of the payload start.
IOStatus AddBlobRecord(const WriteOptions& wo, const Slice& payload,
CompressionType comp_type, uint64_t* blob_offset);
// Write a WriteBatch record. Selects compact or full format automatically.
IOStatus AddWriteBatchRecord(const WriteOptions& wo, const Slice& wb_data,
CompressionType comp_type = kNoCompression);
// Write a preamble-start record (stub). Uses full format with length=0.
IOStatus AddPreambleStartRecord(const WriteOptions& wo);
// Write a footer index record (full format).
IOStatus AddFooterIndexRecord(const WriteOptions& wo,
const Slice& index_data);
// Write a footer properties record (full format).
IOStatus AddFooterPropertiesRecord(const WriteOptions& wo,
const BlogFileFooterProperties& props);
// Write a footer locator record (full format). Must be the last record.
IOStatus AddFooterLocatorRecord(const WriteOptions& wo,
const BlogFileFooterLocator& locator);
IOStatus WriteBuffer(const WriteOptions& wo);
IOStatus Sync(const WriteOptions& wo, bool use_fsync = false);
IOStatus Close(const WriteOptions& wo);
uint64_t current_offset() const { return offset_; }
WritableFileWriter* file() { return dest_.get(); }
const BlogFileHeader& header() const { return header_; }
private:
// Write a record. Chooses compact format when varint fits in <= 3 bytes
// and type matches compact_record_type, unless force_full is true.
IOStatus AddRecord(const WriteOptions& wo, BlogRecordType type,
const Slice& payload, CompressionType comp_type,
uint64_t* payload_offset, bool force_full = false);
// Emit a compact-format record.
IOStatus EmitCompactRecord(const WriteOptions& wo, const Slice& payload,
CompressionType comp_type,
uint64_t* payload_offset);
// Emit a full-format record.
IOStatus EmitFullRecord(const WriteOptions& wo, BlogRecordType type,
const Slice& payload, CompressionType comp_type,
uint64_t* payload_offset);
// Append raw bytes to the file and advance offset_.
IOStatus EmitBytes(const WriteOptions& wo, const Slice& data);
IOStatus EmitBytes(const WriteOptions& wo, const char* data, size_t len);
std::unique_ptr<WritableFileWriter> dest_;
BlogFileHeader header_;
uint64_t offset_ = 0;
bool manual_flush_;
bool header_written_ = false;
};
} // namespace ROCKSDB_NAMESPACE
+452
View File
@@ -0,0 +1,452 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "db/blog/blog_writer.h"
#include <string>
#include <vector>
#include "db/blog/blog_reader.h"
#include "file/sequence_file_reader.h"
#include "file/writable_file_writer.h"
#include "rocksdb/write_buffer_manager.h"
#include "test_util/testharness.h"
#include "test_util/testutil.h"
namespace ROCKSDB_NAMESPACE {
class BlogWriterTest : public testing::Test {
protected:
// Shared contents buffer: StringSink writes here, StringSource reads from it.
Slice reader_contents_;
test::StringSink* sink_;
BlogWriterTest() : sink_(new test::StringSink(&reader_contents_)) {}
// Create a writer with the given header.
std::unique_ptr<BlogFileWriter> MakeWriter(const BlogFileHeader& header) {
std::unique_ptr<FSWritableFile> sink_holder(sink_);
std::unique_ptr<WritableFileWriter> file_writer(new WritableFileWriter(
std::move(sink_holder), "" /* file name */, FileOptions()));
return std::make_unique<BlogFileWriter>(std::move(file_writer), header);
}
std::unique_ptr<BlogFileReader> MakeReaderWithReporter(
BlogFileReader::Reporter* r) {
return MakeReaderImpl(r);
}
// Create a reader over the data written so far.
std::unique_ptr<BlogFileReader> MakeReader() {
return MakeReaderImpl(nullptr);
}
std::unique_ptr<BlogFileReader> MakeReaderImpl(
BlogFileReader::Reporter* reporter) {
// Create an FSSequentialFile that reads from reader_contents_
class MemSequentialFile : public FSSequentialFile {
public:
Slice& contents_;
size_t pos_ = 0;
explicit MemSequentialFile(Slice& contents) : contents_(contents) {}
IOStatus Read(size_t n, const IOOptions& /*opts*/, Slice* result,
char* scratch, IODebugContext* /*dbg*/) override {
size_t avail = contents_.size() - pos_;
if (n > avail) {
n = avail;
}
if (n > 0) {
memcpy(scratch, contents_.data() + pos_, n);
pos_ += n;
}
*result = Slice(scratch, n);
return IOStatus::OK();
}
IOStatus Skip(uint64_t n) override {
pos_ += static_cast<size_t>(n);
return IOStatus::OK();
}
};
auto* source = new MemSequentialFile(reader_contents_);
std::unique_ptr<FSSequentialFile> source_holder(source);
std::unique_ptr<SequentialFileReader> file_reader(
new SequentialFileReader(std::move(source_holder), "" /* file name */));
return std::make_unique<BlogFileReader>(std::move(file_reader), reporter,
true /* verify_checksums */);
}
// Helper to create a default header for blob files.
BlogFileHeader MakeBlobHeader() {
BlogFileHeader header;
header.checksum_type = kXXH3;
header.compact_record_type = kBlogBlobRecord;
header.GenerateRandomFields();
return header;
}
// Helper to create a default header for WAL files.
BlogFileHeader MakeWalHeader() {
BlogFileHeader header;
header.checksum_type = kXXH3;
header.compact_record_type = kBlogWriteBatchRecord;
header.GenerateRandomFields();
header.SetProperty("role", "wal");
return header;
}
};
TEST_F(BlogWriterTest, WriteAndReadHeader) {
auto header = MakeBlobHeader();
auto writer = MakeWriter(header);
WriteOptions wo;
ASSERT_OK(writer->WriteHeader(wo));
auto reader = MakeReader();
BlogFileHeader read_header;
ASSERT_OK(reader->ReadHeader(&read_header));
ASSERT_EQ(read_header.schema_version, header.schema_version);
ASSERT_EQ(read_header.checksum_type, header.checksum_type);
ASSERT_EQ(read_header.compact_record_type, header.compact_record_type);
ASSERT_EQ(memcmp(read_header.escape_sequence, header.escape_sequence,
kBlogEscapeSequenceSize),
0);
}
TEST_F(BlogWriterTest, WriteSingleBlobRecord) {
auto header = MakeBlobHeader();
auto writer = MakeWriter(header);
WriteOptions wo;
ASSERT_OK(writer->WriteHeader(wo));
std::string blob_data(100, 'A');
uint64_t blob_offset = 0;
ASSERT_OK(writer->AddBlobRecord(wo, blob_data, kNoCompression, &blob_offset));
ASSERT_GT(blob_offset, 0u);
// Verify alignment: offset after record should be 4-byte aligned
ASSERT_EQ(writer->current_offset() % 4, 0u);
// Read it back
auto reader = MakeReader();
BlogFileHeader read_header;
ASSERT_OK(reader->ReadHeader(&read_header));
BlogRecordType type;
Slice payload;
std::string scratch;
uint64_t rec_offset;
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch, &rec_offset));
ASSERT_EQ(type, kBlogBlobRecord);
ASSERT_EQ(payload.size(), 100u);
ASSERT_EQ(payload.ToString(), blob_data);
}
TEST_F(BlogWriterTest, WriteMultipleBlobRecords) {
auto header = MakeBlobHeader();
auto writer = MakeWriter(header);
WriteOptions wo;
ASSERT_OK(writer->WriteHeader(wo));
std::vector<std::string> blobs = {"hello", std::string(1000, 'X'), "short",
std::string(500, 'Y')};
for (const auto& blob : blobs) {
uint64_t offset;
ASSERT_OK(writer->AddBlobRecord(wo, blob, kNoCompression, &offset));
}
auto reader = MakeReader();
BlogFileHeader rh;
ASSERT_OK(reader->ReadHeader(&rh));
for (const auto& expected : blobs) {
BlogRecordType type;
Slice payload;
std::string scratch;
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
ASSERT_EQ(type, kBlogBlobRecord);
ASSERT_EQ(payload.ToString(), expected);
}
// No more records
BlogRecordType type;
Slice payload;
std::string scratch;
ASSERT_TRUE(reader->ReadRecord(&type, &payload, &scratch).IsNotFound());
}
TEST_F(BlogWriterTest, WriteWriteBatchRecord) {
auto header = MakeWalHeader();
auto writer = MakeWriter(header);
WriteOptions wo;
ASSERT_OK(writer->WriteHeader(wo));
std::string wb_data(200, 'W');
ASSERT_OK(writer->AddWriteBatchRecord(wo, wb_data));
auto reader = MakeReader();
BlogFileHeader rh;
ASSERT_OK(reader->ReadHeader(&rh));
BlogRecordType type;
Slice payload;
std::string scratch;
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
ASSERT_EQ(type, kBlogWriteBatchRecord);
ASSERT_EQ(payload.ToString(), wb_data);
}
TEST_F(BlogWriterTest, CompactVsFullFormat) {
auto header = MakeBlobHeader();
auto writer = MakeWriter(header);
WriteOptions wo;
ASSERT_OK(writer->WriteHeader(wo));
// Small blob (< 2 MiB) should use compact format (kBlogBlobRecord matches
// compact_record_type)
std::string small_blob(100, 'S');
uint64_t offset1;
ASSERT_OK(writer->AddBlobRecord(wo, small_blob, kNoCompression, &offset1));
// Large blob (> 2 MiB) should use full format
std::string large_blob(3 * 1024 * 1024, 'L');
uint64_t offset2;
ASSERT_OK(writer->AddBlobRecord(wo, large_blob, kNoCompression, &offset2));
// Read both back
auto reader = MakeReader();
BlogFileHeader rh;
ASSERT_OK(reader->ReadHeader(&rh));
BlogRecordType type;
Slice payload;
std::string scratch;
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
ASSERT_EQ(type, kBlogBlobRecord);
ASSERT_EQ(payload.size(), small_blob.size());
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
ASSERT_EQ(type, kBlogBlobRecord);
ASSERT_EQ(payload.size(), large_blob.size());
ASSERT_EQ(payload, Slice(large_blob));
}
TEST_F(BlogWriterTest, PreambleStartRecord) {
auto header = MakeWalHeader();
auto writer = MakeWriter(header);
WriteOptions wo;
ASSERT_OK(writer->WriteHeader(wo));
uint64_t pre_offset = writer->current_offset();
ASSERT_EQ(pre_offset % 4, 0u) << "Pre-record offset not aligned";
ASSERT_OK(writer->AddPreambleStartRecord(wo));
uint64_t post_offset = writer->current_offset();
ASSERT_EQ(post_offset % 4, 0u) << "Post-record offset not aligned";
// Use a reporter to capture errors
class TestReporter : public BlogFileReader::Reporter {
public:
std::string last_msg;
void Corruption(size_t /*bytes*/, const Status& s) override {
last_msg = s.ToString();
}
};
TestReporter reporter;
auto reader = MakeReaderWithReporter(&reporter);
BlogFileHeader rh;
ASSERT_OK(reader->ReadHeader(&rh));
BlogRecordType type;
Slice payload;
std::string scratch;
Status rs = reader->ReadRecord(&type, &payload, &scratch);
if (!rs.ok()) {
fprintf(stderr, "ReadRecord failed: %s, Reporter: %s, EOF=%d\n",
rs.ToString().c_str(), reporter.last_msg.c_str(), reader->IsEOF());
}
ASSERT_OK(rs);
ASSERT_EQ(type, kBlogPreambleStartRecord);
ASSERT_TRUE(payload.empty());
}
TEST_F(BlogWriterTest, FooterRecords) {
auto header = MakeBlobHeader();
auto writer = MakeWriter(header);
WriteOptions wo;
ASSERT_OK(writer->WriteHeader(wo));
// Write a blob record first
std::string blob_data(50, 'B');
uint64_t blob_offset;
ASSERT_OK(writer->AddBlobRecord(wo, blob_data, kNoCompression, &blob_offset));
// Write footer properties
uint64_t props_offset = writer->current_offset();
BlogFileFooterProperties props;
props.SetBlobCount(1);
props.SetTotalBlobBytes(50);
ASSERT_OK(writer->AddFooterPropertiesRecord(wo, props));
// Write footer locator
BlogFileFooterLocator locator;
uint64_t locator_offset = writer->current_offset();
locator.entries.push_back(
{kBlogFooterPropertiesRecord,
static_cast<uint32_t>((locator_offset - props_offset) / 4)});
ASSERT_OK(writer->AddFooterLocatorRecord(wo, locator));
// Read all records
auto reader = MakeReader();
BlogFileHeader rh;
ASSERT_OK(reader->ReadHeader(&rh));
BlogRecordType type;
Slice payload;
std::string scratch;
// Blob record
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
ASSERT_EQ(type, kBlogBlobRecord);
// Footer properties record
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
ASSERT_EQ(type, kBlogFooterPropertiesRecord);
BlogFileFooterProperties read_props;
ASSERT_OK(read_props.DecodeFrom(payload));
ASSERT_EQ(read_props.properties.size(), 2u);
// Footer locator record
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
ASSERT_EQ(type, kBlogFooterLocatorRecord);
BlogFileFooterLocator read_locator;
ASSERT_OK(read_locator.DecodeFrom(payload));
ASSERT_EQ(read_locator.entries.size(), 1u);
ASSERT_EQ(read_locator.entries[0].record_type, kBlogFooterPropertiesRecord);
}
TEST_F(BlogWriterTest, ChecksumCorruptionDetected) {
auto header = MakeBlobHeader();
auto writer = MakeWriter(header);
WriteOptions wo;
ASSERT_OK(writer->WriteHeader(wo));
std::string blob_data(100, 'C');
uint64_t blob_offset;
ASSERT_OK(writer->AddBlobRecord(wo, blob_data, kNoCompression, &blob_offset));
// Corrupt one byte in the blob payload
std::string& contents = sink_->contents_;
ASSERT_GT(contents.size(), blob_offset + 10);
contents[blob_offset + 10] ^= 0xFF;
// Reading should detect the corruption
auto reader = MakeReader();
BlogFileHeader rh;
ASSERT_OK(reader->ReadHeader(&rh));
BlogRecordType type;
Slice payload;
std::string scratch;
ASSERT_TRUE(reader->ReadRecord(&type, &payload, &scratch).IsCorruption());
}
TEST_F(BlogWriterTest, MixedRecordTypes) {
// Use blob compact type but also write WriteBatch records (full format)
auto header = MakeBlobHeader();
auto writer = MakeWriter(header);
WriteOptions wo;
ASSERT_OK(writer->WriteHeader(wo));
// Blob (compact format)
std::string blob(50, 'B');
uint64_t blob_offset;
ASSERT_OK(writer->AddBlobRecord(wo, blob, kNoCompression, &blob_offset));
// WriteBatch (full format since it doesn't match compact_record_type)
std::string wb(60, 'W');
ASSERT_OK(writer->AddWriteBatchRecord(wo, wb));
// Another blob (compact)
std::string blob2(70, 'D');
uint64_t blob2_offset;
ASSERT_OK(writer->AddBlobRecord(wo, blob2, kNoCompression, &blob2_offset));
// Read all back
auto reader = MakeReader();
BlogFileHeader rh;
ASSERT_OK(reader->ReadHeader(&rh));
BlogRecordType type;
Slice payload;
std::string scratch;
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
ASSERT_EQ(type, kBlogBlobRecord);
ASSERT_EQ(payload.size(), 50u);
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
ASSERT_EQ(type, kBlogWriteBatchRecord);
ASSERT_EQ(payload.size(), 60u);
ASSERT_OK(reader->ReadRecord(&type, &payload, &scratch));
ASSERT_EQ(type, kBlogBlobRecord);
ASSERT_EQ(payload.size(), 70u);
ASSERT_TRUE(reader->ReadRecord(&type, &payload, &scratch).IsNotFound());
}
TEST_F(BlogWriterTest, AlignmentInvariant) {
auto header = MakeBlobHeader();
auto writer = MakeWriter(header);
WriteOptions wo;
ASSERT_OK(writer->WriteHeader(wo));
// Write records of various sizes and verify alignment is maintained
for (int size = 1; size <= 200; ++size) {
std::string data(size, static_cast<char>(size & 0xFF));
uint64_t offset;
ASSERT_OK(writer->AddBlobRecord(wo, data, kNoCompression, &offset));
ASSERT_EQ(writer->current_offset() % 4, 0u)
<< "Alignment violated after record of size " << size;
}
}
TEST_F(BlogWriterTest, HeaderWithProperties) {
auto header = MakeBlobHeader();
header.SetProperty("CompressionCompatibilityName", "zstd");
header.SetProperty("role", "blob");
header.SetProperty("compressionSettings", "level=3");
auto writer = MakeWriter(header);
WriteOptions wo;
ASSERT_OK(writer->WriteHeader(wo));
auto reader = MakeReader();
BlogFileHeader rh;
ASSERT_OK(reader->ReadHeader(&rh));
ASSERT_EQ(rh.GetProperty("CompressionCompatibilityName"), "zstd");
ASSERT_EQ(rh.GetProperty("role"), "blob");
ASSERT_EQ(rh.GetProperty("compressionSettings"), "level=3");
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
-36
View File
@@ -734,42 +734,6 @@ TEST_P(ColumnFamilyTest, AddDrop) {
std::vector<std::string>({"default", "four", "three"}));
}
TEST_P(ColumnFamilyTest, EmptyNameRejected) {
// Creating a column family with an empty-string name should be rejected with
// InvalidArgument. Empty strings are reserved by various RocksDB APIs and
// serialization formats to mean "unknown/unspecified column family" (e.g.
// TablePropertiesCollectorFactory::Context::kUnknownColumnFamily, table
// properties).
//
// Prior behavior was silently broken: CreateColumnFamily("") would return
// OK and a usable handle, but the CF was not persisted in the manifest --
// data written to it was lost on DB reopen.
Open();
ColumnFamilyHandle* handle = nullptr;
Status s =
db_->CreateColumnFamily(column_family_options_, std::string(), &handle);
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
ASSERT_EQ(handle, nullptr);
// Same via the bulk-by-names API. The first valid name should still get
// created; the failure should come on the empty one.
std::vector<ColumnFamilyHandle*> handles;
s = db_->CreateColumnFamilies(column_family_options_,
{"ok_name", "", "another"}, &handles);
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
// Clean up the partially-created handle ("ok_name") before the failure.
ASSERT_EQ(handles.size(), 1U);
ASSERT_OK(db_->DropColumnFamily(handles[0]));
ASSERT_OK(db_->DestroyColumnFamilyHandle(handles[0]));
handles.clear();
// Same via the bulk-by-descriptor API.
s = db_->CreateColumnFamilies(
{ColumnFamilyDescriptor("", column_family_options_)}, &handles);
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
ASSERT_TRUE(handles.empty());
}
TEST_P(ColumnFamilyTest, BulkAddDrop) {
constexpr int kNumCF = 1000;
ColumnFamilyOptions cf_options;
+1 -1
View File
@@ -75,7 +75,7 @@ class CompactionBlobResolver : public WideColumnBlobResolver {
bool track_resolve_error_ = false;
// Cache for resolved blob values to avoid re-fetching. Uses a vector of
// (column_index, PinnableSlice) pairs -- typical entities have few blob
// (column_index, PinnableSlice) pairs — typical entities have few blob
// columns (<5), making linear scan cheaper than hash map overhead.
std::vector<std::pair<size_t, std::unique_ptr<PinnableSlice>>>
resolved_cache_;
+9 -9
View File
@@ -251,7 +251,7 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
// When using blob-aware sizing, use proportional estimation (same
// principle as EstimateTotalDataForSST): each SST "owns"
// effective_size / num_files of total data. This is an approximation
// -- individual SSTs may reference different amounts of blob data,
// — individual SSTs may reference different amounts of blob data,
// but uniform distribution is a reasonable estimate for FIFO dropping.
uint64_t remaining_size = effective_size;
const uint64_t num_files = last_level_files.size();
@@ -477,12 +477,12 @@ Compaction* FIFOCompactionPicker::PickIntraL0Compaction(
if (fifo_opts.max_data_files_size == 0) {
ROCKS_LOG_BUFFER(
log_buffer,
"[%s] FIFO kv-ratio compaction: skipping -- "
"[%s] FIFO kv-ratio compaction: skipping — "
"max_data_files_size is 0, cannot compute target file size. ",
cf_name.c_str());
} else if (fifo_opts.max_data_files_size < fifo_opts.max_table_files_size) {
ROCKS_LOG_BUFFER(log_buffer,
"[%s] FIFO kv-ratio compaction: skipping -- "
"[%s] FIFO kv-ratio compaction: skipping — "
"max_data_files_size (%" PRIu64
") < max_table_files_size "
"(%" PRIu64 ").",
@@ -547,7 +547,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
for (int level = 1; level < vstorage->num_levels(); ++level) {
if (!vstorage->LevelFiles(level).empty()) {
ROCKS_LOG_BUFFER(log_buffer,
"[%s] FIFO kv-ratio compaction: skipping -- non-L0 "
"[%s] FIFO kv-ratio compaction: skipping — non-L0 "
"level %d still has %" ROCKSDB_PRIszt
" files (migration in progress)",
cf_name.c_str(), level,
@@ -587,7 +587,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
// once per flush or compaction completion, so no caching is needed.
uint64_t target = 0;
if (mutable_cf_options.max_compaction_bytes > 0) {
// User explicitly set max_compaction_bytes -- use it as target
// User explicitly set max_compaction_bytes — use it as target
target = mutable_cf_options.max_compaction_bytes;
} else {
// Auto-calculate from capacity and observed SST/blob ratio
@@ -642,7 +642,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
// of linear) write amplification.
// Build tier boundaries from smallest to largest.
// Stop at 10KB minimum -- SST files of most workloads are larger than
// Stop at 10KB minimum — SST files of most workloads are larger than
// this, so lower boundaries would only waste CPU scanning L0 files.
// Files smaller than the lowest boundary simply merge at that boundary.
static constexpr uint64_t kMinTierBoundary = 10 * 1024; // 10KB
@@ -651,7 +651,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
boundaries.push_back(b);
}
if (boundaries.empty()) {
// target itself is below kMinTierBoundary -- use target as the
// target itself is below kMinTierBoundary — use target as the
// sole boundary so we can still compact at the target size.
boundaries.push_back(target);
}
@@ -668,7 +668,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
continue;
}
// Found a file < boundary -- collect contiguous batch
// Found a file < boundary — collect contiguous batch
std::vector<FileMetaData*> batch;
uint64_t accumulated = 0;
size_t pos = scan;
@@ -713,7 +713,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
return c;
}
// This batch wasn't enough -- advance past it
// This batch wasn't enough — advance past it
scan = pos;
}
}
+1 -1
View File
@@ -1101,7 +1101,7 @@ TEST_F(CompactionPickerTest, ReadTriggeredSkipsLastLevel) {
Add(0, 1U, "150", "200", kFileSize, 0, 500, 550);
Add(4, 3U, "301", "350", kFileSize, 0, 101, 150);
// File 3 is at the last non-empty level -- should NOT be marked for
// File 3 is at the last non-empty level — should NOT be marked for
// read-triggered compaction. Bottommost file cleanup is handled
// separately by ComputeBottommostFilesMarkedForCompaction().
file_map_[3U].first->stats.num_collapsible_entry_reads_sampled.store(
+5 -10
View File
@@ -846,16 +846,11 @@ static std::unordered_map<std::string, OptionTypeInfo>
{offsetof(struct InternalStats::CompactionStats, count),
OptionType::kUInt64T, OptionVerificationType::kNormal,
OptionTypeFlags::kNone}},
{"counts",
OptionTypeInfo::Array<
/* In release 11.2, a new compaction reason was added. This broken
* the reader and writer. To unblock release 11.1, we temporarily
* reduce the count array size to the old one. TODO add a proper
* serialization and deserialization method. */
int, static_cast<int>(CompactionReason::kNumOfReasons) - 1>(
offsetof(struct InternalStats::CompactionStats, counts),
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
{0, OptionType::kInt})},
{"counts", OptionTypeInfo::Array<
int, static_cast<int>(CompactionReason::kNumOfReasons)>(
offsetof(struct InternalStats::CompactionStats, counts),
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
{0, OptionType::kInt})},
};
static std::unordered_map<std::string, OptionTypeInfo>
-751
View File
@@ -102,757 +102,6 @@ TEST_F(DBBasicTest, OpenWhenOpen) {
ASSERT_TRUE(strstr(s.getState(), "lock ") != nullptr);
}
namespace {
// Helper that captures per-branch SkippedNoopEdit counts for tests.
struct RecoveryOptimizationCounters {
std::atomic<int> setup_dbid{0};
std::atomic<int> per_cf{0};
std::atomic<int> wal_deletion{0};
std::atomic<int> next_file_number{0};
void Install() {
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:SetupDBId",
[this](void*) { setup_dbid.fetch_add(1); });
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:PerCF",
[this](void*) { per_cf.fetch_add(1); });
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:WalDeletion",
[this](void*) { wal_deletion.fetch_add(1); });
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber",
[this](void*) { next_file_number.fetch_add(1); });
sp->EnableProcessing();
}
void Uninstall() {
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
sp->DisableProcessing();
sp->ClearAllCallBacks();
}
};
} // namespace
// optimize_manifest_for_recovery=true: a clean reopen of a flushed DB must
// append fewer individual records to the MANIFEST than the default-off path.
// Verified by counting AddRecord calls (one per VersionEdit written to the
// MANIFEST log).
TEST_F(DBBasicTest,
OptimizeManifestForRecoveryReducesManifestWritesOnCleanReopen) {
Options options = CurrentOptions();
options.create_if_missing = true;
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
// Measure MANIFEST records with optimize_manifest_for_recovery OFF (default).
DestroyAndReopen(options);
ASSERT_OK(Put("k1", "v1"));
ASSERT_OK(Flush());
Close();
std::atomic<int> records_off{0};
std::atomic<int> next_file_skips_off{0};
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
[&](void*) { records_off.fetch_add(1); });
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber",
[&](void*) { next_file_skips_off.fetch_add(1); });
sp->EnableProcessing();
Reopen(options);
sp->DisableProcessing();
sp->ClearAllCallBacks();
ASSERT_EQ("v1", Get("k1"));
int off_count = records_off.load();
ASSERT_GT(off_count, 0);
ASSERT_EQ(0, next_file_skips_off.load());
Close();
// Measure MANIFEST records with optimize_manifest_for_recovery ON.
options.optimize_manifest_for_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k2", "v2"));
ASSERT_OK(Flush());
Close();
std::atomic<int> records_on{0};
std::atomic<int> next_file_skips_on{0};
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
[&](void*) { records_on.fetch_add(1); });
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber",
[&](void*) { next_file_skips_on.fetch_add(1); });
sp->EnableProcessing();
Reopen(options);
sp->DisableProcessing();
sp->ClearAllCallBacks();
ASSERT_EQ("v2", Get("k2"));
int on_count = records_on.load();
ASSERT_GT(next_file_skips_on.load(), 0);
ASSERT_LT(on_count, off_count);
}
// When the per-CF log_number actually advances, the per-CF skip must NOT
// fire -- the edit carries real information and must be emitted.
TEST_F(DBBasicTest, OptimizeManifestForRecoveryEmitsPerCFWhenLogAdvances) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
// Write data and close WITHOUT flushing -- the WAL has un-replayed
// records, so on reopen recovery flushes the memtable and the per-CF
// edit's log_number must advance past the prior WAL.
Close();
RecoveryOptimizationCounters counters;
counters.Install();
Reopen(options);
counters.Uninstall();
ASSERT_EQ(0, counters.per_cf.load());
ASSERT_EQ("v", Get("k"));
}
// With track_and_verify_wals_in_manifest=true, the wal_deletion edit
// must STILL be emitted (the DeleteWalsBefore record is required).
TEST_F(DBBasicTest, OptimizeManifestForRecoveryPreservesWalTracking) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
options.track_and_verify_wals_in_manifest = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
RecoveryOptimizationCounters counters;
counters.Install();
Reopen(options);
counters.Uninstall();
ASSERT_EQ(0, counters.wal_deletion.load());
}
// best_efforts_recovery requires a fresh MANIFEST + CURRENT to be
// produced on every open (the salvage contract). Even with the option
// on, none of the SkippedNoopEdit branches must fire under
// best_efforts_recovery -- otherwise CURRENT can be left missing or
// stale (regression caught by DBBasicTest.RecoverWithNoCurrentFile and
// DBTest2.BestEffortsRecoveryWithSstUniqueIdVerification under an
// option-on default).
TEST_F(DBBasicTest, OptimizeManifestForRecoveryDisabledByBestEffortsRecovery) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
options.best_efforts_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
RecoveryOptimizationCounters counters;
counters.Install();
Reopen(options);
counters.Uninstall();
ASSERT_EQ(0, counters.setup_dbid.load());
ASSERT_EQ(0, counters.per_cf.load());
ASSERT_EQ(0, counters.wal_deletion.load());
ASSERT_EQ(0, counters.next_file_number.load());
ASSERT_EQ("v", Get("k"));
}
// Multi-column-family coverage: with one CF flushed and another not,
// the un-flushed CF's edit must still be emitted.
TEST_F(DBBasicTest, OptimizeManifestForRecoveryMultiCF) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
CreateAndReopenWithCF({"pikachu", "raichu"}, options);
ASSERT_OK(Put(0, "k", "v0"));
ASSERT_OK(Put(1, "k", "v1"));
ASSERT_OK(Put(2, "k", "v2"));
ASSERT_OK(Flush(0));
// CF 1 and 2 keep dirty memtables -- recovery must emit their per-CF
// edits (log_number advance from WAL replay).
Close();
RecoveryOptimizationCounters counters;
counters.Install();
ReopenWithColumnFamilies({"default", "pikachu", "raichu"}, options);
counters.Uninstall();
ASSERT_EQ("v0", Get(0, "k"));
ASSERT_EQ("v1", Get(1, "k"));
ASSERT_EQ("v2", Get(2, "k"));
}
// MaybeUpdateNextFileNumber's seed value (next_file_number_) makes the
// post-loop comparison trivially true on every clean recovery, causing a
// no-op SetNextFile edit to be appended to the MANIFEST. With
// optimize_manifest_for_recovery=true, that emission is gated and
// the SkippedNoopEdit:NextFileNumber sync point fires in its place.
TEST_F(DBBasicTest, OptimizeManifestForRecoverySkipsNextFileNumber) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
RecoveryOptimizationCounters counters;
counters.Install();
Reopen(options);
counters.Uninstall();
ASSERT_EQ(1, counters.next_file_number.load());
}
// With option=true and a synthetic on-disk file whose number is at or
// above next_file_number_, MaybeUpdateNextFileNumber MUST emit the
// SetNextFile edit and advance the counter past the synthetic number.
TEST_F(DBBasicTest,
OptimizeManifestForRecoveryEmitsNextFileNumberWhenJustified) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
const uint64_t next_before_close =
dbfull()->GetVersionSet()->current_next_file_number();
Close();
// Drop an empty .sst with a number well above next_file_number into
// the DB dir. MaybeUpdateNextFileNumber must observe it and advance.
const uint64_t synthetic_number = next_before_close + 100;
const std::string synthetic_sst =
dbname_ + "/" + MakeTableFileName("", synthetic_number);
ASSERT_OK(WriteStringToFile(env_, "" /*data*/, synthetic_sst,
/*should_sync=*/true));
RecoveryOptimizationCounters counters;
counters.Install();
Reopen(options);
counters.Uninstall();
ASSERT_EQ(0, counters.next_file_number.load());
ASSERT_GT(dbfull()->GetVersionSet()->current_next_file_number(),
synthetic_number);
}
// optimize_manifest_for_recovery=true: after a clean Put + Flush + Close, the
// next Open's min_log_number_to_keep equals (max_wal_before_close + 1),
// proving the close-time MANIFEST write took effect.
TEST_F(DBBasicTest, OptimizeManifestForRecoveryAdvancesWalMarkersOnClose) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
uint64_t max_wal_before_close = 0;
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
sp->SetCallBack("DBImpl::CloseHelper:CapturedMaxWal", [&](void* arg) {
max_wal_before_close = *static_cast<uint64_t*>(arg);
});
sp->EnableProcessing();
Close();
sp->DisableProcessing();
sp->ClearAllCallBacks();
ASSERT_GT(max_wal_before_close, 0u);
Reopen(options);
ASSERT_EQ(max_wal_before_close + 1,
dbfull()->GetVersionSet()->min_log_number_to_keep());
ASSERT_EQ("v", Get("k"));
}
// Shared-option matrix: default-off and disable-before-close should both fall
// back to recovery-time MANIFEST work, while enable-through-close should avoid
// MANIFEST appends on a clean reopen.
TEST_F(DBBasicTest, OptimizeManifestForRecoveryCleanReopenMatrix) {
struct TestCase {
const char* name;
bool enable_on_open;
bool disable_before_close;
bool expect_close_write;
};
const TestCase test_cases[] = {
{"default_off", false, false, false},
{"enabled", true, false, true},
{"disabled_before_close", true, true, false},
};
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.name);
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = test_case.enable_on_open;
DestroyAndReopen(options);
ASSERT_OK(Put("k", test_case.name));
ASSERT_OK(Flush());
if (test_case.disable_before_close) {
ASSERT_OK(dbfull()->SetDBOptions(
{{"optimize_manifest_for_recovery", "false"}}));
}
std::atomic<int> entered{0};
sp->SetCallBack("DBImpl::CloseHelper:WriteWalMarkersOnCloseEntered",
[&](void*) { entered.fetch_add(1); });
sp->EnableProcessing();
Close();
sp->DisableProcessing();
sp->ClearAllCallBacks();
std::atomic<int> records{0};
RecoveryOptimizationCounters counters;
counters.Install();
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
[&](void*) { records.fetch_add(1); });
Reopen(options);
counters.Uninstall();
if (test_case.expect_close_write) {
ASSERT_EQ(1, entered.load());
ASSERT_EQ(0, records.load());
ASSERT_GT(counters.per_cf.load(), 0);
} else {
ASSERT_EQ(0, entered.load());
ASSERT_GT(records.load(), 0);
}
ASSERT_EQ(test_case.name, Get("k"));
Close();
}
}
// allow_2pc=true: even with the option on, the close-time write must NOT
// advance MinLogNumberToKeep / DeleteWalsBefore (2pc requires the WAL to
// remain replayable for uncommitted prepared transactions).
TEST_F(DBBasicTest, OptimizeManifestForRecoveryRespectsTwoPC) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
options.allow_2pc = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
const uint64_t min_log_before_close =
dbfull()->GetVersionSet()->min_log_number_to_keep();
Close();
Reopen(options);
ASSERT_EQ(min_log_before_close,
dbfull()->GetVersionSet()->min_log_number_to_keep());
ASSERT_EQ("v", Get("k"));
}
// Mixed-CF emptiness: on reopen, recovery should still have MANIFEST work to
// do for the dirty CF while skipping per-CF recovery edits for the empty CFs
// whose markers were persisted at close.
TEST_F(DBBasicTest, OptimizeManifestForRecoverySkipsNonEmptyCF) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
CreateAndReopenWithCF({"empty_cf", "dirty_cf"}, options);
ASSERT_OK(Put(1, "k", "v1"));
ASSERT_OK(Flush(1));
ASSERT_OK(Put(2, "k", "v2"));
Close();
RecoveryOptimizationCounters counters;
std::atomic<int> records{0};
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
counters.Install();
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
[&](void*) { records.fetch_add(1); });
ReopenWithColumnFamilies({"default", "empty_cf", "dirty_cf"}, options);
counters.Uninstall();
ASSERT_GT(counters.per_cf.load(), 0);
ASSERT_GT(records.load(), 0);
ASSERT_EQ("v1", Get(1, "k"));
ASSERT_EQ("v2", Get(2, "k"));
}
// track_and_verify_wals_in_manifest=true: the close-time write must leave
// recovery with only the mandatory WAL-tracking MANIFEST work. The per-CF
// recovery edits should still skip.
TEST_F(DBBasicTest, OptimizeManifestForRecoveryEmitsWalDeletionWhenTracking) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
options.track_and_verify_wals_in_manifest = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
RecoveryOptimizationCounters counters;
std::atomic<int> records{0};
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
counters.Install();
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
[&](void*) { records.fetch_add(1); });
Reopen(options);
counters.Uninstall();
ASSERT_GT(counters.per_cf.load(), 0);
ASSERT_EQ(1, records.load());
ASSERT_GT(dbfull()->GetVersionSet()->GetWalSet().GetMinWalNumberToKeep(), 0u);
ASSERT_EQ("v", Get("k"));
}
// Regression for the close-time marker path: if a new WAL is created while an
// otherwise-empty user CF stays empty, Close() must reserve the next file
// number before persisting SetLogNumber(cur_wal + 1) for that CF.
TEST_F(DBBasicTest, OptimizeManifestForRecoveryImmediateCloseAfterWarmReopen) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
CreateAndReopenWithCF({"empty_cf"}, options);
ASSERT_OK(Put(0, "k", "v"));
const uint64_t wal_before_switch = dbfull()->TEST_LogfileNumber();
ASSERT_OK(dbfull()->TEST_SwitchMemtable());
ASSERT_GT(dbfull()->TEST_LogfileNumber(), wal_before_switch);
const uint64_t expected_new_log_num = dbfull()->TEST_LogfileNumber() + 1;
ASSERT_EQ(expected_new_log_num,
dbfull()->GetVersionSet()->current_next_file_number());
Close();
ReopenWithColumnFamilies({"default", "empty_cf"}, options);
ASSERT_GT(dbfull()->GetVersionSet()->current_next_file_number(),
expected_new_log_num);
ASSERT_EQ("v", Get(0, "k"));
}
// Dropped-CF safety: dropped column families must NOT have markers written for
// them at close time (the !IsDropped() guard protects against attaching the
// global edit to a dropped CF, which would later trip MANIFEST replay
// assertions).
TEST_F(DBBasicTest, OptimizeManifestForRecoverySkipsDroppedCF) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
CreateAndReopenWithCF({"to_drop", "keeper"}, options);
ASSERT_OK(Put(1, "k", "v1"));
ASSERT_OK(Put(2, "k", "v2"));
ASSERT_OK(Flush(1));
ASSERT_OK(Flush(2));
ASSERT_OK(db_->DropColumnFamily(handles_[1]));
Close();
ReopenWithColumnFamilies({"default", "keeper"}, options);
ASSERT_EQ("v2", Get(1, "k"));
}
// reuse_manifest_on_open=true: the next LogAndApply after Recover must
// append to the existing MANIFEST file instead of allocating a fresh
// one. Force a write after reopen and verify the MANIFEST file number
// stays unchanged.
TEST_F(DBBasicTest, ReuseManifestOnOpenAppendsToExistingFile) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.reuse_manifest_on_open = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
Reopen(options);
const uint64_t manifest_after_reopen =
dbfull()->TEST_Current_Manifest_FileNo();
ASSERT_OK(Put("k2", "v2"));
ASSERT_OK(Flush());
const uint64_t manifest_after_flush =
dbfull()->TEST_Current_Manifest_FileNo();
EXPECT_EQ(manifest_after_reopen, manifest_after_flush);
EXPECT_EQ("v", Get("k"));
EXPECT_EQ("v2", Get("k2"));
}
// Default off: ReopenManifestForAppend must NOT be invoked.
TEST_F(DBBasicTest, ReuseManifestOnOpenDefaultOffSkipsReopen) {
Options options = CurrentOptions();
options.create_if_missing = true;
// reuse_manifest_on_open defaults to false
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
std::atomic<int> reopened{0};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ReopenManifestForAppend:Reopened",
[&](void* /*arg*/) { reopened.fetch_add(1); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Reopen(options);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_EQ(0, reopened.load());
}
// Regression for the WritableFileWriter::filesize_=0 bug fixed via
// constructor-time initial_file_size: after ReopenManifestForAppend binds a
// writer to the existing MANIFEST, GetFileSize() must return the on-disk
// size, not 0.
// (If it returned 0, the size-limit check in ProcessManifestWrites would
// compare against 0 and Close-time Truncate could shrink the file.)
TEST_F(DBBasicTest, ReuseManifestOnOpenAdoptsOnDiskSize) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.reuse_manifest_on_open = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
// Capture the on-disk MANIFEST size before reopen.
Reopen(options);
const uint64_t manifest_no = dbfull()->TEST_Current_Manifest_FileNo();
const std::string manifest_path = DescriptorFileName(dbname_, manifest_no);
uint64_t on_disk_size = 0;
ASSERT_OK(env_->GetFileSize(manifest_path, &on_disk_size));
ASSERT_GT(on_disk_size, 0u);
// Force a write to flush the writer's buffer; verify Close doesn't
// shrink the file (which would happen if Truncate(filesize_=0) ran).
ASSERT_OK(Put("k2", "v2"));
ASSERT_OK(Flush());
Close();
uint64_t after_close_size = 0;
ASSERT_OK(env_->GetFileSize(manifest_path, &after_close_size));
EXPECT_GE(after_close_size, on_disk_size);
}
// MANIFEST rotation continues to work under reuse: when the file grows
// past tuned_max_manifest_file_size_, ProcessManifestWrites must rotate
// to a fresh MANIFEST (same as legacy). Use the rotation SyncPoint to
// observe rotation directly, since tuned_max_manifest_file_size_ is
// auto-derived and not directly = max_manifest_file_size.
TEST_F(DBBasicTest, ReuseManifestOnOpenStillRotatesOnSizeCap) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.reuse_manifest_on_open = true;
options.max_manifest_file_size = 1;
options.max_manifest_space_amp_pct = 0; // disable amp-based tuning
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
std::atomic<int> rotations{0};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ProcessManifestWrites:BeforeNewManifest",
[&](void* /*arg*/) { rotations.fetch_add(1); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Reopen(options);
ASSERT_OK(Put("k2", "v2"));
ASSERT_OK(Flush());
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
// The Flush after Reopen must have triggered a rotation given the
// tiny size cap -- proves the size-driven rotation path still runs
// when descriptor_log_ is bound by reuse.
EXPECT_GT(rotations.load(), 0);
EXPECT_EQ("v", Get("k"));
EXPECT_EQ("v2", Get("k2"));
}
// Multi-CF reuse: append-mode MANIFEST must correctly handle edits
// from multiple CFs after reopen.
TEST_F(DBBasicTest, ReuseManifestOnOpenMultiCF) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.reuse_manifest_on_open = true;
CreateAndReopenWithCF({"alpha", "beta"}, options);
ASSERT_OK(Put(0, "k", "v0"));
ASSERT_OK(Put(1, "k", "v1"));
ASSERT_OK(Put(2, "k", "v2"));
ASSERT_OK(Flush(0));
ASSERT_OK(Flush(1));
ASSERT_OK(Flush(2));
Close();
ReopenWithColumnFamilies({"default", "alpha", "beta"}, options);
// Trigger more writes post-reopen on each CF -- these get appended to
// the reused MANIFEST.
ASSERT_OK(Put(0, "k2", "v0b"));
ASSERT_OK(Put(1, "k2", "v1b"));
ASSERT_OK(Put(2, "k2", "v2b"));
ASSERT_OK(Flush(0));
ASSERT_OK(Flush(1));
ASSERT_OK(Flush(2));
EXPECT_EQ("v0", Get(0, "k"));
EXPECT_EQ("v1", Get(1, "k"));
EXPECT_EQ("v2", Get(2, "k"));
EXPECT_EQ("v0b", Get(0, "k2"));
EXPECT_EQ("v1b", Get(1, "k2"));
EXPECT_EQ("v2b", Get(2, "k2"));
}
// Disabled under best_efforts_recovery: that mode rebuilds CURRENT and
// MANIFEST as the side-effect of LogAndApplyForRecovery emitting an
// edit; reusing the prior MANIFEST contradicts the salvage contract.
TEST_F(DBBasicTest, ReuseManifestOnOpenDisabledByBestEffortsRecovery) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.reuse_manifest_on_open = true;
options.best_efforts_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
std::atomic<int> reopened{0};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ReopenManifestForAppend:Reopened",
[&](void* /*arg*/) { reopened.fetch_add(1); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Reopen(options);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_EQ(0, reopened.load());
ASSERT_EQ("v", Get("k"));
}
// Full-stack composition: optimize_manifest_for_recovery plus
// reuse_manifest_on_open. Verifies that Close writes recovery markers,
// Reopen skips clean-recovery MANIFEST edits, and the MANIFEST is reused
// instead of recreated for the next metadata update.
TEST_F(DBBasicTest, ReuseManifestOnOpenFullStackComposition) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
options.reuse_manifest_on_open = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
// Capture the MANIFEST file number before reopen.
std::atomic<int> reopened{0};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ReopenManifestForAppend:Reopened",
[&](void* /*arg*/) { reopened.fetch_add(1); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Reopen(options);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
// All three composing: reuse fired, data is intact, no fresh MANIFEST.
EXPECT_EQ(1, reopened.load());
EXPECT_EQ("v", Get("k"));
}
// Direct unit test for WritableFileWriter's initial_file_size parameter:
// verifies the visible size accessors report the existing on-disk size
// immediately, rather than the constructor's zero default.
TEST_F(DBBasicTest, WritableFileWriterInitialFileSizeAdoptsExistingSize) {
Env* env = Env::Default();
std::string fname = test::PerThreadDBPath("set_file_size_test");
ASSERT_OK(env->CreateDirIfMissing(test::TmpDir(env)));
// Create a file with some bytes, close it.
{
std::unique_ptr<WritableFile> raw;
ASSERT_OK(env->NewWritableFile(fname, &raw, EnvOptions()));
ASSERT_OK(raw->Append("hello world"));
ASSERT_OK(raw->Close());
}
// Reopen and wrap in a WritableFileWriter without initial_file_size --
// GetFileSize() should be 0 (the constructor's default).
std::unique_ptr<FSWritableFile> fs_file;
ASSERT_OK(env->GetFileSystem()->ReopenWritableFile(
fname, FileOptions(), &fs_file, /*dbg=*/nullptr));
std::unique_ptr<WritableFileWriter> writer(
new WritableFileWriter(std::move(fs_file), fname, FileOptions()));
EXPECT_EQ(0u, writer->GetFileSize());
// Reopen again and seed the writer's size accounting from the existing
// bytes. GetFileSize and GetFlushedSize must reflect it immediately.
ASSERT_OK(env->GetFileSystem()->ReopenWritableFile(
fname, FileOptions(), &fs_file, /*dbg=*/nullptr));
writer.reset(new WritableFileWriter(
std::move(fs_file), fname, FileOptions(),
/*clock=*/nullptr, /*io_tracer=*/nullptr, /*stats=*/nullptr,
Histograms::HISTOGRAM_ENUM_MAX, /*listeners=*/{},
/*file_checksum_gen_factory=*/nullptr,
/*perform_data_verification=*/false,
/*buffered_data_with_checksum=*/false,
/*initial_file_size=*/11));
EXPECT_EQ(11u, writer->GetFileSize());
EXPECT_EQ(11u, writer->GetFlushedSize());
ASSERT_OK(env->DeleteFile(fname));
}
// Tail corruption: appending garbage bytes to the MANIFEST after a
// clean close must prevent reuse -- the physical size exceeds the
// last valid record end.
TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.reuse_manifest_on_open = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
// Find the MANIFEST file and append garbage to it.
std::string manifest_path;
{
std::vector<std::string> files;
ASSERT_OK(env_->GetChildren(dbname_, &files));
for (const auto& f : files) {
uint64_t number;
FileType type;
if (ParseFileName(f, &number, &type) && type == kDescriptorFile) {
manifest_path = dbname_ + "/" + f;
break;
}
}
}
ASSERT_FALSE(manifest_path.empty());
{
std::string contents;
ASSERT_OK(ReadFileToString(env_, manifest_path, &contents));
contents.append("garbage!");
ASSERT_OK(WriteStringToFile(env_, contents, manifest_path));
}
std::atomic<int> reopened{0};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ReopenManifestForAppend:Reopened",
[&](void* /*arg*/) { reopened.fetch_add(1); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Reopen(options);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_EQ(0, reopened.load());
ASSERT_EQ("v", Get("k"));
}
TEST_F(DBBasicTest, EnableDirectIOWithZeroBuf) {
if (!IsDirectIOSupported()) {
ROCKSDB_GTEST_BYPASS("Direct IO not supported");
+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"));
+11 -92
View File
@@ -9057,10 +9057,11 @@ class DBCompactionTestBlobError
std::string sync_point_;
};
INSTANTIATE_TEST_CASE_P(DBCompactionTestBlobError, DBCompactionTestBlobError,
::testing::ValuesIn(std::vector<std::string>{
"BlobFileBuilder::WriteBlobToFile:AddRecord",
"BlobFileBuilder::WriteBlobToFile:AppendFooter"}));
INSTANTIATE_TEST_CASE_P(
DBCompactionTestBlobError, DBCompactionTestBlobError,
::testing::ValuesIn(std::vector<std::string>{
"BlobFileBuilder::WriteBlobToFile:AddRecord",
"BlobFileBuilder::WriteBlobToFile:LegacyAppendFooterAndClose"}));
TEST_P(DBCompactionTestBlobError, CompactionError) {
Options options = CurrentOptions();
@@ -11833,7 +11834,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 +11865,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 +11877,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 +11890,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 +11972,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);
@@ -12032,7 +12033,7 @@ TEST_F(DBCompactionTest, ObsoleteFileTableCacheEntryWithConcurrentRef) {
TableCache::Lookup(table_cache, target_file_number);
ASSERT_NE(concurrent_handle, nullptr);
// Call ReleaseObsolete directly -- this is what PurgeObsoleteFiles calls
// 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,
@@ -12049,88 +12050,6 @@ TEST_F(DBCompactionTest, ObsoleteFileTableCacheEntryWithConcurrentRef) {
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;
+6 -12
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();
}
@@ -2514,10 +2507,11 @@ class DBFlushTestBlobError : public DBFlushTest,
std::string sync_point_;
};
INSTANTIATE_TEST_CASE_P(DBFlushTestBlobError, DBFlushTestBlobError,
::testing::ValuesIn(std::vector<std::string>{
"BlobFileBuilder::WriteBlobToFile:AddRecord",
"BlobFileBuilder::WriteBlobToFile:AppendFooter"}));
INSTANTIATE_TEST_CASE_P(
DBFlushTestBlobError, DBFlushTestBlobError,
::testing::ValuesIn(std::vector<std::string>{
"BlobFileBuilder::WriteBlobToFile:AddRecord",
"BlobFileBuilder::WriteBlobToFile:LegacyAppendFooterAndClose"}));
TEST_P(DBFlushTestBlobError, FlushError) {
Options options;
@@ -3890,7 +3884,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 -338
View File
@@ -15,7 +15,6 @@
#include <cinttypes>
#include <cstdio>
#include <deque>
#include <map>
#include <memory>
#include <optional>
@@ -30,7 +29,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"
@@ -483,18 +481,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,
@@ -650,105 +636,6 @@ void DBImpl::UnregisterBlobDirectWriteColumnFamily() {
assert(false);
}
Status DBImpl::MaybeWriteWalMarkersToManifestOnClose() {
mutex_.AssertHeld();
if (!mutable_db_options_.optimize_manifest_for_recovery ||
!opened_successfully_ || versions_ == nullptr || logs_.empty()) {
return Status::OK();
}
TEST_SYNC_POINT("DBImpl::CloseHelper:WriteWalMarkersOnCloseEntered");
const ReadOptions read_options(Env::IOActivity::kUnknown);
const WriteOptions write_options(Env::IOActivity::kUnknown);
uint64_t max_wal_number = logs_.back().number;
TEST_SYNC_POINT_CALLBACK("DBImpl::CloseHelper:CapturedMaxWal",
&max_wal_number);
const uint64_t new_log_num = max_wal_number + 1;
// std::deque keeps VersionEdit pointers stable across emplace_back.
std::deque<VersionEdit> edits;
autovector<ColumnFamilyData*> cfds;
autovector<autovector<VersionEdit*>> edit_lists;
VersionEdit global_edit;
bool persist_global_edit = false;
bool reserved_log_number = false;
bool all_cfs_empty = true;
for (auto* cfd : *versions_->GetColumnFamilySet()) {
if (cfd->IsDropped()) {
continue;
}
// cfd->IsEmpty() checks both mem and imm. An immutable memtable queued for
// flush still pins an older WAL; advancing past it would drop unflushed
// records on the next open.
if (!cfd->IsEmpty()) {
all_cfs_empty = false;
continue;
}
if (new_log_num > cfd->GetLogNumber()) {
if (!reserved_log_number) {
const uint64_t next_file_number = versions_->current_next_file_number();
if (next_file_number <= new_log_num) {
versions_->FetchAddFileNumber(new_log_num + 1 - next_file_number);
global_edit.SetNextFile(versions_->current_next_file_number());
persist_global_edit = true;
}
reserved_log_number = true;
}
edits.emplace_back();
VersionEdit& edit = edits.back();
edit.SetLogNumber(new_log_num);
edit.SetColumnFamily(cfd->GetID());
cfds.push_back(cfd);
edit_lists.emplace_back();
edit_lists.back().push_back(&edit);
}
}
if (all_cfs_empty && !allow_2pc()) {
if (new_log_num > versions_->min_log_number_to_keep()) {
global_edit.SetMinLogNumberToKeep(new_log_num);
persist_global_edit = true;
}
// Mirror the monotonicity guard in db_impl_files.cc:
// PrecomputeWalDeletionEdit emits DeleteWalsBefore only when the
// marker actually advances the WalSet's existing min. Skipping
// this guard would let a stale-replay edit regress WalSet state.
if (immutable_db_options_.track_and_verify_wals_in_manifest &&
new_log_num > versions_->GetWalSet().GetMinWalNumberToKeep()) {
global_edit.DeleteWalsBefore(new_log_num);
persist_global_edit = true;
}
}
if (persist_global_edit) {
ColumnFamilyData* default_cfd =
versions_->GetColumnFamilySet()->GetDefault();
if (default_cfd && !default_cfd->IsDropped()) {
bool attached_to_existing = false;
for (size_t i = 0; i < cfds.size(); ++i) {
if (cfds[i] == default_cfd) {
edit_lists[i].push_back(&global_edit);
attached_to_existing = true;
break;
}
}
if (!attached_to_existing) {
cfds.push_back(default_cfd);
edit_lists.emplace_back();
edit_lists.back().push_back(&global_edit);
}
}
}
if (cfds.empty()) {
return Status::OK();
}
return versions_->LogAndApply(cfds, read_options, write_options, edit_lists,
&mutex_, directories_.GetDbDir(),
/*new_descriptor_log=*/false);
}
Status DBImpl::CloseHelper() {
// Guarantee that there is no background error recovery in progress before
// continuing with the shutdown
@@ -853,17 +740,6 @@ Status DBImpl::CloseHelper() {
job_context.Clean();
mutex_.Lock();
}
// Best-effort warm-reopen optimization: after obsolete-file cleanup and
// before logs_.clear(), persist close-time WAL markers that can reduce
// recovery work on the next open. This matters most when some CFs have no
// unflushed writes and/or the newest WAL is empty, since recovery would
// otherwise append marker-only MANIFEST edits on reopen.
Status manifest_s = MaybeWriteWalMarkersToManifestOnClose();
if (!manifest_s.ok()) {
ROCKS_LOG_WARN(immutable_db_options_.info_log,
"optimize_manifest_for_recovery close-time write failed: %s",
manifest_s.ToString().c_str());
}
{
InstrumentedMutexLock lock(&wal_write_mutex_);
for (auto l : wals_to_free_) {
@@ -2872,148 +2748,6 @@ Status DBImpl::ResolveDirectWriteWideColumns(const ReadOptions& read_options,
return status;
}
bool DBImpl::MaybeResolveMemtableBlobValue(const Slice& key,
const BlobFetcher* blob_fetcher,
PinnableSlice* value,
PinnableWideColumns* columns,
Status* s, bool* is_blob_index,
bool* value_found) {
if (!s->ok() || (!value && !columns)) {
return false;
}
auto reset_outputs = [&]() {
if (value != nullptr) {
value->Reset();
}
if (columns != nullptr) {
columns->Reset();
}
};
auto clear_blob_state = [&]() {
if (is_blob_index != nullptr) {
*is_blob_index = false;
}
};
const bool needs_plain_value_resolution =
is_blob_index != nullptr && *is_blob_index;
const bool needs_wide_column_resolution =
columns != nullptr && !columns->unresolved_blob_column_indices_.empty();
if (!needs_plain_value_resolution && !needs_wide_column_resolution) {
return false;
}
if (blob_fetcher == nullptr) {
reset_outputs();
*s = Status::NotSupported(
"Encountered blob-backed memtable value without blob fetcher.");
clear_blob_state();
return true;
}
if (needs_plain_value_resolution) {
Slice blob_index_slice;
std::string blob_index_storage;
if (value != nullptr) {
if (value->size() > 0) {
blob_index_slice = Slice(value->data(), value->size());
} else {
blob_index_slice = Slice(*(value->GetSelf()));
}
} else {
assert(columns != nullptr);
const WideColumns& plain_value_columns = columns->columns();
assert(plain_value_columns.size() == 1);
assert(plain_value_columns.front().name() == kDefaultWideColumnName);
blob_index_slice = plain_value_columns.front().value();
}
PinnableSlice resolved_value;
PinnableSlice* target = value != nullptr ? value : &resolved_value;
if (value != nullptr) {
// BlobIndex::DecodeFrom can retain Slices into the encoded bytes for
// inlined blob indices, so take an owned copy before resetting `value`
// and reusing the same PinnableSlice as the output target.
blob_index_storage.assign(blob_index_slice.data(),
blob_index_slice.size());
blob_index_slice = Slice(blob_index_storage);
value->Reset();
}
*s = blob_fetcher->FetchBlob(key, blob_index_slice,
nullptr /* prefetch_buffer */, target,
nullptr /* bytes_read */);
if (s->ok() && columns != nullptr) {
columns->SetPlainValue(std::move(*target));
} else if (!s->ok()) {
reset_outputs();
if (s->IsIncomplete() && value_found != nullptr) {
*value_found = false;
}
}
clear_blob_state();
return true;
}
assert(columns != nullptr);
std::string resolved_entity;
bool resolved = false;
*s = WideColumnSerialization::ResolveEntityBlobColumns(
columns->value_, key, blob_fetcher, nullptr /* prefetch_buffers */,
resolved_entity, resolved, nullptr /* total_bytes_read */,
nullptr /* num_blobs_resolved */);
if (s->ok()) {
assert(resolved);
if (resolved) {
*s = columns->SetWideColumnValue(std::move(resolved_entity));
}
}
if (!s->ok()) {
reset_outputs();
if (s->IsIncomplete() && value_found != nullptr) {
*value_found = false;
}
}
clear_blob_state();
return true;
}
void DBImpl::PostprocessMemtableValueRead(
const Slice& key, const std::string* timestamp,
bool resolve_blob_backed_memtable_value,
const BlobFetcher* memtable_blob_fetcher, PinnableSlice* value,
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
bool* value_found) {
if (resolve_blob_backed_memtable_value) {
std::string blob_lookup_key_storage;
const bool value_resolved = MaybeResolveMemtableBlobValue(
GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage),
memtable_blob_fetcher, value, columns, s, is_blob_index, value_found);
if (!value_resolved && value != nullptr && s->ok()) {
value->PinSelf();
}
return;
}
if (s->ok()) {
if (value != nullptr) {
value->PinSelf();
}
} else {
if (value != nullptr) {
value->Reset();
}
if (columns != nullptr) {
columns->Reset();
}
}
}
bool DBImpl::MaybeResolveDirectWriteValue(
const ReadOptions& read_options, const Slice& key,
bool resolve_direct_write_value, const Version* current,
@@ -3229,14 +2963,6 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
}
const bool resolve_direct_write_value =
partition_mgr != nullptr && (is_blob_ptr == &is_blob_index);
std::optional<BlobFetcher> memtable_blob_fetcher;
if (partition_mgr != nullptr) {
memtable_blob_fetcher.emplace(sv->current, read_options,
cfd->blob_file_cache(),
/*allow_write_path_fallback=*/true);
}
const BlobFetcher* memtable_blob_fetcher_ptr =
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
std::string blob_lookup_key_storage;
auto get_blob_lookup_key = [&]() -> Slice {
return GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage);
@@ -3264,21 +2990,19 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
get_impl_options.columns, timestamp, &s, &merge_context,
&max_covering_tombstone_seq, read_options,
false /* immutable_memtable */,
get_impl_options.callback, is_blob_ptr,
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
get_impl_options.callback, is_blob_ptr)) {
done = true;
maybe_resolve_memtable_value();
RecordTick(stats_, MEMTABLE_HIT);
} else if ((s.ok() || s.IsMergeInProgress()) &&
sv->imm->Get(lkey,
get_impl_options.value
? get_impl_options.value->GetSelf()
: nullptr,
get_impl_options.columns, timestamp, &s,
&merge_context, &max_covering_tombstone_seq,
read_options, get_impl_options.callback,
is_blob_ptr, memtable_blob_fetcher_ptr)) {
sv->imm->Get(
lkey,
get_impl_options.value ? get_impl_options.value->GetSelf()
: nullptr,
get_impl_options.columns, timestamp, &s, &merge_context,
&max_covering_tombstone_seq, read_options,
get_impl_options.callback, is_blob_ptr)) {
done = true;
maybe_resolve_memtable_value();
@@ -3290,14 +3014,14 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
if (sv->mem->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr,
/*timestamp=*/nullptr, &s, &merge_context,
&max_covering_tombstone_seq, read_options,
false /* immutable_memtable */, nullptr, nullptr, false,
memtable_blob_fetcher_ptr)) {
false /* immutable_memtable */, nullptr, nullptr,
false)) {
done = true;
RecordTick(stats_, MEMTABLE_HIT);
} else if ((s.ok() || s.IsMergeInProgress()) &&
sv->imm->GetMergeOperands(
lkey, &s, &merge_context, &max_covering_tombstone_seq,
read_options, memtable_blob_fetcher_ptr)) {
sv->imm->GetMergeOperands(lkey, &s, &merge_context,
&max_covering_tombstone_seq,
read_options)) {
done = true;
RecordTick(stats_, MEMTABLE_HIT);
}
@@ -3993,14 +3717,6 @@ Status DBImpl::MultiGetImpl(
(*sorted_keys)[start_key]->column_family);
ColumnFamilyData* cfd = cfh->cfd();
auto* partition_mgr = cfd->blob_partition_manager();
std::optional<BlobFetcher> memtable_blob_fetcher;
if (partition_mgr != nullptr) {
memtable_blob_fetcher.emplace(super_version->current, read_options,
cfd->blob_file_cache(),
/*allow_write_path_fallback=*/true);
}
const BlobFetcher* memtable_blob_fetcher_ptr =
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
// Clear the timestamps for returning results so that we can distinguish
// between tombstone or key that has never been written
for (size_t i = start_key; i < start_key + num_keys; ++i) {
@@ -4047,11 +3763,9 @@ Status DBImpl::MultiGetImpl(
has_unpersisted_data_.load(std::memory_order_relaxed));
if (!skip_memtable) {
super_version->mem->MultiGet(read_options, &range, callback,
false /* immutable_memtable */,
memtable_blob_fetcher_ptr);
false /* immutable_memtable */);
if (!range.empty()) {
super_version->imm->MultiGet(read_options, &range, callback,
memtable_blob_fetcher_ptr);
super_version->imm->MultiGet(read_options, &range, callback);
}
if (!range.empty()) {
uint64_t left = range.KeysLeft();
@@ -4428,20 +4142,8 @@ Status DBImpl::CreateColumnFamilyImpl(const ReadOptions& read_options,
const std::string& column_family_name,
ColumnFamilyHandle** handle) {
options_mutex_.AssertHeld();
*handle = nullptr;
// The empty string is reserved as a "no/unknown column family name" sentinel
// in various APIs and serialization formats (e.g.
// TablePropertiesCollectorFactory::Context::kUnknownColumnFamily, table
// properties), so reject it to avoid ambiguity. Note: prior to this check,
// CreateColumnFamily("") would silently "succeed" but the CF was not
// persisted in the manifest, causing data loss across DB reopens.
if (column_family_name.empty()) {
return Status::InvalidArgument(
"Column family name cannot be the empty string");
}
Status s;
*handle = nullptr;
DBOptions db_options =
BuildDBOptions(immutable_db_options_, mutable_db_options_);
@@ -6498,20 +6200,12 @@ Status DBImpl::GetLatestSequenceForKey(
*seq = kMaxSequenceNumber;
*found_record_for_key = false;
std::optional<BlobFetcher> memtable_blob_fetcher;
if (cfd->blob_partition_manager() != nullptr) {
memtable_blob_fetcher.emplace(sv->current, read_options,
cfd->blob_file_cache(),
/*allow_write_path_fallback=*/true);
}
const BlobFetcher* memtable_blob_fetcher_ptr =
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
// Check if there is a record for this key in the latest memtable
sv->mem->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr, timestamp, &s,
&merge_context, &max_covering_tombstone_seq, seq, read_options,
false /* immutable_memtable */, nullptr /*read_callback*/,
is_blob_index, /*do_merge=*/true, memtable_blob_fetcher_ptr);
is_blob_index);
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
// unexpected error reading memtable.
@@ -6544,8 +6238,7 @@ Status DBImpl::GetLatestSequenceForKey(
// Check if there is a record for this key in the immutable memtables
sv->imm->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr, timestamp, &s,
&merge_context, &max_covering_tombstone_seq, seq, read_options,
nullptr /*read_callback*/, is_blob_index,
memtable_blob_fetcher_ptr);
nullptr /*read_callback*/, is_blob_index);
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
// unexpected error reading memtable.
@@ -6578,7 +6271,7 @@ Status DBImpl::GetLatestSequenceForKey(
sv->imm->GetFromHistory(lkey, /*value=*/nullptr, /*columns=*/nullptr,
timestamp, &s, &merge_context,
&max_covering_tombstone_seq, seq, read_options,
is_blob_index, memtable_blob_fetcher_ptr);
is_blob_index);
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
// unexpected error reading memtable.
@@ -7580,7 +7273,6 @@ Status DBImpl::ReserveFileNumbersBeforeIngestion(
Status DBImpl::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
if (mutable_db_options_.max_open_files == -1) {
uint64_t oldest_time = std::numeric_limits<uint64_t>::max();
std::once_flag waited_for_async_open;
for (auto cfd : *versions_->GetColumnFamilySet()) {
if (!cfd->IsDropped()) {
uint64_t ctime;
@@ -7588,17 +7280,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);
}
-39
View File
@@ -1096,7 +1096,6 @@ class DBImpl : public DB {
superversions_to_free_queue_.push_back(sv);
}
void EnableTrackPublishedSeqInSnapshotContext();
void SetSnapshotChecker(SnapshotChecker* snapshot_checker);
// Fill JobContext with snapshot information needed by flush and compaction.
@@ -1855,11 +1854,6 @@ class DBImpl : public DB {
// Background work function for async file opening.
static void BGWorkAsyncFileOpen(void* arg);
// Block the until any in-flight async file open work has
// completed. No-op when open_files_async is false. Returns early if
// shutdown begins.
void WaitForAsyncFileOpen();
void InvokeWalFilterIfNeededOnColumnFamilyToWalNumberMap();
// Return true to proceed with current WAL record whose content is stored in
@@ -1875,7 +1869,6 @@ class DBImpl : public DB {
private:
friend class DB;
friend class DBImplReadOnly;
friend class DBImplSecondary;
friend class ErrorHandler;
friend class InternalStats;
@@ -2750,8 +2743,6 @@ class DBImpl : public DB {
Status MaybeReleaseTimestampedSnapshotsAndCheck();
Status MaybeWriteWalMarkersToManifestOnClose();
Status CloseHelper();
void WaitForBackgroundWork();
@@ -2953,29 +2944,6 @@ class DBImpl : public DB {
bool resolve_direct_write_value, const Version* current,
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns,
Status* s, bool* is_blob_index, bool* value_found = nullptr);
// Resolves memtable read results that still carry blob references through
// either a raw blob-index payload in `value` or unresolved blob columns in
// `columns`. Unlike the direct-write helper above, this path only depends on
// a BlobFetcher and therefore works for read-only/secondary DBs.
static bool MaybeResolveMemtableBlobValue(const Slice& key,
const BlobFetcher* blob_fetcher,
PinnableSlice* value,
PinnableWideColumns* columns,
Status* s, bool* is_blob_index,
bool* value_found = nullptr);
// Completes read-only/secondary memtable Get()/GetEntity() hits by resolving
// blob-backed payloads when `resolve_blob_backed_memtable_value` is true,
// pinning plain values on success, and clearing outputs on error. When the
// caller explicitly requested raw blob indices via
// `GetImplOptions::is_blob_index`, this helper leaves that payload
// untouched. `memtable_blob_fetcher` may be null when blob support is
// disabled for the column family.
static void PostprocessMemtableValueRead(
const Slice& key, const std::string* timestamp,
bool resolve_blob_backed_memtable_value,
const BlobFetcher* memtable_blob_fetcher, PinnableSlice* value,
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
bool* value_found = nullptr);
template <typename IterType, typename ImplType,
typename ErrorIteratorFuncType>
@@ -3380,13 +3348,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
-36
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) {
@@ -5132,11 +5114,6 @@ void DBImpl::MarkAsGrabbedForPurge(uint64_t file_number) {
files_grabbed_for_purge_.insert(file_number);
}
void DBImpl::EnableTrackPublishedSeqInSnapshotContext() {
InstrumentedMutexLock l(&mutex_);
track_published_seq_in_snapshot_context_ = true;
}
void DBImpl::SetSnapshotChecker(SnapshotChecker* snapshot_checker) {
InstrumentedMutexLock l(&mutex_);
// snapshot_checker_ should only set once. If we need to set it multiple
@@ -5170,19 +5147,6 @@ void DBImpl::InitSnapshotContext(JobContext* job_context) {
SequenceNumber earliest_write_conflict_snapshot = kMaxSequenceNumber;
std::vector<SequenceNumber> snapshot_seqs =
snapshots_.GetAll(&earliest_write_conflict_snapshot);
// The SnapshotChecker path above already adds a managed snapshot to
// snapshot_seqs.
if (track_published_seq_in_snapshot_context_ && !snapshot_checker) {
// Pin the published-sequence boundary into snapshot_seqs without taking
// a real snapshot. This prevents flush/compaction from collapsing a version
// visible at the published boundary into a newer unpublished seqno.
const SequenceNumber published_seq = GetLastPublishedSequence();
if (snapshot_seqs.empty() || snapshot_seqs.back() < published_seq) {
snapshot_seqs.push_back(published_seq);
}
}
TEST_SYNC_POINT("DBImpl::InitSnapshotContext:BeforeInit");
job_context->InitSnapshotContext(
snapshot_checker, std::move(managed_snapshot),
earliest_write_conflict_snapshot, std::move(snapshot_seqs));
+12 -36
View File
@@ -1186,9 +1186,8 @@ std::set<std::string> DBImpl::CollectAllDBPaths() {
Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
mutex_.AssertHeld();
const uint64_t prev_next_file_number = versions_->current_next_file_number();
uint64_t largest_file_number = prev_next_file_number;
bool on_disk_file_advanced_counter = false;
uint64_t next_file_number = versions_->current_next_file_number();
uint64_t largest_file_number = next_file_number;
Status s;
for (const auto& path : CollectAllDBPaths()) {
std::vector<std::string> files;
@@ -1202,15 +1201,7 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
if (!ParseFileName(fname, &number, &type)) {
continue;
}
std::string normalized_fpath = path;
normalized_fpath += kFilePathSeparator;
normalized_fpath.append(fname);
// Use >= so a crashed-mid-allocation file at exactly
// prev_next_file_number triggers the advance -- otherwise the next
// NewFileNumber() would collide with it.
if (number >= prev_next_file_number) {
on_disk_file_advanced_counter = true;
}
const std::string normalized_fpath = path + kFilePathSeparator + fname;
largest_file_number = std::max(largest_file_number, number);
if ((type == kTableFile || type == kBlobFile)) {
recovery_ctx->existing_data_files_.push_back(normalized_fpath);
@@ -1221,31 +1212,16 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
return s;
}
// Legacy bug: initializing largest_file_number from prev_next_file_number
// made the post-loop check trivially true and emitted a noop SetNextFile
// on every recovery.
// Preserved verbatim by default (some tests pin file numbers); skipped
// only when the option is on AND we're not in salvage mode (which
// requires the MANIFEST rewrite this emit triggers).
const bool optimize_manifest_for_recovery =
mutable_db_options_.optimize_manifest_for_recovery &&
!immutable_db_options_.best_efforts_recovery;
if (!optimize_manifest_for_recovery || on_disk_file_advanced_counter) {
if (largest_file_number >= prev_next_file_number) {
versions_->next_file_number_.store(largest_file_number + 1);
}
TEST_SYNC_POINT("DBImpl::MaybeUpdateNextFileNumber:EmitEdit");
VersionEdit edit;
edit.SetNextFile(versions_->next_file_number_.load());
assert(versions_->GetColumnFamilySet());
ColumnFamilyData* default_cfd =
versions_->GetColumnFamilySet()->GetDefault();
assert(default_cfd);
recovery_ctx->UpdateVersionEdits(default_cfd, edit);
} else {
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber");
if (largest_file_number >= next_file_number) {
versions_->next_file_number_.store(largest_file_number + 1);
}
VersionEdit edit;
edit.SetNextFile(versions_->next_file_number_.load());
assert(versions_->GetColumnFamilySet());
ColumnFamilyData* default_cfd = versions_->GetColumnFamilySet()->GetDefault();
assert(default_cfd);
recovery_ctx->UpdateVersionEdits(default_cfd, edit);
return s;
}
} // namespace ROCKSDB_NAMESPACE
+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();
+11 -51
View File
@@ -680,18 +680,8 @@ Status DBImpl::Recover(
} else if (immutable_db_options_.write_dbid_to_manifest && recovery_ctx) {
VersionEdit edit;
s = SetupDBId(write_options, read_only, is_new_db, is_retry, &edit);
// best_efforts_recovery rebuilds CURRENT/MANIFEST as the side-effect
// of LogAndApplyForRecovery emitting an edit, so the optimization is
// disabled there (see optimize_manifest_for_recovery doc).
const bool optimize_manifest_for_recovery =
mutable_db_options_.optimize_manifest_for_recovery &&
!immutable_db_options_.best_efforts_recovery;
if (!optimize_manifest_for_recovery || edit.HasDbId()) {
recovery_ctx->UpdateVersionEdits(
versions_->GetColumnFamilySet()->GetDefault(), edit);
} else {
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:SetupDBId");
}
recovery_ctx->UpdateVersionEdits(
versions_->GetColumnFamilySet()->GetDefault(), edit);
} else {
s = SetupDBId(write_options, read_only, is_new_db, is_retry, nullptr);
}
@@ -780,10 +770,6 @@ Status DBImpl::Recover(
// otherwise, in the future, if WAL tracking is enabled again,
// since the WALs deleted when WAL tracking is disabled are not persisted
// into MANIFEST, WAL check may fail.
//
// Intentionally NOT gated by optimize_manifest_for_recovery:
// this edit is safety-critical for a future re-enable of WAL tracking,
// even though it can look noop-ish under the current session.
VersionEdit edit;
WalNumber max_wal_number =
versions_->GetWalSet().GetWals().rbegin()->first;
@@ -997,11 +983,7 @@ Status DBImpl::InitPersistStatsColumnFamily() {
Status DBImpl::LogAndApplyForRecovery(const RecoveryContext& recovery_ctx) {
mutex_.AssertHeld();
// descriptor_log_ is normally null after Recover, but when
// reuse_manifest_on_open is set VersionSet::Recover may have already
// bound a log::Writer to the existing MANIFEST for append.
assert(versions_->descriptor_log_ == nullptr ||
immutable_db_options_.reuse_manifest_on_open);
assert(versions_->descriptor_log_ == nullptr);
const ReadOptions read_options(Env::IOActivity::kDBOpen);
const WriteOptions write_options(Env::IOActivity::kDBOpen);
@@ -1888,47 +1870,25 @@ Status DBImpl::MaybeFlushFinalMemtableOrRestoreActiveLogFiles(
versions_->MarkFileNumberUsed(max_wal_number + 1);
assert(recovery_ctx != nullptr);
const bool optimize_manifest_for_recovery =
mutable_db_options_.optimize_manifest_for_recovery &&
!immutable_db_options_.best_efforts_recovery;
for (auto* cfd : *versions_->GetColumnFamilySet()) {
auto iter = version_edits->find(cfd->GetID());
assert(iter != version_edits->end());
const VersionEdit& cf_edit = iter->second;
if (!optimize_manifest_for_recovery ||
cf_edit.ShouldEmitPerColumnFamilyRecoveryEdit(
cfd->GetLogNumber())) {
recovery_ctx->UpdateVersionEdits(cfd, cf_edit);
} else {
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:PerCF");
}
recovery_ctx->UpdateVersionEdits(cfd, iter->second);
}
if (flushed || !data_seen) {
const uint64_t new_min_log = max_wal_number + 1;
VersionEdit wal_deletion;
bool emit_wal_deletion = false;
if (immutable_db_options_.track_and_verify_wals_in_manifest) {
// Determining whether DeleteWalsBefore actually shrinks WalSet
// membership requires WalSet state outside this site, so emit
// unconditionally (pre-existing behavior).
wal_deletion.DeleteWalsBefore(new_min_log);
emit_wal_deletion = true;
wal_deletion.DeleteWalsBefore(max_wal_number + 1);
}
const bool min_log_advances =
new_min_log > versions_->min_log_number_to_keep();
if (!allow_2pc() &&
(!optimize_manifest_for_recovery || min_log_advances)) {
wal_deletion.SetMinLogNumberToKeep(new_min_log);
emit_wal_deletion = true;
if (!allow_2pc()) {
// In non-2pc mode, flushing the memtables of the column families
// means we can advance min_log_number_to_keep.
wal_deletion.SetMinLogNumberToKeep(max_wal_number + 1);
}
assert(versions_->GetColumnFamilySet() != nullptr);
if (!optimize_manifest_for_recovery || emit_wal_deletion) {
recovery_ctx->UpdateVersionEdits(
versions_->GetColumnFamilySet()->GetDefault(), wal_deletion);
} else {
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:WalDeletion");
}
recovery_ctx->UpdateVersionEdits(
versions_->GetColumnFamilySet()->GetDefault(), wal_deletion);
}
}
}
+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
View File
@@ -1546,10 +1546,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);
}
}
+4 -4
View File
@@ -623,7 +623,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 &&
@@ -1097,7 +1097,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 +1141,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 +1196,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;
+1 -1
View File
@@ -589,7 +589,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;
+22 -22
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,13 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedIteratorWithBounds) {
// Both directions encounter two tombstone runs (a-d and g-j).
ASSERT_EQ(attempted_insert_ranges_.size(), 2);
if (Forward()) {
// Forward: sees a-d tombstones first -> live e terminates -> [a, e).
// Then g-j -> exhaustion past j, saved_key_="j" fallback -> [g, j),
// Forward: sees a-d tombstones first → live e terminates → [a, e).
// Then g-j → exhaustion past j, saved_key_="j" fallback → [g, j),
// covering g,h,i. j remains a point tombstone.
AssertRange(0, "a", "e");
AssertRange(1, "g", "j");
} else {
// Reverse: sees j-g tombstones first, then f,e live, then d-a -> exhausts.
// Reverse: sees j-g tombstones first, then f,e live, then d-a → exhausts.
AssertRange(0, "g", "z");
AssertRange(1, "a", "e");
}
@@ -5863,8 +5863,8 @@ 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
// 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");
@@ -6061,7 +6061,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 +6103,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"));
@@ -6311,7 +6311,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,7 +6674,7 @@ 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).
// Forward exhaustion past h: saved_key_="h"+ts fallback → [e+ts, h+ts).
AssertRange(0, std::string("e") + ts, std::string("h") + ts);
} else {
// Reverse exhaustion: range is [a+ts, e+ts).
@@ -6687,9 +6687,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
// 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).
// 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 +6727,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());
}
@@ -6738,7 +6738,7 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
if (Forward()) {
// Forward exhausts past h -> saved_key_="h" fallback -> [e, h),
// 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);
@@ -6761,11 +6761,11 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
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
// 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
// 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
// run and uses it via range_tomb_end_key_ — [e, i) when SeekToLast
// delegates to SeekForPrev("i").
for (bool use_udt : {false, true}) {
SCOPED_TRACE(use_udt ? "with UDT" : "without UDT");
@@ -6837,8 +6837,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 +6975,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 +6994,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");
+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_;
-92
View File
@@ -8203,98 +8203,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();
+5 -4
View File
@@ -900,10 +900,11 @@ class DBRecoveryTestBlobError
std::string sync_point_;
};
INSTANTIATE_TEST_CASE_P(DBRecoveryTestBlobError, DBRecoveryTestBlobError,
::testing::ValuesIn(std::vector<std::string>{
"BlobFileBuilder::WriteBlobToFile:AddRecord",
"BlobFileBuilder::WriteBlobToFile:AppendFooter"}));
INSTANTIATE_TEST_CASE_P(
DBRecoveryTestBlobError, DBRecoveryTestBlobError,
::testing::ValuesIn(std::vector<std::string>{
"BlobFileBuilder::WriteBlobToFile:AddRecord",
"BlobFileBuilder::WriteBlobToFile:LegacyAppendFooterAndClose"}));
TEST_P(DBRecoveryTestBlobError, RecoverWithBlobError) {
// Write a value. Note that blob files are not actually enabled at this point.
+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 -3
View File
@@ -1696,7 +1696,7 @@ TEST_F(EventListenerTest, BackgroundJobPressure) {
Env::Priority::LOW);
sleeping_task.WaitUntilSleeping();
// Phase 1: No pressure -- 3 SST files (below slowdown trigger=4).
// Phase 1: No pressure — 3 SST files (below slowdown trigger=4).
for (int i = 0; i < 3; i++) {
ASSERT_OK(Put("k" + std::to_string(i), std::string(100, 'x')));
ASSERT_OK(Flush());
@@ -1714,7 +1714,7 @@ TEST_F(EventListenerTest, BackgroundJobPressure) {
// MaybeScheduleFlushOrCompaction() runs before the pressure callback and
// may schedule new flush work, making flush counts non-deterministic.
// Phase 2: Build pressure -- flush past slowdown trigger (4 L0 SST files).
// Phase 2: Build pressure — flush past slowdown trigger (4 L0 SST files).
// Compaction is blocked, so L0 SST files pile up.
listener->Reset();
{
@@ -1750,7 +1750,7 @@ TEST_F(EventListenerTest, BackgroundJobPressure) {
ASSERT_TRUE(found_compaction_scheduled);
ASSERT_TRUE(found_high_proximity);
// Phase 3: Relieve pressure -- unblock compaction, wait for completion.
// Phase 3: Relieve pressure — unblock compaction, wait for completion.
listener->Reset();
sleeping_task.WakeUp();
sleeping_task.WaitUntilDone();
+2 -3
View File
@@ -22,10 +22,9 @@ namespace ROCKSDB_NAMESPACE::log {
Writer::Writer(std::unique_ptr<WritableFileWriter>&& dest, uint64_t log_number,
bool recycle_log_files, bool manual_flush,
CompressionType compression_type, bool track_and_verify_wals,
size_t initial_block_offset)
CompressionType compression_type, bool track_and_verify_wals)
: dest_(std::move(dest)),
block_offset_(initial_block_offset),
block_offset_(0),
log_number_(log_number),
recycle_log_files_(recycle_log_files),
// Header size varies depending on whether we are recycling or not.
+3 -8
View File
@@ -75,20 +75,15 @@ namespace log {
class Writer {
public:
// Create a writer that will append data to "*dest".
// "*dest" must remain live while this Writer is in use. By default
// "*dest" is expected to be empty (initial_block_offset = 0). When
// resuming append into an existing log file (e.g.
// VersionSet::ReopenManifestForAppend), pass the offset within the
// current 32 KiB block at which writes should resume so that record
// framing aligns to the existing block layout.
// "*dest" must be initially empty.
// "*dest" must remain live while this Writer is in use.
// TODO(hx235): separate WAL related parameters from general `Reader`
// parameters
explicit Writer(std::unique_ptr<WritableFileWriter>&& dest,
uint64_t log_number, bool recycle_log_files,
bool manual_flush = false,
CompressionType compressionType = kNoCompression,
bool track_and_verify_wals = false,
size_t initial_block_offset = 0);
bool track_and_verify_wals = false);
// No copying allowed
Writer(const Writer&) = delete;
void operator=(const Writer&) = delete;
+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 -2
View File
@@ -52,8 +52,7 @@ void WriteFooterlessBlobFile(const ImmutableOptions& immutable_options,
constexpr bool use_fsync = false;
constexpr bool do_flush = false;
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
statistics, blob_file_number, use_fsync,
do_flush);
statistics, use_fsync, do_flush);
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range;
+1 -12
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;
@@ -109,17 +109,6 @@ Status FileMetaData::UpdateBoundaries(const Slice& key, const Slice& value,
void VersionEdit::Clear() { *this = VersionEdit(); }
bool VersionEdit::ShouldEmitPerColumnFamilyRecoveryEdit(
uint64_t current_log_number) const {
return (HasLogNumber() && GetLogNumber() > current_log_number) ||
NumEntries() > 0 || HasComparatorName() || HasPrevLogNumber() ||
HasNextFile() || HasMaxColumnFamily() || HasMinLogNumberToKeep() ||
HasLastSequence() || !GetCompactCursors().empty() || HasDbId() ||
IsColumnFamilyManipulation() || IsInAtomicGroup() ||
HasFullHistoryTsLow() || HasPersistUserDefinedTimestamps() ||
HasSubcompactionProgress();
}
bool VersionEdit::EncodeTo(std::string* dst,
std::optional<size_t> ts_sz) const {
assert(!IsNoManifestWriteDummy());
-4
View File
@@ -1016,10 +1016,6 @@ class VersionEdit {
subcompaction_progress_.Clear();
}
// 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;
// return true on success.
// `ts_sz` is the size in bytes for the user-defined timestamp contained in
// a user key. This argument is optional because it's only required for
-2
View File
@@ -35,7 +35,6 @@ void VersionEditHandlerBase::Iterate(log::Reader& reader,
VersionEdit edit;
s = edit.DecodeFrom(record);
if (s.ok()) {
last_valid_record_end_ = reader.LastRecordEnd();
s = read_buffer_.AddEdit(&edit);
}
if (s.ok()) {
@@ -461,7 +460,6 @@ void VersionEditHandler::CheckIterationResult(const log::Reader& reader,
if (s->ok()) {
version_set_->manifest_file_size_ = reader.GetReadOffset();
assert(version_set_->manifest_file_size_ > 0);
version_set_->manifest_last_valid_record_end_ = last_valid_record_end_;
version_set_->next_file_number_.store(version_edit_params_.GetNextFile() +
1);
SequenceNumber last_seq = version_edit_params_.GetLastSequence();
-6
View File
@@ -31,8 +31,6 @@ class VersionEditHandlerBase {
AtomicGroupReadBuffer& GetReadBuffer() { return read_buffer_; }
uint64_t GetLastValidRecordEnd() const { return last_valid_record_end_; }
protected:
explicit VersionEditHandlerBase(const ReadOptions& read_options,
uint64_t max_read_size)
@@ -54,10 +52,6 @@ class VersionEditHandlerBase {
const ReadOptions& read_options_;
// File offset at the end of the last successfully completed and
// decoded logical record.
uint64_t last_valid_record_end_ = 0;
private:
AtomicGroupReadBuffer read_buffer_;
const uint64_t max_manifest_read_size_;
+24 -124
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++) {
@@ -5620,7 +5618,6 @@ VersionSet::VersionSet(
prev_log_number_(0),
current_version_number_(0),
manifest_file_size_(0),
manifest_last_valid_record_end_(0),
last_compacted_manifest_file_size_(0),
file_options_(storage_options),
block_cache_tracer_(block_cache_tracer),
@@ -5692,7 +5689,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,
@@ -5744,7 +5741,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 +5752,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 "
@@ -5830,7 +5827,6 @@ void VersionSet::Reset() {
current_version_number_ = 0;
manifest_writers_.clear();
manifest_file_size_ = 0;
manifest_last_valid_record_end_ = 0;
last_compacted_manifest_file_size_ = 0;
TuneMaxManifestFileSize();
obsolete_files_.clear();
@@ -6165,7 +6161,11 @@ Status VersionSet::ProcessManifestWrites(
}
}
} else {
FileOptions opt_file_opts = GetFileOptionsForManifestWrite();
FileOptions opt_file_opts = fs_->OptimizeForManifestWrite(file_options_);
// DB option (in file_options_) takes precedence when not kUnknown
if (file_options_.temperature != Temperature::kUnknown) {
opt_file_opts.temperature = file_options_.temperature;
}
mu->Unlock();
TEST_SYNC_POINT("VersionSet::LogAndApply:WriteManifestStart");
TEST_SYNC_POINT_CALLBACK("VersionSet::LogAndApply:WriteManifest", nullptr);
@@ -6200,9 +6200,16 @@ Status VersionSet::ProcessManifestWrites(
io_s = NewWritableFile(fs_.get(), descriptor_fname, &descriptor_file,
opt_file_opts);
if (io_s.ok()) {
new_desc_log_ptr =
CreateManifestWriter(std::move(descriptor_file), descriptor_fname,
opt_file_opts, manifest_preallocation_size);
descriptor_file->SetPreallocationBlockSize(manifest_preallocation_size);
FileTypeSet tmp_set = db_options_->checksum_handoff_file_types;
std::unique_ptr<WritableFileWriter> file_writer(new WritableFileWriter(
std::move(descriptor_file), descriptor_fname, opt_file_opts, clock_,
io_tracer_, nullptr, Histograms::HISTOGRAM_ENUM_MAX /* hist_type */,
db_options_->listeners, nullptr,
tmp_set.Contains(FileType::kDescriptorFile),
tmp_set.Contains(FileType::kDescriptorFile)));
new_desc_log_ptr.reset(
new log::Writer(std::move(file_writer), 0, false));
raw_desc_log_ptr = new_desc_log_ptr.get();
s = WriteCurrentStateToManifest(write_options, curr_state,
wal_additions, raw_desc_log_ptr, io_s);
@@ -6252,7 +6259,6 @@ Status VersionSet::ProcessManifestWrites(
}
++idx;
#endif /* !NDEBUG */
TEST_SYNC_POINT("VersionSet::ProcessManifestWrites:AddRecord");
io_s = raw_desc_log_ptr->AddRecord(write_options, record);
if (!io_s.ok()) {
s = io_s;
@@ -6674,106 +6680,6 @@ Status VersionSet::LogAndApplyHelper(ColumnFamilyData* cfd,
return builder ? builder->Apply(edit) : Status::OK();
}
FileOptions VersionSet::GetFileOptionsForManifestWrite() const {
FileOptions opt_file_opts = fs_->OptimizeForManifestWrite(file_options_);
// DB-configured temperature wins over the FS's default (when not kUnknown).
if (file_options_.temperature != Temperature::kUnknown) {
opt_file_opts.temperature = file_options_.temperature;
}
return opt_file_opts;
}
std::unique_ptr<log::Writer> VersionSet::CreateManifestWriter(
std::unique_ptr<FSWritableFile> file, const std::string& fname,
const FileOptions& opts, uint64_t preallocation_size,
uint64_t initial_file_size) const {
file->SetPreallocationBlockSize(preallocation_size);
FileTypeSet tmp_set = db_options_->checksum_handoff_file_types;
auto file_writer = std::make_unique<WritableFileWriter>(
std::move(file), fname, opts, clock_, io_tracer_,
/*stats=*/nullptr, Histograms::HISTOGRAM_ENUM_MAX, db_options_->listeners,
/*file_checksum_gen_factory=*/nullptr,
tmp_set.Contains(FileType::kDescriptorFile),
tmp_set.Contains(FileType::kDescriptorFile), initial_file_size);
const size_t block_offset = initial_file_size % log::kBlockSize;
return std::make_unique<log::Writer>(
std::move(file_writer), /*log_number=*/0, /*recycle_log_files=*/false,
/*manual_flush=*/false, kNoCompression,
/*track_and_verify_wals=*/false, block_offset);
}
Status VersionSet::ReopenManifestForAppend(const std::string& manifest_path) {
assert(db_options_->reuse_manifest_on_open);
assert(manifest_last_valid_record_end_ > 0);
// Disabled under best_efforts_recovery: that mode rebuilds the
// MANIFEST + CURRENT from scratch via the side-effect of
// LogAndApplyForRecovery emitting an edit. Reusing the (possibly
// stale) prior MANIFEST contradicts the salvage contract.
if (db_options_->best_efforts_recovery) {
return Status::OK();
}
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
// would mis-frame the record stream.
uint64_t physical_size = 0;
IOStatus stat_s = fs_->GetFileSize(manifest_path, IOOptions(), &physical_size,
/*dbg=*/nullptr);
if (!stat_s.ok() || physical_size != manifest_last_valid_record_end_) {
ROCKS_LOG_WARN(db_options_->info_log,
"reuse_manifest_on_open: physical size %" PRIu64
" != last valid record end %" PRIu64
" (tail corruption?); falling back to fresh MANIFEST",
physical_size, manifest_last_valid_record_end_);
return Status::OK();
}
// Direct-writes path is hard to reason about for an already-populated
// file (alignment + tail-pad). POSIX's OptimizeForManifestWrite forces
// it off, but custom FileSystems may not.
if (opt_file_opts.use_direct_writes) {
ROCKS_LOG_WARN(db_options_->info_log,
"reuse_manifest_on_open: direct writes enabled; "
"falling back to fresh MANIFEST");
return Status::OK();
}
std::unique_ptr<FSWritableFile> manifest_file;
IOStatus io_s = fs_->ReopenWritableFile(manifest_path, opt_file_opts,
&manifest_file, /*dbg=*/nullptr);
if (!io_s.ok()) {
ROCKS_LOG_WARN(db_options_->info_log,
"Failed to reopen MANIFEST for append: %s",
io_s.ToString().c_str());
return Status::OK();
}
const uint64_t reopened_size =
manifest_file->GetFileSize(opt_file_opts.io_options, /*dbg=*/nullptr);
if (reopened_size != manifest_last_valid_record_end_) {
ROCKS_LOG_WARN(db_options_->info_log,
"reuse_manifest_on_open: reopened handle size %" PRIu64
" != last valid record end %" PRIu64
"; falling back to fresh MANIFEST",
reopened_size, manifest_last_valid_record_end_);
return Status::OK();
}
descriptor_log_ = CreateManifestWriter(
std::move(manifest_file), manifest_path, opt_file_opts,
manifest_preallocation_size_, reopened_size);
ROCKS_LOG_INFO(db_options_->info_log,
"Reusing existing MANIFEST file: %s (valid data size: %" PRIu64
")",
manifest_path.c_str(), manifest_last_valid_record_end_);
TEST_SYNC_POINT("VersionSet::ReopenManifestForAppend:Reopened");
return Status::OK();
}
Status VersionSet::Recover(
const std::vector<ColumnFamilyDescriptor>& column_families, bool read_only,
std::string* db_id, bool no_error_if_files_missing, bool is_retry,
@@ -6861,11 +6767,6 @@ Status VersionSet::Recover(
}
}
if (s.ok() && !read_only && db_options_->reuse_manifest_on_open &&
manifest_last_valid_record_end_ > 0) {
s = ReopenManifestForAppend(manifest_path);
}
return s;
}
@@ -8163,12 +8064,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;
+1 -41
View File
@@ -1719,33 +1719,6 @@ class VersionSet {
const std::unordered_map<uint32_t, MutableCFState>& curr_state,
const VersionEdit& wal_additions, log::Writer* log, IOStatus& io_s);
// Reopen the existing MANIFEST file for append at the end of Recover()
// when reuse_manifest_on_open is set, so the next LogAndApply appends
// to it instead of creating a fresh MANIFEST. Falls back to OK
// (descriptor_log_ stays null, next LogAndApply creates a fresh
// MANIFEST as before) if ReopenWritableFile fails.
Status ReopenManifestForAppend(const std::string& manifest_path);
// 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;
// Create a log::Writer over the given FSWritableFile with all standard
// MANIFEST setup applied (preallocation block size, checksum-handoff
// classification, listeners, etc.). preallocation_size should be a
// snapshot of manifest_preallocation_size_ taken under the DB mutex
// (the fresh-path call site reads the field before releasing mu).
// When initial_file_size > 0 the writer is treated as resuming over
// an already-populated file: WritableFileWriter is constructed with the
// existing size so size accounting is correct, and the log writer's
// initial_block_offset aligns to the existing file's tail within the
// current 32 KiB block.
std::unique_ptr<log::Writer> CreateManifestWriter(
std::unique_ptr<FSWritableFile> file, const std::string& fname,
const FileOptions& opts, uint64_t preallocation_size,
uint64_t initial_file_size = 0) const;
void AppendVersion(ColumnFamilyData* column_family_data, Version* v);
ColumnFamilyData* CreateColumnFamily(const ColumnFamilyOptions& cf_options,
@@ -1815,17 +1788,6 @@ class VersionSet {
// Current size of manifest file
uint64_t manifest_file_size_;
// File offset at the end of the last successfully completed logical
// record during MANIFEST recovery. Unlike manifest_file_size_ (the
// reader's I/O high-water mark, which includes any tolerated tail
// garbage), this value points to the byte after the last valid record.
// Used by ReopenManifestForAppend to detect intra-block tail
// corruption that doesn't extend the physical file size.
// manifest_file_size_ is kept separate because it is used for
// rotation decisions (ProcessManifestWrites), close-time verification
// (Close), and backup metadata.
uint64_t manifest_last_valid_record_end_;
// Size of the populated manifest file last time it was re-written from
// scratch.
uint64_t last_compacted_manifest_file_size_;
@@ -1898,9 +1860,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;
+9 -9
View File
@@ -1203,7 +1203,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());
}
@@ -2440,7 +2440,7 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseClean) {
// Verify content validation actually ran
ASSERT_TRUE(content_validation_ran);
// No corruption -- counter should be zero
// No corruption — counter should be zero
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
// Manifest path should be unchanged (no rotation)
@@ -2513,7 +2513,7 @@ 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;
@@ -2538,7 +2538,7 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseDisabled) {
ASSERT_FALSE(content_validation_ran);
// Validation disabled -- counter should be zero
// 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)
@@ -2585,7 +2585,7 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseSizeCheckFails) {
SyncPoint::GetInstance()->DisableProcessing();
// Size check caught the issue; content validation on rewritten manifest
// should pass -- no content validation failure recorded
// should pass — no content validation failure recorded
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
// Size check should have triggered rotation
@@ -2697,7 +2697,7 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseOpenFails) {
ASSERT_TRUE(close_s.IsIOError()) << close_s.ToString();
// File couldn't be opened -- no content validation ran
// File couldn't be opened — no content validation ran
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
}
@@ -4463,7 +4463,7 @@ class BestEffortsRecoverIncompleteVersionTest
std::move(file), blob_file_path, FileOptions(), options.clock));
BlobLogWriter blob_log_writer(std::move(file_writer), options.clock,
/*statistics*/ nullptr, blob_file_number,
/*statistics*/ nullptr,
/*use_fsync*/ true,
/*do_flush*/ false);
@@ -4481,8 +4481,8 @@ class BestEffortsRecoverIncompleteVersionTest
footer.expiration_range = expiration_range;
std::string checksum_method;
std::string checksum_value;
ASSERT_OK(blob_log_writer.AppendFooter(WriteOptions(), footer,
&checksum_method, &checksum_value));
ASSERT_OK(blob_log_writer.LegacyAppendFooterAndClose(
WriteOptions(), footer, &checksum_method, &checksum_value));
}
void RecoverFromManifestWithMissingFiles(
+2 -398
View File
@@ -30,10 +30,6 @@ class DBWideBasicTest : public DBTestBase {
return wide_column_test_util::GetOptionsForBlobTest(GetDefaultOptions());
}
Options GetDirectWriteOptions() {
return wide_column_test_util::GetDirectWriteOptions(GetDefaultOptions());
}
// Helper: runs the EntityBlobAfterFlush test logic with the given options.
void RunEntityBlobAfterFlush(const Options& options);
@@ -2163,57 +2159,6 @@ TEST_F(DBWideBasicTest, MultiGetEntityWithBlobResolution) {
}
}
TEST_F(DBWideBasicTest,
MultiGetBlobBackedEntityDirectWriteMemtableBatchLookup) {
// Goal: force the memtable batch MultiGet optimization to read a direct-write
// blob-backed entity from the active memtable. One key exercises merge
// resolution against a blob-backed entity base, which requires Saver to carry
// the BlobFetcher through the batched callback path. The second key proves
// the non-merge default-column path still resolves correctly in the same
// batched request.
Options options = GetDirectWriteOptions();
options.min_blob_size = 50;
options.memtable_batch_lookup_optimization = true;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
DestroyAndReopen(options);
const std::string merge_key = "multiget_direct_write_merge_key";
const std::string plain_key = "multiget_direct_write_plain_key";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string other_default_value = GenerateLargeValue(110, 'p');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
const std::string merge_operand = "tail";
WideColumns merge_columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
WideColumns plain_columns{{kDefaultWideColumnName, other_default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
merge_key, merge_columns));
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
plain_key, plain_columns));
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), merge_key,
merge_operand));
std::array<Slice, 2> keys{{merge_key, plain_key}};
std::array<PinnableSlice, 2> values;
std::array<Status, 2> statuses;
db_->MultiGet(ReadOptions(), db_->DefaultColumnFamily(), keys.size(),
keys.data(), values.data(), statuses.data());
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0], default_value + "|" + merge_operand);
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1], other_default_value);
}
void DBWideBasicTest::RunEntityBlobAfterFlush(const Options& options) {
Reopen(options);
@@ -2831,7 +2776,7 @@ TEST_F(DBWideBasicTest, EntityBlobBlockCacheTierGet) {
// Reopen to clear any caches
Reopen(options);
// Read with kBlockCacheTier -- blob is not in cache
// Read with kBlockCacheTier — blob is not in cache
ReadOptions read_opts;
read_opts.read_tier = kBlockCacheTier;
PinnableSlice result;
@@ -2964,347 +2909,6 @@ TEST_F(DBWideBasicTest, MergeEntityWithBlobColumns) {
}
}
TEST_F(DBWideBasicTest, GetMergeOperandsWithBlobBackedEntityDefaultColumn) {
// Goal: cover both GetMergeOperands code paths that read a compacted V2
// wide-column entity whose default column was moved to a blob file. The
// first read exercises the base-value path directly, then a merge operand is
// added so the second read exercises the merge-plus-base path.
Options options = GetBlobTestOptions();
options.min_blob_size = 50;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
DestroyAndReopen(options);
const std::string key = "merge_operands_blob_entity";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
const std::string merge_operand = "suffix";
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_FALSE(GetBlobFileNumbers().empty());
{
// The compacted V2 entity becomes the first operand when there are no
// newer merge operands above it.
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 1;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 1);
ASSERT_EQ(merge_operands[0], default_value);
}
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
merge_operand));
{
// After a merge is added, GetMergeOperands must still resolve the blob
// backed base default column while traversing the older base entry below
// the newer merge operand.
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 2;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 2);
ASSERT_EQ(merge_operands[0], default_value);
ASSERT_EQ(merge_operands[1], merge_operand);
}
}
TEST_F(DBWideBasicTest,
GetMergeOperandsWithBlobBackedEntityDefaultColumnDirectWriteMemtable) {
// Goal: cover the pre-flush memtable path when blob direct write stores a
// blob-backed V2 entity in memory. The first GetMergeOperands call exercises
// the no-merge base-operand path. After adding a merge, DB::Get exercises
// merge resolution against the same memtable entity, and a second
// GetMergeOperands call exercises the merge-plus-base path.
Options options = GetDirectWriteOptions();
options.min_blob_size = 50;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
DestroyAndReopen(options);
const std::string key = "direct_write_merge_operands_blob_entity";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
const std::string merge_operand = "suffix";
const std::string expected_merged = default_value + "|" + merge_operand;
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 1;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 1);
ASSERT_EQ(merge_operands[0], default_value);
}
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
merge_operand));
{
PinnableSlice result;
ASSERT_OK(
db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &result));
ASSERT_EQ(result, expected_merged);
}
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 2;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 2);
ASSERT_EQ(merge_operands[0], default_value);
ASSERT_EQ(merge_operands[1], merge_operand);
}
}
TEST_F(DBWideBasicTest,
GetAndGetEntityWithBlobBackedDefaultColumnDirectWriteMemtableReadOnly) {
// Goal: cover the read-only reopen path after writing a blob-backed
// direct-write entity without manually flushing it first. The test checks
// both `Get()`, which must resolve the default-column blob reference, and
// `GetEntity()`, which must eagerly resolve the entity's unresolved blob
// columns before returning them to the caller.
Options options = GetDirectWriteOptions();
options.min_blob_size = 50;
DestroyAndReopen(options);
const std::string key = "readonly_direct_write_memtable_entity";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
Close();
options.avoid_flush_during_recovery = true;
ASSERT_OK(ReadOnlyReopen(options));
{
PinnableSlice result;
ASSERT_OK(
db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &result));
ASSERT_EQ(result, default_value);
}
{
PinnableWideColumns result;
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), key,
&result));
ASSERT_EQ(result.columns(), columns);
}
}
TEST_F(DBWideBasicTest, ReadOnlyDirectWriteMemtableBlobBlockCacheTier) {
// Goal: cover the read-only memtable path under kBlockCacheTier. The test
// keeps a blob-backed direct-write entity in WAL-backed recovery state so
// resolving either Get() or GetEntity() would require blob I/O, which must
// surface as Incomplete rather than silently reading the blob file.
Options options = GetDirectWriteOptions();
options.min_blob_size = 50;
DestroyAndReopen(options);
const std::string key = "readonly_direct_write_memtable_block_cache_tier";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
Close();
options.avoid_flush_during_recovery = true;
ASSERT_OK(ReadOnlyReopen(options));
ReadOptions read_opts;
read_opts.read_tier = kBlockCacheTier;
{
PinnableSlice result;
const Status s =
db_->Get(read_opts, db_->DefaultColumnFamily(), key, &result);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
ASSERT_TRUE(result.empty());
}
{
PinnableWideColumns result;
const Status s =
db_->GetEntity(read_opts, db_->DefaultColumnFamily(), key, &result);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
ASSERT_TRUE(result.columns().empty());
}
}
TEST_F(DBWideBasicTest,
GetMergeOperandsWithBlobBackedEntityDefaultColumnReadOnly) {
// Goal: exercise OpenForReadOnly on a blob-backed V2 entity base in SST with
// a newer merge operand in a separate SST. The read-only DB must resolve the
// blob-backed default column for both Get() and GetMergeOperands() without
// relying on mutable memtable state.
Options options = GetBlobTestOptions();
options.min_blob_size = 50;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
DestroyAndReopen(options);
const std::string key = "readonly_merge_operands_blob_entity";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
const std::string merge_operand = "suffix";
const std::string expected_merged = default_value + "|" + merge_operand;
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_FALSE(GetBlobFileNumbers().empty());
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
merge_operand));
ASSERT_OK(Flush());
Close();
ASSERT_OK(ReadOnlyReopen(options));
{
PinnableSlice result;
ASSERT_OK(
db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &result));
ASSERT_EQ(result, expected_merged);
}
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 2;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 2);
ASSERT_EQ(merge_operands[0], default_value);
ASSERT_EQ(merge_operands[1], merge_operand);
}
}
TEST_F(DBWideBasicTest,
GetMergeOperandsWithBlobBackedEntityDefaultColumnBlockCacheTier) {
// Goal: prove the SST-backed GetMergeOperands path propagates Incomplete
// when resolving a blob-backed default column would require I/O. One key
// covers the direct base-entity path, and the other covers the merge-plus-
// base path with the merge operand flushed to a newer SST.
Options options = GetBlobTestOptions();
options.min_blob_size = 50;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
DestroyAndReopen(options);
const std::string base_key = "merge_operands_blob_entity_cache_base";
const std::string merge_key = "merge_operands_blob_entity_cache_merge";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), base_key,
columns));
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
merge_key, columns));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), merge_key,
"tail"));
ASSERT_OK(Flush());
// Reopen to clear any cached blob readers/values so kBlockCacheTier must
// report Incomplete instead of succeeding through a warm cache.
Reopen(options);
ReadOptions read_opts;
read_opts.read_tier = kBlockCacheTier;
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 1;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
const Status s = db_->GetMergeOperands(
read_opts, db_->DefaultColumnFamily(), base_key, merge_operands.data(),
&get_merge_opts, &number_of_operands);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
}
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 2;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
const Status s = db_->GetMergeOperands(
read_opts, db_->DefaultColumnFamily(), merge_key, merge_operands.data(),
&get_merge_opts, &number_of_operands);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
}
}
TEST_F(DBWideBasicTest, MergeEntityWithBlobColumnsNoDefault) {
// Test: Merge on a V2 entity without a default column. The merge result
// should produce a valid entity with all original columns plus a default
@@ -3394,7 +2998,7 @@ TEST_F(DBWideBasicTest, MergeEntityWithBlobColumnsBlockCacheTier) {
// Reopen to clear caches
Reopen(options);
// Read with kBlockCacheTier -- blob not in cache, merge needs blob resolution
// Read with kBlockCacheTier — blob not in cache, merge needs blob resolution
ReadOptions read_opts;
read_opts.read_tier = kBlockCacheTier;
PinnableSlice result;
+1 -1
View File
@@ -59,7 +59,7 @@ Status ReadPathBlobResolver::ResolveColumn(size_t column_index,
blob_resolver_util::FindBlobColumn(blob_columns_, column_index);
if (blob_index_ptr == nullptr) {
// Inline column -- return the value directly
// Inline column — return the value directly
*resolved_value = (*columns_)[column_index].value();
} else {
// Check if already resolved
-4
View File
@@ -153,8 +153,6 @@ class WideColumnSerialization {
// value() is initially the serialized BlobIndex bytes from the entity.
// blob_columns: receives (column_index, blob_index) pairs identifying which
// entries in `columns` are blob references and their decoded BlobIndex data.
// For valid input, this must agree with HasBlobColumns() about whether blob
// column references are present.
static Status DeserializeV2(
Slice& input, std::vector<WideColumn>& columns,
std::vector<std::pair<size_t, BlobIndex>>& blob_columns);
@@ -162,8 +160,6 @@ class WideColumnSerialization {
// Check if the serialized entity has any blob column references.
// Sets *has_blob_columns to true if version >= 2 and at least one column
// has blob type; false otherwise.
// For valid input, this must agree with DeserializeV2() about whether blob
// column references are present.
// Returns Status::Corruption on decode errors.
static Status HasBlobColumns(const Slice& input, bool& has_blob_columns);
+2 -2
View File
@@ -391,7 +391,7 @@ static BlobIndex MakeBlobIndex(uint64_t file_number, uint64_t offset,
return bi;
}
// Helper: V2 serialize -> DeserializeV2 round-trip, returning
// Helper: V2 serialize → DeserializeV2 round-trip, returning
// deserialized columns and blob column info.
static void V2SerializeAndDeserialize(
const std::vector<std::pair<std::string, std::string>>& columns,
@@ -798,7 +798,7 @@ TEST_F(WideColumnSerializationTest, RandomizedSerializeDeserializeRoundTrip) {
}
}
// V2 serialize -> DeserializeV2 round-trip
// V2 serialize → DeserializeV2 round-trip
std::string serialized;
std::vector<WideColumn> deserialized;
std::vector<std::pair<size_t, BlobIndex>> blob_out;
+2 -2
View File
@@ -279,9 +279,7 @@ DECLARE_bool(use_full_merge_v1);
DECLARE_int32(sync_wal_one_in);
DECLARE_bool(avoid_unnecessary_blocking_io);
DECLARE_bool(write_dbid_to_manifest);
DECLARE_bool(optimize_manifest_for_recovery);
DECLARE_bool(write_identity_file);
DECLARE_bool(reuse_manifest_on_open);
DECLARE_bool(avoid_flush_during_recovery);
DECLARE_bool(enforce_write_buffer_manager_during_recovery);
DECLARE_uint64(max_write_batch_group_size_bytes);
@@ -365,6 +363,8 @@ DECLARE_uint64(wp_commit_cache_bits);
DECLARE_bool(adaptive_readahead);
DECLARE_bool(async_io);
DECLARE_string(wal_compression);
DECLARE_bool(use_blog_format_for_blobs);
DECLARE_string(blog_checksum);
DECLARE_bool(verify_sst_unique_id_in_manifest);
DECLARE_bool(fast_sst_open);
+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());
}
}
+5 -9
View File
@@ -1133,19 +1133,10 @@ DEFINE_bool(write_dbid_to_manifest,
ROCKSDB_NAMESPACE::Options().write_dbid_to_manifest,
"Write DB_ID to manifest");
DEFINE_bool(optimize_manifest_for_recovery,
ROCKSDB_NAMESPACE::Options().optimize_manifest_for_recovery,
"Reduce recovery work after a clean shutdown");
DEFINE_bool(write_identity_file,
ROCKSDB_NAMESPACE::Options().write_identity_file,
"Write DB_ID to IDENTITY file");
DEFINE_bool(reuse_manifest_on_open,
ROCKSDB_NAMESPACE::Options().reuse_manifest_on_open,
"Reopen existing MANIFEST for append after recovery instead of "
"creating a fresh one");
DEFINE_bool(avoid_flush_during_recovery,
ROCKSDB_NAMESPACE::Options().avoid_flush_during_recovery,
"Avoid flush during recovery");
@@ -1329,6 +1320,11 @@ DEFINE_bool(
DEFINE_string(wal_compression, "none",
"Algorithm to use for WAL compression. none to disable.");
DEFINE_bool(use_blog_format_for_blobs, false,
"Use blog file format for new blob files.");
DEFINE_string(blog_checksum, "kXXH3", "Checksum type for blog format files.");
DEFINE_bool(
verify_sst_unique_id_in_manifest, false,
"Enable DB options `verify_sst_unique_id_in_manifest`, if true, during "
+12 -6
View File
@@ -3747,10 +3747,6 @@ void StressTest::PrintEnv() const {
static_cast<int>(FLAGS_avoid_unnecessary_blocking_io));
fprintf(stdout, "Write DB ID to manifest : %d\n",
static_cast<int>(FLAGS_write_dbid_to_manifest));
fprintf(stdout, "Optimize manifest recovery: %d\n",
static_cast<int>(FLAGS_optimize_manifest_for_recovery));
fprintf(stdout, "Reuse manifest on open : %d\n",
static_cast<int>(FLAGS_reuse_manifest_on_open));
fprintf(stdout, "Max Write Batch Group Size: %" PRIu64 "\n",
FLAGS_max_write_batch_group_size_bytes);
fprintf(stdout, "Use dynamic level : %d\n",
@@ -4670,9 +4666,7 @@ void InitializeOptionsFromFlags(
options.manual_wal_flush = FLAGS_manual_wal_flush_one_in > 0 ? true : false;
options.avoid_unnecessary_blocking_io = FLAGS_avoid_unnecessary_blocking_io;
options.write_dbid_to_manifest = FLAGS_write_dbid_to_manifest;
options.optimize_manifest_for_recovery = FLAGS_optimize_manifest_for_recovery;
options.write_identity_file = FLAGS_write_identity_file;
options.reuse_manifest_on_open = FLAGS_reuse_manifest_on_open;
options.avoid_flush_during_recovery = FLAGS_avoid_flush_during_recovery;
options.enforce_write_buffer_manager_during_recovery =
FLAGS_enforce_write_buffer_manager_during_recovery;
@@ -4746,6 +4740,18 @@ void InitializeOptionsFromFlags(
options.wal_compression =
StringToCompressionType(FLAGS_wal_compression.c_str());
options.use_blog_format_for_blobs = FLAGS_use_blog_format_for_blobs;
// Parse blog_checksum from string. Use the options system for correct
// handling of all ChecksumType values.
{
ConfigOptions config_options;
config_options.ignore_unknown_options = true;
DBOptions tmp_opts;
GetDBOptionsFromString(config_options, tmp_opts,
"blog_checksum=" + FLAGS_blog_checksum, &tmp_opts)
.PermitUncheckedError();
options.blog_checksum = tmp_opts.blog_checksum;
}
options.last_level_temperature =
StringToTemperature(FLAGS_last_level_temperature.c_str());
+1 -1
View File
@@ -2900,7 +2900,7 @@ class NonBatchedOpsStressTest : public StressTest {
op_logs += "N";
}
// backward scan -- skip when backward iteration is not supported
// backward scan — skip when backward iteration is not supported
if (FLAGS_test_backward_scan) {
key_str = Key(ub - 1);
iter->SeekForPrev(key_str);
+1 -5
View File
@@ -88,7 +88,7 @@ hx235:
pdillinger:
full_name: Peter Dillinger
fbid: 593944283
fbid: 513108
alanpaxton:
full_name: Alan Paxton
@@ -101,7 +101,3 @@ anand1976:
poojam23:
full_name: Pooja Malik
joshkang97:
full_name: Josh Kang
fbid: 180489168460795
@@ -1,118 +0,0 @@
---
title: Interpolation search for SST index blocks
layout: post
author: joshkang97
category: blog
---
For workloads with uniformly distributed keys, RocksDB now supports **interpolation search** for SST index blocks as an alternative to the default binary search.
## The idea
Binary search always splits the remaining range in half:
```
mid = low + (high - low) / 2
```
That's Θ(log n) probes regardless of the data. Interpolation search instead estimates where the target should land based on its value relative to the current boundaries:
```
probe = low + (target - key[low]) * (high - low) / (key[high] - key[low])
```
On uniformly distributed keys, that's expected O(log log n) probes. The canonical example: for an index block with restart keys `0, 1, 2, ..., 1023` and a seek for `900`, binary search needs about 10 hops; interpolation search lands on it in 1.
The catch is that pure interpolation search degrades to O(n) on badly skewed data.
## Turning a key into a number
The interpolation formula needs numeric values, but index keys are variable-length byte slices. RocksDB extracts a `uint64_t` per key by reading the first 8 bytes after the common prefix shared by the block's boundary keys, in big-endian, and zero-pads to the right if the remaining bytes are too short.
```cpp
inline uint64_t ReadBe64FromKey(Slice s, bool is_user_key, size_t offset) {
// ... strip internal seq/type bytes if needed ...
if (s.size() - offset >= 8) {
uint64_t val;
memcpy(&val, s.data() + offset, sizeof(val));
return port::kLittleEndian ? EndianSwapValue(val) : val;
}
// pad short tails with zeros on the right (preserves bytewise order)
}
```
Big-endian + zero-pad preserves bytewise ordering, so the linear interpolation formula stays consistent with the comparator. This is also why the feature requires `BytewiseComparator`.
Two distinct keys can still collapse to the same `uint64_t` once you go past the first 8 non-shared bytes. To avoid a divide-by-zero, we simply fall back to binary search in that case.
## How to enable it
To force interpolation search on every index block:
```cpp
rocksdb::BlockBasedTableOptions table_options;
table_options.index_block_search_type =
rocksdb::BlockBasedTableOptions::kInterpolation;
```
## kAuto: per-block selection
`kAuto` is the recommended way to use the feature. It chooses the search algorithm for each index block automatically, based on a uniformity hint written into the block footer at SST construction time:
```cpp
table_options.index_block_search_type =
rocksdb::BlockBasedTableOptions::kAuto;
table_options.uniform_cv_threshold = 0.2;
```
When `uniform_cv_threshold >= 0`, the SST writer scans each index block's restart keys and computes the **coefficient of variation (CV)** of the gaps between consecutive numeric key values:
```
gap[i] = key_value[i + 1] - key_value[i]
CV = stddev(gap) / mean(gap)
```
Lower CV means the gaps are more uniform — and the more likely interpolation search will outperform binary search. The CV is computed incrementally with Welford's online algorithm, so the scan is one pass over the restart points.
If `CV < uniform_cv_threshold`, RocksDB sets an `is_uniform` bit in the block footer. At read time, `kAuto` resolves to `kInterpolation` only when that bit is set *and* the comparator is bytewise; otherwise it uses `kBinary`.
### Write overhead
Computing the `is_uniform` bit is a cheap operation as it is only computed for the index blocks in a SST file. CPU profiling of `db_bench -benchmarks=fillseq,compact -compression_type=none -disable_wal=1` attributes only ~0.08% of write-path CPU to `ScanForUniformity`.
After a few more releases, we plan to enable kAuto and uniform_cv_threshold by default.
## Benchmarks
Setup — populate a DB and force a single-level shape so all reads hit the same index structure, then measure point-read throughput:
```
# Build a release binary
make clean && DEBUG_LEVEL=0 make db_bench
# Load + compact, varying the index_shortening_mode
./db_bench -benchmarks=fillrandom,compact \
-index_shortening_mode=1
# Then point-read against the populated DB
./db_bench -use_existing_db=true -benchmarks=readrandom \
-index_block_search_type=binary_search # or interpolation_search / auto_search
```
`index_shortening_mode=1` (`kShortenSeparators`) keeps the file's last index key intact, which preserves a roughly uniform numeric distribution for the benchmark.
Results, averaged over multiple runs:
| Mode | ops/s | vs `binary_search` |
|---|---|---|
| `binary_search` | 335,749 | baseline |
| `interpolation_search` | 366,598 | **+9.2%** |
| `auto_search` | 366,832 | **+9.2%** |
## Compatibility
The `is_uniform` bit reuses a previously-reserved bit in the data block footer. SSTs written by older RocksDB never set it and decode as `is_uniform = false`, so they read with binary search under `kAuto`. However, after the bit is set, if read by an older RocksDB version < 11.0.0, it will read it as a corruption error.
## Future work
Some future opportunities can involve extending interpolation search to data blocks, as well as supporting other comparators such as `ReverseBytewiseComparator`.
+2 -2
View File
@@ -1892,7 +1892,7 @@ TEST_F(EnvPosixTest, SupportedOpsNoAsyncIOOnIOUringInitFailure) {
int64_t thread_supported_ops2 = 0;
std::thread t([&]() {
fs->SupportedOps(thread_supported_ops);
// Second call on the same thread -- cached failure, no retry.
// Second call on the same thread — cached failure, no retry.
fs->SupportedOps(thread_supported_ops2);
});
t.join();
@@ -1903,7 +1903,7 @@ TEST_F(EnvPosixTest, SupportedOpsNoAsyncIOOnIOUringInitFailure) {
// Second call should also lack kAsyncIO.
ASSERT_EQ(thread_supported_ops2 & (1 << FSSupportedOps::kAsyncIO), 0);
ASSERT_NE(thread_supported_ops2 & (1 << FSSupportedOps::kFSPrefetch), 0);
// CreateIOUring must have been called exactly once -- not retried.
// CreateIOUring must have been called exactly once — not retried.
ASSERT_EQ(create_count, 1);
SyncPoint::GetInstance()->DisableProcessing();
+1 -1
View File
@@ -221,7 +221,7 @@ IOStatus GenerateOneFileChecksum(
std::unique_ptr<FSRandomAccessFile> r_file;
FileOptions fopts = file_options;
if (fopts.file_checksum.empty()) {
// No expected checksum is known -- this is a from-scratch computation.
// No expected checksum is known — this is a from-scratch computation.
fopts.file_checksum_func_name = kNoFileChecksumFuncName;
}
io_s = fs->NewRandomAccessFile(file_path, fopts, &r_file, nullptr);
+1 -1
View File
@@ -3899,7 +3899,7 @@ TEST_P(FSBufferPrefetchTest, FSBufferPrefetchStatsInternals) {
&result, &s, for_compaction);
// Platforms that don't have IO uring may not support async IO.
// With the ReadAsync sync fallback, s will be OK even when async IO is
// unavailable -- detect by checking if the second buffer has an async read
// unavailable — detect by checking if the second buffer has an async read
// in progress.
if (use_async_prefetch && s.IsNotSupported()) {
return;
+5 -8
View File
@@ -169,9 +169,6 @@ class WritableFileWriter {
Temperature temperature_;
public:
// `initial_file_size` seeds size accounting when wrapping a reopened file
// that will continue appending from its current end. Leave at 0 for newly
// created files and reuse-from-offset-0 paths.
WritableFileWriter(
std::unique_ptr<FSWritableFile>&& file, const std::string& _file_name,
const FileOptions& options, SystemClock* clock = nullptr,
@@ -181,21 +178,21 @@ class WritableFileWriter {
const std::vector<std::shared_ptr<EventListener>>& listeners = {},
FileChecksumGenFactory* file_checksum_gen_factory = nullptr,
bool perform_data_verification = false,
bool buffered_data_with_checksum = false, uint64_t initial_file_size = 0)
bool buffered_data_with_checksum = false)
: file_name_(_file_name),
writable_file_(std::move(file), io_tracer, _file_name),
clock_(clock),
buf_(),
max_buffer_size_(options.writable_file_max_buffer_size),
filesize_(initial_file_size),
flushed_size_(initial_file_size),
next_write_offset_(initial_file_size),
filesize_(0),
flushed_size_(0),
next_write_offset_(0),
pending_sync_(false),
seen_error_(false),
#ifndef NDEBUG
seen_injected_error_(false),
#endif // NDEBUG
last_sync_size_(initial_file_size),
last_sync_size_(0),
bytes_per_sync_(options.bytes_per_sync),
rate_limiter_(options.rate_limiter),
stats_(stats),
+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,
+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
+26
View File
@@ -0,0 +1,26 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#pragma once
#include "rocksdb/rocksdb_namespace.h"
namespace ROCKSDB_NAMESPACE {
// Types of checksums to use for checking integrity of logical blocks within
// files. All checksums currently use 32 bits of checking power (1 in 4B
// chance of failing to detect random corruption). Traditionally, the actual
// checking power can be far from ideal if the corruption is due to misplaced
// data (e.g. physical blocks out of order in a file, or from another file),
// which is fixed in format_version=6 (see below).
enum ChecksumType : char {
kNoChecksum = 0x0,
kCRC32c = 0x1,
kxxHash = 0x2,
kxxHash64 = 0x3,
kXXH3 = 0x4, // Supported since RocksDB 6.27
};
} // namespace ROCKSDB_NAMESPACE
+1 -5
View File
@@ -287,10 +287,6 @@ class CompactionFilter : public Customizable {
// may not realize there is a write conflict and may allow a Transaction to
// Commit that should have failed. Instead, it is better to implement any
// Merge filtering inside the MergeOperator.
//
// Note: Under WritePrepared, WriteUnprepared, and WriteCommitted
// TransactionDB, this method is skipped for operands at or below the latest
// published sequence, typically every operand in the input.
virtual bool FilterMergeOperand(int /*level*/, const Slice& /*key*/,
const Slice& /*operand*/) const {
return false;
@@ -420,7 +416,7 @@ class CompactionFilter : public Customizable {
// Returns true if the filter overrides FilterV4 and can handle
// WideColumnBlobResolver (lazy blob loading). When false (default),
// FilterV4 delegates to FilterV3 with fully resolved column values --
// FilterV4 delegates to FilterV3 with fully resolved column values —
// blob columns are eagerly fetched before the filter is called, ensuring
// backward compatibility with FilterV3-only filters.
//
+6 -1
View File
@@ -28,7 +28,12 @@ enum CompressionType : unsigned char {
kZSTD = 0x07,
kLastBuiltinCompression = kZSTD,
// Reserved for future use: up to 0x7F
// Values here reserved for future use by RocksDB
// Sentinel value used in blog file record trailers to indicate the
// payload uses streaming compression configured in the file header.
// See WriteBatchStreamingCompressionType header property.
kStreamingCompressionSentinel = 0x7F,
// For use by user custom CompressionManagers
kCustomCompression80 = 0x80,
-7
View File
@@ -1868,13 +1868,6 @@ class DB {
// and may not have file_creation_time property. In both the cases
// file_creation_time is considered 0 which means this API will return
// creation_time = 0 as there wouldn't be a timestamp lower than 0.
//
// NOTE: When the DB was opened with open_files_async = true, this API may
// block to wait for background SST file loading to complete, but only when
// necessary (i.e., when a file's manifest does not carry file_creation_time
// and the value must come from the SST reader). Modern DBs return the real
// value immediately. It is possible to have an incomplete result if the DB
// is closed while files are still being opened in the background.
virtual Status GetCreationTimeOfOldestFile(uint64_t* creation_time) = 0;
// Note: this API is not yet consistent with WritePrepared transactions.
+1 -1
View File
@@ -556,7 +556,7 @@ struct IOErrorInfo {
uint64_t offset;
};
// EXPERIMENTAL -- under active development, fields may change.
// EXPERIMENTAL — under active development, fields may change.
// Point-in-time snapshot of background job pressure for one DB: how busy
// compaction and flush are, and how close the DB is to write-stalling.
struct BackgroundJobPressure {
+17 -31
View File
@@ -19,6 +19,7 @@
#include <vector>
#include "rocksdb/advanced_options.h"
#include "rocksdb/checksum_type.h"
#include "rocksdb/comparator.h"
#include "rocksdb/compression_type.h"
#include "rocksdb/customizable.h"
@@ -1001,36 +1002,6 @@ struct DBOptions {
// This option is mutable with SetDBOptions().
bool verify_manifest_content_on_close = false;
// EXPERIMENTAL: If true, RocksDB can reduce recovery work after a clean
// shutdown, which may reduce DB::Open latency on warm reopens, especially on
// storage where metadata appends are expensive.
//
// Best-effort optimization: if it is disabled or unavailable, RocksDB falls
// back to the standard recovery path.
//
// Temporary rollout / kill switch for an optimization that is intended to be
// correct and eventually always enabled. Mutable via SetDBOptions().
bool optimize_manifest_for_recovery = false;
// EXPERIMENTAL: If true, DB::Open can try to reuse the existing MANIFEST
// for the first post-open metadata update instead of creating a fresh one.
// This can reduce warm-open latency for DBs whose MANIFEST is expensive to
// rebuild.
//
// Best-effort optimization: even when enabled, RocksDB may still create a
// fresh MANIFEST if the FileSystem does not support reopening the existing
// MANIFEST for append, or if RocksDB decides reuse is unsafe. That fallback
// is normal behavior.
//
// With very small `max_manifest_file_size` settings, the reused MANIFEST can
// still rotate earlier than expected after open, because RocksDB may keep a
// conservative auto-tuned rotation threshold until it later refreshes its
// compacted-size estimate.
//
// Temporary rollout / kill switch while this optimization is being
// validated.
bool reuse_manifest_on_open = false;
// This option mostly replaces max_manifest_file_size to control an auto-tuned
// balance of manifest write amplification and space amplification. A new
// manifest file is created with the "compacted" contents of the old one when
@@ -1510,6 +1481,21 @@ struct DBOptions {
// the WAL is read.
CompressionType wal_compression = kNoCompression;
// If true, new blob files use the blog file format instead of the legacy
// blob log format. The blog format provides context checksums and a
// unified file structure shared with WAL. Existing legacy blob files are
// still readable regardless of this setting.
//
// Default: false
bool use_blog_format_for_blobs = false;
// Checksum type used for integrity checks within blog format files
// (both WAL and blob). Uses the same ChecksumType enum as
// BlockBasedTableOptions::checksum.
//
// Default: kXXH3
ChecksumType blog_checksum = kXXH3;
// Set to true to re-instate an old behavior of keeping complete, synced WAL
// files open for write until they are collected for deletion by a
// background thread. This should not be needed unless there is a
@@ -2952,7 +2938,7 @@ struct SizeApproximationOptions {
// blob file data in the key range. When enabled, the total blob file size
// is prorated by the ratio of SST data in the range to the total SST data:
//
// blob_size_in_range ~= total_blob_size * (sst_in_range / total_sst)
// blob_size_in_range ≈ total_blob_size * (sst_in_range / total_sst)
//
// Limitations of this approximation:
// - Assumes blob data is distributed proportionally to SST data, which
-7
View File
@@ -9,7 +9,6 @@
#include "rocksdb/options.h"
#include "rocksdb/slice.h"
#include "rocksdb/table_properties.h"
#include "rocksdb/types.h"
namespace ROCKSDB_NAMESPACE {
@@ -57,12 +56,6 @@ class SstFileReader {
// capabilities like filling cache, read ahead are disabled.
std::unique_ptr<Iterator> NewTableIterator();
// Parses a raw key returned by NewTableIterator().
// The Slice fields in parsed_key point into raw_table_key and are valid only
// while raw_table_key remains valid.
Status ParseTableIteratorKey(const Slice& raw_table_key,
ParsedEntryInfo* parsed_key) const;
std::shared_ptr<const TableProperties> GetTableProperties() const;
// Verifies whether there is corruption in this table.
+3 -13
View File
@@ -46,19 +46,9 @@ struct ConfigOptions;
struct EnvOptions;
class UserDefinedIndexFactory;
// Types of checksums to use for checking integrity of logical blocks within
// files. All checksums currently use 32 bits of checking power (1 in 4B
// chance of failing to detect random corruption). Traditionally, the actual
// checking power can be far from ideal if the corruption is due to misplaced
// data (e.g. physical blocks out of order in a file, or from another file),
// which is fixed in format_version=6 (see below).
enum ChecksumType : char {
kNoChecksum = 0x0,
kCRC32c = 0x1,
kxxHash = 0x2,
kxxHash64 = 0x3,
kXXH3 = 0x4, // Supported since RocksDB 6.27
};
// ChecksumType is defined in its own header for use by both table options
// and DB options (blog file checksums).
#include "rocksdb/checksum_type.h"
// `PinningTier` is used to specify which tier of block-based tables should
// be affected by a block cache pinning setting (see
+2 -2
View File
@@ -43,7 +43,7 @@ class UserDefinedIndexBuilder {
kOther = 3, // Other types (e.g., blob reference, wide-column entity).
// The value format is type-specific and may not be the
// actual user data.
kTypeMax, // Sentinel -- must be last. Value may change across releases.
kTypeMax, // Sentinel — must be last. Value may change across releases.
};
// File offset and size of the data block
@@ -106,7 +106,7 @@ class UserDefinedIndexBuilder {
// (e.g., trie-based indexes) can leave this as a no-op.
//
// @key: The user key (without sequence number or type suffix).
// @type: The entry type -- kValue (Put), kDelete, kMerge, or kOther.
// @type: The entry type — kValue (Put), kDelete, kMerge, or kOther.
// For kDelete entries, the value may be empty. For kOther, the
// value format is type-specific and may not be actual user data.
// @value: The associated value (may be empty for deletions).
+3 -3
View File
@@ -559,7 +559,7 @@ TEST_F(InlineSkipTest, MultiGetDuplicateKeysWithCallbackWalk) {
}
// Callback that walks forward through multiple entries before stopping.
// This simulates the Merge operand accumulation in SaveValue -- the callback
// This simulates the Merge operand accumulation in SaveValue — the callback
// returns true for entries until it reaches one >= stop_at, simulating
// walking through merge chain entries.
struct WalkingCallbackArg {
@@ -1068,11 +1068,11 @@ static void ConcurrentMultiGetWorker(void* arg) {
// Validate all results: each found result must equal its query key
// (since all queried keys were inserted, an exact match is expected).
// However, due to concurrent inserts, a key sampled from the ring
// might not yet be visible -- so we only check that if a result is
// might not yet be visible — so we only check that if a result is
// found, it equals the query key (i.e., no wrong key returned).
for (size_t j = 0; j < batch_size; j++) {
if (results[j] != 0 && results[j] != query_keys[j]) {
// Got a result that doesn't match the query -- either a bug or
// Got a result that doesn't match the query — either a bug or
// the exact key wasn't inserted and we got the next one. Since
// all our query keys are inserted, this means the result should
// be >= query. For the just-inserted key we already checked
+2 -12
View File
@@ -48,18 +48,13 @@ bool WBWIMemTable::Get(const LookupKey& key, std::string* value,
SequenceNumber* max_covering_tombstone_seq,
SequenceNumber* out_seq, const ReadOptions&,
bool immutable_memtable, ReadCallback* callback,
bool* is_blob_index, bool do_merge,
const BlobFetcher* blob_fetcher) {
bool* is_blob_index, bool do_merge) {
assert(s->ok() || s->IsMergeInProgress());
(void)immutable_memtable;
(void)timestamp;
(void)columns;
(void)blob_fetcher;
assert(immutable_memtable);
assert(!timestamp); // TODO: support UDT
// IngestWriteBatchWithIndex() is rejected while any live column family has
// blob direct write enabled, so WBWI should never need blob resolution.
assert(blob_fetcher == nullptr);
assert(assigned_seqno_.upper_bound != kMaxSequenceNumber);
assert(assigned_seqno_.lower_bound != kMaxSequenceNumber);
// WBWI does not support DeleteRange yet.
@@ -159,15 +154,10 @@ bool WBWIMemTable::Get(const LookupKey& key, std::string* value,
void WBWIMemTable::MultiGet(const ReadOptions& read_options,
MultiGetRange* range, ReadCallback* callback,
bool immutable_memtable,
const BlobFetcher* blob_fetcher) {
bool immutable_memtable) {
(void)immutable_memtable;
(void)blob_fetcher;
// Should only be used as immutable memtable.
assert(immutable_memtable);
// IngestWriteBatchWithIndex() is rejected while any live column family has
// blob direct write enabled, so WBWI should never need blob resolution.
assert(blob_fetcher == nullptr);
// TODO: reuse the InternalIterator created in Get().
for (auto iter = range->begin(); iter != range->end(); ++iter) {
SequenceNumber dummy_seq = 0;
Loaded 100 of 155 files, more files were not shown because too many files have changed in this diff. Show more