mirror of
https://github.com/facebook/rocksdb.git
synced 2026-07-07 14:47:40 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8600f85f6 | |||
| 1f910525a7 | |||
| 5239c4caeb | |||
| 2e21612c3e | |||
| 5ba6eb5497 |
@@ -18,7 +18,7 @@ module.exports = function parseClaude({executionFile, conclusion, meta}) {
|
|||||||
if (meta.trigger !== 'auto') {
|
if (meta.trigger !== 'auto') {
|
||||||
return `*Requested by @${meta.reviewer}*`;
|
return `*Requested by @${meta.reviewer}*`;
|
||||||
}
|
}
|
||||||
const shortSha = meta.headSha.substring(0, 7);
|
const shortSha = meta.headSha ? meta.headSha.substring(0, 7) : 'unknown';
|
||||||
if (meta.autoMode === 'early') {
|
if (meta.autoMode === 'early') {
|
||||||
return `*Auto-triggered after CI reached the early-review threshold — reviewing commit ${
|
return `*Auto-triggered after CI reached the early-review threshold — reviewing commit ${
|
||||||
shortSha}*`;
|
shortSha}*`;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ module.exports = function parseCodex(
|
|||||||
if (meta.trigger !== 'auto') {
|
if (meta.trigger !== 'auto') {
|
||||||
return `*Requested by @${meta.reviewer}*`;
|
return `*Requested by @${meta.reviewer}*`;
|
||||||
}
|
}
|
||||||
const shortSha = meta.headSha.substring(0, 7);
|
const shortSha = meta.headSha ? meta.headSha.substring(0, 7) : 'unknown';
|
||||||
if (meta.autoMode === 'early') {
|
if (meta.autoMode === 'early') {
|
||||||
return `*Auto-triggered after CI reached the early-review threshold — reviewing commit ${
|
return `*Auto-triggered after CI reached the early-review threshold — reviewing commit ${
|
||||||
shortSha}*`;
|
shortSha}*`;
|
||||||
|
|||||||
@@ -549,6 +549,10 @@ jobs:
|
|||||||
CODEX_HOME: /tmp/codex-home
|
CODEX_HOME: /tmp/codex-home
|
||||||
CODEX_MODEL: ${{ inputs.default_model }}
|
CODEX_MODEL: ${{ inputs.default_model }}
|
||||||
run: |
|
run: |
|
||||||
|
if [ -z "${OPENAI_API_KEY}" ]; then
|
||||||
|
echo "OPENAI_API_KEY not configured, skipping Codex review"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
mkdir -p "${CODEX_HOME}"
|
mkdir -p "${CODEX_HOME}"
|
||||||
codex exec \
|
codex exec \
|
||||||
-m "${CODEX_MODEL}" \
|
-m "${CODEX_MODEL}" \
|
||||||
@@ -698,6 +702,10 @@ jobs:
|
|||||||
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
|
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
|
||||||
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
|
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
|
||||||
run: |
|
run: |
|
||||||
|
if [ -z "${OPENAI_API_KEY}" ]; then
|
||||||
|
echo "OPENAI_API_KEY not configured, skipping Codex review"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
mkdir -p "${CODEX_HOME}"
|
mkdir -p "${CODEX_HOME}"
|
||||||
set +e
|
set +e
|
||||||
codex exec review \
|
codex exec review \
|
||||||
@@ -853,6 +861,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
if: >-
|
if: >-
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
|||||||
@@ -30,10 +30,9 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- claude-opus-4-7
|
|
||||||
- claude-opus-4-6
|
- claude-opus-4-6
|
||||||
- claude-sonnet-4-6
|
- claude-sonnet-4-6
|
||||||
default: claude-opus-4-7
|
default: claude-opus-4-6
|
||||||
thinking_budget:
|
thinking_budget:
|
||||||
description: Override MAX_THINKING_TOKENS (blank = auto-classify)
|
description: Override MAX_THINKING_TOKENS (blank = auto-classify)
|
||||||
required: false
|
required: false
|
||||||
@@ -61,7 +60,7 @@ jobs:
|
|||||||
query_command: /claude-query
|
query_command: /claude-query
|
||||||
result_artifact_name: claude-review-result
|
result_artifact_name: claude-review-result
|
||||||
comment_file: claude-review-comment.md
|
comment_file: claude-review-comment.md
|
||||||
default_model: claude-opus-4-7
|
default_model: claude-opus-4-6
|
||||||
selected_model: ${{ github.event_name == 'workflow_dispatch' && inputs.model || '' }}
|
selected_model: ${{ github.event_name == 'workflow_dispatch' && inputs.model || '' }}
|
||||||
classifier_model: claude-sonnet-4-6
|
classifier_model: claude-sonnet-4-6
|
||||||
recovery_model: claude-sonnet-4-6
|
recovery_model: claude-sonnet-4-6
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
|||||||
"db/blob/blob_source.cc",
|
"db/blob/blob_source.cc",
|
||||||
"db/blob/blob_write_batch_transformer.cc",
|
"db/blob/blob_write_batch_transformer.cc",
|
||||||
"db/blob/prefetch_buffer_collection.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/builder.cc",
|
||||||
"db/c.cc",
|
"db/c.cc",
|
||||||
"db/coalescing_iterator.cc",
|
"db/coalescing_iterator.cc",
|
||||||
@@ -4592,6 +4595,18 @@ cpp_unittest_wrapper(name="block_test",
|
|||||||
extra_compiler_flags=[])
|
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",
|
cpp_unittest_wrapper(name="bloom_test",
|
||||||
srcs=["util/bloom_test.cc"],
|
srcs=["util/bloom_test.cc"],
|
||||||
deps=[":rocksdb_test_lib"],
|
deps=[":rocksdb_test_lib"],
|
||||||
|
|||||||
@@ -743,6 +743,9 @@ set(SOURCES
|
|||||||
db/blob/blob_source.cc
|
db/blob/blob_source.cc
|
||||||
db/blob/blob_write_batch_transformer.cc
|
db/blob/blob_write_batch_transformer.cc
|
||||||
db/blob/prefetch_buffer_collection.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/builder.cc
|
||||||
db/c.cc
|
db/c.cc
|
||||||
db/coalescing_iterator.cc
|
db/coalescing_iterator.cc
|
||||||
@@ -1431,6 +1434,8 @@ if(WITH_TESTS)
|
|||||||
db/blob/db_blob_compaction_test.cc
|
db/blob/db_blob_compaction_test.cc
|
||||||
db/blob/db_blob_corruption_test.cc
|
db/blob/db_blob_corruption_test.cc
|
||||||
db/blob/db_blob_index_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/column_family_test.cc
|
||||||
db/compact_files_test.cc
|
db/compact_files_test.cc
|
||||||
db/compaction/clipping_iterator_test.cc
|
db/compaction/clipping_iterator_test.cc
|
||||||
|
|||||||
@@ -1536,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)
|
db_blob_index_test: $(OBJ_DIR)/db/blob/db_blob_index_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||||
$(AM_LINK)
|
$(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)
|
db_block_cache_test: $(OBJ_DIR)/db/db_block_cache_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||||
$(AM_LINK)
|
$(AM_LINK)
|
||||||
|
|
||||||
|
|||||||
@@ -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 | 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 ≤ 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 > 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 >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 > 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 ≤ 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 < 16 KiB.</p>
|
||||||
|
|
||||||
|
<h3>Full Record Format (varint length > 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 > 0)
|
||||||
|
[compression_type: 1B] [checksum: 4B] (if length > 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 ≥ 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 ≤ 2B (payload < 16 KiB), matches compact_record_type, no type byte. Full: varint > 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>
|
||||||
+169
-44
@@ -14,6 +14,8 @@
|
|||||||
#include "db/blob/blob_log_format.h"
|
#include "db/blob/blob_log_format.h"
|
||||||
#include "db/blob/blob_log_writer.h"
|
#include "db/blob/blob_log_writer.h"
|
||||||
#include "db/blob/blob_source.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/event_helpers.h"
|
||||||
#include "db/version_set.h"
|
#include "db/version_set.h"
|
||||||
#include "file/filename.h"
|
#include "file/filename.h"
|
||||||
@@ -67,10 +69,13 @@ BlobFileBuilder::BlobFileBuilder(
|
|||||||
min_blob_size_(mutable_cf_options->min_blob_size),
|
min_blob_size_(mutable_cf_options->min_blob_size),
|
||||||
blob_file_size_(mutable_cf_options->blob_file_size),
|
blob_file_size_(mutable_cf_options->blob_file_size),
|
||||||
blob_compression_type_(mutable_cf_options->blob_compression_type),
|
blob_compression_type_(mutable_cf_options->blob_compression_type),
|
||||||
// TODO with schema change: support custom compression manager and options
|
// Blog format uses the CF's compression manager; legacy format must
|
||||||
// such as max_compressed_bytes_per_kb
|
// use the builtin for compatibility.
|
||||||
// NOTE: returns nullptr for kNoCompression
|
blob_compression_manager_(immutable_options->use_blog_format_for_blobs &&
|
||||||
blob_compressor_(GetBuiltinV2CompressionManager()->GetCompressor(
|
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_)),
|
mutable_cf_options->blob_compression_opts, blob_compression_type_)),
|
||||||
blob_compressor_wa_(blob_compressor_
|
blob_compressor_wa_(blob_compressor_
|
||||||
? blob_compressor_->ObtainWorkingArea()
|
? blob_compressor_->ObtainWorkingArea()
|
||||||
@@ -123,8 +128,10 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
|
|||||||
Slice blob = value;
|
Slice blob = value;
|
||||||
GrowableBuffer compressed_blob;
|
GrowableBuffer compressed_blob;
|
||||||
|
|
||||||
{
|
if (!blog_writer_) {
|
||||||
const Status s = CompressBlobIfNeeded(&blob, &compressed_blob);
|
// 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()) {
|
if (!s.ok()) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
@@ -132,10 +139,13 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
|
|||||||
|
|
||||||
uint64_t blob_file_number = 0;
|
uint64_t blob_file_number = 0;
|
||||||
uint64_t blob_offset = 0;
|
uint64_t blob_offset = 0;
|
||||||
|
uint64_t blob_on_disk_size = 0;
|
||||||
|
CompressionType actual_compression_type = blob_compression_type_;
|
||||||
|
|
||||||
{
|
{
|
||||||
const Status s =
|
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()) {
|
if (!s.ok()) {
|
||||||
return s;
|
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(),
|
BlobIndex::EncodeBlob(blob_index, blob_file_number, blob_offset,
|
||||||
blob_compression_type_);
|
blob_on_disk_size, actual_compression_type);
|
||||||
|
|
||||||
return Status::OK();
|
return Status::OK();
|
||||||
}
|
}
|
||||||
@@ -172,7 +182,9 @@ Status BlobFileBuilder::Finish() {
|
|||||||
return CloseBlobFile();
|
return CloseBlobFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BlobFileBuilder::IsBlobFileOpen() const { return !!writer_; }
|
bool BlobFileBuilder::IsBlobFileOpen() const {
|
||||||
|
return !!writer_ || !!blog_writer_;
|
||||||
|
}
|
||||||
|
|
||||||
Status BlobFileBuilder::OpenBlobFileIfNeeded() {
|
Status BlobFileBuilder::OpenBlobFileIfNeeded() {
|
||||||
if (IsBlobFileOpen()) {
|
if (IsBlobFileOpen()) {
|
||||||
@@ -231,37 +243,73 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
|
|||||||
immutable_options_->file_checksum_gen_factory.get(),
|
immutable_options_->file_checksum_gen_factory.get(),
|
||||||
tmp_set.Contains(FileType::kBlobFile), false));
|
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(
|
if (immutable_options_->use_blog_format_for_blobs) {
|
||||||
std::move(file_writer), immutable_options_->clock, statistics,
|
// Blog format path
|
||||||
blob_file_number, immutable_options_->use_fsync, do_flush));
|
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;
|
blog_writer_ =
|
||||||
constexpr ExpirationRange expiration_range;
|
std::make_unique<BlogFileWriter>(std::move(file_writer), blog_header);
|
||||||
|
|
||||||
BlobLogHeader header(column_family_id_, blob_compression_type_, has_ttl,
|
Status s = blog_writer_->WriteHeader(*write_options_);
|
||||||
expiration_range);
|
|
||||||
|
|
||||||
{
|
|
||||||
Status s = blob_log_writer->WriteHeader(*write_options_, header);
|
|
||||||
|
|
||||||
TEST_SYNC_POINT_CALLBACK(
|
TEST_SYNC_POINT_CALLBACK(
|
||||||
"BlobFileBuilder::OpenBlobFileIfNeeded:WriteHeader", &s);
|
"BlobFileBuilder::OpenBlobFileIfNeeded:WriteBlogHeader", &s);
|
||||||
|
|
||||||
if (!s.ok()) {
|
if (!s.ok()) {
|
||||||
return s;
|
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());
|
assert(IsBlobFileOpen());
|
||||||
|
|
||||||
return Status::OK();
|
return Status::OK();
|
||||||
}
|
}
|
||||||
|
|
||||||
Status BlobFileBuilder::CompressBlobIfNeeded(
|
Status BlobFileBuilder::LegacyCompressBlob(
|
||||||
Slice* blob, GrowableBuffer* compressed_blob) const {
|
Slice* blob, GrowableBuffer* compressed_blob) const {
|
||||||
assert(blob);
|
assert(blob);
|
||||||
assert(compressed_blob);
|
assert(compressed_blob);
|
||||||
@@ -290,17 +338,48 @@ Status BlobFileBuilder::CompressBlobIfNeeded(
|
|||||||
return Status::OK();
|
return Status::OK();
|
||||||
}
|
}
|
||||||
|
|
||||||
Status BlobFileBuilder::WriteBlobToFile(const Slice& key, const Slice& blob,
|
Status BlobFileBuilder::WriteBlobToFile(
|
||||||
uint64_t* blob_file_number,
|
const Slice& key, const Slice& blob, uint64_t* blob_file_number,
|
||||||
uint64_t* blob_offset) {
|
uint64_t* blob_offset, uint64_t* blob_on_disk_size,
|
||||||
|
CompressionType* actual_compression_type) {
|
||||||
assert(IsBlobFileOpen());
|
assert(IsBlobFileOpen());
|
||||||
assert(blob_file_number);
|
assert(blob_file_number);
|
||||||
assert(blob_offset);
|
assert(blob_offset);
|
||||||
|
assert(blob_on_disk_size);
|
||||||
|
assert(actual_compression_type);
|
||||||
|
|
||||||
uint64_t key_offset = 0;
|
Status s;
|
||||||
|
|
||||||
Status s =
|
if (blog_writer_) {
|
||||||
writer_->AddRecord(*write_options_, key, blob, &key_offset, blob_offset);
|
// 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);
|
TEST_SYNC_POINT_CALLBACK("BlobFileBuilder::WriteBlobToFile:AddRecord", &s);
|
||||||
|
|
||||||
@@ -308,10 +387,16 @@ Status BlobFileBuilder::WriteBlobToFile(const Slice& key, const Slice& blob,
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
*blob_file_number = writer_->get_log_number();
|
*blob_file_number = blob_file_number_;
|
||||||
|
|
||||||
++blob_count_;
|
++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();
|
return Status::OK();
|
||||||
}
|
}
|
||||||
@@ -319,22 +404,60 @@ Status BlobFileBuilder::WriteBlobToFile(const Slice& key, const Slice& blob,
|
|||||||
Status BlobFileBuilder::CloseBlobFile() {
|
Status BlobFileBuilder::CloseBlobFile() {
|
||||||
assert(IsBlobFileOpen());
|
assert(IsBlobFileOpen());
|
||||||
|
|
||||||
BlobLogFooter footer;
|
|
||||||
footer.blob_count = blob_count_;
|
|
||||||
|
|
||||||
std::string checksum_method;
|
std::string checksum_method;
|
||||||
std::string checksum_value;
|
std::string checksum_value;
|
||||||
|
Status s;
|
||||||
|
|
||||||
Status s = writer_->AppendFooter(*write_options_, footer, &checksum_method,
|
if (blog_writer_) {
|
||||||
&checksum_value);
|
// 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()) {
|
if (!s.ok()) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint64_t blob_file_number = writer_->get_log_number();
|
const uint64_t blob_file_number = blob_file_number_;
|
||||||
|
|
||||||
if (blob_callback_) {
|
if (blob_callback_) {
|
||||||
s = blob_callback_->OnBlobFileCompleted(
|
s = blob_callback_->OnBlobFileCompleted(
|
||||||
@@ -356,6 +479,7 @@ Status BlobFileBuilder::CloseBlobFile() {
|
|||||||
blob_count_, blob_bytes_);
|
blob_count_, blob_bytes_);
|
||||||
|
|
||||||
writer_.reset();
|
writer_.reset();
|
||||||
|
blog_writer_.reset();
|
||||||
blob_count_ = 0;
|
blob_count_ = 0;
|
||||||
blob_bytes_ = 0;
|
blob_bytes_ = 0;
|
||||||
|
|
||||||
@@ -365,7 +489,8 @@ Status BlobFileBuilder::CloseBlobFile() {
|
|||||||
Status BlobFileBuilder::CloseBlobFileIfNeeded() {
|
Status BlobFileBuilder::CloseBlobFileIfNeeded() {
|
||||||
assert(IsBlobFileOpen());
|
assert(IsBlobFileOpen());
|
||||||
|
|
||||||
const WritableFileWriter* const file_writer = writer_->file();
|
const WritableFileWriter* const file_writer =
|
||||||
|
blog_writer_ ? blog_writer_->file() : writer_->file();
|
||||||
assert(file_writer);
|
assert(file_writer);
|
||||||
|
|
||||||
if (file_writer->GetFileSize() < blob_file_size_) {
|
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 files. So we can ignore the below error.
|
||||||
blob_callback_
|
blob_callback_
|
||||||
->OnBlobFileCompleted(blob_file_paths_->back(), column_family_name_,
|
->OnBlobFileCompleted(blob_file_paths_->back(), column_family_name_,
|
||||||
job_id_, writer_->get_log_number(),
|
job_id_, blob_file_number_, creation_reason_, s,
|
||||||
creation_reason_, s, "", "", blob_count_,
|
"", "", blob_count_, blob_bytes_)
|
||||||
blob_bytes_)
|
|
||||||
.PermitUncheckedError();
|
.PermitUncheckedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
writer_.reset();
|
writer_.reset();
|
||||||
|
blog_writer_.reset();
|
||||||
blob_count_ = 0;
|
blob_count_ = 0;
|
||||||
blob_bytes_ = 0;
|
blob_bytes_ = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class BlobLogWriter;
|
|||||||
class IOTracer;
|
class IOTracer;
|
||||||
class BlobFileCompletionCallback;
|
class BlobFileCompletionCallback;
|
||||||
|
|
||||||
|
class BlogFileWriter;
|
||||||
|
|
||||||
class BlobFileBuilder {
|
class BlobFileBuilder {
|
||||||
public:
|
public:
|
||||||
BlobFileBuilder(VersionSet* versions, FileSystem* fs,
|
BlobFileBuilder(VersionSet* versions, FileSystem* fs,
|
||||||
@@ -78,10 +80,11 @@ class BlobFileBuilder {
|
|||||||
private:
|
private:
|
||||||
bool IsBlobFileOpen() const;
|
bool IsBlobFileOpen() const;
|
||||||
Status OpenBlobFileIfNeeded();
|
Status OpenBlobFileIfNeeded();
|
||||||
Status CompressBlobIfNeeded(Slice* blob,
|
Status LegacyCompressBlob(Slice* blob, GrowableBuffer* compressed_blob) const;
|
||||||
GrowableBuffer* compressed_blob) const;
|
|
||||||
Status WriteBlobToFile(const Slice& key, const Slice& blob,
|
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 CloseBlobFile();
|
||||||
Status CloseBlobFileIfNeeded();
|
Status CloseBlobFileIfNeeded();
|
||||||
|
|
||||||
@@ -94,6 +97,7 @@ class BlobFileBuilder {
|
|||||||
uint64_t min_blob_size_;
|
uint64_t min_blob_size_;
|
||||||
uint64_t blob_file_size_;
|
uint64_t blob_file_size_;
|
||||||
CompressionType blob_compression_type_;
|
CompressionType blob_compression_type_;
|
||||||
|
std::shared_ptr<CompressionManager> blob_compression_manager_;
|
||||||
std::unique_ptr<Compressor> blob_compressor_;
|
std::unique_ptr<Compressor> blob_compressor_;
|
||||||
mutable Compressor::ManagedWorkingArea blob_compressor_wa_;
|
mutable Compressor::ManagedWorkingArea blob_compressor_wa_;
|
||||||
PrepopulateBlobCache prepopulate_blob_cache_;
|
PrepopulateBlobCache prepopulate_blob_cache_;
|
||||||
@@ -111,6 +115,8 @@ class BlobFileBuilder {
|
|||||||
std::vector<std::string>* blob_file_paths_;
|
std::vector<std::string>* blob_file_paths_;
|
||||||
std::vector<BlobFileAddition>* blob_file_additions_;
|
std::vector<BlobFileAddition>* blob_file_additions_;
|
||||||
std::unique_ptr<BlobLogWriter> writer_;
|
std::unique_ptr<BlobLogWriter> writer_;
|
||||||
|
std::unique_ptr<BlogFileWriter> blog_writer_;
|
||||||
|
uint64_t blob_file_number_ = 0;
|
||||||
uint64_t blob_count_;
|
uint64_t blob_count_;
|
||||||
uint64_t blob_bytes_;
|
uint64_t blob_bytes_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -598,7 +598,7 @@ INSTANTIATE_TEST_CASE_P(
|
|||||||
"BlobFileBuilder::OpenBlobFileIfNeeded:NewWritableFile",
|
"BlobFileBuilder::OpenBlobFileIfNeeded:NewWritableFile",
|
||||||
"BlobFileBuilder::OpenBlobFileIfNeeded:WriteHeader",
|
"BlobFileBuilder::OpenBlobFileIfNeeded:WriteHeader",
|
||||||
"BlobFileBuilder::WriteBlobToFile:AddRecord",
|
"BlobFileBuilder::WriteBlobToFile:AddRecord",
|
||||||
"BlobFileBuilder::WriteBlobToFile:AppendFooter"}));
|
"BlobFileBuilder::WriteBlobToFile:LegacyAppendFooterAndClose"}));
|
||||||
|
|
||||||
TEST_P(BlobFileBuilderIOErrorTest, IOError) {
|
TEST_P(BlobFileBuilderIOErrorTest, IOError) {
|
||||||
// Simulate an I/O error during the specified step of Add()
|
// Simulate an I/O error during the specified step of Add()
|
||||||
|
|||||||
@@ -48,8 +48,7 @@ void WriteBlobFile(uint32_t column_family_id,
|
|||||||
constexpr bool do_flush = false;
|
constexpr bool do_flush = false;
|
||||||
|
|
||||||
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
|
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
|
||||||
statistics, blob_file_number, use_fsync,
|
statistics, use_fsync, do_flush);
|
||||||
do_flush);
|
|
||||||
|
|
||||||
constexpr bool has_ttl = false;
|
constexpr bool has_ttl = false;
|
||||||
constexpr ExpirationRange expiration_range;
|
constexpr ExpirationRange expiration_range;
|
||||||
@@ -77,8 +76,8 @@ void WriteBlobFile(uint32_t column_family_id,
|
|||||||
std::string checksum_method;
|
std::string checksum_method;
|
||||||
std::string checksum_value;
|
std::string checksum_value;
|
||||||
|
|
||||||
ASSERT_OK(blob_log_writer.AppendFooter(WriteOptions(), footer,
|
ASSERT_OK(blob_log_writer.LegacyAppendFooterAndClose(
|
||||||
&checksum_method, &checksum_value));
|
WriteOptions(), footer, &checksum_method, &checksum_value));
|
||||||
}
|
}
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
@@ -222,8 +221,7 @@ TEST_F(BlobFileCacheTest, RefreshBlobFileReaderPrefersLargestObservedFileSize) {
|
|||||||
constexpr bool use_fsync = false;
|
constexpr bool use_fsync = false;
|
||||||
constexpr bool do_flush = false;
|
constexpr bool do_flush = false;
|
||||||
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
|
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
|
||||||
statistics, blob_file_number, use_fsync,
|
statistics, use_fsync, do_flush);
|
||||||
do_flush);
|
|
||||||
|
|
||||||
constexpr bool has_ttl = false;
|
constexpr bool has_ttl = false;
|
||||||
constexpr ExpirationRange expiration_range;
|
constexpr ExpirationRange expiration_range;
|
||||||
|
|||||||
@@ -214,10 +214,10 @@ Status BlobFilePartitionManager::OpenNewBlobFile(Partition* partition,
|
|||||||
|
|
||||||
// This only drains WritableFileWriter's buffered bytes so readers can see
|
// This only drains WritableFileWriter's buffered bytes so readers can see
|
||||||
// each appended record promptly. Durability still comes from SyncAllOpenFiles
|
// 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;
|
constexpr bool kDoFlushEachRecord = true;
|
||||||
auto blob_log_writer = std::make_unique<BlobLogWriter>(
|
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);
|
kDoFlushEachRecord);
|
||||||
|
|
||||||
constexpr bool has_ttl = false;
|
constexpr bool has_ttl = false;
|
||||||
@@ -300,8 +300,8 @@ Status BlobFilePartitionManager::FinalizeBlobFile(
|
|||||||
|
|
||||||
std::string checksum_method;
|
std::string checksum_method;
|
||||||
std::string checksum_value;
|
std::string checksum_value;
|
||||||
Status s = writer->AppendFooter(write_options, footer, &checksum_method,
|
Status s = writer->LegacyAppendFooterAndClose(
|
||||||
&checksum_value);
|
write_options, footer, &checksum_method, &checksum_value);
|
||||||
if (!s.ok()) {
|
if (!s.ok()) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|||||||
+179
-64
@@ -5,11 +5,14 @@
|
|||||||
|
|
||||||
#include "db/blob/blob_file_reader.h"
|
#include "db/blob/blob_file_reader.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "db/blob/blob_contents.h"
|
#include "db/blob/blob_contents.h"
|
||||||
#include "db/blob/blob_log_format.h"
|
#include "db/blob/blob_log_format.h"
|
||||||
|
#include "db/blog/blog_format.h"
|
||||||
#include "file/file_prefetch_buffer.h"
|
#include "file/file_prefetch_buffer.h"
|
||||||
#include "file/filename.h"
|
#include "file/filename.h"
|
||||||
#include "monitoring/statistics_impl.h"
|
#include "monitoring/statistics_impl.h"
|
||||||
@@ -51,37 +54,30 @@ Status BlobFileReader::Create(
|
|||||||
|
|
||||||
Statistics* const statistics = immutable_options.stats;
|
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 =
|
const Status s =
|
||||||
ReadHeader(file_reader.get(), read_options, column_family_id,
|
(*blob_file_reader)->ReadHeader(read_options, column_family_id);
|
||||||
statistics, &compression_type);
|
|
||||||
if (!s.ok()) {
|
if (!s.ok()) {
|
||||||
|
blob_file_reader->reset();
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!skip_footer_validation) {
|
if (!skip_footer_validation) {
|
||||||
const Status s =
|
const Status s = (*blob_file_reader)->ReadFooter(read_options);
|
||||||
ReadFooter(file_reader.get(), read_options, file_size, statistics);
|
|
||||||
if (!s.ok()) {
|
if (!s.ok()) {
|
||||||
|
blob_file_reader->reset();
|
||||||
return s;
|
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();
|
return Status::OK();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,13 +151,17 @@ Status BlobFileReader::OpenFile(
|
|||||||
return Status::OK();
|
return Status::OK();
|
||||||
}
|
}
|
||||||
|
|
||||||
Status BlobFileReader::ReadHeader(const RandomAccessFileReader* file_reader,
|
Status BlobFileReader::ReadHeader(const ReadOptions& read_options,
|
||||||
const ReadOptions& read_options,
|
uint32_t column_family_id) {
|
||||||
uint32_t column_family_id,
|
assert(file_reader_);
|
||||||
Statistics* statistics,
|
|
||||||
CompressionType* compression_type) {
|
// Read ~4 KiB from the start to cover both legacy (30B) and blog (40B
|
||||||
assert(file_reader);
|
// fixed + property section) headers in a single I/O. This is enough for
|
||||||
assert(compression_type);
|
// 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;
|
Slice header_slice;
|
||||||
Buffer buf;
|
Buffer buf;
|
||||||
@@ -170,12 +170,9 @@ Status BlobFileReader::ReadHeader(const RandomAccessFileReader* file_reader,
|
|||||||
{
|
{
|
||||||
TEST_SYNC_POINT("BlobFileReader::ReadHeader:ReadFromFile");
|
TEST_SYNC_POINT("BlobFileReader::ReadHeader:ReadFromFile");
|
||||||
|
|
||||||
constexpr uint64_t read_offset = 0;
|
|
||||||
constexpr size_t read_size = BlobLogHeader::kSize;
|
|
||||||
|
|
||||||
const Status s =
|
const Status s =
|
||||||
ReadFromFile(file_reader, read_options, read_offset, read_size,
|
ReadFromFile(file_reader_.get(), read_options, 0, read_size,
|
||||||
statistics, &header_slice, &buf, &aligned_buf);
|
statistics_, &header_slice, &buf, &aligned_buf);
|
||||||
if (!s.ok()) {
|
if (!s.ok()) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
@@ -184,10 +181,58 @@ Status BlobFileReader::ReadHeader(const RandomAccessFileReader* file_reader,
|
|||||||
&header_slice);
|
&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;
|
BlobLogHeader header;
|
||||||
|
|
||||||
{
|
{
|
||||||
const Status s = header.DecodeFrom(header_slice);
|
const Status s = header.DecodeFrom(legacy_slice);
|
||||||
if (!s.ok()) {
|
if (!s.ok()) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
@@ -203,16 +248,62 @@ Status BlobFileReader::ReadHeader(const RandomAccessFileReader* file_reader,
|
|||||||
return Status::Corruption("Column family ID mismatch");
|
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();
|
return Status::OK();
|
||||||
}
|
}
|
||||||
|
|
||||||
Status BlobFileReader::ReadFooter(const RandomAccessFileReader* file_reader,
|
Status BlobFileReader::ReadFooter(const ReadOptions& read_options) {
|
||||||
const ReadOptions& read_options,
|
assert(file_reader_);
|
||||||
uint64_t file_size, Statistics* statistics) {
|
|
||||||
assert(file_size >= BlobLogHeader::kSize + BlobLogFooter::kSize);
|
if (is_blog_format_) {
|
||||||
assert(file_reader);
|
// 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;
|
Slice footer_slice;
|
||||||
Buffer buf;
|
Buffer buf;
|
||||||
@@ -221,12 +312,12 @@ Status BlobFileReader::ReadFooter(const RandomAccessFileReader* file_reader,
|
|||||||
{
|
{
|
||||||
TEST_SYNC_POINT("BlobFileReader::ReadFooter:ReadFromFile");
|
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;
|
constexpr size_t read_size = BlobLogFooter::kSize;
|
||||||
|
|
||||||
const Status s =
|
const Status s =
|
||||||
ReadFromFile(file_reader, read_options, read_offset, read_size,
|
ReadFromFile(file_reader_.get(), read_options, read_offset, read_size,
|
||||||
statistics, &footer_slice, &buf, &aligned_buf);
|
statistics_, &footer_slice, &buf, &aligned_buf);
|
||||||
if (!s.ok()) {
|
if (!s.ok()) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
@@ -250,6 +341,7 @@ Status BlobFileReader::ReadFooter(const RandomAccessFileReader* file_reader,
|
|||||||
return Status::Corruption("Unexpected TTL blob file");
|
return Status::Corruption("Unexpected TTL blob file");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
has_footer_ = true;
|
||||||
return Status::OK();
|
return Status::OK();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,30 +415,40 @@ Status BlobFileReader::GetBlob(
|
|||||||
std::unique_ptr<BlobContents>* result, uint64_t* bytes_read) const {
|
std::unique_ptr<BlobContents>* result, uint64_t* bytes_read) const {
|
||||||
assert(result);
|
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_,
|
if (is_blog_format_) {
|
||||||
has_footer_)) {
|
if (read_options.verify_checksums) {
|
||||||
return Status::Corruption("Invalid blob offset");
|
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_) {
|
assert(offset >= adjustment_before);
|
||||||
return Status::Corruption("Compression type mismatch when reading blob");
|
const uint64_t record_offset = offset - adjustment_before;
|
||||||
}
|
const uint64_t record_size =
|
||||||
|
adjustment_before + value_size + adjustment_after;
|
||||||
// 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;
|
|
||||||
|
|
||||||
|
// Read the data: try prefetch buffer first, then fall back to file read.
|
||||||
Slice record_slice;
|
Slice record_slice;
|
||||||
Buffer buf;
|
Buffer buf;
|
||||||
AlignedBuf aligned_buf;
|
AlignedBuf aligned_buf;
|
||||||
@@ -388,17 +490,30 @@ Status BlobFileReader::GetBlob(
|
|||||||
TEST_SYNC_POINT_CALLBACK("BlobFileReader::GetBlob:TamperWithResult",
|
TEST_SYNC_POINT_CALLBACK("BlobFileReader::GetBlob:TamperWithResult",
|
||||||
&record_slice);
|
&record_slice);
|
||||||
|
|
||||||
|
// Verify checksum (format-specific).
|
||||||
|
CompressionType actual_comp_type = compression_type;
|
||||||
if (read_options.verify_checksums) {
|
if (read_options.verify_checksums) {
|
||||||
const Status s = VerifyBlob(record_slice, user_key, value_size);
|
if (is_blog_format_) {
|
||||||
if (!s.ok()) {
|
const Status s = VerifyBlogRecordTrailer(
|
||||||
return s;
|
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,
|
decompressor_.get(), allocator,
|
||||||
clock_, statistics_, result);
|
clock_, statistics_, result);
|
||||||
if (!s.ok()) {
|
if (!s.ok()) {
|
||||||
|
|||||||
@@ -93,14 +93,15 @@ class BlobFileReader {
|
|||||||
std::unique_ptr<RandomAccessFileReader>* file_reader,
|
std::unique_ptr<RandomAccessFileReader>* file_reader,
|
||||||
bool skip_footer_size_check);
|
bool skip_footer_size_check);
|
||||||
|
|
||||||
static Status ReadHeader(const RandomAccessFileReader* file_reader,
|
// Read and validate the file header. Detects blog format vs legacy format,
|
||||||
const ReadOptions& read_options,
|
// setting is_blog_format_ and related fields accordingly. For legacy format,
|
||||||
uint32_t column_family_id, Statistics* statistics,
|
// validates column_family_id and sets compression_type_.
|
||||||
CompressionType* compression_type);
|
Status ReadHeader(const ReadOptions& read_options, uint32_t column_family_id);
|
||||||
|
|
||||||
static Status ReadFooter(const RandomAccessFileReader* file_reader,
|
// Validate the file footer. For legacy format, reads and validates the
|
||||||
const ReadOptions& read_options, uint64_t file_size,
|
// BlobLogFooter. For blog format, scans backward for the footer locator
|
||||||
Statistics* statistics);
|
// record to verify the file was cleanly sealed.
|
||||||
|
Status ReadFooter(const ReadOptions& read_options);
|
||||||
|
|
||||||
using Buffer = std::unique_ptr<char[]>;
|
using Buffer = std::unique_ptr<char[]>;
|
||||||
|
|
||||||
@@ -129,6 +130,13 @@ class BlobFileReader {
|
|||||||
Statistics* statistics_;
|
Statistics* statistics_;
|
||||||
// False when the reader was opened before the blob file footer was written.
|
// False when the reader was opened before the blob file footer was written.
|
||||||
bool has_footer_;
|
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
|
} // namespace ROCKSDB_NAMESPACE
|
||||||
|
|||||||
@@ -57,8 +57,7 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
|||||||
constexpr bool do_flush = false;
|
constexpr bool do_flush = false;
|
||||||
|
|
||||||
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
|
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
|
||||||
statistics, blob_file_number, use_fsync,
|
statistics, use_fsync, do_flush);
|
||||||
do_flush);
|
|
||||||
|
|
||||||
BlobLogHeader header(column_family_id, compression, has_ttl,
|
BlobLogHeader header(column_family_id, compression, has_ttl,
|
||||||
expiration_range_header);
|
expiration_range_header);
|
||||||
@@ -98,8 +97,8 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
|||||||
|
|
||||||
std::string checksum_method;
|
std::string checksum_method;
|
||||||
std::string checksum_value;
|
std::string checksum_value;
|
||||||
ASSERT_OK(blob_log_writer.AppendFooter(WriteOptions(), footer,
|
ASSERT_OK(blob_log_writer.LegacyAppendFooterAndClose(
|
||||||
&checksum_method, &checksum_value));
|
WriteOptions(), footer, &checksum_method, &checksum_value));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a test blob file with a single blob in it. Note: this method
|
// 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),
|
BlobLogWriter blob_log_writer(std::move(file_writer),
|
||||||
immutable_options.clock, statistics,
|
immutable_options.clock, statistics,
|
||||||
blob_file_number, use_fsync, do_flush);
|
use_fsync, do_flush);
|
||||||
|
|
||||||
BlobLogHeader header(column_family_id, kNoCompression, has_ttl,
|
BlobLogHeader header(column_family_id, kNoCompression, has_ttl,
|
||||||
expiration_range);
|
expiration_range);
|
||||||
|
|||||||
@@ -20,12 +20,10 @@ namespace ROCKSDB_NAMESPACE {
|
|||||||
|
|
||||||
BlobLogWriter::BlobLogWriter(std::unique_ptr<WritableFileWriter>&& dest,
|
BlobLogWriter::BlobLogWriter(std::unique_ptr<WritableFileWriter>&& dest,
|
||||||
SystemClock* clock, Statistics* statistics,
|
SystemClock* clock, Statistics* statistics,
|
||||||
uint64_t log_number, bool use_fs, bool do_flush,
|
bool use_fs, bool do_flush, uint64_t boffset)
|
||||||
uint64_t boffset)
|
|
||||||
: dest_(std::move(dest)),
|
: dest_(std::move(dest)),
|
||||||
clock_(clock),
|
clock_(clock),
|
||||||
statistics_(statistics),
|
statistics_(statistics),
|
||||||
log_number_(log_number),
|
|
||||||
block_offset_(boffset),
|
block_offset_(boffset),
|
||||||
use_fsync_(use_fs),
|
use_fsync_(use_fs),
|
||||||
do_flush_(do_flush),
|
do_flush_(do_flush),
|
||||||
@@ -74,10 +72,9 @@ Status BlobLogWriter::WriteHeader(const WriteOptions& write_options,
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
Status BlobLogWriter::AppendFooter(const WriteOptions& write_options,
|
Status BlobLogWriter::LegacyAppendFooterAndClose(
|
||||||
BlobLogFooter& footer,
|
const WriteOptions& write_options, BlobLogFooter& footer,
|
||||||
std::string* checksum_method,
|
std::string* checksum_method, std::string* checksum_value) {
|
||||||
std::string* checksum_value) {
|
|
||||||
assert(block_offset_ != 0);
|
assert(block_offset_ != 0);
|
||||||
assert(last_elem_type_ == kEtFileHdr || last_elem_type_ == kEtRecord);
|
assert(last_elem_type_ == kEtFileHdr || last_elem_type_ == kEtRecord);
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ class BlobLogWriter {
|
|||||||
// "*dest" must be initially empty.
|
// "*dest" must be initially empty.
|
||||||
// "*dest" must remain live while this BlobLogWriter is in use.
|
// "*dest" must remain live while this BlobLogWriter is in use.
|
||||||
BlobLogWriter(std::unique_ptr<WritableFileWriter>&& dest, SystemClock* clock,
|
BlobLogWriter(std::unique_ptr<WritableFileWriter>&& dest, SystemClock* clock,
|
||||||
Statistics* statistics, uint64_t log_number, bool use_fsync,
|
Statistics* statistics, bool use_fsync, bool do_flush,
|
||||||
bool do_flush, uint64_t boffset = 0);
|
uint64_t boffset = 0);
|
||||||
// No copying allowed
|
// No copying allowed
|
||||||
BlobLogWriter(const BlobLogWriter&) = delete;
|
BlobLogWriter(const BlobLogWriter&) = delete;
|
||||||
BlobLogWriter& operator=(const BlobLogWriter&) = delete;
|
BlobLogWriter& operator=(const BlobLogWriter&) = delete;
|
||||||
@@ -56,9 +56,10 @@ class BlobLogWriter {
|
|||||||
const Slice& val, uint64_t* key_offset,
|
const Slice& val, uint64_t* key_offset,
|
||||||
uint64_t* blob_offset);
|
uint64_t* blob_offset);
|
||||||
|
|
||||||
Status AppendFooter(const WriteOptions& write_options, BlobLogFooter& footer,
|
Status LegacyAppendFooterAndClose(const WriteOptions& write_options,
|
||||||
std::string* checksum_method,
|
BlobLogFooter& footer,
|
||||||
std::string* checksum_value);
|
std::string* checksum_method,
|
||||||
|
std::string* checksum_value);
|
||||||
|
|
||||||
Status WriteHeader(const WriteOptions& write_options, BlobLogHeader& header);
|
Status WriteHeader(const WriteOptions& write_options, BlobLogHeader& header);
|
||||||
|
|
||||||
@@ -66,15 +67,12 @@ class BlobLogWriter {
|
|||||||
|
|
||||||
const WritableFileWriter* file() const { return dest_.get(); }
|
const WritableFileWriter* file() const { return dest_.get(); }
|
||||||
|
|
||||||
uint64_t get_log_number() const { return log_number_; }
|
|
||||||
|
|
||||||
Status Sync(const WriteOptions& write_options);
|
Status Sync(const WriteOptions& write_options);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<WritableFileWriter> dest_;
|
std::unique_ptr<WritableFileWriter> dest_;
|
||||||
SystemClock* clock_;
|
SystemClock* clock_;
|
||||||
Statistics* statistics_;
|
Statistics* statistics_;
|
||||||
uint64_t log_number_;
|
|
||||||
uint64_t block_offset_; // Current offset in block
|
uint64_t block_offset_; // Current offset in block
|
||||||
bool use_fsync_;
|
bool use_fsync_;
|
||||||
bool do_flush_;
|
bool do_flush_;
|
||||||
|
|||||||
@@ -59,8 +59,7 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
|||||||
constexpr bool do_flush = false;
|
constexpr bool do_flush = false;
|
||||||
|
|
||||||
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
|
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
|
||||||
statistics, blob_file_number, use_fsync,
|
statistics, use_fsync, do_flush);
|
||||||
do_flush);
|
|
||||||
|
|
||||||
BlobLogHeader header(column_family_id, compression, has_ttl,
|
BlobLogHeader header(column_family_id, compression, has_ttl,
|
||||||
expiration_range_header);
|
expiration_range_header);
|
||||||
@@ -100,8 +99,8 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
|
|||||||
|
|
||||||
std::string checksum_method;
|
std::string checksum_method;
|
||||||
std::string checksum_value;
|
std::string checksum_value;
|
||||||
ASSERT_OK(blob_log_writer.AppendFooter(WriteOptions(), footer,
|
ASSERT_OK(blob_log_writer.LegacyAppendFooterAndClose(
|
||||||
&checksum_method, &checksum_value));
|
WriteOptions(), footer, &checksum_method, &checksum_value));
|
||||||
}
|
}
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -9057,10 +9057,11 @@ class DBCompactionTestBlobError
|
|||||||
std::string sync_point_;
|
std::string sync_point_;
|
||||||
};
|
};
|
||||||
|
|
||||||
INSTANTIATE_TEST_CASE_P(DBCompactionTestBlobError, DBCompactionTestBlobError,
|
INSTANTIATE_TEST_CASE_P(
|
||||||
::testing::ValuesIn(std::vector<std::string>{
|
DBCompactionTestBlobError, DBCompactionTestBlobError,
|
||||||
"BlobFileBuilder::WriteBlobToFile:AddRecord",
|
::testing::ValuesIn(std::vector<std::string>{
|
||||||
"BlobFileBuilder::WriteBlobToFile:AppendFooter"}));
|
"BlobFileBuilder::WriteBlobToFile:AddRecord",
|
||||||
|
"BlobFileBuilder::WriteBlobToFile:LegacyAppendFooterAndClose"}));
|
||||||
|
|
||||||
TEST_P(DBCompactionTestBlobError, CompactionError) {
|
TEST_P(DBCompactionTestBlobError, CompactionError) {
|
||||||
Options options = CurrentOptions();
|
Options options = CurrentOptions();
|
||||||
|
|||||||
+5
-4
@@ -2507,10 +2507,11 @@ class DBFlushTestBlobError : public DBFlushTest,
|
|||||||
std::string sync_point_;
|
std::string sync_point_;
|
||||||
};
|
};
|
||||||
|
|
||||||
INSTANTIATE_TEST_CASE_P(DBFlushTestBlobError, DBFlushTestBlobError,
|
INSTANTIATE_TEST_CASE_P(
|
||||||
::testing::ValuesIn(std::vector<std::string>{
|
DBFlushTestBlobError, DBFlushTestBlobError,
|
||||||
"BlobFileBuilder::WriteBlobToFile:AddRecord",
|
::testing::ValuesIn(std::vector<std::string>{
|
||||||
"BlobFileBuilder::WriteBlobToFile:AppendFooter"}));
|
"BlobFileBuilder::WriteBlobToFile:AddRecord",
|
||||||
|
"BlobFileBuilder::WriteBlobToFile:LegacyAppendFooterAndClose"}));
|
||||||
|
|
||||||
TEST_P(DBFlushTestBlobError, FlushError) {
|
TEST_P(DBFlushTestBlobError, FlushError) {
|
||||||
Options options;
|
Options options;
|
||||||
|
|||||||
+5
-4
@@ -900,10 +900,11 @@ class DBRecoveryTestBlobError
|
|||||||
std::string sync_point_;
|
std::string sync_point_;
|
||||||
};
|
};
|
||||||
|
|
||||||
INSTANTIATE_TEST_CASE_P(DBRecoveryTestBlobError, DBRecoveryTestBlobError,
|
INSTANTIATE_TEST_CASE_P(
|
||||||
::testing::ValuesIn(std::vector<std::string>{
|
DBRecoveryTestBlobError, DBRecoveryTestBlobError,
|
||||||
"BlobFileBuilder::WriteBlobToFile:AddRecord",
|
::testing::ValuesIn(std::vector<std::string>{
|
||||||
"BlobFileBuilder::WriteBlobToFile:AppendFooter"}));
|
"BlobFileBuilder::WriteBlobToFile:AddRecord",
|
||||||
|
"BlobFileBuilder::WriteBlobToFile:LegacyAppendFooterAndClose"}));
|
||||||
|
|
||||||
TEST_P(DBRecoveryTestBlobError, RecoverWithBlobError) {
|
TEST_P(DBRecoveryTestBlobError, RecoverWithBlobError) {
|
||||||
// Write a value. Note that blob files are not actually enabled at this point.
|
// Write a value. Note that blob files are not actually enabled at this point.
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ void WriteFooterlessBlobFile(const ImmutableOptions& immutable_options,
|
|||||||
constexpr bool use_fsync = false;
|
constexpr bool use_fsync = false;
|
||||||
constexpr bool do_flush = false;
|
constexpr bool do_flush = false;
|
||||||
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
|
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
|
||||||
statistics, blob_file_number, use_fsync,
|
statistics, use_fsync, do_flush);
|
||||||
do_flush);
|
|
||||||
|
|
||||||
constexpr bool has_ttl = false;
|
constexpr bool has_ttl = false;
|
||||||
constexpr ExpirationRange expiration_range;
|
constexpr ExpirationRange expiration_range;
|
||||||
|
|||||||
@@ -4463,7 +4463,7 @@ class BestEffortsRecoverIncompleteVersionTest
|
|||||||
std::move(file), blob_file_path, FileOptions(), options.clock));
|
std::move(file), blob_file_path, FileOptions(), options.clock));
|
||||||
|
|
||||||
BlobLogWriter blob_log_writer(std::move(file_writer), options.clock,
|
BlobLogWriter blob_log_writer(std::move(file_writer), options.clock,
|
||||||
/*statistics*/ nullptr, blob_file_number,
|
/*statistics*/ nullptr,
|
||||||
/*use_fsync*/ true,
|
/*use_fsync*/ true,
|
||||||
/*do_flush*/ false);
|
/*do_flush*/ false);
|
||||||
|
|
||||||
@@ -4481,8 +4481,8 @@ class BestEffortsRecoverIncompleteVersionTest
|
|||||||
footer.expiration_range = expiration_range;
|
footer.expiration_range = expiration_range;
|
||||||
std::string checksum_method;
|
std::string checksum_method;
|
||||||
std::string checksum_value;
|
std::string checksum_value;
|
||||||
ASSERT_OK(blob_log_writer.AppendFooter(WriteOptions(), footer,
|
ASSERT_OK(blob_log_writer.LegacyAppendFooterAndClose(
|
||||||
&checksum_method, &checksum_value));
|
WriteOptions(), footer, &checksum_method, &checksum_value));
|
||||||
}
|
}
|
||||||
|
|
||||||
void RecoverFromManifestWithMissingFiles(
|
void RecoverFromManifestWithMissingFiles(
|
||||||
|
|||||||
@@ -363,6 +363,8 @@ DECLARE_uint64(wp_commit_cache_bits);
|
|||||||
DECLARE_bool(adaptive_readahead);
|
DECLARE_bool(adaptive_readahead);
|
||||||
DECLARE_bool(async_io);
|
DECLARE_bool(async_io);
|
||||||
DECLARE_string(wal_compression);
|
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(verify_sst_unique_id_in_manifest);
|
||||||
DECLARE_bool(fast_sst_open);
|
DECLARE_bool(fast_sst_open);
|
||||||
|
|
||||||
|
|||||||
@@ -1320,6 +1320,11 @@ DEFINE_bool(
|
|||||||
DEFINE_string(wal_compression, "none",
|
DEFINE_string(wal_compression, "none",
|
||||||
"Algorithm to use for WAL compression. none to disable.");
|
"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(
|
DEFINE_bool(
|
||||||
verify_sst_unique_id_in_manifest, false,
|
verify_sst_unique_id_in_manifest, false,
|
||||||
"Enable DB options `verify_sst_unique_id_in_manifest`, if true, during "
|
"Enable DB options `verify_sst_unique_id_in_manifest`, if true, during "
|
||||||
|
|||||||
@@ -4740,6 +4740,18 @@ void InitializeOptionsFromFlags(
|
|||||||
|
|
||||||
options.wal_compression =
|
options.wal_compression =
|
||||||
StringToCompressionType(FLAGS_wal_compression.c_str());
|
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 =
|
options.last_level_temperature =
|
||||||
StringToTemperature(FLAGS_last_level_temperature.c_str());
|
StringToTemperature(FLAGS_last_level_temperature.c_str());
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -28,7 +28,12 @@ enum CompressionType : unsigned char {
|
|||||||
kZSTD = 0x07,
|
kZSTD = 0x07,
|
||||||
kLastBuiltinCompression = kZSTD,
|
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
|
// For use by user custom CompressionManagers
|
||||||
kCustomCompression80 = 0x80,
|
kCustomCompression80 = 0x80,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "rocksdb/advanced_options.h"
|
#include "rocksdb/advanced_options.h"
|
||||||
|
#include "rocksdb/checksum_type.h"
|
||||||
#include "rocksdb/comparator.h"
|
#include "rocksdb/comparator.h"
|
||||||
#include "rocksdb/compression_type.h"
|
#include "rocksdb/compression_type.h"
|
||||||
#include "rocksdb/customizable.h"
|
#include "rocksdb/customizable.h"
|
||||||
@@ -1480,6 +1481,21 @@ struct DBOptions {
|
|||||||
// the WAL is read.
|
// the WAL is read.
|
||||||
CompressionType wal_compression = kNoCompression;
|
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
|
// 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
|
// files open for write until they are collected for deletion by a
|
||||||
// background thread. This should not be needed unless there is a
|
// background thread. This should not be needed unless there is a
|
||||||
|
|||||||
+3
-13
@@ -46,19 +46,9 @@ struct ConfigOptions;
|
|||||||
struct EnvOptions;
|
struct EnvOptions;
|
||||||
class UserDefinedIndexFactory;
|
class UserDefinedIndexFactory;
|
||||||
|
|
||||||
// Types of checksums to use for checking integrity of logical blocks within
|
// ChecksumType is defined in its own header for use by both table options
|
||||||
// files. All checksums currently use 32 bits of checking power (1 in 4B
|
// and DB options (blog file checksums).
|
||||||
// chance of failing to detect random corruption). Traditionally, the actual
|
#include "rocksdb/checksum_type.h"
|
||||||
// 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
|
|
||||||
};
|
|
||||||
|
|
||||||
// `PinningTier` is used to specify which tier of block-based tables should
|
// `PinningTier` is used to specify which tier of block-based tables should
|
||||||
// be affected by a block cache pinning setting (see
|
// be affected by a block cache pinning setting (see
|
||||||
|
|||||||
@@ -415,6 +415,14 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
|||||||
{offsetof(struct ImmutableDBOptions, wal_compression),
|
{offsetof(struct ImmutableDBOptions, wal_compression),
|
||||||
OptionType::kCompressionType, OptionVerificationType::kNormal,
|
OptionType::kCompressionType, OptionVerificationType::kNormal,
|
||||||
OptionTypeFlags::kNone}},
|
OptionTypeFlags::kNone}},
|
||||||
|
{"use_blog_format_for_blobs",
|
||||||
|
{offsetof(struct ImmutableDBOptions, use_blog_format_for_blobs),
|
||||||
|
OptionType::kBoolean, OptionVerificationType::kNormal,
|
||||||
|
OptionTypeFlags::kNone}},
|
||||||
|
{"blog_checksum",
|
||||||
|
{offsetof(struct ImmutableDBOptions, blog_checksum),
|
||||||
|
OptionType::kChecksumType, OptionVerificationType::kNormal,
|
||||||
|
OptionTypeFlags::kNone}},
|
||||||
{"background_close_inactive_wals",
|
{"background_close_inactive_wals",
|
||||||
{offsetof(struct ImmutableDBOptions, background_close_inactive_wals),
|
{offsetof(struct ImmutableDBOptions, background_close_inactive_wals),
|
||||||
OptionType::kBoolean, OptionVerificationType::kNormal,
|
OptionType::kBoolean, OptionVerificationType::kNormal,
|
||||||
@@ -800,6 +808,8 @@ ImmutableDBOptions::ImmutableDBOptions(const DBOptions& options)
|
|||||||
two_write_queues(options.two_write_queues),
|
two_write_queues(options.two_write_queues),
|
||||||
manual_wal_flush(options.manual_wal_flush),
|
manual_wal_flush(options.manual_wal_flush),
|
||||||
wal_compression(options.wal_compression),
|
wal_compression(options.wal_compression),
|
||||||
|
use_blog_format_for_blobs(options.use_blog_format_for_blobs),
|
||||||
|
blog_checksum(options.blog_checksum),
|
||||||
background_close_inactive_wals(options.background_close_inactive_wals),
|
background_close_inactive_wals(options.background_close_inactive_wals),
|
||||||
atomic_flush(options.atomic_flush),
|
atomic_flush(options.atomic_flush),
|
||||||
avoid_unnecessary_blocking_io(options.avoid_unnecessary_blocking_io),
|
avoid_unnecessary_blocking_io(options.avoid_unnecessary_blocking_io),
|
||||||
@@ -973,6 +983,9 @@ void ImmutableDBOptions::Dump(Logger* log) const {
|
|||||||
manual_wal_flush);
|
manual_wal_flush);
|
||||||
ROCKS_LOG_HEADER(log, " Options.wal_compression: %d",
|
ROCKS_LOG_HEADER(log, " Options.wal_compression: %d",
|
||||||
wal_compression);
|
wal_compression);
|
||||||
|
ROCKS_LOG_HEADER(log, " Options.use_blog_format_for_blobs: %d",
|
||||||
|
use_blog_format_for_blobs);
|
||||||
|
ROCKS_LOG_HEADER(log, " Options.blog_checksum: %d", blog_checksum);
|
||||||
ROCKS_LOG_HEADER(log,
|
ROCKS_LOG_HEADER(log,
|
||||||
" Options.background_close_inactive_wals: %d",
|
" Options.background_close_inactive_wals: %d",
|
||||||
background_close_inactive_wals);
|
background_close_inactive_wals);
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ struct ImmutableDBOptions {
|
|||||||
bool two_write_queues;
|
bool two_write_queues;
|
||||||
bool manual_wal_flush;
|
bool manual_wal_flush;
|
||||||
CompressionType wal_compression;
|
CompressionType wal_compression;
|
||||||
|
bool use_blog_format_for_blobs;
|
||||||
|
ChecksumType blog_checksum;
|
||||||
bool background_close_inactive_wals;
|
bool background_close_inactive_wals;
|
||||||
bool atomic_flush;
|
bool atomic_flush;
|
||||||
bool avoid_unnecessary_blocking_io;
|
bool avoid_unnecessary_blocking_io;
|
||||||
|
|||||||
@@ -166,6 +166,9 @@ void BuildDBOptions(const ImmutableDBOptions& immutable_db_options,
|
|||||||
options.two_write_queues = immutable_db_options.two_write_queues;
|
options.two_write_queues = immutable_db_options.two_write_queues;
|
||||||
options.manual_wal_flush = immutable_db_options.manual_wal_flush;
|
options.manual_wal_flush = immutable_db_options.manual_wal_flush;
|
||||||
options.wal_compression = immutable_db_options.wal_compression;
|
options.wal_compression = immutable_db_options.wal_compression;
|
||||||
|
options.use_blog_format_for_blobs =
|
||||||
|
immutable_db_options.use_blog_format_for_blobs;
|
||||||
|
options.blog_checksum = immutable_db_options.blog_checksum;
|
||||||
options.background_close_inactive_wals =
|
options.background_close_inactive_wals =
|
||||||
immutable_db_options.background_close_inactive_wals;
|
immutable_db_options.background_close_inactive_wals;
|
||||||
options.atomic_flush = immutable_db_options.atomic_flush;
|
options.atomic_flush = immutable_db_options.atomic_flush;
|
||||||
|
|||||||
@@ -467,6 +467,8 @@ TEST_F(OptionsSettableTest, DBOptionsAllFieldsSettable) {
|
|||||||
"two_write_queues=false;"
|
"two_write_queues=false;"
|
||||||
"manual_wal_flush=false;"
|
"manual_wal_flush=false;"
|
||||||
"wal_compression=kZSTD;"
|
"wal_compression=kZSTD;"
|
||||||
|
"use_blog_format_for_blobs=false;"
|
||||||
|
"blog_checksum=kXXH3;"
|
||||||
"background_close_inactive_wals=true;"
|
"background_close_inactive_wals=true;"
|
||||||
"seq_per_batch=false;"
|
"seq_per_batch=false;"
|
||||||
"atomic_flush=false;"
|
"atomic_flush=false;"
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ LIB_SOURCES = \
|
|||||||
db/blob/blob_source.cc \
|
db/blob/blob_source.cc \
|
||||||
db/blob/blob_write_batch_transformer.cc \
|
db/blob/blob_write_batch_transformer.cc \
|
||||||
db/blob/prefetch_buffer_collection.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/builder.cc \
|
||||||
db/c.cc \
|
db/c.cc \
|
||||||
db/coalescing_iterator.cc \
|
db/coalescing_iterator.cc \
|
||||||
@@ -485,6 +488,8 @@ TEST_MAIN_SOURCES = \
|
|||||||
db/blob/db_blob_compaction_test.cc \
|
db/blob/db_blob_compaction_test.cc \
|
||||||
db/blob/db_blob_corruption_test.cc \
|
db/blob/db_blob_corruption_test.cc \
|
||||||
db/blob/db_blob_index_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/column_family_test.cc \
|
||||||
db/compact_files_test.cc \
|
db/compact_files_test.cc \
|
||||||
db/compaction/clipping_iterator_test.cc \
|
db/compaction/clipping_iterator_test.cc \
|
||||||
|
|||||||
@@ -907,6 +907,11 @@ DEFINE_string(wal_compression, "none",
|
|||||||
static enum ROCKSDB_NAMESPACE::CompressionType FLAGS_wal_compression_e =
|
static enum ROCKSDB_NAMESPACE::CompressionType FLAGS_wal_compression_e =
|
||||||
ROCKSDB_NAMESPACE::kNoCompression;
|
ROCKSDB_NAMESPACE::kNoCompression;
|
||||||
|
|
||||||
|
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_string(wal_dir, "", "If not empty, use the given dir for WAL");
|
DEFINE_string(wal_dir, "", "If not empty, use the given dir for WAL");
|
||||||
|
|
||||||
DEFINE_string(truth_db, "/dev/shm/truth_db/dbbench",
|
DEFINE_string(truth_db, "/dev/shm/truth_db/dbbench",
|
||||||
@@ -4548,6 +4553,17 @@ class Benchmark {
|
|||||||
FLAGS_use_direct_io_for_flush_and_compaction;
|
FLAGS_use_direct_io_for_flush_and_compaction;
|
||||||
options.manual_wal_flush = FLAGS_manual_wal_flush;
|
options.manual_wal_flush = FLAGS_manual_wal_flush;
|
||||||
options.wal_compression = FLAGS_wal_compression_e;
|
options.wal_compression = FLAGS_wal_compression_e;
|
||||||
|
options.use_blog_format_for_blobs = FLAGS_use_blog_format_for_blobs;
|
||||||
|
// Parse blog_checksum via the options string system.
|
||||||
|
{
|
||||||
|
ConfigOptions blog_config_options;
|
||||||
|
blog_config_options.ignore_unknown_options = true;
|
||||||
|
DBOptions tmp_opts;
|
||||||
|
GetDBOptionsFromString(blog_config_options, tmp_opts,
|
||||||
|
"blog_checksum=" + FLAGS_blog_checksum, &tmp_opts)
|
||||||
|
.PermitUncheckedError();
|
||||||
|
options.blog_checksum = tmp_opts.blog_checksum;
|
||||||
|
}
|
||||||
options.ttl = FLAGS_fifo_compaction_ttl;
|
options.ttl = FLAGS_fifo_compaction_ttl;
|
||||||
options.compaction_options_fifo = CompactionOptionsFIFO(
|
options.compaction_options_fifo = CompactionOptionsFIFO(
|
||||||
FLAGS_fifo_compaction_max_table_files_size_mb * 1024 * 1024,
|
FLAGS_fifo_compaction_max_table_files_size_mb * 1024 * 1024,
|
||||||
|
|||||||
@@ -371,6 +371,10 @@ default_params = {
|
|||||||
"adaptive_readahead": lambda: random.choice([0, 1]),
|
"adaptive_readahead": lambda: random.choice([0, 1]),
|
||||||
"async_io": lambda: random.choice([0, 1]),
|
"async_io": lambda: random.choice([0, 1]),
|
||||||
"wal_compression": lambda: random.choice(["none", "zstd"]),
|
"wal_compression": lambda: random.choice(["none", "zstd"]),
|
||||||
|
"use_blog_format_for_blobs": lambda: random.choice([0, 1]),
|
||||||
|
"blog_checksum": lambda: random.choice(
|
||||||
|
["kNoChecksum", "kCRC32c", "kxxHash", "kxxHash64", "kXXH3"]
|
||||||
|
),
|
||||||
"verify_sst_unique_id_in_manifest": 1, # always do unique_id verification
|
"verify_sst_unique_id_in_manifest": 1, # always do unique_id verification
|
||||||
"fast_sst_open": lambda: random.choice([0, 1]),
|
"fast_sst_open": lambda: random.choice([0, 1]),
|
||||||
"secondary_cache_uri": lambda: random.choice(
|
"secondary_cache_uri": lambda: random.choice(
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Added new DB options `use_blog_format_for_blobs` and `blog_checksum` to enable the new "blog" file format for blob files. The blog format uses escape-sequence record framing with context checksums. Existing legacy format files remain readable regardless of these settings.
|
||||||
@@ -755,9 +755,9 @@ Status BlobDBImpl::CreateWriterLocked(const std::shared_ptr<BlobFile>& bfile) {
|
|||||||
|
|
||||||
constexpr bool do_flush = true;
|
constexpr bool do_flush = true;
|
||||||
|
|
||||||
bfile->log_writer_ = std::make_shared<BlobLogWriter>(
|
bfile->log_writer_ =
|
||||||
std::move(fwriter), clock_, statistics_, bfile->file_number_,
|
std::make_shared<BlobLogWriter>(std::move(fwriter), clock_, statistics_,
|
||||||
db_options_.use_fsync, do_flush, boffset);
|
db_options_.use_fsync, do_flush, boffset);
|
||||||
bfile->log_writer_->last_elem_type_ = et;
|
bfile->log_writer_->last_elem_type_ = et;
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
|
|||||||
@@ -80,9 +80,10 @@ Status BlobFile::WriteFooterAndCloseLocked(const WriteOptions& write_options,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this will close the file and reset the Writable File Pointer.
|
// this will close the file and reset the Writable File Pointer.
|
||||||
Status s = log_writer_->AppendFooter(write_options, footer,
|
Status s =
|
||||||
/* checksum_method */ nullptr,
|
log_writer_->LegacyAppendFooterAndClose(write_options, footer,
|
||||||
/* checksum_value */ nullptr);
|
/* checksum_method */ nullptr,
|
||||||
|
/* checksum_value */ nullptr);
|
||||||
if (s.ok()) {
|
if (s.ok()) {
|
||||||
closed_ = true;
|
closed_ = true;
|
||||||
immutable_sequence_ = sequence;
|
immutable_sequence_ = sequence;
|
||||||
|
|||||||
Reference in New Issue
Block a user