mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-06 11:02:36 -05:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0fe00d3db | ||
|
|
cbd61a3165 | ||
|
|
59633e36ed | ||
|
|
4707775ae9 | ||
|
|
46ce1a03a9 | ||
|
|
795f3bd61f | ||
|
|
330962bff6 | ||
|
|
e07ccc3528 | ||
|
|
224e849e8d | ||
|
|
c734b7cc60 | ||
|
|
60b34f30c4 | ||
|
|
02a2b3501d | ||
|
|
1dc4813d97 | ||
|
|
4af61efaf4 | ||
|
|
e0d549fbba | ||
|
|
c25dcbccba | ||
|
|
c99d5ec06e | ||
|
|
a4045b0a5f | ||
|
|
b874d2546e | ||
|
|
c1dbe0a6ac | ||
|
|
52d8574629 |
No files matched your search
@@ -18,7 +18,7 @@ module.exports = function parseClaude({executionFile, conclusion, meta}) {
|
||||
if (meta.trigger !== 'auto') {
|
||||
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') {
|
||||
return `*Auto-triggered after CI reached the early-review threshold — reviewing commit ${
|
||||
shortSha}*`;
|
||||
|
||||
@@ -22,7 +22,7 @@ module.exports = function parseCodex(
|
||||
if (meta.trigger !== 'auto') {
|
||||
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') {
|
||||
return `*Auto-triggered after CI reached the early-review threshold — reviewing commit ${
|
||||
shortSha}*`;
|
||||
|
||||
@@ -531,7 +531,9 @@ jobs:
|
||||
core.warning(
|
||||
`Could not parse classifier output: ${error.message}`);
|
||||
}
|
||||
const budget = label === 'simple' ? '16000' : '32000';
|
||||
// Keep Claude's thinking budget below the action's effective
|
||||
// max_tokens so the API has room for the final review text.
|
||||
const budget = label === 'simple' ? '16000' : '24000';
|
||||
core.info(
|
||||
`Complexity classified as: ${label} ` +
|
||||
`-> MAX_THINKING_TOKENS=${budget}`
|
||||
@@ -549,13 +551,30 @@ jobs:
|
||||
CODEX_HOME: /tmp/codex-home
|
||||
CODEX_MODEL: ${{ inputs.default_model }}
|
||||
run: |
|
||||
if [ -z "${OPENAI_API_KEY}" ]; then
|
||||
echo "OPENAI_API_KEY not configured, skipping Codex review"
|
||||
printf 'complex\n' > codex-complexity-output.txt
|
||||
exit 0
|
||||
fi
|
||||
mkdir -p "${CODEX_HOME}"
|
||||
set +e
|
||||
codex exec \
|
||||
-m "${CODEX_MODEL}" \
|
||||
-c model_reasoning_effort="medium" \
|
||||
--dangerously-bypass-approvals-and-sandbox \
|
||||
--output-last-message codex-complexity-output.txt \
|
||||
- < /tmp/complexity_prompt.txt > codex-complexity.log 2>&1
|
||||
exit_code=$?
|
||||
set -e
|
||||
if [ "${exit_code}" -ne 0 ]; then
|
||||
echo "Codex complexity classification failed with exit code ${exit_code}"
|
||||
echo "Defaulting to complex review budget"
|
||||
if [ -s codex-complexity.log ]; then
|
||||
echo "Last 100 lines of codex-complexity.log:"
|
||||
tail -100 codex-complexity.log
|
||||
fi
|
||||
printf 'complex\n' > codex-complexity-output.txt
|
||||
fi
|
||||
|
||||
- name: Pick thinking budget (Codex)
|
||||
if: >-
|
||||
@@ -695,14 +714,14 @@ jobs:
|
||||
CODEX_HOME: /tmp/codex-home
|
||||
CODEX_MODEL: ${{ inputs.default_model }}
|
||||
CODEX_REASONING_EFFORT: ${{ steps.budget_codex.outputs.effort }}
|
||||
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
|
||||
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
|
||||
run: |
|
||||
if [ -z "${OPENAI_API_KEY}" ]; then
|
||||
echo "OPENAI_API_KEY not configured, skipping Codex review"
|
||||
exit 0
|
||||
fi
|
||||
mkdir -p "${CODEX_HOME}"
|
||||
set +e
|
||||
codex exec review \
|
||||
--base "${BASE_SHA}" \
|
||||
--title "${PR_TITLE}" \
|
||||
codex exec \
|
||||
-m "${CODEX_MODEL}" \
|
||||
-c model_reasoning_effort="${CODEX_REASONING_EFFORT}" \
|
||||
--dangerously-bypass-approvals-and-sandbox \
|
||||
@@ -853,6 +872,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
@@ -1093,9 +1113,19 @@ jobs:
|
||||
}
|
||||
const override = (process.env.OVERRIDE || '').trim();
|
||||
if (override && /^\d+$/.test(override)) {
|
||||
core.info(`Using thinking_budget override: ${override}`);
|
||||
const maxSafeBudget = 24000;
|
||||
const requested = parseInt(override, 10);
|
||||
const budget = Math.min(requested, maxSafeBudget).toString();
|
||||
if (budget !== override) {
|
||||
core.info(
|
||||
`Clamping thinking_budget override from ${override} ` +
|
||||
`to ${budget} so max_tokens can exceed thinking tokens.`
|
||||
);
|
||||
} else {
|
||||
core.info(`Using thinking_budget override: ${budget}`);
|
||||
}
|
||||
core.setOutput('label', 'override');
|
||||
core.setOutput('tokens', override);
|
||||
core.setOutput('tokens', budget);
|
||||
return;
|
||||
}
|
||||
let label = 'complex';
|
||||
@@ -1113,7 +1143,9 @@ jobs:
|
||||
core.warning(
|
||||
`Could not parse classifier output: ${error.message}`);
|
||||
}
|
||||
const budget = label === 'simple' ? '16000' : '32000';
|
||||
// Keep Claude's thinking budget below the action's effective
|
||||
// max_tokens so the API has room for the final review text.
|
||||
const budget = label === 'simple' ? '16000' : '24000';
|
||||
core.info(
|
||||
`Complexity classified as: ${label} ` +
|
||||
`-> MAX_THINKING_TOKENS=${budget}`
|
||||
@@ -1131,13 +1163,30 @@ jobs:
|
||||
CODEX_HOME: /tmp/codex-home
|
||||
CODEX_MODEL: ${{ inputs.selected_model != '' && inputs.selected_model || inputs.default_model }}
|
||||
run: |
|
||||
if [ -z "${OPENAI_API_KEY}" ]; then
|
||||
echo "OPENAI_API_KEY not configured, skipping Codex review"
|
||||
printf 'complex\n' > codex-complexity-output.txt
|
||||
exit 0
|
||||
fi
|
||||
mkdir -p "${CODEX_HOME}"
|
||||
set +e
|
||||
codex exec \
|
||||
-m "${CODEX_MODEL}" \
|
||||
-c model_reasoning_effort="medium" \
|
||||
--dangerously-bypass-approvals-and-sandbox \
|
||||
--output-last-message codex-complexity-output.txt \
|
||||
- < /tmp/complexity_prompt.txt > codex-complexity.log 2>&1
|
||||
exit_code=$?
|
||||
set -e
|
||||
if [ "${exit_code}" -ne 0 ]; then
|
||||
echo "Codex complexity classification failed with exit code ${exit_code}"
|
||||
echo "Defaulting to complex review budget"
|
||||
if [ -s codex-complexity.log ]; then
|
||||
echo "Last 100 lines of codex-complexity.log:"
|
||||
tail -100 codex-complexity.log
|
||||
fi
|
||||
printf 'complex\n' > codex-complexity-output.txt
|
||||
fi
|
||||
|
||||
- name: Pick thinking budget (Codex)
|
||||
if: steps.request.outputs.skip != 'true' && inputs.provider == 'codex'
|
||||
@@ -1287,8 +1336,6 @@ jobs:
|
||||
CODEX_HOME: /tmp/codex-home
|
||||
CODEX_MODEL: ${{ inputs.selected_model != '' && inputs.selected_model || inputs.default_model }}
|
||||
CODEX_REASONING_EFFORT: ${{ steps.manual_budget_codex.outputs.effort }}
|
||||
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
|
||||
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
|
||||
COMMAND_TYPE: ${{ steps.request.outputs.type }}
|
||||
run: |
|
||||
mkdir -p "${CODEX_HOME}"
|
||||
@@ -1301,9 +1348,7 @@ jobs:
|
||||
--output-last-message codex-review-output.md \
|
||||
- < /tmp/prompt.txt > codex-review-execution.log 2>&1
|
||||
else
|
||||
codex exec review \
|
||||
--base "${BASE_SHA}" \
|
||||
--title "${PR_TITLE}" \
|
||||
codex exec \
|
||||
-m "${CODEX_MODEL}" \
|
||||
-c model_reasoning_effort="${CODEX_REASONING_EFFORT}" \
|
||||
--dangerously-bypass-approvals-and-sandbox \
|
||||
|
||||
@@ -30,12 +30,11 @@ on:
|
||||
required: false
|
||||
type: choice
|
||||
options:
|
||||
- claude-opus-4-7
|
||||
- claude-opus-4-6
|
||||
- claude-sonnet-4-6
|
||||
default: claude-opus-4-7
|
||||
default: claude-opus-4-6
|
||||
thinking_budget:
|
||||
description: Override MAX_THINKING_TOKENS (blank = auto-classify)
|
||||
description: Override MAX_THINKING_TOKENS (blank = auto-classify, capped at 24000)
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
@@ -61,7 +60,7 @@ jobs:
|
||||
query_command: /claude-query
|
||||
result_artifact_name: claude-review-result
|
||||
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 || '' }}
|
||||
classifier_model: claude-sonnet-4-6
|
||||
recovery_model: claude-sonnet-4-6
|
||||
|
||||
@@ -384,6 +384,10 @@ endif
|
||||
# TSAN doesn't work well with jemalloc. If we're compiling with TSAN, we should use regular malloc.
|
||||
ifdef COMPILE_WITH_TSAN
|
||||
DISABLE_JEMALLOC=1
|
||||
# Use a suppressions file instead of the process-wide TSAN default
|
||||
# suppressions hook, which belongs to the final application.
|
||||
TSAN_OPTIONS?=suppressions=$(CURDIR)/tools/tsan_suppressions.txt
|
||||
export TSAN_OPTIONS
|
||||
EXEC_LDFLAGS += -fsanitize=thread
|
||||
PLATFORM_CCFLAGS += -fsanitize=thread -fPIC -DFOLLY_SANITIZE_THREAD
|
||||
PLATFORM_CXXFLAGS += -fsanitize=thread -fPIC -DFOLLY_SANITIZE_THREAD
|
||||
|
||||
@@ -37,7 +37,7 @@ if [ "$?" != "1" ]; then
|
||||
BAD=1
|
||||
fi
|
||||
|
||||
git grep -n -P "[\x80-\xFF]" -- ':!docs' ':!*.md'
|
||||
LC_ALL=C git grep -n $'[\x80-\xff]' -- ':!docs' ':!*.md' ':!.github'
|
||||
if [ "$?" != "1" ]; then
|
||||
echo '^^^^ Use only ASCII characters in source files'
|
||||
BAD=1
|
||||
|
||||
@@ -7,6 +7,13 @@ DB_STRESS_CMD?=./db_stress
|
||||
|
||||
include common.mk
|
||||
|
||||
ifdef COMPILE_WITH_TSAN
|
||||
# Keep direct `make -f crash_test.mk COMPILE_WITH_TSAN=1 ...` runs
|
||||
# aligned with the main Makefile's TSAN runtime options.
|
||||
TSAN_OPTIONS?=suppressions=$(CURDIR)/tools/tsan_suppressions.txt
|
||||
export TSAN_OPTIONS
|
||||
endif
|
||||
|
||||
CRASHTEST_MAKE=$(MAKE) -f crash_test.mk
|
||||
CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --cleanup_cmd='$(DB_CLEANUP_CMD)' --destroy_db_initially=1
|
||||
|
||||
|
||||
@@ -786,13 +786,19 @@ Status BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
|
||||
}
|
||||
}
|
||||
|
||||
Status s = version != nullptr ? Status::Corruption("Invalid blob file number")
|
||||
: Status::NotFound();
|
||||
|
||||
if (blob_file_cache == nullptr) {
|
||||
return s;
|
||||
return version != nullptr ? Status::Corruption("Invalid blob file number")
|
||||
: Status::NotFound();
|
||||
}
|
||||
|
||||
if (read_options.read_tier == kBlockCacheTier) {
|
||||
// The direct-write fallback below may need to open the blob file reader,
|
||||
// which `kBlockCacheTier` forbids. Keep the normal Version-backed path
|
||||
// above eligible for cache-only hits.
|
||||
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
}
|
||||
|
||||
Status s;
|
||||
CacheHandleGuard<BlobFileReader> reader;
|
||||
s = blob_file_cache->GetBlobFileReader(read_options, blob_idx.file_number(),
|
||||
&reader,
|
||||
|
||||
@@ -278,7 +278,7 @@ Status BlobWriteBatchTransformer::MergeCF(uint32_t column_family_id,
|
||||
Status BlobWriteBatchTransformer::PutBlobIndexCF(uint32_t column_family_id,
|
||||
const Slice& key,
|
||||
const Slice& value) {
|
||||
// Already a blob index — pass through unchanged.
|
||||
// Already a blob index -- pass through unchanged.
|
||||
return WriteBatchInternal::PutBlobIndex(output_batch_, column_family_id, key,
|
||||
value);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ struct BlobDirectWriteSettings {
|
||||
CompressionType compression_type = kNoCompression;
|
||||
// Compression options for newly written blob records.
|
||||
CompressionOptions compression_opts;
|
||||
// Raw pointer — the Cache is owned by ColumnFamilyOptions and outlives all
|
||||
// Raw pointer -- the Cache is owned by ColumnFamilyOptions and outlives all
|
||||
// settings snapshots. Using raw avoids 2 atomic ref-count ops per Put().
|
||||
Cache* blob_cache = nullptr;
|
||||
// Blob-cache prepopulation policy for direct-write records.
|
||||
|
||||
@@ -1620,6 +1620,47 @@ TEST_P(DBBlobBasicIOErrorTest, GetBlob_IOError) {
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_P(DBBlobBasicIOErrorTest, GetEntityMergeWithBlobBaseIOError) {
|
||||
// Goal: verify GetEntity preserves injected blob-read IOErrors when merge
|
||||
// reads a blob-backed base value, instead of laundering them into Corruption.
|
||||
// The test writes a blob-backed base value plus a merge operand, then injects
|
||||
// an IOError at blob read time and checks both GetEntity and Get see it.
|
||||
Options options;
|
||||
options.env = fault_injection_env_.get();
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 0;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
|
||||
Reopen(options);
|
||||
|
||||
constexpr char key[] = "key";
|
||||
constexpr char base_value[] = "base_value";
|
||||
|
||||
ASSERT_OK(Put(key, base_value));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(Merge(key, "merge_operand"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(sync_point_, [this](void* /* arg */) {
|
||||
fault_injection_env_->SetFilesystemActive(false,
|
||||
Status::IOError(sync_point_));
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
PinnableWideColumns entity_result;
|
||||
Status s = db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), key,
|
||||
&entity_result);
|
||||
ASSERT_TRUE(s.IsIOError()) << "Expected IOError but got: " << s.ToString();
|
||||
|
||||
PinnableSlice get_result;
|
||||
s = db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &get_result);
|
||||
ASSERT_TRUE(s.IsIOError()) << "Expected IOError but got: " << s.ToString();
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_P(DBBlobBasicIOErrorMultiGetTest, MultiGetBlobs_IOError) {
|
||||
Options options = GetDefaultOptions();
|
||||
options.env = fault_injection_env_.get();
|
||||
|
||||
@@ -128,6 +128,16 @@ class DBBlobIndexTest : public DBTestBase {
|
||||
columns, s, is_blob_index, value_found);
|
||||
}
|
||||
|
||||
bool MaybeResolveMemtableBlobValueForTest(const Slice& key,
|
||||
const BlobFetcher* blob_fetcher,
|
||||
PinnableSlice* value,
|
||||
PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index,
|
||||
bool* value_found = nullptr) {
|
||||
return DBImpl::MaybeResolveMemtableBlobValue(
|
||||
key, blob_fetcher, value, columns, s, is_blob_index, value_found);
|
||||
}
|
||||
|
||||
Options GetTestOptions() {
|
||||
Options options;
|
||||
options.env = CurrentOptions().env;
|
||||
@@ -271,6 +281,55 @@ TEST_F(DBBlobIndexTest,
|
||||
ASSERT_EQ(static_cast<char>(BlobIndex::Type::kUnknown), blob_index.front());
|
||||
}
|
||||
|
||||
TEST_F(DBBlobIndexTest,
|
||||
MaybeResolveMemtableBlobValueWithoutFetcherFailsClosed) {
|
||||
// Goal: if a readonly/secondary memtable hit produces a blob-backed payload
|
||||
// but no BlobFetcher is available, the helper must fail closed instead of
|
||||
// handing raw blob-index bytes back to the caller as if they were the value.
|
||||
std::string blob_index;
|
||||
BlobIndex::EncodeBlob(&blob_index, /*file_number=*/123, /*offset=*/456,
|
||||
/*size=*/789, kNoCompression);
|
||||
|
||||
PinnableSlice value;
|
||||
value.GetSelf()->assign(blob_index.data(), blob_index.size());
|
||||
value.PinSelf();
|
||||
|
||||
Status s = Status::OK();
|
||||
bool is_blob_index = true;
|
||||
ASSERT_TRUE(MaybeResolveMemtableBlobValueForTest(
|
||||
Slice("key"), /*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr, &s,
|
||||
&is_blob_index));
|
||||
|
||||
ASSERT_TRUE(s.IsNotSupported()) << s.ToString();
|
||||
ASSERT_TRUE(value.empty());
|
||||
ASSERT_FALSE(is_blob_index);
|
||||
}
|
||||
|
||||
TEST_F(DBBlobIndexTest, ReadOnlyGetImplReturnsBlobIndexWhenRequested) {
|
||||
// Goal: cover the internal read-only GetImpl contract when the caller
|
||||
// explicitly asks for raw blob-index bytes via `is_blob_index`. Recovery
|
||||
// keeps the blob index in the memtable, and the read-only path must preserve
|
||||
// the encoded index instead of eagerly resolving or rejecting it.
|
||||
Options options = GetTestOptions();
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
std::string blob_index;
|
||||
BlobIndex::EncodeInlinedTTL(&blob_index, /*expiration=*/9876543210, "blob");
|
||||
|
||||
WriteBatch batch;
|
||||
ASSERT_OK(PutBlobIndex(&batch, "blob_key", blob_index));
|
||||
ASSERT_OK(Write(&batch));
|
||||
|
||||
Close();
|
||||
options.avoid_flush_during_recovery = true;
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
|
||||
bool is_blob_index = false;
|
||||
ASSERT_EQ(blob_index, GetImpl("blob_key", &is_blob_index));
|
||||
ASSERT_TRUE(is_blob_index);
|
||||
}
|
||||
|
||||
class PlainBlobValueFilterV3 : public CompactionFilter {
|
||||
public:
|
||||
PlainBlobValueFilterV3(std::atomic<int>* filter_call_count,
|
||||
@@ -2138,7 +2197,7 @@ TEST_F(DBBlobIndexTest, EntityBlobFilterV3Remove) {
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Compact to trigger the filter — entity should be removed
|
||||
// Compact to trigger the filter -- entity should be removed
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
// Key should be gone
|
||||
|
||||
@@ -734,6 +734,42 @@ TEST_P(ColumnFamilyTest, AddDrop) {
|
||||
std::vector<std::string>({"default", "four", "three"}));
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, EmptyNameRejected) {
|
||||
// Creating a column family with an empty-string name should be rejected with
|
||||
// InvalidArgument. Empty strings are reserved by various RocksDB APIs and
|
||||
// serialization formats to mean "unknown/unspecified column family" (e.g.
|
||||
// TablePropertiesCollectorFactory::Context::kUnknownColumnFamily, table
|
||||
// properties).
|
||||
//
|
||||
// Prior behavior was silently broken: CreateColumnFamily("") would return
|
||||
// OK and a usable handle, but the CF was not persisted in the manifest --
|
||||
// data written to it was lost on DB reopen.
|
||||
Open();
|
||||
ColumnFamilyHandle* handle = nullptr;
|
||||
Status s =
|
||||
db_->CreateColumnFamily(column_family_options_, std::string(), &handle);
|
||||
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
|
||||
ASSERT_EQ(handle, nullptr);
|
||||
|
||||
// Same via the bulk-by-names API. The first valid name should still get
|
||||
// created; the failure should come on the empty one.
|
||||
std::vector<ColumnFamilyHandle*> handles;
|
||||
s = db_->CreateColumnFamilies(column_family_options_,
|
||||
{"ok_name", "", "another"}, &handles);
|
||||
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
|
||||
// Clean up the partially-created handle ("ok_name") before the failure.
|
||||
ASSERT_EQ(handles.size(), 1U);
|
||||
ASSERT_OK(db_->DropColumnFamily(handles[0]));
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(handles[0]));
|
||||
handles.clear();
|
||||
|
||||
// Same via the bulk-by-descriptor API.
|
||||
s = db_->CreateColumnFamilies(
|
||||
{ColumnFamilyDescriptor("", column_family_options_)}, &handles);
|
||||
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
|
||||
ASSERT_TRUE(handles.empty());
|
||||
}
|
||||
|
||||
TEST_P(ColumnFamilyTest, BulkAddDrop) {
|
||||
constexpr int kNumCF = 1000;
|
||||
ColumnFamilyOptions cf_options;
|
||||
|
||||
@@ -75,7 +75,7 @@ class CompactionBlobResolver : public WideColumnBlobResolver {
|
||||
bool track_resolve_error_ = false;
|
||||
|
||||
// Cache for resolved blob values to avoid re-fetching. Uses a vector of
|
||||
// (column_index, PinnableSlice) pairs — typical entities have few blob
|
||||
// (column_index, PinnableSlice) pairs -- typical entities have few blob
|
||||
// columns (<5), making linear scan cheaper than hash map overhead.
|
||||
std::vector<std::pair<size_t, std::unique_ptr<PinnableSlice>>>
|
||||
resolved_cache_;
|
||||
|
||||
@@ -251,7 +251,7 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
|
||||
// When using blob-aware sizing, use proportional estimation (same
|
||||
// principle as EstimateTotalDataForSST): each SST "owns"
|
||||
// effective_size / num_files of total data. This is an approximation
|
||||
// — individual SSTs may reference different amounts of blob data,
|
||||
// -- individual SSTs may reference different amounts of blob data,
|
||||
// but uniform distribution is a reasonable estimate for FIFO dropping.
|
||||
uint64_t remaining_size = effective_size;
|
||||
const uint64_t num_files = last_level_files.size();
|
||||
@@ -477,12 +477,12 @@ Compaction* FIFOCompactionPicker::PickIntraL0Compaction(
|
||||
if (fifo_opts.max_data_files_size == 0) {
|
||||
ROCKS_LOG_BUFFER(
|
||||
log_buffer,
|
||||
"[%s] FIFO kv-ratio compaction: skipping — "
|
||||
"[%s] FIFO kv-ratio compaction: skipping -- "
|
||||
"max_data_files_size is 0, cannot compute target file size. ",
|
||||
cf_name.c_str());
|
||||
} else if (fifo_opts.max_data_files_size < fifo_opts.max_table_files_size) {
|
||||
ROCKS_LOG_BUFFER(log_buffer,
|
||||
"[%s] FIFO kv-ratio compaction: skipping — "
|
||||
"[%s] FIFO kv-ratio compaction: skipping -- "
|
||||
"max_data_files_size (%" PRIu64
|
||||
") < max_table_files_size "
|
||||
"(%" PRIu64 ").",
|
||||
@@ -547,7 +547,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
|
||||
for (int level = 1; level < vstorage->num_levels(); ++level) {
|
||||
if (!vstorage->LevelFiles(level).empty()) {
|
||||
ROCKS_LOG_BUFFER(log_buffer,
|
||||
"[%s] FIFO kv-ratio compaction: skipping — non-L0 "
|
||||
"[%s] FIFO kv-ratio compaction: skipping -- non-L0 "
|
||||
"level %d still has %" ROCKSDB_PRIszt
|
||||
" files (migration in progress)",
|
||||
cf_name.c_str(), level,
|
||||
@@ -587,7 +587,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
|
||||
// once per flush or compaction completion, so no caching is needed.
|
||||
uint64_t target = 0;
|
||||
if (mutable_cf_options.max_compaction_bytes > 0) {
|
||||
// User explicitly set max_compaction_bytes — use it as target
|
||||
// User explicitly set max_compaction_bytes -- use it as target
|
||||
target = mutable_cf_options.max_compaction_bytes;
|
||||
} else {
|
||||
// Auto-calculate from capacity and observed SST/blob ratio
|
||||
@@ -642,7 +642,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
|
||||
// of linear) write amplification.
|
||||
|
||||
// Build tier boundaries from smallest to largest.
|
||||
// Stop at 10KB minimum — SST files of most workloads are larger than
|
||||
// Stop at 10KB minimum -- SST files of most workloads are larger than
|
||||
// this, so lower boundaries would only waste CPU scanning L0 files.
|
||||
// Files smaller than the lowest boundary simply merge at that boundary.
|
||||
static constexpr uint64_t kMinTierBoundary = 10 * 1024; // 10KB
|
||||
@@ -651,7 +651,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
|
||||
boundaries.push_back(b);
|
||||
}
|
||||
if (boundaries.empty()) {
|
||||
// target itself is below kMinTierBoundary — use target as the
|
||||
// target itself is below kMinTierBoundary -- use target as the
|
||||
// sole boundary so we can still compact at the target size.
|
||||
boundaries.push_back(target);
|
||||
}
|
||||
@@ -668,7 +668,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found a file < boundary — collect contiguous batch
|
||||
// Found a file < boundary -- collect contiguous batch
|
||||
std::vector<FileMetaData*> batch;
|
||||
uint64_t accumulated = 0;
|
||||
size_t pos = scan;
|
||||
@@ -713,7 +713,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
|
||||
return c;
|
||||
}
|
||||
|
||||
// This batch wasn't enough — advance past it
|
||||
// This batch wasn't enough -- advance past it
|
||||
scan = pos;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1101,7 +1101,7 @@ TEST_F(CompactionPickerTest, ReadTriggeredSkipsLastLevel) {
|
||||
Add(0, 1U, "150", "200", kFileSize, 0, 500, 550);
|
||||
Add(4, 3U, "301", "350", kFileSize, 0, 101, 150);
|
||||
|
||||
// File 3 is at the last non-empty level — should NOT be marked for
|
||||
// File 3 is at the last non-empty level -- should NOT be marked for
|
||||
// read-triggered compaction. Bottommost file cleanup is handled
|
||||
// separately by ComputeBottommostFilesMarkedForCompaction().
|
||||
file_map_[3U].first->stats.num_collapsible_entry_reads_sampled.store(
|
||||
|
||||
@@ -846,11 +846,16 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
||||
{offsetof(struct InternalStats::CompactionStats, count),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"counts", OptionTypeInfo::Array<
|
||||
int, static_cast<int>(CompactionReason::kNumOfReasons)>(
|
||||
offsetof(struct InternalStats::CompactionStats, counts),
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
|
||||
{0, OptionType::kInt})},
|
||||
{"counts",
|
||||
OptionTypeInfo::Array<
|
||||
/* In release 11.2, a new compaction reason was added. This broken
|
||||
* the reader and writer. To unblock release 11.1, we temporarily
|
||||
* reduce the count array size to the old one. TODO add a proper
|
||||
* serialization and deserialization method. */
|
||||
int, static_cast<int>(CompactionReason::kNumOfReasons) - 1>(
|
||||
offsetof(struct InternalStats::CompactionStats, counts),
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
|
||||
{0, OptionType::kInt})},
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo>
|
||||
|
||||
@@ -102,6 +102,757 @@ TEST_F(DBBasicTest, OpenWhenOpen) {
|
||||
ASSERT_TRUE(strstr(s.getState(), "lock ") != nullptr);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Helper that captures per-branch SkippedNoopEdit counts for tests.
|
||||
struct RecoveryOptimizationCounters {
|
||||
std::atomic<int> setup_dbid{0};
|
||||
std::atomic<int> per_cf{0};
|
||||
std::atomic<int> wal_deletion{0};
|
||||
std::atomic<int> next_file_number{0};
|
||||
|
||||
void Install() {
|
||||
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
|
||||
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:SetupDBId",
|
||||
[this](void*) { setup_dbid.fetch_add(1); });
|
||||
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:PerCF",
|
||||
[this](void*) { per_cf.fetch_add(1); });
|
||||
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:WalDeletion",
|
||||
[this](void*) { wal_deletion.fetch_add(1); });
|
||||
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber",
|
||||
[this](void*) { next_file_number.fetch_add(1); });
|
||||
sp->EnableProcessing();
|
||||
}
|
||||
void Uninstall() {
|
||||
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
|
||||
sp->DisableProcessing();
|
||||
sp->ClearAllCallBacks();
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
// optimize_manifest_for_recovery=true: a clean reopen of a flushed DB must
|
||||
// append fewer individual records to the MANIFEST than the default-off path.
|
||||
// Verified by counting AddRecord calls (one per VersionEdit written to the
|
||||
// MANIFEST log).
|
||||
TEST_F(DBBasicTest,
|
||||
OptimizeManifestForRecoveryReducesManifestWritesOnCleanReopen) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
|
||||
|
||||
// Measure MANIFEST records with optimize_manifest_for_recovery OFF (default).
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k1", "v1"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
std::atomic<int> records_off{0};
|
||||
std::atomic<int> next_file_skips_off{0};
|
||||
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
|
||||
[&](void*) { records_off.fetch_add(1); });
|
||||
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber",
|
||||
[&](void*) { next_file_skips_off.fetch_add(1); });
|
||||
sp->EnableProcessing();
|
||||
Reopen(options);
|
||||
sp->DisableProcessing();
|
||||
sp->ClearAllCallBacks();
|
||||
ASSERT_EQ("v1", Get("k1"));
|
||||
int off_count = records_off.load();
|
||||
ASSERT_GT(off_count, 0);
|
||||
ASSERT_EQ(0, next_file_skips_off.load());
|
||||
Close();
|
||||
|
||||
// Measure MANIFEST records with optimize_manifest_for_recovery ON.
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k2", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
std::atomic<int> records_on{0};
|
||||
std::atomic<int> next_file_skips_on{0};
|
||||
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
|
||||
[&](void*) { records_on.fetch_add(1); });
|
||||
sp->SetCallBack("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber",
|
||||
[&](void*) { next_file_skips_on.fetch_add(1); });
|
||||
sp->EnableProcessing();
|
||||
Reopen(options);
|
||||
sp->DisableProcessing();
|
||||
sp->ClearAllCallBacks();
|
||||
ASSERT_EQ("v2", Get("k2"));
|
||||
int on_count = records_on.load();
|
||||
ASSERT_GT(next_file_skips_on.load(), 0);
|
||||
ASSERT_LT(on_count, off_count);
|
||||
}
|
||||
|
||||
// When the per-CF log_number actually advances, the per-CF skip must NOT
|
||||
// fire -- the edit carries real information and must be emitted.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryEmitsPerCFWhenLogAdvances) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
// Write data and close WITHOUT flushing -- the WAL has un-replayed
|
||||
// records, so on reopen recovery flushes the memtable and the per-CF
|
||||
// edit's log_number must advance past the prior WAL.
|
||||
Close();
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_EQ(0, counters.per_cf.load());
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// With track_and_verify_wals_in_manifest=true, the wal_deletion edit
|
||||
// must STILL be emitted (the DeleteWalsBefore record is required).
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryPreservesWalTracking) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.track_and_verify_wals_in_manifest = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_EQ(0, counters.wal_deletion.load());
|
||||
}
|
||||
|
||||
// best_efforts_recovery requires a fresh MANIFEST + CURRENT to be
|
||||
// produced on every open (the salvage contract). Even with the option
|
||||
// on, none of the SkippedNoopEdit branches must fire under
|
||||
// best_efforts_recovery -- otherwise CURRENT can be left missing or
|
||||
// stale (regression caught by DBBasicTest.RecoverWithNoCurrentFile and
|
||||
// DBTest2.BestEffortsRecoveryWithSstUniqueIdVerification under an
|
||||
// option-on default).
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryDisabledByBestEffortsRecovery) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.best_efforts_recovery = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_EQ(0, counters.setup_dbid.load());
|
||||
ASSERT_EQ(0, counters.per_cf.load());
|
||||
ASSERT_EQ(0, counters.wal_deletion.load());
|
||||
ASSERT_EQ(0, counters.next_file_number.load());
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// Multi-column-family coverage: with one CF flushed and another not,
|
||||
// the un-flushed CF's edit must still be emitted.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryMultiCF) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
CreateAndReopenWithCF({"pikachu", "raichu"}, options);
|
||||
ASSERT_OK(Put(0, "k", "v0"));
|
||||
ASSERT_OK(Put(1, "k", "v1"));
|
||||
ASSERT_OK(Put(2, "k", "v2"));
|
||||
ASSERT_OK(Flush(0));
|
||||
// CF 1 and 2 keep dirty memtables -- recovery must emit their per-CF
|
||||
// edits (log_number advance from WAL replay).
|
||||
Close();
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
ReopenWithColumnFamilies({"default", "pikachu", "raichu"}, options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_EQ("v0", Get(0, "k"));
|
||||
ASSERT_EQ("v1", Get(1, "k"));
|
||||
ASSERT_EQ("v2", Get(2, "k"));
|
||||
}
|
||||
|
||||
// MaybeUpdateNextFileNumber's seed value (next_file_number_) makes the
|
||||
// post-loop comparison trivially true on every clean recovery, causing a
|
||||
// no-op SetNextFile edit to be appended to the MANIFEST. With
|
||||
// optimize_manifest_for_recovery=true, that emission is gated and
|
||||
// the SkippedNoopEdit:NextFileNumber sync point fires in its place.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoverySkipsNextFileNumber) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_EQ(1, counters.next_file_number.load());
|
||||
}
|
||||
|
||||
// With option=true and a synthetic on-disk file whose number is at or
|
||||
// above next_file_number_, MaybeUpdateNextFileNumber MUST emit the
|
||||
// SetNextFile edit and advance the counter past the synthetic number.
|
||||
TEST_F(DBBasicTest,
|
||||
OptimizeManifestForRecoveryEmitsNextFileNumberWhenJustified) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
const uint64_t next_before_close =
|
||||
dbfull()->GetVersionSet()->current_next_file_number();
|
||||
Close();
|
||||
|
||||
// Drop an empty .sst with a number well above next_file_number into
|
||||
// the DB dir. MaybeUpdateNextFileNumber must observe it and advance.
|
||||
const uint64_t synthetic_number = next_before_close + 100;
|
||||
const std::string synthetic_sst =
|
||||
dbname_ + "/" + MakeTableFileName("", synthetic_number);
|
||||
ASSERT_OK(WriteStringToFile(env_, "" /*data*/, synthetic_sst,
|
||||
/*should_sync=*/true));
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_EQ(0, counters.next_file_number.load());
|
||||
ASSERT_GT(dbfull()->GetVersionSet()->current_next_file_number(),
|
||||
synthetic_number);
|
||||
}
|
||||
|
||||
// optimize_manifest_for_recovery=true: after a clean Put + Flush + Close, the
|
||||
// next Open's min_log_number_to_keep equals (max_wal_before_close + 1),
|
||||
// proving the close-time MANIFEST write took effect.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryAdvancesWalMarkersOnClose) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
uint64_t max_wal_before_close = 0;
|
||||
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
|
||||
sp->SetCallBack("DBImpl::CloseHelper:CapturedMaxWal", [&](void* arg) {
|
||||
max_wal_before_close = *static_cast<uint64_t*>(arg);
|
||||
});
|
||||
sp->EnableProcessing();
|
||||
Close();
|
||||
sp->DisableProcessing();
|
||||
sp->ClearAllCallBacks();
|
||||
ASSERT_GT(max_wal_before_close, 0u);
|
||||
|
||||
Reopen(options);
|
||||
ASSERT_EQ(max_wal_before_close + 1,
|
||||
dbfull()->GetVersionSet()->min_log_number_to_keep());
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// Shared-option matrix: default-off and disable-before-close should both fall
|
||||
// back to recovery-time MANIFEST work, while enable-through-close should avoid
|
||||
// MANIFEST appends on a clean reopen.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryCleanReopenMatrix) {
|
||||
struct TestCase {
|
||||
const char* name;
|
||||
bool enable_on_open;
|
||||
bool disable_before_close;
|
||||
bool expect_close_write;
|
||||
};
|
||||
const TestCase test_cases[] = {
|
||||
{"default_off", false, false, false},
|
||||
{"enabled", true, false, true},
|
||||
{"disabled_before_close", true, true, false},
|
||||
};
|
||||
|
||||
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
|
||||
for (const auto& test_case : test_cases) {
|
||||
SCOPED_TRACE(test_case.name);
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = test_case.enable_on_open;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", test_case.name));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
if (test_case.disable_before_close) {
|
||||
ASSERT_OK(dbfull()->SetDBOptions(
|
||||
{{"optimize_manifest_for_recovery", "false"}}));
|
||||
}
|
||||
|
||||
std::atomic<int> entered{0};
|
||||
sp->SetCallBack("DBImpl::CloseHelper:WriteWalMarkersOnCloseEntered",
|
||||
[&](void*) { entered.fetch_add(1); });
|
||||
sp->EnableProcessing();
|
||||
Close();
|
||||
sp->DisableProcessing();
|
||||
sp->ClearAllCallBacks();
|
||||
|
||||
std::atomic<int> records{0};
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
|
||||
[&](void*) { records.fetch_add(1); });
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
if (test_case.expect_close_write) {
|
||||
ASSERT_EQ(1, entered.load());
|
||||
ASSERT_EQ(0, records.load());
|
||||
ASSERT_GT(counters.per_cf.load(), 0);
|
||||
} else {
|
||||
ASSERT_EQ(0, entered.load());
|
||||
ASSERT_GT(records.load(), 0);
|
||||
}
|
||||
ASSERT_EQ(test_case.name, Get("k"));
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
// allow_2pc=true: even with the option on, the close-time write must NOT
|
||||
// advance MinLogNumberToKeep / DeleteWalsBefore (2pc requires the WAL to
|
||||
// remain replayable for uncommitted prepared transactions).
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryRespectsTwoPC) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.allow_2pc = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
const uint64_t min_log_before_close =
|
||||
dbfull()->GetVersionSet()->min_log_number_to_keep();
|
||||
Close();
|
||||
Reopen(options);
|
||||
ASSERT_EQ(min_log_before_close,
|
||||
dbfull()->GetVersionSet()->min_log_number_to_keep());
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// Mixed-CF emptiness: on reopen, recovery should still have MANIFEST work to
|
||||
// do for the dirty CF while skipping per-CF recovery edits for the empty CFs
|
||||
// whose markers were persisted at close.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoverySkipsNonEmptyCF) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
CreateAndReopenWithCF({"empty_cf", "dirty_cf"}, options);
|
||||
ASSERT_OK(Put(1, "k", "v1"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(Put(2, "k", "v2"));
|
||||
Close();
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
std::atomic<int> records{0};
|
||||
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
|
||||
counters.Install();
|
||||
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
|
||||
[&](void*) { records.fetch_add(1); });
|
||||
ReopenWithColumnFamilies({"default", "empty_cf", "dirty_cf"}, options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_GT(counters.per_cf.load(), 0);
|
||||
ASSERT_GT(records.load(), 0);
|
||||
ASSERT_EQ("v1", Get(1, "k"));
|
||||
ASSERT_EQ("v2", Get(2, "k"));
|
||||
}
|
||||
|
||||
// track_and_verify_wals_in_manifest=true: the close-time write must leave
|
||||
// recovery with only the mandatory WAL-tracking MANIFEST work. The per-CF
|
||||
// recovery edits should still skip.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryEmitsWalDeletionWhenTracking) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.track_and_verify_wals_in_manifest = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
std::atomic<int> records{0};
|
||||
auto* sp = ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
|
||||
counters.Install();
|
||||
sp->SetCallBack("VersionSet::ProcessManifestWrites:AddRecord",
|
||||
[&](void*) { records.fetch_add(1); });
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_GT(counters.per_cf.load(), 0);
|
||||
ASSERT_EQ(1, records.load());
|
||||
ASSERT_GT(dbfull()->GetVersionSet()->GetWalSet().GetMinWalNumberToKeep(), 0u);
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// Regression for the close-time marker path: if a new WAL is created while an
|
||||
// otherwise-empty user CF stays empty, Close() must reserve the next file
|
||||
// number before persisting SetLogNumber(cur_wal + 1) for that CF.
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoveryImmediateCloseAfterWarmReopen) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
CreateAndReopenWithCF({"empty_cf"}, options);
|
||||
|
||||
ASSERT_OK(Put(0, "k", "v"));
|
||||
const uint64_t wal_before_switch = dbfull()->TEST_LogfileNumber();
|
||||
ASSERT_OK(dbfull()->TEST_SwitchMemtable());
|
||||
ASSERT_GT(dbfull()->TEST_LogfileNumber(), wal_before_switch);
|
||||
|
||||
const uint64_t expected_new_log_num = dbfull()->TEST_LogfileNumber() + 1;
|
||||
ASSERT_EQ(expected_new_log_num,
|
||||
dbfull()->GetVersionSet()->current_next_file_number());
|
||||
|
||||
Close();
|
||||
ReopenWithColumnFamilies({"default", "empty_cf"}, options);
|
||||
|
||||
ASSERT_GT(dbfull()->GetVersionSet()->current_next_file_number(),
|
||||
expected_new_log_num);
|
||||
ASSERT_EQ("v", Get(0, "k"));
|
||||
}
|
||||
|
||||
// Dropped-CF safety: dropped column families must NOT have markers written for
|
||||
// them at close time (the !IsDropped() guard protects against attaching the
|
||||
// global edit to a dropped CF, which would later trip MANIFEST replay
|
||||
// assertions).
|
||||
TEST_F(DBBasicTest, OptimizeManifestForRecoverySkipsDroppedCF) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
CreateAndReopenWithCF({"to_drop", "keeper"}, options);
|
||||
ASSERT_OK(Put(1, "k", "v1"));
|
||||
ASSERT_OK(Put(2, "k", "v2"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(Flush(2));
|
||||
ASSERT_OK(db_->DropColumnFamily(handles_[1]));
|
||||
Close();
|
||||
|
||||
ReopenWithColumnFamilies({"default", "keeper"}, options);
|
||||
ASSERT_EQ("v2", Get(1, "k"));
|
||||
}
|
||||
|
||||
// reuse_manifest_on_open=true: the next LogAndApply after Recover must
|
||||
// append to the existing MANIFEST file instead of allocating a fresh
|
||||
// one. Force a write after reopen and verify the MANIFEST file number
|
||||
// stays unchanged.
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenAppendsToExistingFile) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
Reopen(options);
|
||||
const uint64_t manifest_after_reopen =
|
||||
dbfull()->TEST_Current_Manifest_FileNo();
|
||||
ASSERT_OK(Put("k2", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
const uint64_t manifest_after_flush =
|
||||
dbfull()->TEST_Current_Manifest_FileNo();
|
||||
|
||||
EXPECT_EQ(manifest_after_reopen, manifest_after_flush);
|
||||
EXPECT_EQ("v", Get("k"));
|
||||
EXPECT_EQ("v2", Get("k2"));
|
||||
}
|
||||
|
||||
// Default off: ReopenManifestForAppend must NOT be invoked.
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenDefaultOffSkipsReopen) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
// reuse_manifest_on_open defaults to false
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
std::atomic<int> reopened{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ReopenManifestForAppend:Reopened",
|
||||
[&](void* /*arg*/) { reopened.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Reopen(options);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_EQ(0, reopened.load());
|
||||
}
|
||||
|
||||
// Regression for the WritableFileWriter::filesize_=0 bug fixed via
|
||||
// constructor-time initial_file_size: after ReopenManifestForAppend binds a
|
||||
// writer to the existing MANIFEST, GetFileSize() must return the on-disk
|
||||
// size, not 0.
|
||||
// (If it returned 0, the size-limit check in ProcessManifestWrites would
|
||||
// compare against 0 and Close-time Truncate could shrink the file.)
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenAdoptsOnDiskSize) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
// Capture the on-disk MANIFEST size before reopen.
|
||||
Reopen(options);
|
||||
const uint64_t manifest_no = dbfull()->TEST_Current_Manifest_FileNo();
|
||||
const std::string manifest_path = DescriptorFileName(dbname_, manifest_no);
|
||||
uint64_t on_disk_size = 0;
|
||||
ASSERT_OK(env_->GetFileSize(manifest_path, &on_disk_size));
|
||||
ASSERT_GT(on_disk_size, 0u);
|
||||
|
||||
// Force a write to flush the writer's buffer; verify Close doesn't
|
||||
// shrink the file (which would happen if Truncate(filesize_=0) ran).
|
||||
ASSERT_OK(Put("k2", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
uint64_t after_close_size = 0;
|
||||
ASSERT_OK(env_->GetFileSize(manifest_path, &after_close_size));
|
||||
EXPECT_GE(after_close_size, on_disk_size);
|
||||
}
|
||||
|
||||
// MANIFEST rotation continues to work under reuse: when the file grows
|
||||
// past tuned_max_manifest_file_size_, ProcessManifestWrites must rotate
|
||||
// to a fresh MANIFEST (same as legacy). Use the rotation SyncPoint to
|
||||
// observe rotation directly, since tuned_max_manifest_file_size_ is
|
||||
// auto-derived and not directly = max_manifest_file_size.
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenStillRotatesOnSizeCap) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
options.max_manifest_file_size = 1;
|
||||
options.max_manifest_space_amp_pct = 0; // disable amp-based tuning
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
std::atomic<int> rotations{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ProcessManifestWrites:BeforeNewManifest",
|
||||
[&](void* /*arg*/) { rotations.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Reopen(options);
|
||||
ASSERT_OK(Put("k2", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// The Flush after Reopen must have triggered a rotation given the
|
||||
// tiny size cap -- proves the size-driven rotation path still runs
|
||||
// when descriptor_log_ is bound by reuse.
|
||||
EXPECT_GT(rotations.load(), 0);
|
||||
EXPECT_EQ("v", Get("k"));
|
||||
EXPECT_EQ("v2", Get("k2"));
|
||||
}
|
||||
|
||||
// Multi-CF reuse: append-mode MANIFEST must correctly handle edits
|
||||
// from multiple CFs after reopen.
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenMultiCF) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
CreateAndReopenWithCF({"alpha", "beta"}, options);
|
||||
ASSERT_OK(Put(0, "k", "v0"));
|
||||
ASSERT_OK(Put(1, "k", "v1"));
|
||||
ASSERT_OK(Put(2, "k", "v2"));
|
||||
ASSERT_OK(Flush(0));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(Flush(2));
|
||||
Close();
|
||||
|
||||
ReopenWithColumnFamilies({"default", "alpha", "beta"}, options);
|
||||
// Trigger more writes post-reopen on each CF -- these get appended to
|
||||
// the reused MANIFEST.
|
||||
ASSERT_OK(Put(0, "k2", "v0b"));
|
||||
ASSERT_OK(Put(1, "k2", "v1b"));
|
||||
ASSERT_OK(Put(2, "k2", "v2b"));
|
||||
ASSERT_OK(Flush(0));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(Flush(2));
|
||||
|
||||
EXPECT_EQ("v0", Get(0, "k"));
|
||||
EXPECT_EQ("v1", Get(1, "k"));
|
||||
EXPECT_EQ("v2", Get(2, "k"));
|
||||
EXPECT_EQ("v0b", Get(0, "k2"));
|
||||
EXPECT_EQ("v1b", Get(1, "k2"));
|
||||
EXPECT_EQ("v2b", Get(2, "k2"));
|
||||
}
|
||||
|
||||
// Disabled under best_efforts_recovery: that mode rebuilds CURRENT and
|
||||
// MANIFEST as the side-effect of LogAndApplyForRecovery emitting an
|
||||
// edit; reusing the prior MANIFEST contradicts the salvage contract.
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenDisabledByBestEffortsRecovery) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
options.best_efforts_recovery = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
std::atomic<int> reopened{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ReopenManifestForAppend:Reopened",
|
||||
[&](void* /*arg*/) { reopened.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Reopen(options);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_EQ(0, reopened.load());
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// Full-stack composition: optimize_manifest_for_recovery plus
|
||||
// reuse_manifest_on_open. Verifies that Close writes recovery markers,
|
||||
// Reopen skips clean-recovery MANIFEST edits, and the MANIFEST is reused
|
||||
// instead of recreated for the next metadata update.
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenFullStackComposition) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
// Capture the MANIFEST file number before reopen.
|
||||
std::atomic<int> reopened{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ReopenManifestForAppend:Reopened",
|
||||
[&](void* /*arg*/) { reopened.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Reopen(options);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// All three composing: reuse fired, data is intact, no fresh MANIFEST.
|
||||
EXPECT_EQ(1, reopened.load());
|
||||
EXPECT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// Direct unit test for WritableFileWriter's initial_file_size parameter:
|
||||
// verifies the visible size accessors report the existing on-disk size
|
||||
// immediately, rather than the constructor's zero default.
|
||||
TEST_F(DBBasicTest, WritableFileWriterInitialFileSizeAdoptsExistingSize) {
|
||||
Env* env = Env::Default();
|
||||
std::string fname = test::PerThreadDBPath("set_file_size_test");
|
||||
ASSERT_OK(env->CreateDirIfMissing(test::TmpDir(env)));
|
||||
|
||||
// Create a file with some bytes, close it.
|
||||
{
|
||||
std::unique_ptr<WritableFile> raw;
|
||||
ASSERT_OK(env->NewWritableFile(fname, &raw, EnvOptions()));
|
||||
ASSERT_OK(raw->Append("hello world"));
|
||||
ASSERT_OK(raw->Close());
|
||||
}
|
||||
|
||||
// Reopen and wrap in a WritableFileWriter without initial_file_size --
|
||||
// GetFileSize() should be 0 (the constructor's default).
|
||||
std::unique_ptr<FSWritableFile> fs_file;
|
||||
ASSERT_OK(env->GetFileSystem()->ReopenWritableFile(
|
||||
fname, FileOptions(), &fs_file, /*dbg=*/nullptr));
|
||||
std::unique_ptr<WritableFileWriter> writer(
|
||||
new WritableFileWriter(std::move(fs_file), fname, FileOptions()));
|
||||
EXPECT_EQ(0u, writer->GetFileSize());
|
||||
|
||||
// Reopen again and seed the writer's size accounting from the existing
|
||||
// bytes. GetFileSize and GetFlushedSize must reflect it immediately.
|
||||
ASSERT_OK(env->GetFileSystem()->ReopenWritableFile(
|
||||
fname, FileOptions(), &fs_file, /*dbg=*/nullptr));
|
||||
writer.reset(new WritableFileWriter(
|
||||
std::move(fs_file), fname, FileOptions(),
|
||||
/*clock=*/nullptr, /*io_tracer=*/nullptr, /*stats=*/nullptr,
|
||||
Histograms::HISTOGRAM_ENUM_MAX, /*listeners=*/{},
|
||||
/*file_checksum_gen_factory=*/nullptr,
|
||||
/*perform_data_verification=*/false,
|
||||
/*buffered_data_with_checksum=*/false,
|
||||
/*initial_file_size=*/11));
|
||||
EXPECT_EQ(11u, writer->GetFileSize());
|
||||
EXPECT_EQ(11u, writer->GetFlushedSize());
|
||||
|
||||
ASSERT_OK(env->DeleteFile(fname));
|
||||
}
|
||||
|
||||
// Tail corruption: appending garbage bytes to the MANIFEST after a
|
||||
// clean close must prevent reuse -- the physical size exceeds the
|
||||
// last valid record end.
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
// Find the MANIFEST file and append garbage to it.
|
||||
std::string manifest_path;
|
||||
{
|
||||
std::vector<std::string> files;
|
||||
ASSERT_OK(env_->GetChildren(dbname_, &files));
|
||||
for (const auto& f : files) {
|
||||
uint64_t number;
|
||||
FileType type;
|
||||
if (ParseFileName(f, &number, &type) && type == kDescriptorFile) {
|
||||
manifest_path = dbname_ + "/" + f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_FALSE(manifest_path.empty());
|
||||
{
|
||||
std::string contents;
|
||||
ASSERT_OK(ReadFileToString(env_, manifest_path, &contents));
|
||||
contents.append("garbage!");
|
||||
ASSERT_OK(WriteStringToFile(env_, contents, manifest_path));
|
||||
}
|
||||
|
||||
std::atomic<int> reopened{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ReopenManifestForAppend:Reopened",
|
||||
[&](void* /*arg*/) { reopened.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Reopen(options);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_EQ(0, reopened.load());
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, EnableDirectIOWithZeroBuf) {
|
||||
if (!IsDirectIOSupported()) {
|
||||
ROCKSDB_GTEST_BYPASS("Direct IO not supported");
|
||||
|
||||
@@ -542,7 +542,7 @@ TEST_F(DBBlockCacheTest, WarmCacheWithDataBlocksDuringCompaction) {
|
||||
EXPECT_TRUE(tracking_cache->HasPriority(Cache::Priority::BOTTOM));
|
||||
EXPECT_FALSE(tracking_cache->HasPriority(Cache::Priority::LOW));
|
||||
|
||||
// Compaction output is in cache — reads should have zero misses.
|
||||
// Compaction output is in cache -- reads should have zero misses.
|
||||
auto data_miss_before =
|
||||
options.statistics->getTickerCount(BLOCK_CACHE_DATA_MISS);
|
||||
ASSERT_EQ(value + "2", Get("key"));
|
||||
|
||||
@@ -11833,7 +11833,7 @@ TEST_F(DBCompactionTest, RoundRobinCleanCutWithSharedBoundary) {
|
||||
// 2. A post-verification step fails (injected here via sync point), setting
|
||||
// compact_->status to error while each subcompaction's status stays OK.
|
||||
// 3. SubcompactionState::Cleanup checks individual status (OK) and skips
|
||||
// ReleaseObsolete — the cache entries leak.
|
||||
// ReleaseObsolete -- the cache entries leak.
|
||||
// 4. FaultInjectionTestFS injects metadata read errors, causing GetChildren
|
||||
// to fail in FindObsoleteFiles.
|
||||
// 5. Close()'s FindObsoleteFiles also fails to find the orphan for the same
|
||||
@@ -11864,7 +11864,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
|
||||
// fail while individual subcompaction statuses stay OK (so Cleanup skips
|
||||
// ReleaseObsolete). The filesystem deactivation makes GetChildren fail
|
||||
// in FindObsoleteFiles, preventing the backstop from evicting the leaked
|
||||
// cache entries — matching the crash test's metadata read fault injection.
|
||||
// cache entries -- matching the crash test's metadata read fault injection.
|
||||
std::atomic<bool> inject_error{true};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionJob::Run():AfterVerifyOutputFiles", [&](void* arg) {
|
||||
@@ -11876,7 +11876,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
|
||||
// Enable metadata read fault injection on the bg compaction thread after
|
||||
// the compaction job finishes but before FindObsoleteFiles runs. This
|
||||
// makes GetChildren fail (metadata read), matching crash test's
|
||||
// --open_metadata_read_fault_one_in=8. Only metadata reads fail —
|
||||
// --open_metadata_read_fault_one_in=8. Only metadata reads fail --
|
||||
// logging and other IO operations continue normally.
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"BackgroundCallCompaction:1", [&](void*) {
|
||||
@@ -11889,7 +11889,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Trigger compaction — fails after VerifyOutputFiles.
|
||||
// Trigger compaction -- fails after VerifyOutputFiles.
|
||||
Status s = dbfull()->TEST_CompactRange(0, nullptr, nullptr);
|
||||
ASSERT_NOK(s);
|
||||
|
||||
@@ -11971,7 +11971,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnInstallFailure) {
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Trigger compaction — Run() succeeds but Install() fails.
|
||||
// Trigger compaction -- Run() succeeds but Install() fails.
|
||||
Status s = dbfull()->TEST_CompactRange(0, nullptr, nullptr);
|
||||
ASSERT_NOK(s);
|
||||
|
||||
@@ -12032,7 +12032,7 @@ TEST_F(DBCompactionTest, ObsoleteFileTableCacheEntryWithConcurrentRef) {
|
||||
TableCache::Lookup(table_cache, target_file_number);
|
||||
ASSERT_NE(concurrent_handle, nullptr);
|
||||
|
||||
// Call ReleaseObsolete directly — this is what PurgeObsoleteFiles calls
|
||||
// Call ReleaseObsolete directly -- this is what PurgeObsoleteFiles calls
|
||||
// when a file becomes obsolete. Pass nullptr for the handle to make
|
||||
// ReleaseObsolete do its own Lookup internally.
|
||||
TableCache::ReleaseObsolete(table_cache, target_file_number,
|
||||
@@ -12049,6 +12049,88 @@ TEST_F(DBCompactionTest, ObsoleteFileTableCacheEntryWithConcurrentRef) {
|
||||
ASSERT_EQ(leaked, nullptr);
|
||||
}
|
||||
|
||||
// Regression test for the atomic flush path leaking table cache entries when
|
||||
// InstallMemtableAtomicFlushResults (MANIFEST write) fails.
|
||||
// 1. atomic_flush is enabled with two column families.
|
||||
// 2. Both CFs have data, a flush is triggered.
|
||||
// 3. FlushJob::Run() succeeds for both (files are added to table cache by
|
||||
// BuildTable), but write_manifest=false so no install happens in Run().
|
||||
// 4. The combined MANIFEST write (InstallMemtableAtomicFlushResults) fails
|
||||
// due to injected I/O error.
|
||||
// 5. Without the fix, the files remain in the table cache but are not in
|
||||
// any Version, causing TEST_VerifyNoObsoleteFilesCached to fire during
|
||||
// Close() (especially when metadata_read_fault_one_in disables the
|
||||
// FindObsoleteFiles backstop).
|
||||
TEST_F(DBCompactionTest, LeakedTableCacheEntryOnAtomicFlushInstallFailure) {
|
||||
auto fault_fs = std::make_shared<FaultInjectionTestFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.env = fault_env.get();
|
||||
options.atomic_flush = true;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
// Enough buffer so auto-flush doesn't trigger prematurely.
|
||||
options.write_buffer_size = 64 << 20;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Create a second column family.
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
ASSERT_EQ(2, handles_.size());
|
||||
|
||||
// Write data to both CFs.
|
||||
ASSERT_OK(Put(0, "key0", std::string(1024, 'a')));
|
||||
ASSERT_OK(Put(1, "key1", std::string(1024, 'b')));
|
||||
|
||||
// Inject a write error during the combined MANIFEST write that happens
|
||||
// inside InstallMemtableAtomicFlushResults (via LogAndApply).
|
||||
std::atomic<bool> inject_error{true};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::LogAndApply:WriteManifest", [&](void*) {
|
||||
if (inject_error.exchange(false)) {
|
||||
fault_fs->SetThreadLocalErrorContext(
|
||||
FaultInjectionIOType::kWrite, /*seed=*/0, /*one_in=*/1,
|
||||
/*retryable=*/false, /*has_data_loss=*/false);
|
||||
fault_fs->EnableThreadLocalErrorInjection(
|
||||
FaultInjectionIOType::kWrite);
|
||||
}
|
||||
});
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Trigger atomic flush - Run() succeeds but MANIFEST write fails.
|
||||
FlushOptions flush_opts;
|
||||
flush_opts.wait = true;
|
||||
Status s = db_->Flush(flush_opts, handles_);
|
||||
ASSERT_NOK(s);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// Disable the write fault so Close() can proceed.
|
||||
fault_fs->DisableThreadLocalErrorInjection(FaultInjectionIOType::kWrite);
|
||||
|
||||
// Enable metadata read fault injection on the main thread so that
|
||||
// Close()'s FindObsoleteFiles full-scan backstop cannot find the orphan
|
||||
// files (simulating metadata_read_fault_one_in from the stress test).
|
||||
fault_fs->SetThreadLocalErrorContext(
|
||||
FaultInjectionIOType::kMetadataRead, /*seed=*/0, /*one_in=*/1,
|
||||
/*retryable=*/false, /*has_data_loss=*/false);
|
||||
fault_fs->EnableThreadLocalErrorInjection(
|
||||
FaultInjectionIOType::kMetadataRead);
|
||||
|
||||
// Destroy column family handles before closing.
|
||||
for (auto h : handles_) {
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(h));
|
||||
}
|
||||
handles_.clear();
|
||||
|
||||
// Close the DB. Without the fix, TEST_VerifyNoObsoleteFilesCached fires.
|
||||
s = db_->Close();
|
||||
ASSERT_OK(s);
|
||||
db_ = nullptr;
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, VerifyFileChecksumOnCompactionOutput) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
+8
-1
@@ -1852,6 +1852,8 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_OK(WaitForFlushCallbacks());
|
||||
|
||||
// Check that there was at least one mempurge
|
||||
uint32_t expected_min_mempurge_count = 1;
|
||||
// Check that there was no SST files created during flush.
|
||||
@@ -1872,6 +1874,8 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
|
||||
ASSERT_EQ(Get(key), value);
|
||||
}
|
||||
|
||||
ASSERT_OK(WaitForFlushCallbacks());
|
||||
|
||||
// Check that there was at least one SST files created during flush.
|
||||
expected_sst_count = 1;
|
||||
EXPECT_GE(sst_count.load(), expected_sst_count);
|
||||
@@ -1891,6 +1895,9 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
|
||||
// Extra check of database consistency.
|
||||
ASSERT_EQ(Get(key), value);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
@@ -3883,7 +3890,7 @@ TEST_F(DBFlushTest, LeakedTableCacheEntryOnFlushInstallFailure) {
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Trigger flush — BuildTable succeeds but LogAndApply fails.
|
||||
// Trigger flush -- BuildTable succeeds but LogAndApply fails.
|
||||
Status s = Flush();
|
||||
ASSERT_NOK(s);
|
||||
|
||||
|
||||
+338
-19
@@ -15,6 +15,7 @@
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstdio>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
@@ -29,6 +30,7 @@
|
||||
|
||||
#include "db/arena_wrapped_db_iter.h"
|
||||
#include "db/attribute_group_iterator_impl.h"
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/blob/blob_file_partition_manager.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/builder.h"
|
||||
@@ -481,6 +483,18 @@ void DBImpl::WaitForBackgroundWork() {
|
||||
}
|
||||
}
|
||||
|
||||
void DBImpl::WaitForAsyncFileOpen() {
|
||||
if (!immutable_db_options_.open_files_async) {
|
||||
return;
|
||||
}
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
TEST_SYNC_POINT("DBImpl::WaitForAsyncFileOpen::BeforeWait");
|
||||
while (bg_async_file_open_state_ == AsyncFileOpenState::kScheduled &&
|
||||
!shutting_down_.load(std::memory_order_acquire)) {
|
||||
bg_cv_.Wait();
|
||||
}
|
||||
}
|
||||
|
||||
// Will lock the mutex_, will wait for completion if wait is true
|
||||
void DBImpl::CancelAllBackgroundWork(bool wait) {
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
@@ -636,6 +650,105 @@ void DBImpl::UnregisterBlobDirectWriteColumnFamily() {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
Status DBImpl::MaybeWriteWalMarkersToManifestOnClose() {
|
||||
mutex_.AssertHeld();
|
||||
if (!mutable_db_options_.optimize_manifest_for_recovery ||
|
||||
!opened_successfully_ || versions_ == nullptr || logs_.empty()) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT("DBImpl::CloseHelper:WriteWalMarkersOnCloseEntered");
|
||||
const ReadOptions read_options(Env::IOActivity::kUnknown);
|
||||
const WriteOptions write_options(Env::IOActivity::kUnknown);
|
||||
uint64_t max_wal_number = logs_.back().number;
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::CloseHelper:CapturedMaxWal",
|
||||
&max_wal_number);
|
||||
const uint64_t new_log_num = max_wal_number + 1;
|
||||
|
||||
// std::deque keeps VersionEdit pointers stable across emplace_back.
|
||||
std::deque<VersionEdit> edits;
|
||||
autovector<ColumnFamilyData*> cfds;
|
||||
autovector<autovector<VersionEdit*>> edit_lists;
|
||||
VersionEdit global_edit;
|
||||
bool persist_global_edit = false;
|
||||
bool reserved_log_number = false;
|
||||
|
||||
bool all_cfs_empty = true;
|
||||
for (auto* cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (cfd->IsDropped()) {
|
||||
continue;
|
||||
}
|
||||
// cfd->IsEmpty() checks both mem and imm. An immutable memtable queued for
|
||||
// flush still pins an older WAL; advancing past it would drop unflushed
|
||||
// records on the next open.
|
||||
if (!cfd->IsEmpty()) {
|
||||
all_cfs_empty = false;
|
||||
continue;
|
||||
}
|
||||
if (new_log_num > cfd->GetLogNumber()) {
|
||||
if (!reserved_log_number) {
|
||||
const uint64_t next_file_number = versions_->current_next_file_number();
|
||||
if (next_file_number <= new_log_num) {
|
||||
versions_->FetchAddFileNumber(new_log_num + 1 - next_file_number);
|
||||
global_edit.SetNextFile(versions_->current_next_file_number());
|
||||
persist_global_edit = true;
|
||||
}
|
||||
reserved_log_number = true;
|
||||
}
|
||||
edits.emplace_back();
|
||||
VersionEdit& edit = edits.back();
|
||||
edit.SetLogNumber(new_log_num);
|
||||
edit.SetColumnFamily(cfd->GetID());
|
||||
cfds.push_back(cfd);
|
||||
edit_lists.emplace_back();
|
||||
edit_lists.back().push_back(&edit);
|
||||
}
|
||||
}
|
||||
|
||||
if (all_cfs_empty && !allow_2pc()) {
|
||||
if (new_log_num > versions_->min_log_number_to_keep()) {
|
||||
global_edit.SetMinLogNumberToKeep(new_log_num);
|
||||
persist_global_edit = true;
|
||||
}
|
||||
// Mirror the monotonicity guard in db_impl_files.cc:
|
||||
// PrecomputeWalDeletionEdit emits DeleteWalsBefore only when the
|
||||
// marker actually advances the WalSet's existing min. Skipping
|
||||
// this guard would let a stale-replay edit regress WalSet state.
|
||||
if (immutable_db_options_.track_and_verify_wals_in_manifest &&
|
||||
new_log_num > versions_->GetWalSet().GetMinWalNumberToKeep()) {
|
||||
global_edit.DeleteWalsBefore(new_log_num);
|
||||
persist_global_edit = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (persist_global_edit) {
|
||||
ColumnFamilyData* default_cfd =
|
||||
versions_->GetColumnFamilySet()->GetDefault();
|
||||
if (default_cfd && !default_cfd->IsDropped()) {
|
||||
bool attached_to_existing = false;
|
||||
for (size_t i = 0; i < cfds.size(); ++i) {
|
||||
if (cfds[i] == default_cfd) {
|
||||
edit_lists[i].push_back(&global_edit);
|
||||
attached_to_existing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!attached_to_existing) {
|
||||
cfds.push_back(default_cfd);
|
||||
edit_lists.emplace_back();
|
||||
edit_lists.back().push_back(&global_edit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cfds.empty()) {
|
||||
return Status::OK();
|
||||
}
|
||||
return versions_->LogAndApply(cfds, read_options, write_options, edit_lists,
|
||||
&mutex_, directories_.GetDbDir(),
|
||||
/*new_descriptor_log=*/false);
|
||||
}
|
||||
|
||||
Status DBImpl::CloseHelper() {
|
||||
// Guarantee that there is no background error recovery in progress before
|
||||
// continuing with the shutdown
|
||||
@@ -740,6 +853,17 @@ Status DBImpl::CloseHelper() {
|
||||
job_context.Clean();
|
||||
mutex_.Lock();
|
||||
}
|
||||
// Best-effort warm-reopen optimization: after obsolete-file cleanup and
|
||||
// before logs_.clear(), persist close-time WAL markers that can reduce
|
||||
// recovery work on the next open. This matters most when some CFs have no
|
||||
// unflushed writes and/or the newest WAL is empty, since recovery would
|
||||
// otherwise append marker-only MANIFEST edits on reopen.
|
||||
Status manifest_s = MaybeWriteWalMarkersToManifestOnClose();
|
||||
if (!manifest_s.ok()) {
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"optimize_manifest_for_recovery close-time write failed: %s",
|
||||
manifest_s.ToString().c_str());
|
||||
}
|
||||
{
|
||||
InstrumentedMutexLock lock(&wal_write_mutex_);
|
||||
for (auto l : wals_to_free_) {
|
||||
@@ -2748,6 +2872,148 @@ Status DBImpl::ResolveDirectWriteWideColumns(const ReadOptions& read_options,
|
||||
return status;
|
||||
}
|
||||
|
||||
bool DBImpl::MaybeResolveMemtableBlobValue(const Slice& key,
|
||||
const BlobFetcher* blob_fetcher,
|
||||
PinnableSlice* value,
|
||||
PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index,
|
||||
bool* value_found) {
|
||||
if (!s->ok() || (!value && !columns)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto reset_outputs = [&]() {
|
||||
if (value != nullptr) {
|
||||
value->Reset();
|
||||
}
|
||||
if (columns != nullptr) {
|
||||
columns->Reset();
|
||||
}
|
||||
};
|
||||
auto clear_blob_state = [&]() {
|
||||
if (is_blob_index != nullptr) {
|
||||
*is_blob_index = false;
|
||||
}
|
||||
};
|
||||
|
||||
const bool needs_plain_value_resolution =
|
||||
is_blob_index != nullptr && *is_blob_index;
|
||||
const bool needs_wide_column_resolution =
|
||||
columns != nullptr && !columns->unresolved_blob_column_indices_.empty();
|
||||
if (!needs_plain_value_resolution && !needs_wide_column_resolution) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (blob_fetcher == nullptr) {
|
||||
reset_outputs();
|
||||
*s = Status::NotSupported(
|
||||
"Encountered blob-backed memtable value without blob fetcher.");
|
||||
clear_blob_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (needs_plain_value_resolution) {
|
||||
Slice blob_index_slice;
|
||||
std::string blob_index_storage;
|
||||
if (value != nullptr) {
|
||||
if (value->size() > 0) {
|
||||
blob_index_slice = Slice(value->data(), value->size());
|
||||
} else {
|
||||
blob_index_slice = Slice(*(value->GetSelf()));
|
||||
}
|
||||
} else {
|
||||
assert(columns != nullptr);
|
||||
|
||||
const WideColumns& plain_value_columns = columns->columns();
|
||||
assert(plain_value_columns.size() == 1);
|
||||
assert(plain_value_columns.front().name() == kDefaultWideColumnName);
|
||||
blob_index_slice = plain_value_columns.front().value();
|
||||
}
|
||||
|
||||
PinnableSlice resolved_value;
|
||||
PinnableSlice* target = value != nullptr ? value : &resolved_value;
|
||||
if (value != nullptr) {
|
||||
// BlobIndex::DecodeFrom can retain Slices into the encoded bytes for
|
||||
// inlined blob indices, so take an owned copy before resetting `value`
|
||||
// and reusing the same PinnableSlice as the output target.
|
||||
blob_index_storage.assign(blob_index_slice.data(),
|
||||
blob_index_slice.size());
|
||||
blob_index_slice = Slice(blob_index_storage);
|
||||
value->Reset();
|
||||
}
|
||||
|
||||
*s = blob_fetcher->FetchBlob(key, blob_index_slice,
|
||||
nullptr /* prefetch_buffer */, target,
|
||||
nullptr /* bytes_read */);
|
||||
if (s->ok() && columns != nullptr) {
|
||||
columns->SetPlainValue(std::move(*target));
|
||||
} else if (!s->ok()) {
|
||||
reset_outputs();
|
||||
if (s->IsIncomplete() && value_found != nullptr) {
|
||||
*value_found = false;
|
||||
}
|
||||
}
|
||||
|
||||
clear_blob_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
assert(columns != nullptr);
|
||||
|
||||
std::string resolved_entity;
|
||||
bool resolved = false;
|
||||
*s = WideColumnSerialization::ResolveEntityBlobColumns(
|
||||
columns->value_, key, blob_fetcher, nullptr /* prefetch_buffers */,
|
||||
resolved_entity, resolved, nullptr /* total_bytes_read */,
|
||||
nullptr /* num_blobs_resolved */);
|
||||
if (s->ok()) {
|
||||
assert(resolved);
|
||||
if (resolved) {
|
||||
*s = columns->SetWideColumnValue(std::move(resolved_entity));
|
||||
}
|
||||
}
|
||||
if (!s->ok()) {
|
||||
reset_outputs();
|
||||
if (s->IsIncomplete() && value_found != nullptr) {
|
||||
*value_found = false;
|
||||
}
|
||||
}
|
||||
|
||||
clear_blob_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DBImpl::PostprocessMemtableValueRead(
|
||||
const Slice& key, const std::string* timestamp,
|
||||
bool resolve_blob_backed_memtable_value,
|
||||
const BlobFetcher* memtable_blob_fetcher, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
|
||||
bool* value_found) {
|
||||
if (resolve_blob_backed_memtable_value) {
|
||||
std::string blob_lookup_key_storage;
|
||||
const bool value_resolved = MaybeResolveMemtableBlobValue(
|
||||
GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage),
|
||||
memtable_blob_fetcher, value, columns, s, is_blob_index, value_found);
|
||||
if (!value_resolved && value != nullptr && s->ok()) {
|
||||
value->PinSelf();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (s->ok()) {
|
||||
if (value != nullptr) {
|
||||
value->PinSelf();
|
||||
}
|
||||
} else {
|
||||
if (value != nullptr) {
|
||||
value->Reset();
|
||||
}
|
||||
if (columns != nullptr) {
|
||||
columns->Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool DBImpl::MaybeResolveDirectWriteValue(
|
||||
const ReadOptions& read_options, const Slice& key,
|
||||
bool resolve_direct_write_value, const Version* current,
|
||||
@@ -2963,6 +3229,14 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
|
||||
}
|
||||
const bool resolve_direct_write_value =
|
||||
partition_mgr != nullptr && (is_blob_ptr == &is_blob_index);
|
||||
std::optional<BlobFetcher> memtable_blob_fetcher;
|
||||
if (partition_mgr != nullptr) {
|
||||
memtable_blob_fetcher.emplace(sv->current, read_options,
|
||||
cfd->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/true);
|
||||
}
|
||||
const BlobFetcher* memtable_blob_fetcher_ptr =
|
||||
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
|
||||
std::string blob_lookup_key_storage;
|
||||
auto get_blob_lookup_key = [&]() -> Slice {
|
||||
return GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage);
|
||||
@@ -2990,19 +3264,21 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
|
||||
get_impl_options.columns, timestamp, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */,
|
||||
get_impl_options.callback, is_blob_ptr)) {
|
||||
get_impl_options.callback, is_blob_ptr,
|
||||
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
maybe_resolve_memtable_value();
|
||||
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
sv->imm->Get(
|
||||
lkey,
|
||||
get_impl_options.value ? get_impl_options.value->GetSelf()
|
||||
: nullptr,
|
||||
get_impl_options.columns, timestamp, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
get_impl_options.callback, is_blob_ptr)) {
|
||||
sv->imm->Get(lkey,
|
||||
get_impl_options.value
|
||||
? get_impl_options.value->GetSelf()
|
||||
: nullptr,
|
||||
get_impl_options.columns, timestamp, &s,
|
||||
&merge_context, &max_covering_tombstone_seq,
|
||||
read_options, get_impl_options.callback,
|
||||
is_blob_ptr, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
maybe_resolve_memtable_value();
|
||||
|
||||
@@ -3014,14 +3290,14 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
|
||||
if (sv->mem->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr,
|
||||
/*timestamp=*/nullptr, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */, nullptr, nullptr,
|
||||
false)) {
|
||||
false /* immutable_memtable */, nullptr, nullptr, false,
|
||||
memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
sv->imm->GetMergeOperands(lkey, &s, &merge_context,
|
||||
&max_covering_tombstone_seq,
|
||||
read_options)) {
|
||||
sv->imm->GetMergeOperands(
|
||||
lkey, &s, &merge_context, &max_covering_tombstone_seq,
|
||||
read_options, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
}
|
||||
@@ -3717,6 +3993,14 @@ Status DBImpl::MultiGetImpl(
|
||||
(*sorted_keys)[start_key]->column_family);
|
||||
ColumnFamilyData* cfd = cfh->cfd();
|
||||
auto* partition_mgr = cfd->blob_partition_manager();
|
||||
std::optional<BlobFetcher> memtable_blob_fetcher;
|
||||
if (partition_mgr != nullptr) {
|
||||
memtable_blob_fetcher.emplace(super_version->current, read_options,
|
||||
cfd->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/true);
|
||||
}
|
||||
const BlobFetcher* memtable_blob_fetcher_ptr =
|
||||
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
for (size_t i = start_key; i < start_key + num_keys; ++i) {
|
||||
@@ -3763,9 +4047,11 @@ Status DBImpl::MultiGetImpl(
|
||||
has_unpersisted_data_.load(std::memory_order_relaxed));
|
||||
if (!skip_memtable) {
|
||||
super_version->mem->MultiGet(read_options, &range, callback,
|
||||
false /* immutable_memtable */);
|
||||
false /* immutable_memtable */,
|
||||
memtable_blob_fetcher_ptr);
|
||||
if (!range.empty()) {
|
||||
super_version->imm->MultiGet(read_options, &range, callback);
|
||||
super_version->imm->MultiGet(read_options, &range, callback,
|
||||
memtable_blob_fetcher_ptr);
|
||||
}
|
||||
if (!range.empty()) {
|
||||
uint64_t left = range.KeysLeft();
|
||||
@@ -4142,9 +4428,21 @@ Status DBImpl::CreateColumnFamilyImpl(const ReadOptions& read_options,
|
||||
const std::string& column_family_name,
|
||||
ColumnFamilyHandle** handle) {
|
||||
options_mutex_.AssertHeld();
|
||||
Status s;
|
||||
*handle = nullptr;
|
||||
|
||||
// The empty string is reserved as a "no/unknown column family name" sentinel
|
||||
// in various APIs and serialization formats (e.g.
|
||||
// TablePropertiesCollectorFactory::Context::kUnknownColumnFamily, table
|
||||
// properties), so reject it to avoid ambiguity. Note: prior to this check,
|
||||
// CreateColumnFamily("") would silently "succeed" but the CF was not
|
||||
// persisted in the manifest, causing data loss across DB reopens.
|
||||
if (column_family_name.empty()) {
|
||||
return Status::InvalidArgument(
|
||||
"Column family name cannot be the empty string");
|
||||
}
|
||||
|
||||
Status s;
|
||||
|
||||
DBOptions db_options =
|
||||
BuildDBOptions(immutable_db_options_, mutable_db_options_);
|
||||
s = ColumnFamilyData::ValidateOptions(db_options, cf_options);
|
||||
@@ -6200,12 +6498,20 @@ Status DBImpl::GetLatestSequenceForKey(
|
||||
|
||||
*seq = kMaxSequenceNumber;
|
||||
*found_record_for_key = false;
|
||||
std::optional<BlobFetcher> memtable_blob_fetcher;
|
||||
if (cfd->blob_partition_manager() != nullptr) {
|
||||
memtable_blob_fetcher.emplace(sv->current, read_options,
|
||||
cfd->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/true);
|
||||
}
|
||||
const BlobFetcher* memtable_blob_fetcher_ptr =
|
||||
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
|
||||
|
||||
// Check if there is a record for this key in the latest memtable
|
||||
sv->mem->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr, timestamp, &s,
|
||||
&merge_context, &max_covering_tombstone_seq, seq, read_options,
|
||||
false /* immutable_memtable */, nullptr /*read_callback*/,
|
||||
is_blob_index);
|
||||
is_blob_index, /*do_merge=*/true, memtable_blob_fetcher_ptr);
|
||||
|
||||
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
|
||||
// unexpected error reading memtable.
|
||||
@@ -6238,7 +6544,8 @@ Status DBImpl::GetLatestSequenceForKey(
|
||||
// Check if there is a record for this key in the immutable memtables
|
||||
sv->imm->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr, timestamp, &s,
|
||||
&merge_context, &max_covering_tombstone_seq, seq, read_options,
|
||||
nullptr /*read_callback*/, is_blob_index);
|
||||
nullptr /*read_callback*/, is_blob_index,
|
||||
memtable_blob_fetcher_ptr);
|
||||
|
||||
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
|
||||
// unexpected error reading memtable.
|
||||
@@ -6271,7 +6578,7 @@ Status DBImpl::GetLatestSequenceForKey(
|
||||
sv->imm->GetFromHistory(lkey, /*value=*/nullptr, /*columns=*/nullptr,
|
||||
timestamp, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, seq, read_options,
|
||||
is_blob_index);
|
||||
is_blob_index, memtable_blob_fetcher_ptr);
|
||||
|
||||
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
|
||||
// unexpected error reading memtable.
|
||||
@@ -7273,6 +7580,7 @@ Status DBImpl::ReserveFileNumbersBeforeIngestion(
|
||||
Status DBImpl::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
|
||||
if (mutable_db_options_.max_open_files == -1) {
|
||||
uint64_t oldest_time = std::numeric_limits<uint64_t>::max();
|
||||
std::once_flag waited_for_async_open;
|
||||
for (auto cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (!cfd->IsDropped()) {
|
||||
uint64_t ctime;
|
||||
@@ -7280,6 +7588,17 @@ Status DBImpl::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
|
||||
SuperVersion* sv = GetAndRefSuperVersion(cfd);
|
||||
Version* version = sv->current;
|
||||
version->GetCreationTimeOfOldestFile(&ctime);
|
||||
// For modern DBs, manifest carries file_creation_time and the
|
||||
// first call returns the real value. We only need to wait for
|
||||
// BGWorkAsyncFileOpen on legacy DBs whose manifest lacks
|
||||
// file_creation_time -- those rely on the pinned reader, which is
|
||||
// null until async file open completes.
|
||||
if (ctime == 0 && immutable_db_options_.open_files_async) {
|
||||
std::call_once(waited_for_async_open, [&]() {
|
||||
WaitForAsyncFileOpen();
|
||||
version->GetCreationTimeOfOldestFile(&ctime);
|
||||
});
|
||||
}
|
||||
ReturnAndCleanupSuperVersion(cfd, sv);
|
||||
}
|
||||
|
||||
|
||||
@@ -1096,6 +1096,7 @@ class DBImpl : public DB {
|
||||
superversions_to_free_queue_.push_back(sv);
|
||||
}
|
||||
|
||||
void EnableTrackPublishedSeqInSnapshotContext();
|
||||
void SetSnapshotChecker(SnapshotChecker* snapshot_checker);
|
||||
|
||||
// Fill JobContext with snapshot information needed by flush and compaction.
|
||||
@@ -1854,6 +1855,11 @@ class DBImpl : public DB {
|
||||
// Background work function for async file opening.
|
||||
static void BGWorkAsyncFileOpen(void* arg);
|
||||
|
||||
// Block the until any in-flight async file open work has
|
||||
// completed. No-op when open_files_async is false. Returns early if
|
||||
// shutdown begins.
|
||||
void WaitForAsyncFileOpen();
|
||||
|
||||
void InvokeWalFilterIfNeededOnColumnFamilyToWalNumberMap();
|
||||
|
||||
// Return true to proceed with current WAL record whose content is stored in
|
||||
@@ -1869,6 +1875,7 @@ class DBImpl : public DB {
|
||||
|
||||
private:
|
||||
friend class DB;
|
||||
friend class DBImplReadOnly;
|
||||
friend class DBImplSecondary;
|
||||
friend class ErrorHandler;
|
||||
friend class InternalStats;
|
||||
@@ -2743,6 +2750,8 @@ class DBImpl : public DB {
|
||||
|
||||
Status MaybeReleaseTimestampedSnapshotsAndCheck();
|
||||
|
||||
Status MaybeWriteWalMarkersToManifestOnClose();
|
||||
|
||||
Status CloseHelper();
|
||||
|
||||
void WaitForBackgroundWork();
|
||||
@@ -2944,6 +2953,29 @@ class DBImpl : public DB {
|
||||
bool resolve_direct_write_value, const Version* current,
|
||||
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index, bool* value_found = nullptr);
|
||||
// Resolves memtable read results that still carry blob references through
|
||||
// either a raw blob-index payload in `value` or unresolved blob columns in
|
||||
// `columns`. Unlike the direct-write helper above, this path only depends on
|
||||
// a BlobFetcher and therefore works for read-only/secondary DBs.
|
||||
static bool MaybeResolveMemtableBlobValue(const Slice& key,
|
||||
const BlobFetcher* blob_fetcher,
|
||||
PinnableSlice* value,
|
||||
PinnableWideColumns* columns,
|
||||
Status* s, bool* is_blob_index,
|
||||
bool* value_found = nullptr);
|
||||
// Completes read-only/secondary memtable Get()/GetEntity() hits by resolving
|
||||
// blob-backed payloads when `resolve_blob_backed_memtable_value` is true,
|
||||
// pinning plain values on success, and clearing outputs on error. When the
|
||||
// caller explicitly requested raw blob indices via
|
||||
// `GetImplOptions::is_blob_index`, this helper leaves that payload
|
||||
// untouched. `memtable_blob_fetcher` may be null when blob support is
|
||||
// disabled for the column family.
|
||||
static void PostprocessMemtableValueRead(
|
||||
const Slice& key, const std::string* timestamp,
|
||||
bool resolve_blob_backed_memtable_value,
|
||||
const BlobFetcher* memtable_blob_fetcher, PinnableSlice* value,
|
||||
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
|
||||
bool* value_found = nullptr);
|
||||
|
||||
template <typename IterType, typename ImplType,
|
||||
typename ErrorIteratorFuncType>
|
||||
@@ -3348,6 +3380,13 @@ class DBImpl : public DB {
|
||||
// Callback for compaction to check if a key is visible to a snapshot.
|
||||
// REQUIRES: mutex held
|
||||
std::unique_ptr<SnapshotChecker> snapshot_checker_;
|
||||
// When set, InitSnapshotContext() appends GetLastPublishedSequence() to the
|
||||
// job's snapshot_seqs (if not already present) so that flush/compaction
|
||||
// preserves the published-sequence boundary even when no explicit user
|
||||
// snapshot exists there. Unlike taking a real ManagedSnapshot, this just
|
||||
// pins the seqno into the snapshot list: no allocation, no snapshot list
|
||||
// mutation, and no SnapshotChecker.
|
||||
bool track_published_seq_in_snapshot_context_ = false;
|
||||
|
||||
// Callback for when the cached_recoverable_state_ is written to memtable
|
||||
// Only to be set during initialization
|
||||
|
||||
@@ -874,6 +874,24 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
|
||||
directories_.GetDbDir(), log_buffer);
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
// If the atomic flush's combined MANIFEST write failed, the output files
|
||||
// are cached in the table cache (added by BuildTable during FlushJob::Run)
|
||||
// but never installed into any Version. Evict them now so that
|
||||
// TEST_VerifyNoObsoleteFilesCached does not fire during Close() when the
|
||||
// FindObsoleteFiles full-scan backstop is disabled by
|
||||
// metadata_read_fault_one_in.
|
||||
for (int i = 0; i != num_cfs; ++i) {
|
||||
if (exec_status[i].first && exec_status[i].second.ok() &&
|
||||
!switched_to_mempurge[i] && file_meta[i].fd.GetFileSize() > 0) {
|
||||
TableCache::ReleaseObsolete(
|
||||
cfds[i]->table_cache()->get_cache().get(),
|
||||
file_meta[i].fd.GetNumber(), nullptr /*handle*/,
|
||||
all_mutable_cf_options[i].uncache_aggressiveness);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i != num_cfs; ++i) {
|
||||
auto* mgr = cfds[i]->blob_partition_manager();
|
||||
if (mgr == nullptr || prepared_blob_generations[i] == 0) {
|
||||
@@ -5114,6 +5132,11 @@ void DBImpl::MarkAsGrabbedForPurge(uint64_t file_number) {
|
||||
files_grabbed_for_purge_.insert(file_number);
|
||||
}
|
||||
|
||||
void DBImpl::EnableTrackPublishedSeqInSnapshotContext() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
track_published_seq_in_snapshot_context_ = true;
|
||||
}
|
||||
|
||||
void DBImpl::SetSnapshotChecker(SnapshotChecker* snapshot_checker) {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
// snapshot_checker_ should only set once. If we need to set it multiple
|
||||
@@ -5147,6 +5170,19 @@ void DBImpl::InitSnapshotContext(JobContext* job_context) {
|
||||
SequenceNumber earliest_write_conflict_snapshot = kMaxSequenceNumber;
|
||||
std::vector<SequenceNumber> snapshot_seqs =
|
||||
snapshots_.GetAll(&earliest_write_conflict_snapshot);
|
||||
|
||||
// The SnapshotChecker path above already adds a managed snapshot to
|
||||
// snapshot_seqs.
|
||||
if (track_published_seq_in_snapshot_context_ && !snapshot_checker) {
|
||||
// Pin the published-sequence boundary into snapshot_seqs without taking
|
||||
// a real snapshot. This prevents flush/compaction from collapsing a version
|
||||
// visible at the published boundary into a newer unpublished seqno.
|
||||
const SequenceNumber published_seq = GetLastPublishedSequence();
|
||||
if (snapshot_seqs.empty() || snapshot_seqs.back() < published_seq) {
|
||||
snapshot_seqs.push_back(published_seq);
|
||||
}
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::InitSnapshotContext:BeforeInit");
|
||||
job_context->InitSnapshotContext(
|
||||
snapshot_checker, std::move(managed_snapshot),
|
||||
earliest_write_conflict_snapshot, std::move(snapshot_seqs));
|
||||
|
||||
+36
-12
@@ -1186,8 +1186,9 @@ std::set<std::string> DBImpl::CollectAllDBPaths() {
|
||||
|
||||
Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
mutex_.AssertHeld();
|
||||
uint64_t next_file_number = versions_->current_next_file_number();
|
||||
uint64_t largest_file_number = next_file_number;
|
||||
const uint64_t prev_next_file_number = versions_->current_next_file_number();
|
||||
uint64_t largest_file_number = prev_next_file_number;
|
||||
bool on_disk_file_advanced_counter = false;
|
||||
Status s;
|
||||
for (const auto& path : CollectAllDBPaths()) {
|
||||
std::vector<std::string> files;
|
||||
@@ -1201,7 +1202,15 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
if (!ParseFileName(fname, &number, &type)) {
|
||||
continue;
|
||||
}
|
||||
const std::string normalized_fpath = path + kFilePathSeparator + fname;
|
||||
std::string normalized_fpath = path;
|
||||
normalized_fpath += kFilePathSeparator;
|
||||
normalized_fpath.append(fname);
|
||||
// Use >= so a crashed-mid-allocation file at exactly
|
||||
// prev_next_file_number triggers the advance -- otherwise the next
|
||||
// NewFileNumber() would collide with it.
|
||||
if (number >= prev_next_file_number) {
|
||||
on_disk_file_advanced_counter = true;
|
||||
}
|
||||
largest_file_number = std::max(largest_file_number, number);
|
||||
if ((type == kTableFile || type == kBlobFile)) {
|
||||
recovery_ctx->existing_data_files_.push_back(normalized_fpath);
|
||||
@@ -1212,16 +1221,31 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (largest_file_number >= next_file_number) {
|
||||
versions_->next_file_number_.store(largest_file_number + 1);
|
||||
}
|
||||
// Legacy bug: initializing largest_file_number from prev_next_file_number
|
||||
// made the post-loop check trivially true and emitted a noop SetNextFile
|
||||
// on every recovery.
|
||||
// Preserved verbatim by default (some tests pin file numbers); skipped
|
||||
// only when the option is on AND we're not in salvage mode (which
|
||||
// requires the MANIFEST rewrite this emit triggers).
|
||||
const bool optimize_manifest_for_recovery =
|
||||
mutable_db_options_.optimize_manifest_for_recovery &&
|
||||
!immutable_db_options_.best_efforts_recovery;
|
||||
|
||||
VersionEdit edit;
|
||||
edit.SetNextFile(versions_->next_file_number_.load());
|
||||
assert(versions_->GetColumnFamilySet());
|
||||
ColumnFamilyData* default_cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
assert(default_cfd);
|
||||
recovery_ctx->UpdateVersionEdits(default_cfd, edit);
|
||||
if (!optimize_manifest_for_recovery || on_disk_file_advanced_counter) {
|
||||
if (largest_file_number >= prev_next_file_number) {
|
||||
versions_->next_file_number_.store(largest_file_number + 1);
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::MaybeUpdateNextFileNumber:EmitEdit");
|
||||
VersionEdit edit;
|
||||
edit.SetNextFile(versions_->next_file_number_.load());
|
||||
assert(versions_->GetColumnFamilySet());
|
||||
ColumnFamilyData* default_cfd =
|
||||
versions_->GetColumnFamilySet()->GetDefault();
|
||||
assert(default_cfd);
|
||||
recovery_ctx->UpdateVersionEdits(default_cfd, edit);
|
||||
} else {
|
||||
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber");
|
||||
}
|
||||
return s;
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -295,7 +295,8 @@ Status DB::OpenAsFollower(
|
||||
impl->versions_.reset(new ReactiveVersionSet(
|
||||
dbname, &impl->immutable_db_options_, impl->mutable_db_options_,
|
||||
impl->file_options_, impl->table_cache_.get(),
|
||||
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_));
|
||||
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_,
|
||||
impl->db_id_, impl->db_session_id_));
|
||||
impl->column_family_memtables_.reset(
|
||||
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
|
||||
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
|
||||
|
||||
+51
-11
@@ -680,8 +680,18 @@ Status DBImpl::Recover(
|
||||
} else if (immutable_db_options_.write_dbid_to_manifest && recovery_ctx) {
|
||||
VersionEdit edit;
|
||||
s = SetupDBId(write_options, read_only, is_new_db, is_retry, &edit);
|
||||
recovery_ctx->UpdateVersionEdits(
|
||||
versions_->GetColumnFamilySet()->GetDefault(), edit);
|
||||
// best_efforts_recovery rebuilds CURRENT/MANIFEST as the side-effect
|
||||
// of LogAndApplyForRecovery emitting an edit, so the optimization is
|
||||
// disabled there (see optimize_manifest_for_recovery doc).
|
||||
const bool optimize_manifest_for_recovery =
|
||||
mutable_db_options_.optimize_manifest_for_recovery &&
|
||||
!immutable_db_options_.best_efforts_recovery;
|
||||
if (!optimize_manifest_for_recovery || edit.HasDbId()) {
|
||||
recovery_ctx->UpdateVersionEdits(
|
||||
versions_->GetColumnFamilySet()->GetDefault(), edit);
|
||||
} else {
|
||||
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:SetupDBId");
|
||||
}
|
||||
} else {
|
||||
s = SetupDBId(write_options, read_only, is_new_db, is_retry, nullptr);
|
||||
}
|
||||
@@ -770,6 +780,10 @@ Status DBImpl::Recover(
|
||||
// otherwise, in the future, if WAL tracking is enabled again,
|
||||
// since the WALs deleted when WAL tracking is disabled are not persisted
|
||||
// into MANIFEST, WAL check may fail.
|
||||
//
|
||||
// Intentionally NOT gated by optimize_manifest_for_recovery:
|
||||
// this edit is safety-critical for a future re-enable of WAL tracking,
|
||||
// even though it can look noop-ish under the current session.
|
||||
VersionEdit edit;
|
||||
WalNumber max_wal_number =
|
||||
versions_->GetWalSet().GetWals().rbegin()->first;
|
||||
@@ -983,7 +997,11 @@ Status DBImpl::InitPersistStatsColumnFamily() {
|
||||
|
||||
Status DBImpl::LogAndApplyForRecovery(const RecoveryContext& recovery_ctx) {
|
||||
mutex_.AssertHeld();
|
||||
assert(versions_->descriptor_log_ == nullptr);
|
||||
// descriptor_log_ is normally null after Recover, but when
|
||||
// reuse_manifest_on_open is set VersionSet::Recover may have already
|
||||
// bound a log::Writer to the existing MANIFEST for append.
|
||||
assert(versions_->descriptor_log_ == nullptr ||
|
||||
immutable_db_options_.reuse_manifest_on_open);
|
||||
const ReadOptions read_options(Env::IOActivity::kDBOpen);
|
||||
const WriteOptions write_options(Env::IOActivity::kDBOpen);
|
||||
|
||||
@@ -1870,25 +1888,47 @@ Status DBImpl::MaybeFlushFinalMemtableOrRestoreActiveLogFiles(
|
||||
versions_->MarkFileNumberUsed(max_wal_number + 1);
|
||||
assert(recovery_ctx != nullptr);
|
||||
|
||||
const bool optimize_manifest_for_recovery =
|
||||
mutable_db_options_.optimize_manifest_for_recovery &&
|
||||
!immutable_db_options_.best_efforts_recovery;
|
||||
for (auto* cfd : *versions_->GetColumnFamilySet()) {
|
||||
auto iter = version_edits->find(cfd->GetID());
|
||||
assert(iter != version_edits->end());
|
||||
recovery_ctx->UpdateVersionEdits(cfd, iter->second);
|
||||
const VersionEdit& cf_edit = iter->second;
|
||||
if (!optimize_manifest_for_recovery ||
|
||||
cf_edit.ShouldEmitPerColumnFamilyRecoveryEdit(
|
||||
cfd->GetLogNumber())) {
|
||||
recovery_ctx->UpdateVersionEdits(cfd, cf_edit);
|
||||
} else {
|
||||
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:PerCF");
|
||||
}
|
||||
}
|
||||
|
||||
if (flushed || !data_seen) {
|
||||
const uint64_t new_min_log = max_wal_number + 1;
|
||||
VersionEdit wal_deletion;
|
||||
bool emit_wal_deletion = false;
|
||||
if (immutable_db_options_.track_and_verify_wals_in_manifest) {
|
||||
wal_deletion.DeleteWalsBefore(max_wal_number + 1);
|
||||
// Determining whether DeleteWalsBefore actually shrinks WalSet
|
||||
// membership requires WalSet state outside this site, so emit
|
||||
// unconditionally (pre-existing behavior).
|
||||
wal_deletion.DeleteWalsBefore(new_min_log);
|
||||
emit_wal_deletion = true;
|
||||
}
|
||||
if (!allow_2pc()) {
|
||||
// In non-2pc mode, flushing the memtables of the column families
|
||||
// means we can advance min_log_number_to_keep.
|
||||
wal_deletion.SetMinLogNumberToKeep(max_wal_number + 1);
|
||||
const bool min_log_advances =
|
||||
new_min_log > versions_->min_log_number_to_keep();
|
||||
if (!allow_2pc() &&
|
||||
(!optimize_manifest_for_recovery || min_log_advances)) {
|
||||
wal_deletion.SetMinLogNumberToKeep(new_min_log);
|
||||
emit_wal_deletion = true;
|
||||
}
|
||||
assert(versions_->GetColumnFamilySet() != nullptr);
|
||||
recovery_ctx->UpdateVersionEdits(
|
||||
versions_->GetColumnFamilySet()->GetDefault(), wal_deletion);
|
||||
if (!optimize_manifest_for_recovery || emit_wal_deletion) {
|
||||
recovery_ctx->UpdateVersionEdits(
|
||||
versions_->GetColumnFamilySet()->GetDefault(), wal_deletion);
|
||||
} else {
|
||||
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:WalDeletion");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
|
||||
#include "db/db_impl/db_impl_readonly.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "db/arena_wrapped_db_iter.h"
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/db_impl/compacted_db_impl.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/manifest_ops.h"
|
||||
@@ -63,13 +66,25 @@ Status DBImplReadOnly::GetImpl(const ReadOptions& read_options,
|
||||
|
||||
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
std::string* ts =
|
||||
ucmp->timestamp_size() > 0 ? get_impl_options.timestamp : nullptr;
|
||||
SequenceNumber snapshot = versions_->LastSequence();
|
||||
GetWithTimestampReadCallback read_cb(snapshot);
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
get_impl_options.column_family);
|
||||
auto cfd = cfh->cfd();
|
||||
bool is_blob_index = false;
|
||||
bool* is_blob_ptr = get_impl_options.is_blob_index;
|
||||
std::string timestamp_storage;
|
||||
std::string* ts = nullptr;
|
||||
if (ucmp->timestamp_size() > 0) {
|
||||
ts = get_impl_options.timestamp != nullptr
|
||||
? get_impl_options.timestamp
|
||||
: (get_impl_options.get_value ? ×tamp_storage : nullptr);
|
||||
}
|
||||
if (!is_blob_ptr && get_impl_options.get_value) {
|
||||
is_blob_ptr = &is_blob_index;
|
||||
}
|
||||
const bool resolve_blob_backed_memtable_value =
|
||||
get_impl_options.get_value && (is_blob_ptr == &is_blob_index);
|
||||
if (tracer_) {
|
||||
InstrumentedMutexLock lock(&trace_mutex_);
|
||||
if (tracer_) {
|
||||
@@ -95,6 +110,18 @@ Status DBImplReadOnly::GetImpl(const ReadOptions& read_options,
|
||||
SequenceNumber max_covering_tombstone_seq = 0;
|
||||
LookupKey lkey(key, snapshot, read_options.timestamp);
|
||||
PERF_TIMER_STOP(get_snapshot_time);
|
||||
std::optional<BlobFetcher> memtable_blob_fetcher;
|
||||
if (cfd->ioptions().enable_blob_direct_write ||
|
||||
cfd->GetLatestMutableCFOptions().enable_blob_files) {
|
||||
// Recovered memtables can still contain older blob references after
|
||||
// mutable blob-file settings change, so keep blob resolution available
|
||||
// whenever either blob knob indicates it may be needed.
|
||||
memtable_blob_fetcher.emplace(super_version->current, read_options,
|
||||
cfd->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/true);
|
||||
}
|
||||
const BlobFetcher* memtable_blob_fetcher_ptr =
|
||||
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
|
||||
|
||||
// Look up starts here
|
||||
if (super_version->mem->Get(
|
||||
@@ -102,11 +129,12 @@ Status DBImplReadOnly::GetImpl(const ReadOptions& read_options,
|
||||
get_impl_options.value ? get_impl_options.value->GetSelf() : nullptr,
|
||||
get_impl_options.columns, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */, &read_cb,
|
||||
/*is_blob_index=*/nullptr, /*do_merge=*/get_impl_options.get_value)) {
|
||||
if (get_impl_options.value) {
|
||||
get_impl_options.value->PinSelf();
|
||||
}
|
||||
false /* immutable_memtable */, &read_cb, is_blob_ptr,
|
||||
/*do_merge=*/get_impl_options.get_value, memtable_blob_fetcher_ptr)) {
|
||||
DBImpl::PostprocessMemtableValueRead(
|
||||
key, ts, resolve_blob_backed_memtable_value, memtable_blob_fetcher_ptr,
|
||||
get_impl_options.value, get_impl_options.columns, &s, &is_blob_index,
|
||||
get_impl_options.value_found);
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else {
|
||||
PERF_TIMER_GUARD(get_from_output_files_time);
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
#include "db/db_impl/db_impl_secondary.h"
|
||||
|
||||
#include <cinttypes>
|
||||
#include <optional>
|
||||
|
||||
#include "db/arena_wrapped_db_iter.h"
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/log_reader.h"
|
||||
#include "db/log_writer.h"
|
||||
#include "db/merge_context.h"
|
||||
@@ -363,13 +365,25 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
||||
|
||||
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
std::string* ts =
|
||||
ucmp->timestamp_size() > 0 ? get_impl_options.timestamp : nullptr;
|
||||
SequenceNumber snapshot = versions_->LastSequence();
|
||||
GetWithTimestampReadCallback read_cb(snapshot);
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
get_impl_options.column_family);
|
||||
auto cfd = cfh->cfd();
|
||||
bool is_blob_index = false;
|
||||
bool* is_blob_ptr = get_impl_options.is_blob_index;
|
||||
std::string timestamp_storage;
|
||||
std::string* ts = nullptr;
|
||||
if (ucmp->timestamp_size() > 0) {
|
||||
ts = get_impl_options.timestamp != nullptr
|
||||
? get_impl_options.timestamp
|
||||
: (get_impl_options.get_value ? ×tamp_storage : nullptr);
|
||||
}
|
||||
if (!is_blob_ptr && get_impl_options.get_value) {
|
||||
is_blob_ptr = &is_blob_index;
|
||||
}
|
||||
const bool resolve_blob_backed_memtable_value =
|
||||
get_impl_options.get_value && (is_blob_ptr == &is_blob_index);
|
||||
if (tracer_) {
|
||||
InstrumentedMutexLock lock(&trace_mutex_);
|
||||
if (tracer_) {
|
||||
@@ -395,6 +409,18 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
||||
LookupKey lkey(key, snapshot, read_options.timestamp);
|
||||
PERF_TIMER_STOP(get_snapshot_time);
|
||||
bool done = false;
|
||||
std::optional<BlobFetcher> memtable_blob_fetcher;
|
||||
if (cfd->ioptions().enable_blob_direct_write ||
|
||||
cfd->GetLatestMutableCFOptions().enable_blob_files) {
|
||||
// Catch-up can rebuild older blob references into memtables after mutable
|
||||
// blob-file settings change, so keep blob resolution available whenever
|
||||
// either blob knob indicates it may be needed.
|
||||
memtable_blob_fetcher.emplace(super_version->current, read_options,
|
||||
cfd->blob_file_cache(),
|
||||
/*allow_write_path_fallback=*/true);
|
||||
}
|
||||
const BlobFetcher* memtable_blob_fetcher_ptr =
|
||||
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
|
||||
|
||||
// Look up starts here
|
||||
if (get_impl_options.get_value) {
|
||||
@@ -404,12 +430,14 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
||||
: nullptr,
|
||||
get_impl_options.columns, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */, &read_cb,
|
||||
/*is_blob_index=*/nullptr, /*do_merge=*/true)) {
|
||||
false /* immutable_memtable */, &read_cb, is_blob_ptr,
|
||||
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
if (get_impl_options.value) {
|
||||
get_impl_options.value->PinSelf();
|
||||
}
|
||||
DBImpl::PostprocessMemtableValueRead(
|
||||
key, ts, resolve_blob_backed_memtable_value,
|
||||
memtable_blob_fetcher_ptr, get_impl_options.value,
|
||||
get_impl_options.columns, &s, &is_blob_index,
|
||||
get_impl_options.value_found);
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
super_version->imm->Get(
|
||||
@@ -417,11 +445,14 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
||||
get_impl_options.value ? get_impl_options.value->GetSelf()
|
||||
: nullptr,
|
||||
get_impl_options.columns, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options, &read_cb)) {
|
||||
&max_covering_tombstone_seq, read_options, &read_cb,
|
||||
is_blob_ptr, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
if (get_impl_options.value) {
|
||||
get_impl_options.value->PinSelf();
|
||||
}
|
||||
DBImpl::PostprocessMemtableValueRead(
|
||||
key, ts, resolve_blob_backed_memtable_value,
|
||||
memtable_blob_fetcher_ptr, get_impl_options.value,
|
||||
get_impl_options.columns, &s, &is_blob_index,
|
||||
get_impl_options.value_found);
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
}
|
||||
} else {
|
||||
@@ -433,13 +464,14 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
||||
get_impl_options.columns, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options,
|
||||
false /* immutable_memtable */, &read_cb,
|
||||
/*is_blob_index=*/nullptr, /*do_merge=*/false)) {
|
||||
/*is_blob_index=*/nullptr, /*do_merge=*/false,
|
||||
memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
super_version->imm->GetMergeOperands(lkey, &s, &merge_context,
|
||||
&max_covering_tombstone_seq,
|
||||
read_options)) {
|
||||
super_version->imm->GetMergeOperands(
|
||||
lkey, &s, &merge_context, &max_covering_tombstone_seq,
|
||||
read_options, memtable_blob_fetcher_ptr)) {
|
||||
done = true;
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
}
|
||||
@@ -457,7 +489,7 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
||||
ts, &s, &merge_context, &max_covering_tombstone_seq, &pinned_iters_mgr,
|
||||
/*value_found*/ nullptr,
|
||||
/*key_exists*/ nullptr, /*seq*/ nullptr, &read_cb, /*is_blob*/ nullptr,
|
||||
/*do_merge*/ true);
|
||||
/*do_merge=*/get_impl_options.get_value);
|
||||
RecordTick(stats_, MEMTABLE_MISS);
|
||||
}
|
||||
{
|
||||
@@ -777,7 +809,8 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
|
||||
impl->versions_.reset(new ReactiveVersionSet(
|
||||
dbname, &impl->immutable_db_options_, impl->mutable_db_options_,
|
||||
impl->file_options_, impl->table_cache_.get(),
|
||||
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_));
|
||||
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_,
|
||||
impl->db_id_, impl->db_session_id_));
|
||||
impl->column_family_memtables_.reset(
|
||||
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
|
||||
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
|
||||
@@ -1553,7 +1586,7 @@ Status DB::OpenAndCompact(
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Open db As Secondary (skip WAL recovery — remote compaction only
|
||||
// 5. Open db As Secondary (skip WAL recovery -- remote compaction only
|
||||
// needs LSM state from MANIFEST, not memtable data from WAL replay)
|
||||
std::unique_ptr<DB> db;
|
||||
std::vector<ColumnFamilyHandle*> handles;
|
||||
|
||||
@@ -1546,6 +1546,10 @@ Status DBImpl::WriteImpl(
|
||||
// Note: if we are to resume after non-OK statuses we need to revisit how
|
||||
// we react to non-OK statuses here.
|
||||
if (w.status.ok()) { // Don't publish a partial batch write
|
||||
TEST_SYNC_POINT(
|
||||
"DBImpl::WriteImpl:AfterWBWIIngestBeforeSetLastSequence:pause");
|
||||
TEST_SYNC_POINT(
|
||||
"DBImpl::WriteImpl:AfterWBWIIngestBeforeSetLastSequence:resume");
|
||||
versions_->SetLastSequence(last_sequence);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -623,7 +623,7 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key) {
|
||||
PERF_COUNTER_ADD(internal_delete_skipped_count, 1);
|
||||
MarkMemtableForFlushForPerOpTrigger(mem_hidden_op_scanned);
|
||||
// Track contiguous tombstones for range conversion.
|
||||
// Skip if outside seek prefix — the top-of-loop check
|
||||
// Skip if outside seek prefix -- the top-of-loop check
|
||||
// flushed any pending run, but we must also avoid starting
|
||||
// a new run outside the prefix.
|
||||
if (min_tombstones_for_range_conversion_ > 0 &&
|
||||
@@ -1097,7 +1097,7 @@ void DBIter::PrevInternal() {
|
||||
if (min_tombstones_for_range_conversion_ > 0 &&
|
||||
range_tomb_end_key_.Size() > 0 && timestamp_lb_ == nullptr) {
|
||||
if (!valid_ && found_visible && PrefixCheck(saved_key_without_ts)) {
|
||||
// Key was deleted and is within the seek prefix — track it.
|
||||
// Key was deleted and is within the seek prefix -- track it.
|
||||
TrackContiguousTombstone(saved_key_.GetUserKey(),
|
||||
/*always_update_first_key=*/true);
|
||||
} else if (valid_) {
|
||||
@@ -1141,7 +1141,7 @@ void DBIter::PrevInternal() {
|
||||
// Sets ikey_ to the last visible entry's internal key. When found_visible
|
||||
// is false, ikey_ is not updated and may contain stale data.
|
||||
// Sets found_visible to true if at least one entry passed the IsVisible()
|
||||
// check (seqno <= snapshot). When false, no entry was visible — the key
|
||||
// check (seqno <= snapshot). When false, no entry was visible -- the key
|
||||
// does not exist at this snapshot and should not be treated as a tombstone.
|
||||
// Returns false if an error occurred, and !status().ok() and !valid_.
|
||||
//
|
||||
@@ -1196,7 +1196,7 @@ bool DBIter::FindValueForCurrentKey(bool& found_visible) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Entry survived the visibility check — at least one visible version
|
||||
// Entry survived the visibility check -- at least one visible version
|
||||
// exists for this user key.
|
||||
found_visible = true;
|
||||
|
||||
|
||||
+1
-1
@@ -589,7 +589,7 @@ class DBIter final : public Iterator {
|
||||
|
||||
// If enough contiguous tombstones have been tracked, insert a range
|
||||
// tombstone [first_key, end_key) into the mutable memtable.
|
||||
// end_key is the exclusive upper bound — typically the next live key.
|
||||
// end_key is the exclusive upper bound -- typically the next live key.
|
||||
void MaybeInsertRangeTombstone(const Slice& end_key);
|
||||
void ResetContiguousTombstoneTracking() {
|
||||
contiguous_tombstone_count_ = 0;
|
||||
|
||||
+22
-22
@@ -4151,7 +4151,7 @@ TEST_P(DBIteratorTest, PrefixSameAsStartSeekToNonInDomainKey) {
|
||||
ASSERT_OK(Put("abc2", "v2"));
|
||||
ASSERT_OK(Put("abc3", "v3"));
|
||||
|
||||
// Seek to "ab" (2 bytes) — out-of-domain for FixedPrefixTransform(3).
|
||||
// Seek to "ab" (2 bytes) -- out-of-domain for FixedPrefixTransform(3).
|
||||
// ShouldSetPrefix returns false for out-of-domain targets, so no prefix
|
||||
// constraint is set. The seek should find "abc1" and iteration should
|
||||
// proceed without prefix_same_as_start enforcement.
|
||||
@@ -5820,13 +5820,13 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedIteratorWithBounds) {
|
||||
// Both directions encounter two tombstone runs (a-d and g-j).
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 2);
|
||||
if (Forward()) {
|
||||
// Forward: sees a-d tombstones first → live e terminates → [a, e).
|
||||
// Then g-j → exhaustion past j, saved_key_="j" fallback → [g, j),
|
||||
// Forward: sees a-d tombstones first -> live e terminates -> [a, e).
|
||||
// Then g-j -> exhaustion past j, saved_key_="j" fallback -> [g, j),
|
||||
// covering g,h,i. j remains a point tombstone.
|
||||
AssertRange(0, "a", "e");
|
||||
AssertRange(1, "g", "j");
|
||||
} else {
|
||||
// Reverse: sees j-g tombstones first, then f,e live, then d-a → exhausts.
|
||||
// Reverse: sees j-g tombstones first, then f,e live, then d-a -> exhausts.
|
||||
AssertRange(0, "g", "z");
|
||||
AssertRange(1, "a", "e");
|
||||
}
|
||||
@@ -5863,8 +5863,8 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedIteratorNoBounds) {
|
||||
VerifyIteration({"e", "f"});
|
||||
|
||||
if (Forward()) {
|
||||
// Forward: a-d run terminates at live e → [a, e). g-j run exhausts
|
||||
// past j → saved_key_ fallback → [g, j), covering g,h,i with j as a
|
||||
// Forward: a-d run terminates at live e -> [a, e). g-j run exhausts
|
||||
// past j -> saved_key_ fallback -> [g, j), covering g,h,i with j as a
|
||||
// point tombstone.
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 2);
|
||||
AssertRange(0, "a", "e");
|
||||
@@ -6061,7 +6061,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterDefaultReadOptions) {
|
||||
}
|
||||
|
||||
TEST_P(ReadPathRangeTombstoneTest, PrefixFilterTotalOrderSeek) {
|
||||
// total_order_seek=true disables prefix filtering — all files visible.
|
||||
// total_order_seek=true disables prefix filtering -- all files visible.
|
||||
// The delete run spans from prefix 'b' into prefix 'c', terminated by
|
||||
// live key "cb" from L0. The tombstone [ba, cb) crosses prefix boundaries,
|
||||
// which is safe because total_order_seek makes all files visible.
|
||||
@@ -6103,7 +6103,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterTotalOrderSeek) {
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Memtable: contiguous deletes spanning prefixes 'b' and 'c'.
|
||||
// No live key between "bb" and "cb" — the run crosses prefix boundaries.
|
||||
// No live key between "bb" and "cb" -- the run crosses prefix boundaries.
|
||||
ASSERT_OK(put("aa", "below"));
|
||||
ASSERT_OK(del("ba"));
|
||||
ASSERT_OK(del("bb"));
|
||||
@@ -6311,7 +6311,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterOutOfDomainSeek) {
|
||||
ro.prefix_same_as_start = true;
|
||||
auto it = std::unique_ptr<Iterator>(db_->NewIterator(ro));
|
||||
if (Forward()) {
|
||||
// Seek with a 1-byte key — out-of-domain for FixedPrefixTransform(4).
|
||||
// Seek with a 1-byte key -- out-of-domain for FixedPrefixTransform(4).
|
||||
// prefix_same_as_start cannot set a prefix bound for this target.
|
||||
it->Seek("b");
|
||||
while (it->Valid()) {
|
||||
@@ -6674,7 +6674,7 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedWithUDT) {
|
||||
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
if (Forward()) {
|
||||
// Forward exhaustion past h: saved_key_="h"+ts fallback → [e+ts, h+ts).
|
||||
// Forward exhaustion past h: saved_key_="h"+ts fallback -> [e+ts, h+ts).
|
||||
AssertRange(0, std::string("e") + ts, std::string("h") + ts);
|
||||
} else {
|
||||
// Reverse exhaustion: range is [a+ts, e+ts).
|
||||
@@ -6687,9 +6687,9 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedWithUDT) {
|
||||
|
||||
TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
|
||||
// SeekForPrev lands directly on tombstones. No upper bound. Forward
|
||||
// exhaustion past h falls back to saved_key_="h" → [e, h), covering
|
||||
// exhaustion past h falls back to saved_key_="h" -> [e, h), covering
|
||||
// e,f,g with h as a point tombstone.
|
||||
// Reverse: SeekForPrev("h") → Delete(h,g,f,e) → live "d" → range [e, h).
|
||||
// Reverse: SeekForPrev("h") -> Delete(h,g,f,e) -> live "d" -> range [e, h).
|
||||
for (bool use_udt : {false, true}) {
|
||||
SCOPED_TRACE(use_udt ? "with UDT" : "without UDT");
|
||||
Options options = CurrentOptions();
|
||||
@@ -6727,7 +6727,7 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
|
||||
keys.push_back(iter->key().ToString());
|
||||
}
|
||||
} else {
|
||||
// SeekForPrev("h") lands on Delete(h), traverses g,f,e → finds "d".
|
||||
// SeekForPrev("h") lands on Delete(h), traverses g,f,e -> finds "d".
|
||||
for (iter->SeekForPrev("h"); iter->Valid(); iter->Prev()) {
|
||||
keys.push_back(iter->key().ToString());
|
||||
}
|
||||
@@ -6738,7 +6738,7 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
|
||||
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
if (Forward()) {
|
||||
// Forward exhausts past h → saved_key_="h" fallback → [e, h),
|
||||
// Forward exhausts past h -> saved_key_="h" fallback -> [e, h),
|
||||
// covering e,f,g with h as a point tombstone.
|
||||
if (use_udt) {
|
||||
AssertRange(0, std::string("e") + ts, std::string("h") + ts);
|
||||
@@ -6761,11 +6761,11 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
|
||||
|
||||
TEST_P(ReadPathRangeTombstoneTest, UpperBoundTombstone) {
|
||||
// iterate_upper_bound lands past the data. Both directions see tombstones
|
||||
// e-h. Forward exhausts naturally past h (no key in DB ≥ upper) so the
|
||||
// e-h. Forward exhausts naturally past h (no key in DB >= upper) so the
|
||||
// forward path falls back to saved_key_ ("h") as the exclusive end_key,
|
||||
// covering n-1 of n deletes — [e, h). The remaining tombstone for h
|
||||
// covering n-1 of n deletes -- [e, h). The remaining tombstone for h
|
||||
// stays as a point delete. Reverse captures the live key "d" before the
|
||||
// run and uses it via range_tomb_end_key_ — [e, i) when SeekToLast
|
||||
// run and uses it via range_tomb_end_key_ -- [e, i) when SeekToLast
|
||||
// delegates to SeekForPrev("i").
|
||||
for (bool use_udt : {false, true}) {
|
||||
SCOPED_TRACE(use_udt ? "with UDT" : "without UDT");
|
||||
@@ -6837,8 +6837,8 @@ TEST_P(ReadPathRangeTombstoneTest, UpperBoundTombstone) {
|
||||
|
||||
TEST_P(ReadPathRangeTombstoneTest, LowerBoundTruncatesReverse) {
|
||||
// Keys a-j, delete a-h. Lower bound "e" truncates reverse iteration
|
||||
// mid-tombstone-run. Forward: tombstones e-h ended by live key i → [e, i).
|
||||
// Reverse: tombstones h,g,f,e then lower_bound hit → flush [e, i).
|
||||
// mid-tombstone-run. Forward: tombstones e-h ended by live key i -> [e, i).
|
||||
// Reverse: tombstones h,g,f,e then lower_bound hit -> flush [e, i).
|
||||
for (bool use_udt : {false, true}) {
|
||||
SCOPED_TRACE(use_udt ? "with UDT" : "without UDT");
|
||||
Options options = CurrentOptions();
|
||||
@@ -6975,14 +6975,14 @@ TEST_P(ReadPathRangeTombstoneTest, InvisibleKeysDontBreakTombstoneRun) {
|
||||
ASSERT_OK(Put("f", "vf"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Delete b, d — visible tombstones.
|
||||
// Delete b, d -- visible tombstones.
|
||||
ASSERT_OK(Delete("b"));
|
||||
ASSERT_OK(Delete("d"));
|
||||
|
||||
// Snapshot S. At S: a(live), b(del), d(del), f(live).
|
||||
const Snapshot* snap = db_->GetSnapshot();
|
||||
|
||||
// Write c, e AFTER the snapshot — invisible at S.
|
||||
// Write c, e AFTER the snapshot -- invisible at S.
|
||||
// At S the iterator sees: a(live), b(del), c(invis), d(del),
|
||||
// e(invis), f(live).
|
||||
ASSERT_OK(Put("c", "vc"));
|
||||
@@ -6994,7 +6994,7 @@ TEST_P(ReadPathRangeTombstoneTest, InvisibleKeysDontBreakTombstoneRun) {
|
||||
VerifyIteration({"a", "f"}, ro);
|
||||
|
||||
// Invisible keys c and e should not break the tombstone run.
|
||||
// 2 tombstones (b, d) ≥ threshold → range [b, f).
|
||||
// 2 tombstones (b, d) >= threshold -> range [b, f).
|
||||
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
|
||||
AssertRange(0, "b", "f");
|
||||
|
||||
|
||||
+195
-2
@@ -7,9 +7,12 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/db_impl/db_impl_secondary.h"
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/db_with_timestamp_test_util.h"
|
||||
#include "db/wide/wide_column_test_util.h"
|
||||
#include "db/write_batch_internal.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/utilities/transaction_db.h"
|
||||
#include "test_util/sync_point.h"
|
||||
@@ -357,8 +360,7 @@ TEST_F(DBSecondaryTest, GetMergeOperands) {
|
||||
const Status s = db_secondary_->GetMergeOperands(
|
||||
ReadOptions(), cfh, "k1", values.data(), &merge_operands_info,
|
||||
&number_of_operands);
|
||||
ASSERT_NOK(s);
|
||||
ASSERT_TRUE(s.IsMergeInProgress());
|
||||
ASSERT_OK(s);
|
||||
|
||||
ASSERT_EQ(number_of_operands, 4);
|
||||
ASSERT_EQ(values[0].ToString(), "v1");
|
||||
@@ -367,6 +369,197 @@ TEST_F(DBSecondaryTest, GetMergeOperands) {
|
||||
ASSERT_EQ(values[3].ToString(), "v4");
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, GetMergeOperandsWithBlobBackedEntityDefaultColumn) {
|
||||
// Goal: exercise the secondary read path with a blob-backed V2 entity base
|
||||
// in SST and a newer merge operand applied through catch-up. The secondary
|
||||
// DB must resolve the blob-backed default column for both Get() and
|
||||
// GetMergeOperands() while combining the newer memtable merge with the older
|
||||
// SST-backed base entity.
|
||||
Options options = GetDefaultOptions();
|
||||
options.create_if_missing = true;
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 50;
|
||||
options.disable_auto_compactions = true;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
|
||||
options.env = env_;
|
||||
Reopen(options);
|
||||
|
||||
const std::string key = "secondary_blob_entity";
|
||||
const std::string default_value(100, 'd');
|
||||
const std::string large_value(120, 'l');
|
||||
const std::string merge_operand = "suffix";
|
||||
const std::string expected_merged = default_value + "|" + merge_operand;
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"meta", "inline"}};
|
||||
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
options.max_open_files = -1;
|
||||
OpenSecondary(options);
|
||||
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
|
||||
merge_operand));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
|
||||
auto* cfh = db_secondary_->DefaultColumnFamily();
|
||||
|
||||
{
|
||||
PinnableSlice result;
|
||||
ASSERT_OK(db_secondary_->Get(ReadOptions(), cfh, key, &result));
|
||||
ASSERT_EQ(result, expected_merged);
|
||||
}
|
||||
|
||||
{
|
||||
GetMergeOperandsOptions get_merge_opts;
|
||||
get_merge_opts.expected_max_number_of_operands = 2;
|
||||
|
||||
std::array<PinnableSlice, 2> merge_operands;
|
||||
int number_of_operands = 0;
|
||||
|
||||
ASSERT_OK(db_secondary_->GetMergeOperands(
|
||||
ReadOptions(), cfh, key, merge_operands.data(), &get_merge_opts,
|
||||
&number_of_operands));
|
||||
ASSERT_EQ(number_of_operands, 2);
|
||||
ASSERT_EQ(merge_operands[0], default_value);
|
||||
ASSERT_EQ(merge_operands[1], merge_operand);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, GetImplReturnsBlobIndexWhenRequested) {
|
||||
// Goal: cover the internal secondary GetImpl contract when the caller
|
||||
// explicitly asks for raw blob-index bytes via `is_blob_index`. Catch-up
|
||||
// rebuilds the blob index into the secondary memtable, and this path must
|
||||
// preserve the encoded index instead of eagerly resolving or rejecting it.
|
||||
Options options = GetDefaultOptions();
|
||||
options.create_if_missing = true;
|
||||
options.env = env_;
|
||||
|
||||
Reopen(options);
|
||||
|
||||
options.max_open_files = -1;
|
||||
OpenSecondary(options);
|
||||
|
||||
std::string blob_index;
|
||||
BlobIndex::EncodeInlinedTTL(&blob_index, /*expiration=*/9876543210, "blob");
|
||||
|
||||
WriteBatch batch;
|
||||
ASSERT_OK(WriteBatchInternal::PutBlobIndex(
|
||||
&batch, db_->DefaultColumnFamily()->GetID(), "blob_key", blob_index));
|
||||
ASSERT_OK(db_->Write(WriteOptions(), &batch));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
|
||||
PinnableSlice value;
|
||||
bool is_blob_index = false;
|
||||
DBImpl::GetImplOptions get_impl_options;
|
||||
get_impl_options.column_family = db_secondary_->DefaultColumnFamily();
|
||||
get_impl_options.value = &value;
|
||||
get_impl_options.is_blob_index = &is_blob_index;
|
||||
|
||||
ASSERT_OK(db_secondary_full()->GetImpl(ReadOptions(), Slice("blob_key"),
|
||||
get_impl_options));
|
||||
ASSERT_TRUE(is_blob_index);
|
||||
ASSERT_EQ(value, blob_index);
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest,
|
||||
GetAndGetEntityWithBlobBackedDefaultColumnDirectWriteMemtable) {
|
||||
// Goal: cover the secondary memtable path after catch-up replays a blob
|
||||
// direct-write entity from the primary WAL. The test checks both `Get()`,
|
||||
// which must resolve the blob-backed default column, and `GetEntity()`,
|
||||
// which must eagerly resolve all unresolved blob columns instead of exposing
|
||||
// encoded blob indices to the caller.
|
||||
Options options =
|
||||
wide_column_test_util::GetDirectWriteOptions(GetDefaultOptions());
|
||||
options.create_if_missing = true;
|
||||
options.min_blob_size = 50;
|
||||
options.env = env_;
|
||||
|
||||
Reopen(options);
|
||||
|
||||
options.max_open_files = -1;
|
||||
OpenSecondary(options);
|
||||
|
||||
const std::string key = "secondary_direct_write_memtable_entity";
|
||||
const std::string default_value =
|
||||
wide_column_test_util::GenerateLargeValue(100, 'd');
|
||||
const std::string large_value =
|
||||
wide_column_test_util::GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = wide_column_test_util::GenerateSmallValue();
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
|
||||
auto* cfh = db_secondary_->DefaultColumnFamily();
|
||||
|
||||
{
|
||||
PinnableSlice result;
|
||||
ASSERT_OK(db_secondary_->Get(ReadOptions(), cfh, key, &result));
|
||||
ASSERT_EQ(result, default_value);
|
||||
}
|
||||
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_secondary_->GetEntity(ReadOptions(), cfh, key, &result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, SecondaryDirectWriteMemtableBlobBlockCacheTier) {
|
||||
// Goal: cover the secondary memtable path under kBlockCacheTier. Catch-up
|
||||
// rebuilds the blob-backed direct-write entity from WAL, so serving Get() or
|
||||
// GetEntity() would require blob I/O through the memtable resolution path,
|
||||
// which must return Incomplete instead of issuing that I/O.
|
||||
Options options =
|
||||
wide_column_test_util::GetDirectWriteOptions(GetDefaultOptions());
|
||||
options.create_if_missing = true;
|
||||
options.min_blob_size = 50;
|
||||
options.env = env_;
|
||||
|
||||
Reopen(options);
|
||||
|
||||
options.max_open_files = -1;
|
||||
OpenSecondary(options);
|
||||
|
||||
const std::string key = "secondary_direct_write_memtable_block_cache_tier";
|
||||
const std::string default_value =
|
||||
wide_column_test_util::GenerateLargeValue(100, 'd');
|
||||
const std::string large_value =
|
||||
wide_column_test_util::GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = wide_column_test_util::GenerateSmallValue();
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
|
||||
auto* cfh = db_secondary_->DefaultColumnFamily();
|
||||
ReadOptions read_opts;
|
||||
read_opts.read_tier = kBlockCacheTier;
|
||||
|
||||
{
|
||||
PinnableSlice result;
|
||||
const Status s = db_secondary_->Get(read_opts, cfh, key, &result);
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
ASSERT_TRUE(result.empty());
|
||||
}
|
||||
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
const Status s = db_secondary_->GetEntity(read_opts, cfh, key, &result);
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
ASSERT_TRUE(result.columns().empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, InternalCompactionCompactedFiles) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
|
||||
@@ -8203,6 +8203,98 @@ TEST_P(OpenFilesAsyncTest, BeforeRead) {
|
||||
}
|
||||
}
|
||||
|
||||
// For modern DBs (manifest carries file_creation_time),
|
||||
// GetCreationTimeOfOldestFile returns the real value without waiting for async
|
||||
// file open.
|
||||
TEST_P(OpenFilesAsyncTest, GetCreationTimeOfOldestFileSkipsWaitForModernDB) {
|
||||
if (max_open_files_ != -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
Options options = CurrentOptions();
|
||||
ASSERT_NO_FATAL_FAILURE(SetupData(options));
|
||||
|
||||
// If WaitForAsyncFileOpen is entered, the test fails -- modern DBs must
|
||||
// never need the wait.
|
||||
std::atomic<bool> waited{false};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::WaitForAsyncFileOpen::BeforeWait",
|
||||
[&](void* /*arg*/) { waited.store(true); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
OpenTestDB(options);
|
||||
|
||||
uint64_t creation_time = std::numeric_limits<uint64_t>::max();
|
||||
ASSERT_OK(dbfull()->GetCreationTimeOfOldestFile(&creation_time));
|
||||
EXPECT_GT(creation_time, 0);
|
||||
EXPECT_LT(creation_time, std::numeric_limits<uint64_t>::max());
|
||||
EXPECT_FALSE(waited.load());
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
Close();
|
||||
}
|
||||
|
||||
// For legacy DBs (manifest lacks file_creation_time, value comes from the
|
||||
// pinned reader), GetCreationTimeOfOldestFile must block until
|
||||
// BGWorkAsyncFileOpen pins the readers; otherwise it returns 0 (the "info
|
||||
// unavailable" sentinel) instead of the real value.
|
||||
TEST_P(OpenFilesAsyncTest,
|
||||
GetCreationTimeOfOldestFileBlocksOnAsyncOpenForLegacyDB) {
|
||||
if (max_open_files_ != -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
Options options = CurrentOptions();
|
||||
ASSERT_NO_FATAL_FAILURE(SetupData(options));
|
||||
|
||||
// Simulate a legacy DB: force TryGetFileCreationTime to return
|
||||
// kUnknownFileCreationTime until BGWorkAsyncFileOpen completes (i.e., what
|
||||
// would happen if the manifest had no file_creation_time and the pinned
|
||||
// reader was the only source).
|
||||
std::atomic<bool> async_open_done{false};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::BGWorkAsyncFileOpen:Done",
|
||||
[&](void* /*arg*/) { async_open_done.store(true); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"Version::GetCreationTimeOfOldestFile::FileCreationTime", [&](void* arg) {
|
||||
if (!async_open_done.load()) {
|
||||
*static_cast<uint64_t*>(arg) = kUnknownFileCreationTime;
|
||||
}
|
||||
});
|
||||
|
||||
// Dependency chain: caller's first pass returns 0, enters
|
||||
// WaitForAsyncFileOpen -> main thread confirms wait -> main thread releases
|
||||
// async open -> background worker completes -> caller wakes -> second pass
|
||||
// sees the real value.
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"DBImpl::WaitForAsyncFileOpen::BeforeWait",
|
||||
"OpenFilesAsyncTest::CreationTime::CallerBlocked"},
|
||||
{"OpenFilesAsyncTest::CreationTime::CallerBlocked",
|
||||
"OpenFilesAsyncTest::CreationTime::ReleaseAsyncOpen"},
|
||||
{"OpenFilesAsyncTest::CreationTime::ReleaseAsyncOpen",
|
||||
"DBImpl::BGWorkAsyncFileOpen::Start"}});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
OpenTestDB(options);
|
||||
|
||||
uint64_t creation_time = std::numeric_limits<uint64_t>::max();
|
||||
port::Thread caller([&]() {
|
||||
ASSERT_OK(dbfull()->GetCreationTimeOfOldestFile(&creation_time));
|
||||
});
|
||||
|
||||
TEST_SYNC_POINT("OpenFilesAsyncTest::CreationTime::CallerBlocked");
|
||||
TEST_SYNC_POINT("OpenFilesAsyncTest::CreationTime::ReleaseAsyncOpen");
|
||||
caller.join();
|
||||
|
||||
EXPECT_GT(creation_time, 0);
|
||||
EXPECT_LT(creation_time, std::numeric_limits<uint64_t>::max());
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(OpenFilesAsyncTest, Shutdown) {
|
||||
Options options = CurrentOptions();
|
||||
ASSERT_NO_FATAL_FAILURE(SetupData(options));
|
||||
|
||||
+62
-3
@@ -107,7 +107,7 @@ TEST_F(DBTest2, ReadOnlyDBWalInDbPathInitialized) {
|
||||
ASSERT_OK(Put("key2", "value2"));
|
||||
Close();
|
||||
|
||||
// Reopen as read-only — wal_in_db_path_ must be properly initialized.
|
||||
// Reopen as read-only -- wal_in_db_path_ must be properly initialized.
|
||||
// Before the fix, closing this DB would read an uninitialized bool in
|
||||
// DeleteObsoleteFileImpl, which UBSan catches as undefined behavior.
|
||||
std::unique_ptr<DB> db_ptr;
|
||||
@@ -115,7 +115,7 @@ TEST_F(DBTest2, ReadOnlyDBWalInDbPathInitialized) {
|
||||
std::string value;
|
||||
ASSERT_OK(db_ptr->Get(ReadOptions(), "key1", &value));
|
||||
ASSERT_EQ("value1", value);
|
||||
// Close the read-only DB — this triggers PurgeObsoleteFiles which reads
|
||||
// Close the read-only DB -- this triggers PurgeObsoleteFiles which reads
|
||||
// wal_in_db_path_. Under UBSan, an uninitialized bool here would fail.
|
||||
db_ptr.reset();
|
||||
|
||||
@@ -7510,6 +7510,65 @@ TEST_F(DBTest2, GetLatestSeqAndTsForKey) {
|
||||
ASSERT_EQ(0, options.statistics->getTickerCount(GET_HIT_L0));
|
||||
}
|
||||
|
||||
TEST_F(DBTest2,
|
||||
GetLatestSequenceForKeyFromHistoryWithBlobBackedWideColumnEntity) {
|
||||
// Goal: exercise the memtable history lookup in GetLatestSequenceForKey()
|
||||
// after blob direct write stores a blob-backed V2 entity in a flushed
|
||||
// memtable. Using cache_only=true ensures the lookup succeeds only if the
|
||||
// history path can resolve the wide-column base value under the newer merge.
|
||||
Destroy(last_options_);
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.enable_blob_files = true;
|
||||
options.enable_blob_direct_write = true;
|
||||
options.min_blob_size = 50;
|
||||
options.allow_concurrent_memtable_write = false;
|
||||
options.blob_direct_write_partitions = 1;
|
||||
options.max_write_buffer_size_to_maintain = 64 << 10;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
|
||||
|
||||
Reopen(options);
|
||||
|
||||
const std::string key = "history_blob_entity";
|
||||
const std::string default_value(100, 'd');
|
||||
const std::string merge_operand = "suffix";
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"meta", "inline"}};
|
||||
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
|
||||
merge_operand));
|
||||
const SequenceNumber expected_seq = dbfull()->GetLatestSequenceNumber();
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_FALSE(GetBlobFileNumbers().empty());
|
||||
|
||||
uint64_t num_immutable_memtables = 0;
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumImmutableMemTable,
|
||||
&num_immutable_memtables));
|
||||
ASSERT_EQ(num_immutable_memtables, 0);
|
||||
|
||||
auto* cfhi = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
dbfull()->DefaultColumnFamily());
|
||||
assert(cfhi);
|
||||
assert(cfhi->cfd());
|
||||
SuperVersion* sv = cfhi->cfd()->GetSuperVersion();
|
||||
|
||||
SequenceNumber seq = kMaxSequenceNumber;
|
||||
bool found_record_for_key = false;
|
||||
bool is_blob_index = false;
|
||||
const Status s = dbfull()->GetLatestSequenceForKey(
|
||||
sv, key, /*cache_only=*/true, /*lower_bound_seq=*/0, &seq,
|
||||
/*timestamp=*/nullptr, &found_record_for_key, &is_blob_index);
|
||||
|
||||
ASSERT_OK(s);
|
||||
ASSERT_TRUE(found_record_for_key);
|
||||
ASSERT_EQ(expected_seq, seq);
|
||||
ASSERT_FALSE(is_blob_index);
|
||||
}
|
||||
|
||||
#if defined(ZSTD)
|
||||
TEST_F(DBTest2, ZSTDChecksum) {
|
||||
// Verify that corruption during decompression is caught.
|
||||
@@ -8173,7 +8232,7 @@ TEST_F(DBTest2, FastSstOpenDisableAfterMetadataPersisted) {
|
||||
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
|
||||
db.reset();
|
||||
|
||||
// Reopen with fast_sst_open DISABLED — metadata is in the MANIFEST
|
||||
// Reopen with fast_sst_open DISABLED -- metadata is in the MANIFEST
|
||||
// but should NOT be passed to the filesystem
|
||||
options.fast_sst_open = false;
|
||||
test_fs->ResetCounters();
|
||||
|
||||
+2
-2
@@ -218,7 +218,7 @@ TEST_P(IterKeySwapTest, SwapAndDestroy) {
|
||||
b_use_secondary] = GetParam();
|
||||
|
||||
std::string expected_a, expected_b;
|
||||
// Storage for pinned keys — must outlive the IterKeys.
|
||||
// Storage for pinned keys -- must outlive the IterKeys.
|
||||
std::string a_storage, b_storage;
|
||||
IterKey a;
|
||||
expected_a =
|
||||
@@ -236,7 +236,7 @@ TEST_P(IterKeySwapTest, SwapAndDestroy) {
|
||||
// After swap: a has b's old data, b has a's old data.
|
||||
ASSERT_EQ(a.GetUserKey().ToString(), expected_b);
|
||||
ASSERT_EQ(b.GetUserKey().ToString(), expected_a);
|
||||
} // b destroyed here — must not corrupt a's data
|
||||
} // b destroyed here -- must not corrupt a's data
|
||||
|
||||
// a must still hold valid data after b's destruction.
|
||||
ASSERT_EQ(a.GetUserKey().ToString(), expected_b);
|
||||
|
||||
+3
-3
@@ -1696,7 +1696,7 @@ TEST_F(EventListenerTest, BackgroundJobPressure) {
|
||||
Env::Priority::LOW);
|
||||
sleeping_task.WaitUntilSleeping();
|
||||
|
||||
// Phase 1: No pressure — 3 SST files (below slowdown trigger=4).
|
||||
// Phase 1: No pressure -- 3 SST files (below slowdown trigger=4).
|
||||
for (int i = 0; i < 3; i++) {
|
||||
ASSERT_OK(Put("k" + std::to_string(i), std::string(100, 'x')));
|
||||
ASSERT_OK(Flush());
|
||||
@@ -1714,7 +1714,7 @@ TEST_F(EventListenerTest, BackgroundJobPressure) {
|
||||
// MaybeScheduleFlushOrCompaction() runs before the pressure callback and
|
||||
// may schedule new flush work, making flush counts non-deterministic.
|
||||
|
||||
// Phase 2: Build pressure — flush past slowdown trigger (4 L0 SST files).
|
||||
// Phase 2: Build pressure -- flush past slowdown trigger (4 L0 SST files).
|
||||
// Compaction is blocked, so L0 SST files pile up.
|
||||
listener->Reset();
|
||||
{
|
||||
@@ -1750,7 +1750,7 @@ TEST_F(EventListenerTest, BackgroundJobPressure) {
|
||||
ASSERT_TRUE(found_compaction_scheduled);
|
||||
ASSERT_TRUE(found_high_proximity);
|
||||
|
||||
// Phase 3: Relieve pressure — unblock compaction, wait for completion.
|
||||
// Phase 3: Relieve pressure -- unblock compaction, wait for completion.
|
||||
listener->Reset();
|
||||
sleeping_task.WakeUp();
|
||||
sleeping_task.WaitUntilDone();
|
||||
|
||||
+3
-2
@@ -22,9 +22,10 @@ namespace ROCKSDB_NAMESPACE::log {
|
||||
|
||||
Writer::Writer(std::unique_ptr<WritableFileWriter>&& dest, uint64_t log_number,
|
||||
bool recycle_log_files, bool manual_flush,
|
||||
CompressionType compression_type, bool track_and_verify_wals)
|
||||
CompressionType compression_type, bool track_and_verify_wals,
|
||||
size_t initial_block_offset)
|
||||
: dest_(std::move(dest)),
|
||||
block_offset_(0),
|
||||
block_offset_(initial_block_offset),
|
||||
log_number_(log_number),
|
||||
recycle_log_files_(recycle_log_files),
|
||||
// Header size varies depending on whether we are recycling or not.
|
||||
|
||||
+8
-3
@@ -75,15 +75,20 @@ namespace log {
|
||||
class Writer {
|
||||
public:
|
||||
// Create a writer that will append data to "*dest".
|
||||
// "*dest" must be initially empty.
|
||||
// "*dest" must remain live while this Writer is in use.
|
||||
// "*dest" must remain live while this Writer is in use. By default
|
||||
// "*dest" is expected to be empty (initial_block_offset = 0). When
|
||||
// resuming append into an existing log file (e.g.
|
||||
// VersionSet::ReopenManifestForAppend), pass the offset within the
|
||||
// current 32 KiB block at which writes should resume so that record
|
||||
// framing aligns to the existing block layout.
|
||||
// TODO(hx235): separate WAL related parameters from general `Reader`
|
||||
// parameters
|
||||
explicit Writer(std::unique_ptr<WritableFileWriter>&& dest,
|
||||
uint64_t log_number, bool recycle_log_files,
|
||||
bool manual_flush = false,
|
||||
CompressionType compressionType = kNoCompression,
|
||||
bool track_and_verify_wals = false);
|
||||
bool track_and_verify_wals = false,
|
||||
size_t initial_block_offset = 0);
|
||||
// No copying allowed
|
||||
Writer(const Writer&) = delete;
|
||||
void operator=(const Writer&) = delete;
|
||||
|
||||
+80
-22
@@ -15,6 +15,7 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/blob/blob_file_partition_manager.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/dbformat.h"
|
||||
@@ -70,6 +71,67 @@ Status GetDefaultColumnBlobIndexSlice(Slice entity, Slice* blob_index_slice) {
|
||||
return status;
|
||||
}
|
||||
|
||||
Status PushWideColumnEntityDefaultOperand(const Slice& user_key,
|
||||
const Slice& entity,
|
||||
MergeContext* merge_context,
|
||||
bool operand_pinned,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
assert(merge_context != nullptr);
|
||||
|
||||
Slice entity_ref = entity;
|
||||
Slice value_of_default;
|
||||
Status status = WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
entity_ref, value_of_default);
|
||||
if (status.ok()) {
|
||||
merge_context->PushOperand(value_of_default, operand_pinned);
|
||||
return status;
|
||||
}
|
||||
if (!status.IsNotSupported()) {
|
||||
return status;
|
||||
}
|
||||
if (blob_fetcher == nullptr) {
|
||||
return Status::Corruption(
|
||||
"Cannot resolve blob-backed default column without a blob fetcher");
|
||||
}
|
||||
|
||||
PinnableSlice resolved_default;
|
||||
bool resolved = false;
|
||||
status = WideColumnSerialization::GetValueOfDefaultColumnResolvingBlobs(
|
||||
entity, user_key, blob_fetcher, resolved_default, resolved);
|
||||
if (status.ok()) {
|
||||
// Resolved blob values are backed by this stack-local PinnableSlice, so
|
||||
// copy them into MergeContext instead of pinning their storage.
|
||||
merge_context->PushOperand(Slice(resolved_default), false);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
Status MergeWithWideColumnEntityBaseValue(
|
||||
const Slice& user_key, const Slice& entity,
|
||||
const MergeOperator* merge_operator, MergeContext* merge_context,
|
||||
Logger* logger, Statistics* statistics, SystemClock* clock,
|
||||
std::string* value, PinnableWideColumns* columns,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
assert(merge_context != nullptr);
|
||||
|
||||
std::string resolved_entity;
|
||||
Slice effective_entity;
|
||||
Status status = WideColumnSerialization::ResolveEntityForMerge(
|
||||
entity, user_key, blob_fetcher, nullptr /* prefetch_buffers */,
|
||||
resolved_entity, effective_entity);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// `op_failure_scope` (an output parameter) is not provided (set to nullptr)
|
||||
// since a failure must be propagated regardless of its value.
|
||||
return MergeHelper::TimedFullMerge(
|
||||
merge_operator, user_key, MergeHelper::kWideBaseValue, effective_entity,
|
||||
merge_context->GetOperands(), logger, statistics, clock,
|
||||
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr, value,
|
||||
columns);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ImmutableMemTableOptions::ImmutableMemTableOptions(
|
||||
@@ -1254,6 +1316,7 @@ struct Saver {
|
||||
bool inplace_update_support;
|
||||
bool do_merge;
|
||||
SystemClock* clock;
|
||||
const BlobFetcher* blob_fetcher;
|
||||
|
||||
ReadCallback* callback_;
|
||||
bool* is_blob_index;
|
||||
@@ -1424,27 +1487,17 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
// Preserve the value with the goal of returning it as part of
|
||||
// raw merge operands to the user
|
||||
|
||||
Slice value_of_default;
|
||||
*(s->status) = WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
v, value_of_default);
|
||||
|
||||
if (s->status->ok()) {
|
||||
merge_context->PushOperand(
|
||||
value_of_default,
|
||||
s->inplace_update_support == false /* operand_pinned */);
|
||||
}
|
||||
*(s->status) = PushWideColumnEntityDefaultOperand(
|
||||
s->key->user_key(), v, merge_context,
|
||||
s->inplace_update_support == false /* operand_pinned */,
|
||||
s->blob_fetcher);
|
||||
} else if (*(s->merge_in_progress)) {
|
||||
assert(s->do_merge);
|
||||
|
||||
if (s->value || s->columns) {
|
||||
// `op_failure_scope` (an output parameter) is not provided (set
|
||||
// to nullptr) since a failure must be propagated regardless of
|
||||
// its value.
|
||||
*(s->status) = MergeHelper::TimedFullMerge(
|
||||
merge_operator, s->key->user_key(), MergeHelper::kWideBaseValue,
|
||||
v, merge_context->GetOperands(), s->logger, s->statistics,
|
||||
s->clock, /* update_num_ops_stats */ true,
|
||||
/* op_failure_scope */ nullptr, s->value, s->columns);
|
||||
*(s->status) = MergeWithWideColumnEntityBaseValue(
|
||||
s->key->user_key(), v, merge_operator, merge_context, s->logger,
|
||||
s->statistics, s->clock, s->value, s->columns, s->blob_fetcher);
|
||||
}
|
||||
} else if (s->value) {
|
||||
Slice value_of_default;
|
||||
@@ -1520,7 +1573,8 @@ bool MemTable::Get(const LookupKey& key, std::string* value,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
SequenceNumber* seq, const ReadOptions& read_opts,
|
||||
bool immutable_memtable, ReadCallback* callback,
|
||||
bool* is_blob_index, bool do_merge) {
|
||||
bool* is_blob_index, bool do_merge,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
// The sequence number is updated synchronously in version_set.h
|
||||
if (IsEmpty()) {
|
||||
// Avoiding recording stats for speed.
|
||||
@@ -1578,7 +1632,7 @@ bool MemTable::Get(const LookupKey& key, std::string* value,
|
||||
}
|
||||
GetFromTable(key, *max_covering_tombstone_seq, do_merge, callback,
|
||||
is_blob_index, value, columns, timestamp, s, merge_context,
|
||||
seq, &found_final_value, &merge_in_progress);
|
||||
seq, &found_final_value, &merge_in_progress, blob_fetcher);
|
||||
}
|
||||
|
||||
// No change to value, since we have not yet found a Put/Delete
|
||||
@@ -1601,7 +1655,8 @@ void MemTable::GetFromTable(const LookupKey& key,
|
||||
PinnableWideColumns* columns,
|
||||
std::string* timestamp, Status* s,
|
||||
MergeContext* merge_context, SequenceNumber* seq,
|
||||
bool* found_final_value, bool* merge_in_progress) {
|
||||
bool* found_final_value, bool* merge_in_progress,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
Saver saver;
|
||||
saver.status = s;
|
||||
saver.found_final_value = found_final_value;
|
||||
@@ -1619,6 +1674,7 @@ void MemTable::GetFromTable(const LookupKey& key,
|
||||
saver.inplace_update_support = moptions_.inplace_update_support;
|
||||
saver.statistics = moptions_.statistics;
|
||||
saver.clock = clock_;
|
||||
saver.blob_fetcher = blob_fetcher;
|
||||
saver.callback_ = callback;
|
||||
saver.is_blob_index = is_blob_index;
|
||||
saver.do_merge = do_merge;
|
||||
@@ -1648,7 +1704,8 @@ Status MemTable::ValidateKey(const char* key, bool allow_data_in_errors) {
|
||||
}
|
||||
|
||||
void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
ReadCallback* callback, bool immutable_memtable) {
|
||||
ReadCallback* callback, bool immutable_memtable,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
// The sequence number is updated synchronously in version_set.h
|
||||
if (IsEmpty()) {
|
||||
// Avoiding recording stats for speed.
|
||||
@@ -1740,6 +1797,7 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
saver.inplace_update_support = moptions_.inplace_update_support;
|
||||
saver.statistics = moptions_.statistics;
|
||||
saver.clock = clock_;
|
||||
saver.blob_fetcher = blob_fetcher;
|
||||
saver.callback_ = callback;
|
||||
saver.is_blob_index = &iter->is_blob_index;
|
||||
saver.do_merge = true;
|
||||
@@ -1831,7 +1889,7 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
*(iter->lkey), iter->max_covering_tombstone_seq, true, callback,
|
||||
&iter->is_blob_index, iter->value ? iter->value->GetSelf() : nullptr,
|
||||
iter->columns, iter->timestamp, iter->s, &(iter->merge_context),
|
||||
&dummy_seq, &found_final_value, &merge_in_progress);
|
||||
&dummy_seq, &found_final_value, &merge_in_progress, blob_fetcher);
|
||||
|
||||
if (!found_final_value && merge_in_progress) {
|
||||
if (iter->s->ok()) {
|
||||
|
||||
+14
-8
@@ -39,6 +39,7 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class BlobFetcher;
|
||||
class BlobFilePartitionManager;
|
||||
struct FlushJobInfo;
|
||||
class Mutex;
|
||||
@@ -236,25 +237,27 @@ class ReadOnlyMemTable {
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
SequenceNumber* seq, const ReadOptions& read_opts,
|
||||
bool immutable_memtable, ReadCallback* callback = nullptr,
|
||||
bool* is_blob_index = nullptr, bool do_merge = true) = 0;
|
||||
bool* is_blob_index = nullptr, bool do_merge = true,
|
||||
const BlobFetcher* blob_fetcher = nullptr) = 0;
|
||||
bool Get(const LookupKey& key, std::string* value,
|
||||
PinnableWideColumns* columns, std::string* timestamp, Status* s,
|
||||
MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
const ReadOptions& read_opts, bool immutable_memtable,
|
||||
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
|
||||
bool do_merge = true) {
|
||||
bool do_merge = true, const BlobFetcher* blob_fetcher = nullptr) {
|
||||
SequenceNumber seq;
|
||||
return Get(key, value, columns, timestamp, s, merge_context,
|
||||
max_covering_tombstone_seq, &seq, read_opts, immutable_memtable,
|
||||
callback, is_blob_index, do_merge);
|
||||
callback, is_blob_index, do_merge, blob_fetcher);
|
||||
}
|
||||
|
||||
// @param immutable_memtable Whether this memtable is immutable. Used
|
||||
// internally by NewRangeTombstoneIterator(). See comment above
|
||||
// NewRangeTombstoneIterator() for more detail.
|
||||
virtual void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
ReadCallback* callback, bool immutable_memtable) = 0;
|
||||
ReadCallback* callback, bool immutable_memtable,
|
||||
const BlobFetcher* blob_fetcher = nullptr) = 0;
|
||||
|
||||
// Get total number of entries in the mem table.
|
||||
// REQUIRES: external synchronization to prevent simultaneous
|
||||
@@ -320,7 +323,7 @@ class ReadOnlyMemTable {
|
||||
virtual uint64_t ApproximateOldestKeyTime() const = 0;
|
||||
|
||||
// Inserts a range tombstone [start_key, end_key) that is logically redundant
|
||||
// — it is derived from existing point tombstones observed during iteration
|
||||
// -- it is derived from existing point tombstones observed during iteration
|
||||
// and does not delete any data that isn't already deleted. This is a
|
||||
// best-effort optimization. It allows future reads to skip iterating over
|
||||
// continuous single deletion tombstones.
|
||||
@@ -688,10 +691,12 @@ class MemTable final : public ReadOnlyMemTable {
|
||||
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
||||
const ReadOptions& read_opts, bool immutable_memtable,
|
||||
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
|
||||
bool do_merge = true) override;
|
||||
bool do_merge = true,
|
||||
const BlobFetcher* blob_fetcher = nullptr) override;
|
||||
|
||||
void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
ReadCallback* callback, bool immutable_memtable) override;
|
||||
ReadCallback* callback, bool immutable_memtable,
|
||||
const BlobFetcher* blob_fetcher = nullptr) override;
|
||||
|
||||
// If `key` exists in current memtable with type value_type and the existing
|
||||
// value is at least as large as the new value, updates it in-place. Otherwise
|
||||
@@ -991,7 +996,8 @@ class MemTable final : public ReadOnlyMemTable {
|
||||
std::string* value, PinnableWideColumns* columns,
|
||||
std::string* timestamp, Status* s,
|
||||
MergeContext* merge_context, SequenceNumber* seq,
|
||||
bool* found_final_value, bool* merge_in_progress);
|
||||
bool* found_final_value, bool* merge_in_progress,
|
||||
const BlobFetcher* blob_fetcher);
|
||||
|
||||
// Always returns non-null and assumes certain pre-checks (e.g.,
|
||||
// is_range_del_table_empty_) are done. This is only valid during the lifetime
|
||||
|
||||
+18
-14
@@ -108,18 +108,19 @@ bool MemTableListVersion::Get(const LookupKey& key, std::string* value,
|
||||
MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
SequenceNumber* seq, const ReadOptions& read_opts,
|
||||
ReadCallback* callback, bool* is_blob_index) {
|
||||
ReadCallback* callback, bool* is_blob_index,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
return GetFromList(&memlist_, key, value, columns, timestamp, s,
|
||||
merge_context, max_covering_tombstone_seq, seq, read_opts,
|
||||
callback, is_blob_index);
|
||||
callback, is_blob_index, blob_fetcher);
|
||||
}
|
||||
|
||||
void MemTableListVersion::MultiGet(const ReadOptions& read_options,
|
||||
MultiGetRange* range,
|
||||
ReadCallback* callback) {
|
||||
MultiGetRange* range, ReadCallback* callback,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
for (auto memtable : memlist_) {
|
||||
memtable->MultiGet(read_options, range, callback,
|
||||
true /* immutable_memtable */);
|
||||
true /* immutable_memtable */, blob_fetcher);
|
||||
if (range->empty()) {
|
||||
return;
|
||||
}
|
||||
@@ -128,12 +129,13 @@ void MemTableListVersion::MultiGet(const ReadOptions& read_options,
|
||||
|
||||
bool MemTableListVersion::GetMergeOperands(
|
||||
const LookupKey& key, Status* s, MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq, const ReadOptions& read_opts) {
|
||||
SequenceNumber* max_covering_tombstone_seq, const ReadOptions& read_opts,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
for (ReadOnlyMemTable* memtable : memlist_) {
|
||||
bool done = memtable->Get(
|
||||
key, /*value=*/nullptr, /*columns=*/nullptr, /*timestamp=*/nullptr, s,
|
||||
merge_context, max_covering_tombstone_seq, read_opts,
|
||||
true /* immutable_memtable */, nullptr, nullptr, false);
|
||||
true /* immutable_memtable */, nullptr, nullptr, false, blob_fetcher);
|
||||
if (done) {
|
||||
return true;
|
||||
}
|
||||
@@ -145,10 +147,11 @@ bool MemTableListVersion::GetFromHistory(
|
||||
const LookupKey& key, std::string* value, PinnableWideColumns* columns,
|
||||
std::string* timestamp, Status* s, MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
||||
const ReadOptions& read_opts, bool* is_blob_index) {
|
||||
const ReadOptions& read_opts, bool* is_blob_index,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
return GetFromList(&memlist_history_, key, value, columns, timestamp, s,
|
||||
merge_context, max_covering_tombstone_seq, seq, read_opts,
|
||||
nullptr /*read_callback*/, is_blob_index);
|
||||
nullptr /*read_callback*/, is_blob_index, blob_fetcher);
|
||||
}
|
||||
|
||||
bool MemTableListVersion::GetFromList(
|
||||
@@ -156,17 +159,18 @@ bool MemTableListVersion::GetFromList(
|
||||
std::string* value, PinnableWideColumns* columns, std::string* timestamp,
|
||||
Status* s, MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
||||
const ReadOptions& read_opts, ReadCallback* callback, bool* is_blob_index) {
|
||||
const ReadOptions& read_opts, ReadCallback* callback, bool* is_blob_index,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
*seq = kMaxSequenceNumber;
|
||||
|
||||
for (auto& memtable : *list) {
|
||||
assert(memtable->IsFragmentedRangeTombstonesConstructed());
|
||||
SequenceNumber current_seq = kMaxSequenceNumber;
|
||||
|
||||
bool done =
|
||||
memtable->Get(key, value, columns, timestamp, s, merge_context,
|
||||
max_covering_tombstone_seq, ¤t_seq, read_opts,
|
||||
true /* immutable_memtable */, callback, is_blob_index);
|
||||
bool done = memtable->Get(key, value, columns, timestamp, s, merge_context,
|
||||
max_covering_tombstone_seq, ¤t_seq,
|
||||
read_opts, true /* immutable_memtable */,
|
||||
callback, is_blob_index, true, blob_fetcher);
|
||||
if (*seq == kMaxSequenceNumber) {
|
||||
// Store the most recent sequence number of any operation on this key.
|
||||
// Since we only care about the most recent change, we only need to
|
||||
|
||||
+16
-9
@@ -61,29 +61,33 @@ class MemTableListVersion {
|
||||
MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
||||
const ReadOptions& read_opts, ReadCallback* callback = nullptr,
|
||||
bool* is_blob_index = nullptr);
|
||||
bool* is_blob_index = nullptr,
|
||||
const BlobFetcher* blob_fetcher = nullptr);
|
||||
|
||||
bool Get(const LookupKey& key, std::string* value,
|
||||
PinnableWideColumns* columns, std::string* timestamp, Status* s,
|
||||
MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
const ReadOptions& read_opts, ReadCallback* callback = nullptr,
|
||||
bool* is_blob_index = nullptr) {
|
||||
bool* is_blob_index = nullptr,
|
||||
const BlobFetcher* blob_fetcher = nullptr) {
|
||||
SequenceNumber seq;
|
||||
return Get(key, value, columns, timestamp, s, merge_context,
|
||||
max_covering_tombstone_seq, &seq, read_opts, callback,
|
||||
is_blob_index);
|
||||
is_blob_index, blob_fetcher);
|
||||
}
|
||||
|
||||
void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
ReadCallback* callback);
|
||||
ReadCallback* callback,
|
||||
const BlobFetcher* blob_fetcher = nullptr);
|
||||
|
||||
// Returns all the merge operands corresponding to the key by searching all
|
||||
// memtables starting from the most recent one.
|
||||
bool GetMergeOperands(const LookupKey& key, Status* s,
|
||||
MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
const ReadOptions& read_opts);
|
||||
const ReadOptions& read_opts,
|
||||
const BlobFetcher* blob_fetcher = nullptr);
|
||||
|
||||
// Similar to Get(), but searches the Memtable history of memtables that
|
||||
// have already been flushed. Should only be used from in-memory only
|
||||
@@ -94,17 +98,19 @@ class MemTableListVersion {
|
||||
Status* s, MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
SequenceNumber* seq, const ReadOptions& read_opts,
|
||||
bool* is_blob_index = nullptr);
|
||||
bool* is_blob_index = nullptr,
|
||||
const BlobFetcher* blob_fetcher = nullptr);
|
||||
bool GetFromHistory(const LookupKey& key, std::string* value,
|
||||
PinnableWideColumns* columns, std::string* timestamp,
|
||||
Status* s, MergeContext* merge_context,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
const ReadOptions& read_opts,
|
||||
bool* is_blob_index = nullptr) {
|
||||
bool* is_blob_index = nullptr,
|
||||
const BlobFetcher* blob_fetcher = nullptr) {
|
||||
SequenceNumber seq;
|
||||
return GetFromHistory(key, value, columns, timestamp, s, merge_context,
|
||||
max_covering_tombstone_seq, &seq, read_opts,
|
||||
is_blob_index);
|
||||
is_blob_index, blob_fetcher);
|
||||
}
|
||||
|
||||
Status AddRangeTombstoneIterators(const ReadOptions& read_opts, Arena* arena,
|
||||
@@ -188,7 +194,8 @@ class MemTableListVersion {
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
SequenceNumber* seq, const ReadOptions& read_opts,
|
||||
ReadCallback* callback = nullptr,
|
||||
bool* is_blob_index = nullptr);
|
||||
bool* is_blob_index = nullptr,
|
||||
const BlobFetcher* blob_fetcher = nullptr);
|
||||
|
||||
void AddMemTable(ReadOnlyMemTable* m);
|
||||
|
||||
|
||||
+12
-1
@@ -30,7 +30,7 @@ PinnedTableReader& PinnedTableReader::operator=(
|
||||
TableReader* r = other.reader_.load(std::memory_order_acquire);
|
||||
// Only read handle_ when reader_ is non-null. Pin() writes handle_ before
|
||||
// reader_ (with release), so a non-null reader_ guarantees handle_ is stable.
|
||||
// If reader_ is null, Pin() may be in progress — avoid reading handle_.
|
||||
// If reader_ is null, Pin() may be in progress -- avoid reading handle_.
|
||||
handle_ = (r != nullptr) ? other.handle_ : nullptr;
|
||||
reader_.store(r, std::memory_order_release);
|
||||
return *this;
|
||||
@@ -109,6 +109,17 @@ Status FileMetaData::UpdateBoundaries(const Slice& key, const Slice& value,
|
||||
|
||||
void VersionEdit::Clear() { *this = VersionEdit(); }
|
||||
|
||||
bool VersionEdit::ShouldEmitPerColumnFamilyRecoveryEdit(
|
||||
uint64_t current_log_number) const {
|
||||
return (HasLogNumber() && GetLogNumber() > current_log_number) ||
|
||||
NumEntries() > 0 || HasComparatorName() || HasPrevLogNumber() ||
|
||||
HasNextFile() || HasMaxColumnFamily() || HasMinLogNumberToKeep() ||
|
||||
HasLastSequence() || !GetCompactCursors().empty() || HasDbId() ||
|
||||
IsColumnFamilyManipulation() || IsInAtomicGroup() ||
|
||||
HasFullHistoryTsLow() || HasPersistUserDefinedTimestamps() ||
|
||||
HasSubcompactionProgress();
|
||||
}
|
||||
|
||||
bool VersionEdit::EncodeTo(std::string* dst,
|
||||
std::optional<size_t> ts_sz) const {
|
||||
assert(!IsNoManifestWriteDummy());
|
||||
|
||||
@@ -1016,6 +1016,10 @@ class VersionEdit {
|
||||
subcompaction_progress_.Clear();
|
||||
}
|
||||
|
||||
// Recovery-time per-column-family edits only need to be written when they
|
||||
// advance the CF's log number or carry some other manifest state.
|
||||
bool ShouldEmitPerColumnFamilyRecoveryEdit(uint64_t current_log_number) const;
|
||||
|
||||
// return true on success.
|
||||
// `ts_sz` is the size in bytes for the user-defined timestamp contained in
|
||||
// a user key. This argument is optional because it's only required for
|
||||
|
||||
@@ -35,6 +35,7 @@ void VersionEditHandlerBase::Iterate(log::Reader& reader,
|
||||
VersionEdit edit;
|
||||
s = edit.DecodeFrom(record);
|
||||
if (s.ok()) {
|
||||
last_valid_record_end_ = reader.LastRecordEnd();
|
||||
s = read_buffer_.AddEdit(&edit);
|
||||
}
|
||||
if (s.ok()) {
|
||||
@@ -460,6 +461,7 @@ void VersionEditHandler::CheckIterationResult(const log::Reader& reader,
|
||||
if (s->ok()) {
|
||||
version_set_->manifest_file_size_ = reader.GetReadOffset();
|
||||
assert(version_set_->manifest_file_size_ > 0);
|
||||
version_set_->manifest_last_valid_record_end_ = last_valid_record_end_;
|
||||
version_set_->next_file_number_.store(version_edit_params_.GetNextFile() +
|
||||
1);
|
||||
SequenceNumber last_seq = version_edit_params_.GetLastSequence();
|
||||
|
||||
@@ -31,6 +31,8 @@ class VersionEditHandlerBase {
|
||||
|
||||
AtomicGroupReadBuffer& GetReadBuffer() { return read_buffer_; }
|
||||
|
||||
uint64_t GetLastValidRecordEnd() const { return last_valid_record_end_; }
|
||||
|
||||
protected:
|
||||
explicit VersionEditHandlerBase(const ReadOptions& read_options,
|
||||
uint64_t max_read_size)
|
||||
@@ -52,6 +54,10 @@ class VersionEditHandlerBase {
|
||||
|
||||
const ReadOptions& read_options_;
|
||||
|
||||
// File offset at the end of the last successfully completed and
|
||||
// decoded logical record.
|
||||
uint64_t last_valid_record_end_ = 0;
|
||||
|
||||
private:
|
||||
AtomicGroupReadBuffer read_buffer_;
|
||||
const uint64_t max_manifest_read_size_;
|
||||
|
||||
+124
-24
@@ -2243,8 +2243,10 @@ void Version::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
|
||||
uint64_t oldest_time = std::numeric_limits<uint64_t>::max();
|
||||
for (int level = 0; level < storage_info_.num_non_empty_levels_; level++) {
|
||||
for (FileMetaData* meta : storage_info_.LevelFiles(level)) {
|
||||
assert(meta->fd.pinned_reader.Get() != nullptr);
|
||||
uint64_t file_creation_time = meta->TryGetFileCreationTime();
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"Version::GetCreationTimeOfOldestFile::FileCreationTime",
|
||||
&file_creation_time);
|
||||
if (file_creation_time == kUnknownFileCreationTime) {
|
||||
*creation_time = 0;
|
||||
return;
|
||||
@@ -4217,10 +4219,10 @@ void VersionStorageInfo::ComputeFilesMarkedForReadTriggeredCompaction(
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip files at the last non-empty level — there is no lower level to
|
||||
// Skip files at the last non-empty level -- there is no lower level to
|
||||
// compact into. Exception: L0 files are allowed even when L0 is the last
|
||||
// non-empty level, because in single-level universal or FIFO compaction, L0
|
||||
// files can be compacted together (L0 → L0).
|
||||
// files can be compacted together (L0 -> L0).
|
||||
int last_non_empty = num_non_empty_levels_ - 1;
|
||||
|
||||
for (int level = 0; level < num_levels(); level++) {
|
||||
@@ -5618,6 +5620,7 @@ VersionSet::VersionSet(
|
||||
prev_log_number_(0),
|
||||
current_version_number_(0),
|
||||
manifest_file_size_(0),
|
||||
manifest_last_valid_record_end_(0),
|
||||
last_compacted_manifest_file_size_(0),
|
||||
file_options_(storage_options),
|
||||
block_cache_tracer_(block_cache_tracer),
|
||||
@@ -5689,7 +5692,7 @@ Status VersionSet::Close(FSDirectory* db_dir, InstrumentedMutex* mu) {
|
||||
content_manifest_name, fs_->OptimizeForManifestRead(file_options_),
|
||||
&manifest_file, nullptr);
|
||||
if (!content_io_s.ok()) {
|
||||
// Surface I/O errors to the caller — users who call DB::Close() and
|
||||
// Surface I/O errors to the caller -- users who call DB::Close() and
|
||||
// check the status should know about filesystem problems.
|
||||
s = content_io_s;
|
||||
ROCKS_LOG_ERROR(db_options_->info_log,
|
||||
@@ -5741,7 +5744,7 @@ Status VersionSet::Close(FSDirectory* db_dir, InstrumentedMutex* mu) {
|
||||
corrupt_io_s.PermitUncheckedError();
|
||||
io_error_info.io_status.PermitUncheckedError();
|
||||
if (content_check == 0) {
|
||||
// First check failed — rewrite and verify again
|
||||
// First check failed -- rewrite and verify again
|
||||
ROCKS_LOG_ERROR(db_options_->info_log,
|
||||
"MANIFEST content verification on Close failed, "
|
||||
"filename %s, rewriting manifest\n",
|
||||
@@ -5752,7 +5755,7 @@ Status VersionSet::Close(FSDirectory* db_dir, InstrumentedMutex* mu) {
|
||||
s = LogAndApply(cfd, ReadOptions(), WriteOptions(), &recovery_edit, mu,
|
||||
db_dir);
|
||||
} else {
|
||||
// Rewritten manifest is also corrupt — likely a recurring filesystem
|
||||
// Rewritten manifest is also corrupt -- likely a recurring filesystem
|
||||
// issue. Surface it so DB::Close() callers can detect the problem.
|
||||
ROCKS_LOG_ERROR(db_options_->info_log,
|
||||
"MANIFEST content verification on Close failed again "
|
||||
@@ -5827,6 +5830,7 @@ void VersionSet::Reset() {
|
||||
current_version_number_ = 0;
|
||||
manifest_writers_.clear();
|
||||
manifest_file_size_ = 0;
|
||||
manifest_last_valid_record_end_ = 0;
|
||||
last_compacted_manifest_file_size_ = 0;
|
||||
TuneMaxManifestFileSize();
|
||||
obsolete_files_.clear();
|
||||
@@ -6161,11 +6165,7 @@ Status VersionSet::ProcessManifestWrites(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FileOptions opt_file_opts = fs_->OptimizeForManifestWrite(file_options_);
|
||||
// DB option (in file_options_) takes precedence when not kUnknown
|
||||
if (file_options_.temperature != Temperature::kUnknown) {
|
||||
opt_file_opts.temperature = file_options_.temperature;
|
||||
}
|
||||
FileOptions opt_file_opts = GetFileOptionsForManifestWrite();
|
||||
mu->Unlock();
|
||||
TEST_SYNC_POINT("VersionSet::LogAndApply:WriteManifestStart");
|
||||
TEST_SYNC_POINT_CALLBACK("VersionSet::LogAndApply:WriteManifest", nullptr);
|
||||
@@ -6200,16 +6200,9 @@ Status VersionSet::ProcessManifestWrites(
|
||||
io_s = NewWritableFile(fs_.get(), descriptor_fname, &descriptor_file,
|
||||
opt_file_opts);
|
||||
if (io_s.ok()) {
|
||||
descriptor_file->SetPreallocationBlockSize(manifest_preallocation_size);
|
||||
FileTypeSet tmp_set = db_options_->checksum_handoff_file_types;
|
||||
std::unique_ptr<WritableFileWriter> file_writer(new WritableFileWriter(
|
||||
std::move(descriptor_file), descriptor_fname, opt_file_opts, clock_,
|
||||
io_tracer_, nullptr, Histograms::HISTOGRAM_ENUM_MAX /* hist_type */,
|
||||
db_options_->listeners, nullptr,
|
||||
tmp_set.Contains(FileType::kDescriptorFile),
|
||||
tmp_set.Contains(FileType::kDescriptorFile)));
|
||||
new_desc_log_ptr.reset(
|
||||
new log::Writer(std::move(file_writer), 0, false));
|
||||
new_desc_log_ptr =
|
||||
CreateManifestWriter(std::move(descriptor_file), descriptor_fname,
|
||||
opt_file_opts, manifest_preallocation_size);
|
||||
raw_desc_log_ptr = new_desc_log_ptr.get();
|
||||
s = WriteCurrentStateToManifest(write_options, curr_state,
|
||||
wal_additions, raw_desc_log_ptr, io_s);
|
||||
@@ -6259,6 +6252,7 @@ Status VersionSet::ProcessManifestWrites(
|
||||
}
|
||||
++idx;
|
||||
#endif /* !NDEBUG */
|
||||
TEST_SYNC_POINT("VersionSet::ProcessManifestWrites:AddRecord");
|
||||
io_s = raw_desc_log_ptr->AddRecord(write_options, record);
|
||||
if (!io_s.ok()) {
|
||||
s = io_s;
|
||||
@@ -6680,6 +6674,106 @@ Status VersionSet::LogAndApplyHelper(ColumnFamilyData* cfd,
|
||||
return builder ? builder->Apply(edit) : Status::OK();
|
||||
}
|
||||
|
||||
FileOptions VersionSet::GetFileOptionsForManifestWrite() const {
|
||||
FileOptions opt_file_opts = fs_->OptimizeForManifestWrite(file_options_);
|
||||
// DB-configured temperature wins over the FS's default (when not kUnknown).
|
||||
if (file_options_.temperature != Temperature::kUnknown) {
|
||||
opt_file_opts.temperature = file_options_.temperature;
|
||||
}
|
||||
return opt_file_opts;
|
||||
}
|
||||
|
||||
std::unique_ptr<log::Writer> VersionSet::CreateManifestWriter(
|
||||
std::unique_ptr<FSWritableFile> file, const std::string& fname,
|
||||
const FileOptions& opts, uint64_t preallocation_size,
|
||||
uint64_t initial_file_size) const {
|
||||
file->SetPreallocationBlockSize(preallocation_size);
|
||||
FileTypeSet tmp_set = db_options_->checksum_handoff_file_types;
|
||||
auto file_writer = std::make_unique<WritableFileWriter>(
|
||||
std::move(file), fname, opts, clock_, io_tracer_,
|
||||
/*stats=*/nullptr, Histograms::HISTOGRAM_ENUM_MAX, db_options_->listeners,
|
||||
/*file_checksum_gen_factory=*/nullptr,
|
||||
tmp_set.Contains(FileType::kDescriptorFile),
|
||||
tmp_set.Contains(FileType::kDescriptorFile), initial_file_size);
|
||||
const size_t block_offset = initial_file_size % log::kBlockSize;
|
||||
return std::make_unique<log::Writer>(
|
||||
std::move(file_writer), /*log_number=*/0, /*recycle_log_files=*/false,
|
||||
/*manual_flush=*/false, kNoCompression,
|
||||
/*track_and_verify_wals=*/false, block_offset);
|
||||
}
|
||||
|
||||
Status VersionSet::ReopenManifestForAppend(const std::string& manifest_path) {
|
||||
assert(db_options_->reuse_manifest_on_open);
|
||||
assert(manifest_last_valid_record_end_ > 0);
|
||||
|
||||
// Disabled under best_efforts_recovery: that mode rebuilds the
|
||||
// MANIFEST + CURRENT from scratch via the side-effect of
|
||||
// LogAndApplyForRecovery emitting an edit. Reusing the (possibly
|
||||
// stale) prior MANIFEST contradicts the salvage contract.
|
||||
if (db_options_->best_efforts_recovery) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
FileOptions opt_file_opts = GetFileOptionsForManifestWrite();
|
||||
|
||||
// Bail if the on-disk size diverges from what Recover's Reader
|
||||
// consumed -- likely a torn tail from a prior crash; mid-block append
|
||||
// would mis-frame the record stream.
|
||||
uint64_t physical_size = 0;
|
||||
IOStatus stat_s = fs_->GetFileSize(manifest_path, IOOptions(), &physical_size,
|
||||
/*dbg=*/nullptr);
|
||||
if (!stat_s.ok() || physical_size != manifest_last_valid_record_end_) {
|
||||
ROCKS_LOG_WARN(db_options_->info_log,
|
||||
"reuse_manifest_on_open: physical size %" PRIu64
|
||||
" != last valid record end %" PRIu64
|
||||
" (tail corruption?); falling back to fresh MANIFEST",
|
||||
physical_size, manifest_last_valid_record_end_);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
// Direct-writes path is hard to reason about for an already-populated
|
||||
// file (alignment + tail-pad). POSIX's OptimizeForManifestWrite forces
|
||||
// it off, but custom FileSystems may not.
|
||||
if (opt_file_opts.use_direct_writes) {
|
||||
ROCKS_LOG_WARN(db_options_->info_log,
|
||||
"reuse_manifest_on_open: direct writes enabled; "
|
||||
"falling back to fresh MANIFEST");
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
std::unique_ptr<FSWritableFile> manifest_file;
|
||||
IOStatus io_s = fs_->ReopenWritableFile(manifest_path, opt_file_opts,
|
||||
&manifest_file, /*dbg=*/nullptr);
|
||||
if (!io_s.ok()) {
|
||||
ROCKS_LOG_WARN(db_options_->info_log,
|
||||
"Failed to reopen MANIFEST for append: %s",
|
||||
io_s.ToString().c_str());
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
const uint64_t reopened_size =
|
||||
manifest_file->GetFileSize(opt_file_opts.io_options, /*dbg=*/nullptr);
|
||||
if (reopened_size != manifest_last_valid_record_end_) {
|
||||
ROCKS_LOG_WARN(db_options_->info_log,
|
||||
"reuse_manifest_on_open: reopened handle size %" PRIu64
|
||||
" != last valid record end %" PRIu64
|
||||
"; falling back to fresh MANIFEST",
|
||||
reopened_size, manifest_last_valid_record_end_);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
descriptor_log_ = CreateManifestWriter(
|
||||
std::move(manifest_file), manifest_path, opt_file_opts,
|
||||
manifest_preallocation_size_, reopened_size);
|
||||
|
||||
ROCKS_LOG_INFO(db_options_->info_log,
|
||||
"Reusing existing MANIFEST file: %s (valid data size: %" PRIu64
|
||||
")",
|
||||
manifest_path.c_str(), manifest_last_valid_record_end_);
|
||||
TEST_SYNC_POINT("VersionSet::ReopenManifestForAppend:Reopened");
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status VersionSet::Recover(
|
||||
const std::vector<ColumnFamilyDescriptor>& column_families, bool read_only,
|
||||
std::string* db_id, bool no_error_if_files_missing, bool is_retry,
|
||||
@@ -6767,6 +6861,11 @@ Status VersionSet::Recover(
|
||||
}
|
||||
}
|
||||
|
||||
if (s.ok() && !read_only && db_options_->reuse_manifest_on_open &&
|
||||
manifest_last_valid_record_end_ > 0) {
|
||||
s = ReopenManifestForAppend(manifest_path);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -8064,11 +8163,12 @@ ReactiveVersionSet::ReactiveVersionSet(
|
||||
const MutableDBOptions& mutable_db_options,
|
||||
const FileOptions& _file_options, Cache* table_cache,
|
||||
WriteBufferManager* write_buffer_manager, WriteController* write_controller,
|
||||
const std::shared_ptr<IOTracer>& io_tracer)
|
||||
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
|
||||
const std::string& db_session_id)
|
||||
: VersionSet(dbname, imm_db_options, mutable_db_options, _file_options,
|
||||
table_cache, write_buffer_manager, write_controller,
|
||||
/*block_cache_tracer=*/nullptr, io_tracer, /*db_id*/ "",
|
||||
/*db_session_id*/ "", /*daily_offpeak_time_utc*/ "",
|
||||
/*block_cache_tracer=*/nullptr, io_tracer, db_id,
|
||||
db_session_id, /*daily_offpeak_time_utc*/ "",
|
||||
/*error_handler=*/nullptr, /*unchanging=*/false) {}
|
||||
|
||||
ReactiveVersionSet::~ReactiveVersionSet() = default;
|
||||
|
||||
+41
-1
@@ -1719,6 +1719,33 @@ class VersionSet {
|
||||
const std::unordered_map<uint32_t, MutableCFState>& curr_state,
|
||||
const VersionEdit& wal_additions, log::Writer* log, IOStatus& io_s);
|
||||
|
||||
// Reopen the existing MANIFEST file for append at the end of Recover()
|
||||
// when reuse_manifest_on_open is set, so the next LogAndApply appends
|
||||
// to it instead of creating a fresh MANIFEST. Falls back to OK
|
||||
// (descriptor_log_ stays null, next LogAndApply creates a fresh
|
||||
// MANIFEST as before) if ReopenWritableFile fails.
|
||||
Status ReopenManifestForAppend(const std::string& manifest_path);
|
||||
|
||||
// FileOptions for MANIFEST writes -- applies the FS's
|
||||
// OptimizeForManifestWrite tuning, then re-applies the user-configured
|
||||
// temperature so a custom FS can't override it.
|
||||
FileOptions GetFileOptionsForManifestWrite() const;
|
||||
|
||||
// Create a log::Writer over the given FSWritableFile with all standard
|
||||
// MANIFEST setup applied (preallocation block size, checksum-handoff
|
||||
// classification, listeners, etc.). preallocation_size should be a
|
||||
// snapshot of manifest_preallocation_size_ taken under the DB mutex
|
||||
// (the fresh-path call site reads the field before releasing mu).
|
||||
// When initial_file_size > 0 the writer is treated as resuming over
|
||||
// an already-populated file: WritableFileWriter is constructed with the
|
||||
// existing size so size accounting is correct, and the log writer's
|
||||
// initial_block_offset aligns to the existing file's tail within the
|
||||
// current 32 KiB block.
|
||||
std::unique_ptr<log::Writer> CreateManifestWriter(
|
||||
std::unique_ptr<FSWritableFile> file, const std::string& fname,
|
||||
const FileOptions& opts, uint64_t preallocation_size,
|
||||
uint64_t initial_file_size = 0) const;
|
||||
|
||||
void AppendVersion(ColumnFamilyData* column_family_data, Version* v);
|
||||
|
||||
ColumnFamilyData* CreateColumnFamily(const ColumnFamilyOptions& cf_options,
|
||||
@@ -1788,6 +1815,17 @@ class VersionSet {
|
||||
// Current size of manifest file
|
||||
uint64_t manifest_file_size_;
|
||||
|
||||
// File offset at the end of the last successfully completed logical
|
||||
// record during MANIFEST recovery. Unlike manifest_file_size_ (the
|
||||
// reader's I/O high-water mark, which includes any tolerated tail
|
||||
// garbage), this value points to the byte after the last valid record.
|
||||
// Used by ReopenManifestForAppend to detect intra-block tail
|
||||
// corruption that doesn't extend the physical file size.
|
||||
// manifest_file_size_ is kept separate because it is used for
|
||||
// rotation decisions (ProcessManifestWrites), close-time verification
|
||||
// (Close), and backup metadata.
|
||||
uint64_t manifest_last_valid_record_end_;
|
||||
|
||||
// Size of the populated manifest file last time it was re-written from
|
||||
// scratch.
|
||||
uint64_t last_compacted_manifest_file_size_;
|
||||
@@ -1860,7 +1898,9 @@ class ReactiveVersionSet : public VersionSet {
|
||||
const FileOptions& _file_options, Cache* table_cache,
|
||||
WriteBufferManager* write_buffer_manager,
|
||||
WriteController* write_controller,
|
||||
const std::shared_ptr<IOTracer>& io_tracer);
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::string& db_id,
|
||||
const std::string& db_session_id);
|
||||
|
||||
~ReactiveVersionSet() override;
|
||||
|
||||
|
||||
@@ -1203,7 +1203,7 @@ class VersionSetTestBase {
|
||||
reactive_versions_ = std::make_shared<ReactiveVersionSet>(
|
||||
dbname_, &imm_db_options_, mutable_db_options_, env_options_,
|
||||
table_cache_.get(), &write_buffer_manager_, &write_controller_,
|
||||
nullptr);
|
||||
/*io_tracer=*/nullptr, /*db_id=*/"", /*db_session_id=*/"");
|
||||
imm_db_options_.db_paths.emplace_back(dbname_,
|
||||
std::numeric_limits<uint64_t>::max());
|
||||
}
|
||||
@@ -2440,7 +2440,7 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseClean) {
|
||||
// Verify content validation actually ran
|
||||
ASSERT_TRUE(content_validation_ran);
|
||||
|
||||
// No corruption — counter should be zero
|
||||
// No corruption -- counter should be zero
|
||||
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
|
||||
|
||||
// Manifest path should be unchanged (no rotation)
|
||||
@@ -2513,7 +2513,7 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseCorruptRecord) {
|
||||
|
||||
TEST_F(VersionSetTest, ManifestContentValidationOnCloseDisabled) {
|
||||
// Default (option disabled), corrupt manifest after writer close,
|
||||
// verify no rotation occurred — corrupt manifest persists.
|
||||
// verify no rotation occurred -- corrupt manifest persists.
|
||||
NewDB();
|
||||
auto stats = CreateDBStatistics();
|
||||
imm_db_options_.statistics = stats;
|
||||
@@ -2538,7 +2538,7 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseDisabled) {
|
||||
|
||||
ASSERT_FALSE(content_validation_ran);
|
||||
|
||||
// Validation disabled — counter should be zero
|
||||
// Validation disabled -- counter should be zero
|
||||
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
|
||||
|
||||
// Manifest path should be unchanged (no rotation since validation is off)
|
||||
@@ -2585,7 +2585,7 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseSizeCheckFails) {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
|
||||
// Size check caught the issue; content validation on rewritten manifest
|
||||
// should pass — no content validation failure recorded
|
||||
// should pass -- no content validation failure recorded
|
||||
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
|
||||
|
||||
// Size check should have triggered rotation
|
||||
@@ -2697,7 +2697,7 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseOpenFails) {
|
||||
|
||||
ASSERT_TRUE(close_s.IsIOError()) << close_s.ToString();
|
||||
|
||||
// File couldn't be opened — no content validation ran
|
||||
// File couldn't be opened -- no content validation ran
|
||||
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ class DBWideBasicTest : public DBTestBase {
|
||||
return wide_column_test_util::GetOptionsForBlobTest(GetDefaultOptions());
|
||||
}
|
||||
|
||||
Options GetDirectWriteOptions() {
|
||||
return wide_column_test_util::GetDirectWriteOptions(GetDefaultOptions());
|
||||
}
|
||||
|
||||
// Helper: runs the EntityBlobAfterFlush test logic with the given options.
|
||||
void RunEntityBlobAfterFlush(const Options& options);
|
||||
|
||||
@@ -2159,6 +2163,57 @@ TEST_F(DBWideBasicTest, MultiGetEntityWithBlobResolution) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBWideBasicTest,
|
||||
MultiGetBlobBackedEntityDirectWriteMemtableBatchLookup) {
|
||||
// Goal: force the memtable batch MultiGet optimization to read a direct-write
|
||||
// blob-backed entity from the active memtable. One key exercises merge
|
||||
// resolution against a blob-backed entity base, which requires Saver to carry
|
||||
// the BlobFetcher through the batched callback path. The second key proves
|
||||
// the non-merge default-column path still resolves correctly in the same
|
||||
// batched request.
|
||||
Options options = GetDirectWriteOptions();
|
||||
options.min_blob_size = 50;
|
||||
options.memtable_batch_lookup_optimization = true;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string merge_key = "multiget_direct_write_merge_key";
|
||||
const std::string plain_key = "multiget_direct_write_plain_key";
|
||||
const std::string default_value = GenerateLargeValue(100, 'd');
|
||||
const std::string other_default_value = GenerateLargeValue(110, 'p');
|
||||
const std::string large_value = GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = GenerateSmallValue();
|
||||
const std::string merge_operand = "tail";
|
||||
|
||||
WideColumns merge_columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
WideColumns plain_columns{{kDefaultWideColumnName, other_default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
|
||||
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
|
||||
merge_key, merge_columns));
|
||||
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
|
||||
plain_key, plain_columns));
|
||||
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), merge_key,
|
||||
merge_operand));
|
||||
|
||||
std::array<Slice, 2> keys{{merge_key, plain_key}};
|
||||
std::array<PinnableSlice, 2> values;
|
||||
std::array<Status, 2> statuses;
|
||||
|
||||
db_->MultiGet(ReadOptions(), db_->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0], default_value + "|" + merge_operand);
|
||||
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1], other_default_value);
|
||||
}
|
||||
|
||||
void DBWideBasicTest::RunEntityBlobAfterFlush(const Options& options) {
|
||||
Reopen(options);
|
||||
|
||||
@@ -2776,7 +2831,7 @@ TEST_F(DBWideBasicTest, EntityBlobBlockCacheTierGet) {
|
||||
// Reopen to clear any caches
|
||||
Reopen(options);
|
||||
|
||||
// Read with kBlockCacheTier — blob is not in cache
|
||||
// Read with kBlockCacheTier -- blob is not in cache
|
||||
ReadOptions read_opts;
|
||||
read_opts.read_tier = kBlockCacheTier;
|
||||
PinnableSlice result;
|
||||
@@ -2909,6 +2964,347 @@ TEST_F(DBWideBasicTest, MergeEntityWithBlobColumns) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBWideBasicTest, GetMergeOperandsWithBlobBackedEntityDefaultColumn) {
|
||||
// Goal: cover both GetMergeOperands code paths that read a compacted V2
|
||||
// wide-column entity whose default column was moved to a blob file. The
|
||||
// first read exercises the base-value path directly, then a merge operand is
|
||||
// added so the second read exercises the merge-plus-base path.
|
||||
Options options = GetBlobTestOptions();
|
||||
options.min_blob_size = 50;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string key = "merge_operands_blob_entity";
|
||||
const std::string default_value = GenerateLargeValue(100, 'd');
|
||||
const std::string large_value = GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = GenerateSmallValue();
|
||||
const std::string merge_operand = "suffix";
|
||||
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_FALSE(GetBlobFileNumbers().empty());
|
||||
|
||||
{
|
||||
// The compacted V2 entity becomes the first operand when there are no
|
||||
// newer merge operands above it.
|
||||
GetMergeOperandsOptions get_merge_opts;
|
||||
get_merge_opts.expected_max_number_of_operands = 1;
|
||||
|
||||
std::array<PinnableSlice, 2> merge_operands;
|
||||
int number_of_operands = 0;
|
||||
|
||||
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
key, merge_operands.data(), &get_merge_opts,
|
||||
&number_of_operands));
|
||||
ASSERT_EQ(number_of_operands, 1);
|
||||
ASSERT_EQ(merge_operands[0], default_value);
|
||||
}
|
||||
|
||||
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
|
||||
merge_operand));
|
||||
|
||||
{
|
||||
// After a merge is added, GetMergeOperands must still resolve the blob
|
||||
// backed base default column while traversing the older base entry below
|
||||
// the newer merge operand.
|
||||
GetMergeOperandsOptions get_merge_opts;
|
||||
get_merge_opts.expected_max_number_of_operands = 2;
|
||||
|
||||
std::array<PinnableSlice, 2> merge_operands;
|
||||
int number_of_operands = 0;
|
||||
|
||||
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
key, merge_operands.data(), &get_merge_opts,
|
||||
&number_of_operands));
|
||||
ASSERT_EQ(number_of_operands, 2);
|
||||
ASSERT_EQ(merge_operands[0], default_value);
|
||||
ASSERT_EQ(merge_operands[1], merge_operand);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBWideBasicTest,
|
||||
GetMergeOperandsWithBlobBackedEntityDefaultColumnDirectWriteMemtable) {
|
||||
// Goal: cover the pre-flush memtable path when blob direct write stores a
|
||||
// blob-backed V2 entity in memory. The first GetMergeOperands call exercises
|
||||
// the no-merge base-operand path. After adding a merge, DB::Get exercises
|
||||
// merge resolution against the same memtable entity, and a second
|
||||
// GetMergeOperands call exercises the merge-plus-base path.
|
||||
Options options = GetDirectWriteOptions();
|
||||
options.min_blob_size = 50;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string key = "direct_write_merge_operands_blob_entity";
|
||||
const std::string default_value = GenerateLargeValue(100, 'd');
|
||||
const std::string large_value = GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = GenerateSmallValue();
|
||||
const std::string merge_operand = "suffix";
|
||||
const std::string expected_merged = default_value + "|" + merge_operand;
|
||||
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
|
||||
{
|
||||
GetMergeOperandsOptions get_merge_opts;
|
||||
get_merge_opts.expected_max_number_of_operands = 1;
|
||||
|
||||
std::array<PinnableSlice, 2> merge_operands;
|
||||
int number_of_operands = 0;
|
||||
|
||||
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
key, merge_operands.data(), &get_merge_opts,
|
||||
&number_of_operands));
|
||||
ASSERT_EQ(number_of_operands, 1);
|
||||
ASSERT_EQ(merge_operands[0], default_value);
|
||||
}
|
||||
|
||||
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
|
||||
merge_operand));
|
||||
|
||||
{
|
||||
PinnableSlice result;
|
||||
ASSERT_OK(
|
||||
db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &result));
|
||||
ASSERT_EQ(result, expected_merged);
|
||||
}
|
||||
|
||||
{
|
||||
GetMergeOperandsOptions get_merge_opts;
|
||||
get_merge_opts.expected_max_number_of_operands = 2;
|
||||
|
||||
std::array<PinnableSlice, 2> merge_operands;
|
||||
int number_of_operands = 0;
|
||||
|
||||
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
key, merge_operands.data(), &get_merge_opts,
|
||||
&number_of_operands));
|
||||
ASSERT_EQ(number_of_operands, 2);
|
||||
ASSERT_EQ(merge_operands[0], default_value);
|
||||
ASSERT_EQ(merge_operands[1], merge_operand);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBWideBasicTest,
|
||||
GetAndGetEntityWithBlobBackedDefaultColumnDirectWriteMemtableReadOnly) {
|
||||
// Goal: cover the read-only reopen path after writing a blob-backed
|
||||
// direct-write entity without manually flushing it first. The test checks
|
||||
// both `Get()`, which must resolve the default-column blob reference, and
|
||||
// `GetEntity()`, which must eagerly resolve the entity's unresolved blob
|
||||
// columns before returning them to the caller.
|
||||
Options options = GetDirectWriteOptions();
|
||||
options.min_blob_size = 50;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string key = "readonly_direct_write_memtable_entity";
|
||||
const std::string default_value = GenerateLargeValue(100, 'd');
|
||||
const std::string large_value = GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = GenerateSmallValue();
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
|
||||
Close();
|
||||
options.avoid_flush_during_recovery = true;
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
|
||||
{
|
||||
PinnableSlice result;
|
||||
ASSERT_OK(
|
||||
db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &result));
|
||||
ASSERT_EQ(result, default_value);
|
||||
}
|
||||
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), key,
|
||||
&result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBWideBasicTest, ReadOnlyDirectWriteMemtableBlobBlockCacheTier) {
|
||||
// Goal: cover the read-only memtable path under kBlockCacheTier. The test
|
||||
// keeps a blob-backed direct-write entity in WAL-backed recovery state so
|
||||
// resolving either Get() or GetEntity() would require blob I/O, which must
|
||||
// surface as Incomplete rather than silently reading the blob file.
|
||||
Options options = GetDirectWriteOptions();
|
||||
options.min_blob_size = 50;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string key = "readonly_direct_write_memtable_block_cache_tier";
|
||||
const std::string default_value = GenerateLargeValue(100, 'd');
|
||||
const std::string large_value = GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = GenerateSmallValue();
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
|
||||
Close();
|
||||
options.avoid_flush_during_recovery = true;
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
|
||||
ReadOptions read_opts;
|
||||
read_opts.read_tier = kBlockCacheTier;
|
||||
|
||||
{
|
||||
PinnableSlice result;
|
||||
const Status s =
|
||||
db_->Get(read_opts, db_->DefaultColumnFamily(), key, &result);
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
ASSERT_TRUE(result.empty());
|
||||
}
|
||||
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
const Status s =
|
||||
db_->GetEntity(read_opts, db_->DefaultColumnFamily(), key, &result);
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
ASSERT_TRUE(result.columns().empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBWideBasicTest,
|
||||
GetMergeOperandsWithBlobBackedEntityDefaultColumnReadOnly) {
|
||||
// Goal: exercise OpenForReadOnly on a blob-backed V2 entity base in SST with
|
||||
// a newer merge operand in a separate SST. The read-only DB must resolve the
|
||||
// blob-backed default column for both Get() and GetMergeOperands() without
|
||||
// relying on mutable memtable state.
|
||||
Options options = GetBlobTestOptions();
|
||||
options.min_blob_size = 50;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string key = "readonly_merge_operands_blob_entity";
|
||||
const std::string default_value = GenerateLargeValue(100, 'd');
|
||||
const std::string large_value = GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = GenerateSmallValue();
|
||||
const std::string merge_operand = "suffix";
|
||||
const std::string expected_merged = default_value + "|" + merge_operand;
|
||||
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_FALSE(GetBlobFileNumbers().empty());
|
||||
|
||||
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
|
||||
merge_operand));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
Close();
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
|
||||
{
|
||||
PinnableSlice result;
|
||||
ASSERT_OK(
|
||||
db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &result));
|
||||
ASSERT_EQ(result, expected_merged);
|
||||
}
|
||||
|
||||
{
|
||||
GetMergeOperandsOptions get_merge_opts;
|
||||
get_merge_opts.expected_max_number_of_operands = 2;
|
||||
|
||||
std::array<PinnableSlice, 2> merge_operands;
|
||||
int number_of_operands = 0;
|
||||
|
||||
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
key, merge_operands.data(), &get_merge_opts,
|
||||
&number_of_operands));
|
||||
ASSERT_EQ(number_of_operands, 2);
|
||||
ASSERT_EQ(merge_operands[0], default_value);
|
||||
ASSERT_EQ(merge_operands[1], merge_operand);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBWideBasicTest,
|
||||
GetMergeOperandsWithBlobBackedEntityDefaultColumnBlockCacheTier) {
|
||||
// Goal: prove the SST-backed GetMergeOperands path propagates Incomplete
|
||||
// when resolving a blob-backed default column would require I/O. One key
|
||||
// covers the direct base-entity path, and the other covers the merge-plus-
|
||||
// base path with the merge operand flushed to a newer SST.
|
||||
Options options = GetBlobTestOptions();
|
||||
options.min_blob_size = 50;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string base_key = "merge_operands_blob_entity_cache_base";
|
||||
const std::string merge_key = "merge_operands_blob_entity_cache_merge";
|
||||
const std::string default_value = GenerateLargeValue(100, 'd');
|
||||
const std::string large_value = GenerateLargeValue(120, 'l');
|
||||
const std::string small_value = GenerateSmallValue();
|
||||
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"col_large", large_value},
|
||||
{"col_small", small_value}};
|
||||
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), base_key,
|
||||
columns));
|
||||
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
|
||||
merge_key, columns));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), merge_key,
|
||||
"tail"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Reopen to clear any cached blob readers/values so kBlockCacheTier must
|
||||
// report Incomplete instead of succeeding through a warm cache.
|
||||
Reopen(options);
|
||||
|
||||
ReadOptions read_opts;
|
||||
read_opts.read_tier = kBlockCacheTier;
|
||||
|
||||
{
|
||||
GetMergeOperandsOptions get_merge_opts;
|
||||
get_merge_opts.expected_max_number_of_operands = 1;
|
||||
|
||||
std::array<PinnableSlice, 2> merge_operands;
|
||||
int number_of_operands = 0;
|
||||
|
||||
const Status s = db_->GetMergeOperands(
|
||||
read_opts, db_->DefaultColumnFamily(), base_key, merge_operands.data(),
|
||||
&get_merge_opts, &number_of_operands);
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
}
|
||||
|
||||
{
|
||||
GetMergeOperandsOptions get_merge_opts;
|
||||
get_merge_opts.expected_max_number_of_operands = 2;
|
||||
|
||||
std::array<PinnableSlice, 2> merge_operands;
|
||||
int number_of_operands = 0;
|
||||
|
||||
const Status s = db_->GetMergeOperands(
|
||||
read_opts, db_->DefaultColumnFamily(), merge_key, merge_operands.data(),
|
||||
&get_merge_opts, &number_of_operands);
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBWideBasicTest, MergeEntityWithBlobColumnsNoDefault) {
|
||||
// Test: Merge on a V2 entity without a default column. The merge result
|
||||
// should produce a valid entity with all original columns plus a default
|
||||
@@ -2998,7 +3394,7 @@ TEST_F(DBWideBasicTest, MergeEntityWithBlobColumnsBlockCacheTier) {
|
||||
// Reopen to clear caches
|
||||
Reopen(options);
|
||||
|
||||
// Read with kBlockCacheTier — blob not in cache, merge needs blob resolution
|
||||
// Read with kBlockCacheTier -- blob not in cache, merge needs blob resolution
|
||||
ReadOptions read_opts;
|
||||
read_opts.read_tier = kBlockCacheTier;
|
||||
PinnableSlice result;
|
||||
|
||||
@@ -59,7 +59,7 @@ Status ReadPathBlobResolver::ResolveColumn(size_t column_index,
|
||||
blob_resolver_util::FindBlobColumn(blob_columns_, column_index);
|
||||
|
||||
if (blob_index_ptr == nullptr) {
|
||||
// Inline column — return the value directly
|
||||
// Inline column -- return the value directly
|
||||
*resolved_value = (*columns_)[column_index].value();
|
||||
} else {
|
||||
// Check if already resolved
|
||||
|
||||
@@ -153,6 +153,8 @@ class WideColumnSerialization {
|
||||
// value() is initially the serialized BlobIndex bytes from the entity.
|
||||
// blob_columns: receives (column_index, blob_index) pairs identifying which
|
||||
// entries in `columns` are blob references and their decoded BlobIndex data.
|
||||
// For valid input, this must agree with HasBlobColumns() about whether blob
|
||||
// column references are present.
|
||||
static Status DeserializeV2(
|
||||
Slice& input, std::vector<WideColumn>& columns,
|
||||
std::vector<std::pair<size_t, BlobIndex>>& blob_columns);
|
||||
@@ -160,6 +162,8 @@ class WideColumnSerialization {
|
||||
// Check if the serialized entity has any blob column references.
|
||||
// Sets *has_blob_columns to true if version >= 2 and at least one column
|
||||
// has blob type; false otherwise.
|
||||
// For valid input, this must agree with DeserializeV2() about whether blob
|
||||
// column references are present.
|
||||
// Returns Status::Corruption on decode errors.
|
||||
static Status HasBlobColumns(const Slice& input, bool& has_blob_columns);
|
||||
|
||||
|
||||
@@ -391,7 +391,7 @@ static BlobIndex MakeBlobIndex(uint64_t file_number, uint64_t offset,
|
||||
return bi;
|
||||
}
|
||||
|
||||
// Helper: V2 serialize → DeserializeV2 round-trip, returning
|
||||
// Helper: V2 serialize -> DeserializeV2 round-trip, returning
|
||||
// deserialized columns and blob column info.
|
||||
static void V2SerializeAndDeserialize(
|
||||
const std::vector<std::pair<std::string, std::string>>& columns,
|
||||
@@ -798,7 +798,7 @@ TEST_F(WideColumnSerializationTest, RandomizedSerializeDeserializeRoundTrip) {
|
||||
}
|
||||
}
|
||||
|
||||
// V2 serialize → DeserializeV2 round-trip
|
||||
// V2 serialize -> DeserializeV2 round-trip
|
||||
std::string serialized;
|
||||
std::vector<WideColumn> deserialized;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_out;
|
||||
|
||||
@@ -279,7 +279,9 @@ DECLARE_bool(use_full_merge_v1);
|
||||
DECLARE_int32(sync_wal_one_in);
|
||||
DECLARE_bool(avoid_unnecessary_blocking_io);
|
||||
DECLARE_bool(write_dbid_to_manifest);
|
||||
DECLARE_bool(optimize_manifest_for_recovery);
|
||||
DECLARE_bool(write_identity_file);
|
||||
DECLARE_bool(reuse_manifest_on_open);
|
||||
DECLARE_bool(avoid_flush_during_recovery);
|
||||
DECLARE_bool(enforce_write_buffer_manager_during_recovery);
|
||||
DECLARE_uint64(max_write_batch_group_size_bytes);
|
||||
|
||||
@@ -197,10 +197,10 @@ class DbStressFSWrapper : public FileSystemWrapper {
|
||||
assert(!file_opts.file_checksum_func_name.empty());
|
||||
if (file_opts.file_checksum_func_name == kUnknownFileChecksumFuncName ||
|
||||
file_opts.file_checksum_func_name == kNoFileChecksumFuncName) {
|
||||
// No checksum available — checksum value must be empty
|
||||
// No checksum available -- checksum value must be empty
|
||||
assert(file_opts.file_checksum.empty());
|
||||
} else {
|
||||
// A real checksum function — checksum value must be present
|
||||
// A real checksum function -- checksum value must be present
|
||||
assert(!file_opts.file_checksum.empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1133,10 +1133,19 @@ DEFINE_bool(write_dbid_to_manifest,
|
||||
ROCKSDB_NAMESPACE::Options().write_dbid_to_manifest,
|
||||
"Write DB_ID to manifest");
|
||||
|
||||
DEFINE_bool(optimize_manifest_for_recovery,
|
||||
ROCKSDB_NAMESPACE::Options().optimize_manifest_for_recovery,
|
||||
"Reduce recovery work after a clean shutdown");
|
||||
|
||||
DEFINE_bool(write_identity_file,
|
||||
ROCKSDB_NAMESPACE::Options().write_identity_file,
|
||||
"Write DB_ID to IDENTITY file");
|
||||
|
||||
DEFINE_bool(reuse_manifest_on_open,
|
||||
ROCKSDB_NAMESPACE::Options().reuse_manifest_on_open,
|
||||
"Reopen existing MANIFEST for append after recovery instead of "
|
||||
"creating a fresh one");
|
||||
|
||||
DEFINE_bool(avoid_flush_during_recovery,
|
||||
ROCKSDB_NAMESPACE::Options().avoid_flush_during_recovery,
|
||||
"Avoid flush during recovery");
|
||||
|
||||
@@ -3747,6 +3747,10 @@ void StressTest::PrintEnv() const {
|
||||
static_cast<int>(FLAGS_avoid_unnecessary_blocking_io));
|
||||
fprintf(stdout, "Write DB ID to manifest : %d\n",
|
||||
static_cast<int>(FLAGS_write_dbid_to_manifest));
|
||||
fprintf(stdout, "Optimize manifest recovery: %d\n",
|
||||
static_cast<int>(FLAGS_optimize_manifest_for_recovery));
|
||||
fprintf(stdout, "Reuse manifest on open : %d\n",
|
||||
static_cast<int>(FLAGS_reuse_manifest_on_open));
|
||||
fprintf(stdout, "Max Write Batch Group Size: %" PRIu64 "\n",
|
||||
FLAGS_max_write_batch_group_size_bytes);
|
||||
fprintf(stdout, "Use dynamic level : %d\n",
|
||||
@@ -4666,7 +4670,9 @@ void InitializeOptionsFromFlags(
|
||||
options.manual_wal_flush = FLAGS_manual_wal_flush_one_in > 0 ? true : false;
|
||||
options.avoid_unnecessary_blocking_io = FLAGS_avoid_unnecessary_blocking_io;
|
||||
options.write_dbid_to_manifest = FLAGS_write_dbid_to_manifest;
|
||||
options.optimize_manifest_for_recovery = FLAGS_optimize_manifest_for_recovery;
|
||||
options.write_identity_file = FLAGS_write_identity_file;
|
||||
options.reuse_manifest_on_open = FLAGS_reuse_manifest_on_open;
|
||||
options.avoid_flush_during_recovery = FLAGS_avoid_flush_during_recovery;
|
||||
options.enforce_write_buffer_manager_during_recovery =
|
||||
FLAGS_enforce_write_buffer_manager_during_recovery;
|
||||
|
||||
@@ -2900,7 +2900,7 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
op_logs += "N";
|
||||
}
|
||||
|
||||
// backward scan — skip when backward iteration is not supported
|
||||
// backward scan -- skip when backward iteration is not supported
|
||||
if (FLAGS_test_backward_scan) {
|
||||
key_str = Key(ub - 1);
|
||||
iter->SeekForPrev(key_str);
|
||||
|
||||
@@ -88,7 +88,7 @@ hx235:
|
||||
|
||||
pdillinger:
|
||||
full_name: Peter Dillinger
|
||||
fbid: 513108
|
||||
fbid: 593944283
|
||||
|
||||
alanpaxton:
|
||||
full_name: Alan Paxton
|
||||
@@ -101,3 +101,7 @@ anand1976:
|
||||
|
||||
poojam23:
|
||||
full_name: Pooja Malik
|
||||
|
||||
joshkang97:
|
||||
full_name: Josh Kang
|
||||
fbid: 180489168460795
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: Interpolation search for SST index blocks
|
||||
layout: post
|
||||
author: joshkang97
|
||||
category: blog
|
||||
---
|
||||
|
||||
For workloads with uniformly distributed keys, RocksDB now supports **interpolation search** for SST index blocks as an alternative to the default binary search.
|
||||
|
||||
## The idea
|
||||
|
||||
Binary search always splits the remaining range in half:
|
||||
|
||||
```
|
||||
mid = low + (high - low) / 2
|
||||
```
|
||||
|
||||
That's Θ(log n) probes regardless of the data. Interpolation search instead estimates where the target should land based on its value relative to the current boundaries:
|
||||
|
||||
```
|
||||
probe = low + (target - key[low]) * (high - low) / (key[high] - key[low])
|
||||
```
|
||||
|
||||
On uniformly distributed keys, that's expected O(log log n) probes. The canonical example: for an index block with restart keys `0, 1, 2, ..., 1023` and a seek for `900`, binary search needs about 10 hops; interpolation search lands on it in 1.
|
||||
|
||||
The catch is that pure interpolation search degrades to O(n) on badly skewed data.
|
||||
|
||||
## Turning a key into a number
|
||||
|
||||
The interpolation formula needs numeric values, but index keys are variable-length byte slices. RocksDB extracts a `uint64_t` per key by reading the first 8 bytes after the common prefix shared by the block's boundary keys, in big-endian, and zero-pads to the right if the remaining bytes are too short.
|
||||
|
||||
```cpp
|
||||
inline uint64_t ReadBe64FromKey(Slice s, bool is_user_key, size_t offset) {
|
||||
// ... strip internal seq/type bytes if needed ...
|
||||
if (s.size() - offset >= 8) {
|
||||
uint64_t val;
|
||||
memcpy(&val, s.data() + offset, sizeof(val));
|
||||
return port::kLittleEndian ? EndianSwapValue(val) : val;
|
||||
}
|
||||
// pad short tails with zeros on the right (preserves bytewise order)
|
||||
}
|
||||
```
|
||||
|
||||
Big-endian + zero-pad preserves bytewise ordering, so the linear interpolation formula stays consistent with the comparator. This is also why the feature requires `BytewiseComparator`.
|
||||
|
||||
Two distinct keys can still collapse to the same `uint64_t` once you go past the first 8 non-shared bytes. To avoid a divide-by-zero, we simply fall back to binary search in that case.
|
||||
|
||||
## How to enable it
|
||||
|
||||
To force interpolation search on every index block:
|
||||
|
||||
```cpp
|
||||
rocksdb::BlockBasedTableOptions table_options;
|
||||
table_options.index_block_search_type =
|
||||
rocksdb::BlockBasedTableOptions::kInterpolation;
|
||||
```
|
||||
|
||||
## kAuto: per-block selection
|
||||
|
||||
`kAuto` is the recommended way to use the feature. It chooses the search algorithm for each index block automatically, based on a uniformity hint written into the block footer at SST construction time:
|
||||
|
||||
```cpp
|
||||
table_options.index_block_search_type =
|
||||
rocksdb::BlockBasedTableOptions::kAuto;
|
||||
table_options.uniform_cv_threshold = 0.2;
|
||||
```
|
||||
|
||||
When `uniform_cv_threshold >= 0`, the SST writer scans each index block's restart keys and computes the **coefficient of variation (CV)** of the gaps between consecutive numeric key values:
|
||||
|
||||
```
|
||||
gap[i] = key_value[i + 1] - key_value[i]
|
||||
CV = stddev(gap) / mean(gap)
|
||||
```
|
||||
|
||||
Lower CV means the gaps are more uniform — and the more likely interpolation search will outperform binary search. The CV is computed incrementally with Welford's online algorithm, so the scan is one pass over the restart points.
|
||||
|
||||
If `CV < uniform_cv_threshold`, RocksDB sets an `is_uniform` bit in the block footer. At read time, `kAuto` resolves to `kInterpolation` only when that bit is set *and* the comparator is bytewise; otherwise it uses `kBinary`.
|
||||
|
||||
### Write overhead
|
||||
|
||||
Computing the `is_uniform` bit is a cheap operation as it is only computed for the index blocks in a SST file. CPU profiling of `db_bench -benchmarks=fillseq,compact -compression_type=none -disable_wal=1` attributes only ~0.08% of write-path CPU to `ScanForUniformity`.
|
||||
|
||||
After a few more releases, we plan to enable kAuto and uniform_cv_threshold by default.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Setup — populate a DB and force a single-level shape so all reads hit the same index structure, then measure point-read throughput:
|
||||
|
||||
```
|
||||
# Build a release binary
|
||||
make clean && DEBUG_LEVEL=0 make db_bench
|
||||
|
||||
# Load + compact, varying the index_shortening_mode
|
||||
./db_bench -benchmarks=fillrandom,compact \
|
||||
-index_shortening_mode=1
|
||||
|
||||
# Then point-read against the populated DB
|
||||
./db_bench -use_existing_db=true -benchmarks=readrandom \
|
||||
-index_block_search_type=binary_search # or interpolation_search / auto_search
|
||||
```
|
||||
|
||||
`index_shortening_mode=1` (`kShortenSeparators`) keeps the file's last index key intact, which preserves a roughly uniform numeric distribution for the benchmark.
|
||||
|
||||
Results, averaged over multiple runs:
|
||||
|
||||
| Mode | ops/s | vs `binary_search` |
|
||||
|---|---|---|
|
||||
| `binary_search` | 335,749 | baseline |
|
||||
| `interpolation_search` | 366,598 | **+9.2%** |
|
||||
| `auto_search` | 366,832 | **+9.2%** |
|
||||
|
||||
## Compatibility
|
||||
|
||||
The `is_uniform` bit reuses a previously-reserved bit in the data block footer. SSTs written by older RocksDB never set it and decode as `is_uniform = false`, so they read with binary search under `kAuto`. However, after the bit is set, if read by an older RocksDB version < 11.0.0, it will read it as a corruption error.
|
||||
|
||||
## Future work
|
||||
|
||||
Some future opportunities can involve extending interpolation search to data blocks, as well as supporting other comparators such as `ReverseBytewiseComparator`.
|
||||
Vendored
+2
-2
@@ -1892,7 +1892,7 @@ TEST_F(EnvPosixTest, SupportedOpsNoAsyncIOOnIOUringInitFailure) {
|
||||
int64_t thread_supported_ops2 = 0;
|
||||
std::thread t([&]() {
|
||||
fs->SupportedOps(thread_supported_ops);
|
||||
// Second call on the same thread — cached failure, no retry.
|
||||
// Second call on the same thread -- cached failure, no retry.
|
||||
fs->SupportedOps(thread_supported_ops2);
|
||||
});
|
||||
t.join();
|
||||
@@ -1903,7 +1903,7 @@ TEST_F(EnvPosixTest, SupportedOpsNoAsyncIOOnIOUringInitFailure) {
|
||||
// Second call should also lack kAsyncIO.
|
||||
ASSERT_EQ(thread_supported_ops2 & (1 << FSSupportedOps::kAsyncIO), 0);
|
||||
ASSERT_NE(thread_supported_ops2 & (1 << FSSupportedOps::kFSPrefetch), 0);
|
||||
// CreateIOUring must have been called exactly once — not retried.
|
||||
// CreateIOUring must have been called exactly once -- not retried.
|
||||
ASSERT_EQ(create_count, 1);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
|
||||
+1
-1
@@ -221,7 +221,7 @@ IOStatus GenerateOneFileChecksum(
|
||||
std::unique_ptr<FSRandomAccessFile> r_file;
|
||||
FileOptions fopts = file_options;
|
||||
if (fopts.file_checksum.empty()) {
|
||||
// No expected checksum is known — this is a from-scratch computation.
|
||||
// No expected checksum is known -- this is a from-scratch computation.
|
||||
fopts.file_checksum_func_name = kNoFileChecksumFuncName;
|
||||
}
|
||||
io_s = fs->NewRandomAccessFile(file_path, fopts, &r_file, nullptr);
|
||||
|
||||
@@ -3899,7 +3899,7 @@ TEST_P(FSBufferPrefetchTest, FSBufferPrefetchStatsInternals) {
|
||||
&result, &s, for_compaction);
|
||||
// Platforms that don't have IO uring may not support async IO.
|
||||
// With the ReadAsync sync fallback, s will be OK even when async IO is
|
||||
// unavailable — detect by checking if the second buffer has an async read
|
||||
// unavailable -- detect by checking if the second buffer has an async read
|
||||
// in progress.
|
||||
if (use_async_prefetch && s.IsNotSupported()) {
|
||||
return;
|
||||
|
||||
@@ -169,6 +169,9 @@ class WritableFileWriter {
|
||||
Temperature temperature_;
|
||||
|
||||
public:
|
||||
// `initial_file_size` seeds size accounting when wrapping a reopened file
|
||||
// that will continue appending from its current end. Leave at 0 for newly
|
||||
// created files and reuse-from-offset-0 paths.
|
||||
WritableFileWriter(
|
||||
std::unique_ptr<FSWritableFile>&& file, const std::string& _file_name,
|
||||
const FileOptions& options, SystemClock* clock = nullptr,
|
||||
@@ -178,21 +181,21 @@ class WritableFileWriter {
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners = {},
|
||||
FileChecksumGenFactory* file_checksum_gen_factory = nullptr,
|
||||
bool perform_data_verification = false,
|
||||
bool buffered_data_with_checksum = false)
|
||||
bool buffered_data_with_checksum = false, uint64_t initial_file_size = 0)
|
||||
: file_name_(_file_name),
|
||||
writable_file_(std::move(file), io_tracer, _file_name),
|
||||
clock_(clock),
|
||||
buf_(),
|
||||
max_buffer_size_(options.writable_file_max_buffer_size),
|
||||
filesize_(0),
|
||||
flushed_size_(0),
|
||||
next_write_offset_(0),
|
||||
filesize_(initial_file_size),
|
||||
flushed_size_(initial_file_size),
|
||||
next_write_offset_(initial_file_size),
|
||||
pending_sync_(false),
|
||||
seen_error_(false),
|
||||
#ifndef NDEBUG
|
||||
seen_injected_error_(false),
|
||||
#endif // NDEBUG
|
||||
last_sync_size_(0),
|
||||
last_sync_size_(initial_file_size),
|
||||
bytes_per_sync_(options.bytes_per_sync),
|
||||
rate_limiter_(options.rate_limiter),
|
||||
stats_(stats),
|
||||
|
||||
@@ -152,7 +152,12 @@ class Cache : public Customizable {
|
||||
|
||||
// For helpers without SecondaryCache support
|
||||
explicit CacheItemHelper(CacheEntryRole _role, DeleterFn _del_cb = nullptr)
|
||||
: CacheItemHelper(_role, _del_cb, nullptr, nullptr, nullptr, this) {}
|
||||
: del_cb(_del_cb),
|
||||
size_cb(nullptr),
|
||||
saveto_cb(nullptr),
|
||||
create_cb(nullptr),
|
||||
role(_role),
|
||||
without_secondary_compat(this) {}
|
||||
|
||||
// For helpers with SecondaryCache support
|
||||
explicit CacheItemHelper(CacheEntryRole _role, DeleterFn _del_cb,
|
||||
|
||||
@@ -946,7 +946,7 @@ struct AdvancedColumnFamilyOptions {
|
||||
// (estimated_reads / file_size) exceeds this threshold. This helps reduce
|
||||
// read amplification for hot keys by compacting frequently-read files.
|
||||
//
|
||||
// Only "collapsible" reads are counted — lookups that return NotFound
|
||||
// Only "collapsible" reads are counted -- lookups that return NotFound
|
||||
// (bloom filter false positive), Delete/SingleDeletion (tombstone), or
|
||||
// Merge (partial result). These are reads where the file contributed no
|
||||
// final value and compaction would eliminate the wasted work.
|
||||
@@ -972,15 +972,15 @@ struct AdvancedColumnFamilyOptions {
|
||||
// r * S * B = 2 * (1 + F) * S
|
||||
// r = 2 * (1 + F) / B
|
||||
//
|
||||
// With F = 10, B = 4096: r = 22 / 4096 ≈ 0.005.
|
||||
// With F = 10, B = 4096: r = 22 / 4096 ~= 0.005.
|
||||
//
|
||||
// With a block-cache hit rate h (0 ≤ h < 1), each collapsible read
|
||||
// With a block-cache hit rate h (0 <= h < 1), each collapsible read
|
||||
// only costs (1 - h) * B bytes of actual disk IO, so:
|
||||
// r = 2 * (1 + F) / ((1 - h) * B)
|
||||
//
|
||||
// h = 0 → r ≈ 0.005
|
||||
// h = 0.5 → r ≈ 0.01
|
||||
// h = 0.9 → r ≈ 0.05
|
||||
// h = 0 -> r ~= 0.005
|
||||
// h = 0.5 -> r ~= 0.01
|
||||
// h = 0.9 -> r ~= 0.05
|
||||
//
|
||||
// A recommended starting point is 0.01, which avoids triggering
|
||||
// compactions that cost more IO than they save for most cache-friendly
|
||||
|
||||
@@ -287,6 +287,10 @@ class CompactionFilter : public Customizable {
|
||||
// may not realize there is a write conflict and may allow a Transaction to
|
||||
// Commit that should have failed. Instead, it is better to implement any
|
||||
// Merge filtering inside the MergeOperator.
|
||||
//
|
||||
// Note: Under WritePrepared, WriteUnprepared, and WriteCommitted
|
||||
// TransactionDB, this method is skipped for operands at or below the latest
|
||||
// published sequence, typically every operand in the input.
|
||||
virtual bool FilterMergeOperand(int /*level*/, const Slice& /*key*/,
|
||||
const Slice& /*operand*/) const {
|
||||
return false;
|
||||
@@ -416,7 +420,7 @@ class CompactionFilter : public Customizable {
|
||||
|
||||
// Returns true if the filter overrides FilterV4 and can handle
|
||||
// WideColumnBlobResolver (lazy blob loading). When false (default),
|
||||
// FilterV4 delegates to FilterV3 with fully resolved column values —
|
||||
// FilterV4 delegates to FilterV3 with fully resolved column values --
|
||||
// blob columns are eagerly fetched before the filter is called, ensuring
|
||||
// backward compatibility with FilterV3-only filters.
|
||||
//
|
||||
|
||||
@@ -1868,6 +1868,13 @@ class DB {
|
||||
// and may not have file_creation_time property. In both the cases
|
||||
// file_creation_time is considered 0 which means this API will return
|
||||
// creation_time = 0 as there wouldn't be a timestamp lower than 0.
|
||||
//
|
||||
// NOTE: When the DB was opened with open_files_async = true, this API may
|
||||
// block to wait for background SST file loading to complete, but only when
|
||||
// necessary (i.e., when a file's manifest does not carry file_creation_time
|
||||
// and the value must come from the SST reader). Modern DBs return the real
|
||||
// value immediately. It is possible to have an incomplete result if the DB
|
||||
// is closed while files are still being opened in the background.
|
||||
virtual Status GetCreationTimeOfOldestFile(uint64_t* creation_time) = 0;
|
||||
|
||||
// Note: this API is not yet consistent with WritePrepared transactions.
|
||||
|
||||
@@ -556,7 +556,7 @@ struct IOErrorInfo {
|
||||
uint64_t offset;
|
||||
};
|
||||
|
||||
// EXPERIMENTAL — under active development, fields may change.
|
||||
// EXPERIMENTAL -- under active development, fields may change.
|
||||
// Point-in-time snapshot of background job pressure for one DB: how busy
|
||||
// compaction and flush are, and how close the DB is to write-stalling.
|
||||
struct BackgroundJobPressure {
|
||||
|
||||
@@ -1001,6 +1001,36 @@ struct DBOptions {
|
||||
// This option is mutable with SetDBOptions().
|
||||
bool verify_manifest_content_on_close = false;
|
||||
|
||||
// EXPERIMENTAL: If true, RocksDB can reduce recovery work after a clean
|
||||
// shutdown, which may reduce DB::Open latency on warm reopens, especially on
|
||||
// storage where metadata appends are expensive.
|
||||
//
|
||||
// Best-effort optimization: if it is disabled or unavailable, RocksDB falls
|
||||
// back to the standard recovery path.
|
||||
//
|
||||
// Temporary rollout / kill switch for an optimization that is intended to be
|
||||
// correct and eventually always enabled. Mutable via SetDBOptions().
|
||||
bool optimize_manifest_for_recovery = false;
|
||||
|
||||
// EXPERIMENTAL: If true, DB::Open can try to reuse the existing MANIFEST
|
||||
// for the first post-open metadata update instead of creating a fresh one.
|
||||
// This can reduce warm-open latency for DBs whose MANIFEST is expensive to
|
||||
// rebuild.
|
||||
//
|
||||
// Best-effort optimization: even when enabled, RocksDB may still create a
|
||||
// fresh MANIFEST if the FileSystem does not support reopening the existing
|
||||
// MANIFEST for append, or if RocksDB decides reuse is unsafe. That fallback
|
||||
// is normal behavior.
|
||||
//
|
||||
// With very small `max_manifest_file_size` settings, the reused MANIFEST can
|
||||
// still rotate earlier than expected after open, because RocksDB may keep a
|
||||
// conservative auto-tuned rotation threshold until it later refreshes its
|
||||
// compacted-size estimate.
|
||||
//
|
||||
// Temporary rollout / kill switch while this optimization is being
|
||||
// validated.
|
||||
bool reuse_manifest_on_open = false;
|
||||
|
||||
// This option mostly replaces max_manifest_file_size to control an auto-tuned
|
||||
// balance of manifest write amplification and space amplification. A new
|
||||
// manifest file is created with the "compacted" contents of the old one when
|
||||
@@ -2922,7 +2952,7 @@ struct SizeApproximationOptions {
|
||||
// blob file data in the key range. When enabled, the total blob file size
|
||||
// is prorated by the ratio of SST data in the range to the total SST data:
|
||||
//
|
||||
// blob_size_in_range ≈ total_blob_size * (sst_in_range / total_sst)
|
||||
// blob_size_in_range ~= total_blob_size * (sst_in_range / total_sst)
|
||||
//
|
||||
// Limitations of this approximation:
|
||||
// - Assumes blob data is distributed proportionally to SST data, which
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/table_properties.h"
|
||||
#include "rocksdb/types.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -56,6 +57,12 @@ class SstFileReader {
|
||||
// capabilities like filling cache, read ahead are disabled.
|
||||
std::unique_ptr<Iterator> NewTableIterator();
|
||||
|
||||
// Parses a raw key returned by NewTableIterator().
|
||||
// The Slice fields in parsed_key point into raw_table_key and are valid only
|
||||
// while raw_table_key remains valid.
|
||||
Status ParseTableIteratorKey(const Slice& raw_table_key,
|
||||
ParsedEntryInfo* parsed_key) const;
|
||||
|
||||
std::shared_ptr<const TableProperties> GetTableProperties() const;
|
||||
|
||||
// Verifies whether there is corruption in this table.
|
||||
|
||||
@@ -43,7 +43,7 @@ class UserDefinedIndexBuilder {
|
||||
kOther = 3, // Other types (e.g., blob reference, wide-column entity).
|
||||
// The value format is type-specific and may not be the
|
||||
// actual user data.
|
||||
kTypeMax, // Sentinel — must be last. Value may change across releases.
|
||||
kTypeMax, // Sentinel -- must be last. Value may change across releases.
|
||||
};
|
||||
|
||||
// File offset and size of the data block
|
||||
@@ -106,7 +106,7 @@ class UserDefinedIndexBuilder {
|
||||
// (e.g., trie-based indexes) can leave this as a no-op.
|
||||
//
|
||||
// @key: The user key (without sequence number or type suffix).
|
||||
// @type: The entry type — kValue (Put), kDelete, kMerge, or kOther.
|
||||
// @type: The entry type -- kValue (Put), kDelete, kMerge, or kOther.
|
||||
// For kDelete entries, the value may be empty. For kOther, the
|
||||
// value format is type-specific and may not be actual user data.
|
||||
// @value: The associated value (may be empty for deletions).
|
||||
|
||||
@@ -559,7 +559,7 @@ TEST_F(InlineSkipTest, MultiGetDuplicateKeysWithCallbackWalk) {
|
||||
}
|
||||
|
||||
// Callback that walks forward through multiple entries before stopping.
|
||||
// This simulates the Merge operand accumulation in SaveValue — the callback
|
||||
// This simulates the Merge operand accumulation in SaveValue -- the callback
|
||||
// returns true for entries until it reaches one >= stop_at, simulating
|
||||
// walking through merge chain entries.
|
||||
struct WalkingCallbackArg {
|
||||
@@ -1068,11 +1068,11 @@ static void ConcurrentMultiGetWorker(void* arg) {
|
||||
// Validate all results: each found result must equal its query key
|
||||
// (since all queried keys were inserted, an exact match is expected).
|
||||
// However, due to concurrent inserts, a key sampled from the ring
|
||||
// might not yet be visible — so we only check that if a result is
|
||||
// might not yet be visible -- so we only check that if a result is
|
||||
// found, it equals the query key (i.e., no wrong key returned).
|
||||
for (size_t j = 0; j < batch_size; j++) {
|
||||
if (results[j] != 0 && results[j] != query_keys[j]) {
|
||||
// Got a result that doesn't match the query — either a bug or
|
||||
// Got a result that doesn't match the query -- either a bug or
|
||||
// the exact key wasn't inserted and we got the next one. Since
|
||||
// all our query keys are inserted, this means the result should
|
||||
// be >= query. For the just-inserted key we already checked
|
||||
|
||||
@@ -48,13 +48,18 @@ bool WBWIMemTable::Get(const LookupKey& key, std::string* value,
|
||||
SequenceNumber* max_covering_tombstone_seq,
|
||||
SequenceNumber* out_seq, const ReadOptions&,
|
||||
bool immutable_memtable, ReadCallback* callback,
|
||||
bool* is_blob_index, bool do_merge) {
|
||||
bool* is_blob_index, bool do_merge,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
assert(s->ok() || s->IsMergeInProgress());
|
||||
(void)immutable_memtable;
|
||||
(void)timestamp;
|
||||
(void)columns;
|
||||
(void)blob_fetcher;
|
||||
assert(immutable_memtable);
|
||||
assert(!timestamp); // TODO: support UDT
|
||||
// IngestWriteBatchWithIndex() is rejected while any live column family has
|
||||
// blob direct write enabled, so WBWI should never need blob resolution.
|
||||
assert(blob_fetcher == nullptr);
|
||||
assert(assigned_seqno_.upper_bound != kMaxSequenceNumber);
|
||||
assert(assigned_seqno_.lower_bound != kMaxSequenceNumber);
|
||||
// WBWI does not support DeleteRange yet.
|
||||
@@ -154,10 +159,15 @@ bool WBWIMemTable::Get(const LookupKey& key, std::string* value,
|
||||
|
||||
void WBWIMemTable::MultiGet(const ReadOptions& read_options,
|
||||
MultiGetRange* range, ReadCallback* callback,
|
||||
bool immutable_memtable) {
|
||||
bool immutable_memtable,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
(void)immutable_memtable;
|
||||
(void)blob_fetcher;
|
||||
// Should only be used as immutable memtable.
|
||||
assert(immutable_memtable);
|
||||
// IngestWriteBatchWithIndex() is rejected while any live column family has
|
||||
// blob direct write enabled, so WBWI should never need blob resolution.
|
||||
assert(blob_fetcher == nullptr);
|
||||
// TODO: reuse the InternalIterator created in Get().
|
||||
for (auto iter = range->begin(); iter != range->end(); ++iter) {
|
||||
SequenceNumber dummy_seq = 0;
|
||||
|
||||
@@ -134,10 +134,12 @@ class WBWIMemTable final : public ReadOnlyMemTable {
|
||||
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
||||
const ReadOptions& read_opts, bool immutable_memtable,
|
||||
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
|
||||
bool do_merge = true) override;
|
||||
bool do_merge = true,
|
||||
const BlobFetcher* blob_fetcher = nullptr) override;
|
||||
|
||||
void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
ReadCallback* callback, bool immutable_memtable) override;
|
||||
ReadCallback* callback, bool immutable_memtable,
|
||||
const BlobFetcher* blob_fetcher = nullptr) override;
|
||||
|
||||
uint64_t NumEntries() const override { return num_entries_; }
|
||||
|
||||
|
||||
@@ -140,6 +140,10 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
||||
{offsetof(struct MutableDBOptions, verify_manifest_content_on_close),
|
||||
OptionType::kBoolean, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kMutable}},
|
||||
{"optimize_manifest_for_recovery",
|
||||
{offsetof(struct MutableDBOptions, optimize_manifest_for_recovery),
|
||||
OptionType::kBoolean, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kMutable}},
|
||||
{"daily_offpeak_time_utc",
|
||||
{offsetof(struct MutableDBOptions, daily_offpeak_time_utc),
|
||||
OptionType::kString, OptionVerificationType::kNormal,
|
||||
@@ -438,6 +442,10 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
||||
{offsetof(struct ImmutableDBOptions, write_dbid_to_manifest),
|
||||
OptionType::kBoolean, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"reuse_manifest_on_open",
|
||||
{offsetof(struct ImmutableDBOptions, reuse_manifest_on_open),
|
||||
OptionType::kBoolean, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"write_identity_file",
|
||||
{offsetof(struct ImmutableDBOptions, write_identity_file),
|
||||
OptionType::kBoolean, OptionVerificationType::kNormal,
|
||||
@@ -806,6 +814,7 @@ ImmutableDBOptions::ImmutableDBOptions(const DBOptions& options)
|
||||
prefix_seek_opt_in_only(options.prefix_seek_opt_in_only),
|
||||
persist_stats_to_disk(options.persist_stats_to_disk),
|
||||
write_dbid_to_manifest(options.write_dbid_to_manifest),
|
||||
reuse_manifest_on_open(options.reuse_manifest_on_open),
|
||||
write_identity_file(options.write_identity_file),
|
||||
log_readahead_size(options.log_readahead_size),
|
||||
file_checksum_gen_factory(options.file_checksum_gen_factory),
|
||||
@@ -985,6 +994,8 @@ void ImmutableDBOptions::Dump(Logger* log) const {
|
||||
persist_stats_to_disk);
|
||||
ROCKS_LOG_HEADER(log, " Options.write_dbid_to_manifest: %d",
|
||||
write_dbid_to_manifest);
|
||||
ROCKS_LOG_HEADER(log, " Options.reuse_manifest_on_open: %d",
|
||||
reuse_manifest_on_open);
|
||||
ROCKS_LOG_HEADER(log, " Options.write_identity_file: %d",
|
||||
write_identity_file);
|
||||
ROCKS_LOG_HEADER(
|
||||
@@ -1073,6 +1084,7 @@ MutableDBOptions::MutableDBOptions(const DBOptions& options)
|
||||
manifest_preallocation_size(options.manifest_preallocation_size),
|
||||
verify_manifest_content_on_close(
|
||||
options.verify_manifest_content_on_close),
|
||||
optimize_manifest_for_recovery(options.optimize_manifest_for_recovery),
|
||||
fast_sst_open(options.fast_sst_open),
|
||||
daily_offpeak_time_utc(options.daily_offpeak_time_utc),
|
||||
max_compaction_trigger_wakeup_seconds(
|
||||
@@ -1131,6 +1143,8 @@ void MutableDBOptions::Dump(Logger* log) const {
|
||||
manifest_preallocation_size);
|
||||
ROCKS_LOG_HEADER(log, " Options.verify_manifest_content_on_close: %d",
|
||||
verify_manifest_content_on_close);
|
||||
ROCKS_LOG_HEADER(log, " Options.optimize_manifest_for_recovery: %d",
|
||||
optimize_manifest_for_recovery);
|
||||
ROCKS_LOG_HEADER(log, "Options.daily_offpeak_time_utc: %s",
|
||||
daily_offpeak_time_utc.c_str());
|
||||
ROCKS_LOG_HEADER(log,
|
||||
|
||||
@@ -88,6 +88,7 @@ struct ImmutableDBOptions {
|
||||
bool prefix_seek_opt_in_only;
|
||||
bool persist_stats_to_disk;
|
||||
bool write_dbid_to_manifest;
|
||||
bool reuse_manifest_on_open;
|
||||
bool write_identity_file;
|
||||
size_t log_readahead_size;
|
||||
std::shared_ptr<FileChecksumGenFactory> file_checksum_gen_factory;
|
||||
@@ -149,6 +150,7 @@ struct MutableDBOptions {
|
||||
int max_manifest_space_amp_pct;
|
||||
size_t manifest_preallocation_size;
|
||||
bool verify_manifest_content_on_close;
|
||||
bool optimize_manifest_for_recovery;
|
||||
bool fast_sst_open;
|
||||
std::string daily_offpeak_time_utc;
|
||||
uint64_t max_compaction_trigger_wakeup_seconds;
|
||||
|
||||
@@ -173,6 +173,7 @@ void BuildDBOptions(const ImmutableDBOptions& immutable_db_options,
|
||||
immutable_db_options.avoid_unnecessary_blocking_io;
|
||||
options.write_dbid_to_manifest = immutable_db_options.write_dbid_to_manifest;
|
||||
options.write_identity_file = immutable_db_options.write_identity_file;
|
||||
options.reuse_manifest_on_open = immutable_db_options.reuse_manifest_on_open;
|
||||
options.prefix_seek_opt_in_only =
|
||||
immutable_db_options.prefix_seek_opt_in_only;
|
||||
options.log_readahead_size = immutable_db_options.log_readahead_size;
|
||||
@@ -192,6 +193,8 @@ void BuildDBOptions(const ImmutableDBOptions& immutable_db_options,
|
||||
immutable_db_options.enforce_single_del_contracts;
|
||||
options.verify_manifest_content_on_close =
|
||||
mutable_db_options.verify_manifest_content_on_close;
|
||||
options.optimize_manifest_for_recovery =
|
||||
mutable_db_options.optimize_manifest_for_recovery;
|
||||
options.daily_offpeak_time_utc = mutable_db_options.daily_offpeak_time_utc;
|
||||
options.max_compaction_trigger_wakeup_seconds =
|
||||
mutable_db_options.max_compaction_trigger_wakeup_seconds;
|
||||
|
||||
@@ -473,6 +473,7 @@ TEST_F(OptionsSettableTest, DBOptionsAllFieldsSettable) {
|
||||
"avoid_unnecessary_blocking_io=false;"
|
||||
"log_readahead_size=0;"
|
||||
"write_dbid_to_manifest=false;"
|
||||
"optimize_manifest_for_recovery=false;"
|
||||
"best_efforts_recovery=false;"
|
||||
"max_bgerror_resume_count=2;"
|
||||
"bgerror_resume_retry_interval=1000000;"
|
||||
@@ -489,10 +490,12 @@ TEST_F(OptionsSettableTest, DBOptionsAllFieldsSettable) {
|
||||
"wal_write_temperature=kHot;"
|
||||
"background_close_inactive_wals=true;"
|
||||
"write_dbid_to_manifest=true;"
|
||||
"optimize_manifest_for_recovery=true;"
|
||||
"write_identity_file=true;"
|
||||
"verify_manifest_content_on_close=false;"
|
||||
"prefix_seek_opt_in_only=true;"
|
||||
"fast_sst_open=true;",
|
||||
"fast_sst_open=true;"
|
||||
"reuse_manifest_on_open=true;",
|
||||
new_options));
|
||||
|
||||
ASSERT_EQ(unset_bytes_base, NumUnsetBytes(new_options_ptr, sizeof(DBOptions),
|
||||
|
||||
+2
-2
@@ -434,14 +434,14 @@ void InstallStackTraceHandler() {
|
||||
// Ignore SIGPIPE so that broken-pipe writes (e.g. to a closed stdout)
|
||||
// return EPIPE instead of killing the process.
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
// Crash signals — invoke full stack trace + ring buffer
|
||||
// Crash signals -- invoke full stack trace + ring buffer
|
||||
signal(SIGILL, StackTraceHandler);
|
||||
signal(SIGSEGV, StackTraceHandler);
|
||||
signal(SIGBUS, StackTraceHandler);
|
||||
signal(SIGABRT, StackTraceHandler);
|
||||
signal(SIGFPE, StackTraceHandler);
|
||||
signal(SIGQUIT, StackTraceHandler);
|
||||
// Termination signals — print ring buffer only, no stack trace
|
||||
// Termination signals -- print ring buffer only, no stack trace
|
||||
signal(SIGTERM, TerminationHandler);
|
||||
signal(SIGINT, TerminationHandler);
|
||||
atexit(AtExit);
|
||||
|
||||
@@ -41,7 +41,7 @@ void BlockBasedTableIterator::SeekImpl(const Slice* target,
|
||||
return;
|
||||
}
|
||||
|
||||
// MultiScan requires an explicit seek key — SeekToFirst() is not supported
|
||||
// MultiScan requires an explicit seek key -- SeekToFirst() is not supported
|
||||
if (multi_scan_read_set_ && !target) {
|
||||
multi_scan_status_ = Status::InvalidArgument("No seek key for MultiScan");
|
||||
RecordTick(table_->GetStatistics(), MULTISCAN_SEEK_ERRORS);
|
||||
@@ -683,7 +683,7 @@ void BlockBasedTableIterator::FindBlockForward() {
|
||||
if (multi_scan_index_iter_ &&
|
||||
multi_scan_index_iter_->IsScanRangeExhausted()) {
|
||||
if (multi_scan_index_iter_->HasMoreScanRanges()) {
|
||||
// More ranges remain — signal out-of-bound so DBIter/LevelIter
|
||||
// More ranges remain -- signal out-of-bound so DBIter/LevelIter
|
||||
// will trigger the next Seek for the next scan range.
|
||||
is_out_of_bound_ = true;
|
||||
}
|
||||
|
||||
@@ -1087,7 +1087,7 @@ class BlockBasedTableReaderMultiScanAsyncIOTest
|
||||
: public BlockBasedTableReaderMultiScanTest {};
|
||||
|
||||
// TODO: test no block cache case
|
||||
TEST_P(BlockBasedTableReaderMultiScanAsyncIOTest, MultiScanPrepare) {
|
||||
TEST_P(BlockBasedTableReaderMultiScanAsyncIOTest, DISABLED_MultiScanPrepare) {
|
||||
auto param = GetParam();
|
||||
auto fill_cache = param.fill_cache;
|
||||
auto use_async_io = param.use_async_io;
|
||||
|
||||
@@ -52,7 +52,7 @@ struct DataBlockFooter {
|
||||
// limit is adequate because a 4GiB block (maximum due to 32-bit block size)
|
||||
// with restart_interval=1 and minimum entries (12 bytes: 3 varint bytes +
|
||||
// 9-byte internal key + empty value) plus 4-byte restart offsets = 16 bytes
|
||||
// per restart, fits at most (2^32 - 4) / 16 ≈ 268 million restarts.
|
||||
// per restart, fits at most (2^32 - 4) / 16 ~= 268 million restarts.
|
||||
static constexpr uint32_t kMaxNumRestarts = (1u << 28) - 1;
|
||||
|
||||
// Maximum encoded length of a DataBlockFooter (for buffer sizing).
|
||||
|
||||
@@ -65,7 +65,7 @@ void MultiScanIndexIterator::Seek(const Slice& target) {
|
||||
|
||||
// Enforce forward-only seek
|
||||
if (!prev_seek_key_.empty() && icomp_.Compare(target, prev_seek_key_) <= 0) {
|
||||
// Seek key is not moving forward — keep current position
|
||||
// Seek key is not moving forward -- keep current position
|
||||
return;
|
||||
}
|
||||
prev_seek_key_ = target.ToString();
|
||||
@@ -87,11 +87,12 @@ void MultiScanIndexIterator::Seek(const Slice& target) {
|
||||
// The following diagram explains different seek targets seeking at various
|
||||
// positions on the table, while the next_scan_idx_ points to PreparedRange 2.
|
||||
//
|
||||
// next_scan_idx_: ------------------┐
|
||||
// ▼
|
||||
// next_scan_idx_: ------------------+
|
||||
// |
|
||||
// v
|
||||
// table: : __[PreparedRange 1]__[PreparedRange 2]__[PreparedRange 3]__
|
||||
// Seek target: <----- Case 1 ------>▲<------------- Case 2 -------------->
|
||||
// │
|
||||
// Seek target: <----- Case 1 ------>^<------------- Case 2 -------------->
|
||||
// |
|
||||
// Case 3
|
||||
//
|
||||
// Case 1: seek before the start of next prepared range. This could happen
|
||||
@@ -103,7 +104,7 @@ void MultiScanIndexIterator::Seek(const Slice& target) {
|
||||
if (compare_next_scan_start_result < 0) {
|
||||
// Case 1: Seek before the start of the next prepared range
|
||||
if (next_scan_idx_ == 0) {
|
||||
// Should not happen — seek before first prepared range
|
||||
// Should not happen -- seek before first prepared range
|
||||
assert(false && "Seek target before the first prepared range");
|
||||
valid_ = false;
|
||||
return;
|
||||
@@ -122,7 +123,7 @@ void MultiScanIndexIterator::Seek(const Slice& target) {
|
||||
valid_ = false;
|
||||
return;
|
||||
}
|
||||
// Seek within a gap — advance to the right scan range and find block
|
||||
// Seek within a gap -- advance to the right scan range and find block
|
||||
SeekToBlock(&user_seek_target);
|
||||
} else if (compare_next_scan_start_result > 0) {
|
||||
// Case 2: Seek after the start of the next prepared range
|
||||
@@ -220,7 +221,7 @@ void MultiScanIndexIterator::SeekToBlockIdx(size_t block_idx) {
|
||||
void MultiScanIndexIterator::SetExhausted() {
|
||||
scan_range_exhausted_ = true;
|
||||
if (next_scan_idx_ < block_index_ranges_per_scan_.size()) {
|
||||
// More ranges remain — signal out-of-bound for current range.
|
||||
// More ranges remain -- signal out-of-bound for current range.
|
||||
valid_ = true;
|
||||
// Position at the start of the next range so that the next Seek()
|
||||
// can find it. We need to be "valid" so that FindBlockForward sets
|
||||
@@ -232,7 +233,7 @@ void MultiScanIndexIterator::SetExhausted() {
|
||||
}
|
||||
valid_ = false;
|
||||
} else {
|
||||
// Last range — natural EOF. Don't set out-of-bound so LevelIterator
|
||||
// Last range -- natural EOF. Don't set out-of-bound so LevelIterator
|
||||
// advances to the next file.
|
||||
valid_ = false;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ class MultiScanIndexIterator : public InternalIteratorBase<IndexValue> {
|
||||
// Move to the first block of the first scan range.
|
||||
void SeekToFirst() override;
|
||||
|
||||
// Not supported — sets valid_ = false.
|
||||
// Not supported -- sets valid_ = false.
|
||||
void SeekForPrev(const Slice& target) override;
|
||||
void SeekToLast() override;
|
||||
void Prev() override;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "rocksdb/external_table.h"
|
||||
|
||||
#include "db/dbformat.h"
|
||||
#include "logging/logging.h"
|
||||
#include "rocksdb/table.h"
|
||||
#include "table/block_based/block.h"
|
||||
@@ -118,7 +119,7 @@ class ExternalTableIteratorAdapter : public InternalIterator {
|
||||
|
||||
Slice key() const override {
|
||||
if (iterator_) {
|
||||
return Slice(*key_.const_rep());
|
||||
return key_.GetInternalKey();
|
||||
}
|
||||
return Slice();
|
||||
}
|
||||
@@ -142,7 +143,7 @@ class ExternalTableIteratorAdapter : public InternalIterator {
|
||||
|
||||
private:
|
||||
std::unique_ptr<ExternalTableIterator> iterator_;
|
||||
InternalKey key_;
|
||||
IterKey key_;
|
||||
bool valid_;
|
||||
Status status_;
|
||||
IterateResult result_;
|
||||
@@ -152,8 +153,8 @@ class ExternalTableIteratorAdapter : public InternalIterator {
|
||||
valid_ = iterator_->Valid();
|
||||
status_ = iterator_->status();
|
||||
if (valid_ && status_.ok()) {
|
||||
key_.Set(res.has_value() ? res.value() : iterator_->key(), 0,
|
||||
ValueType::kTypeValue);
|
||||
key_.SetInternalKey(res.has_value() ? res.value() : iterator_->key(),
|
||||
/*s=*/0, ValueType::kTypeValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+130
-49
@@ -127,6 +127,10 @@ Status GetContext::SaveWideColumnEntityToPinnable(const Slice& user_key,
|
||||
pinnable_val_->PinSelf(value_of_default);
|
||||
}
|
||||
} else if (status.IsNotSupported()) {
|
||||
if (blob_fetcher_ == nullptr) {
|
||||
return Status::Corruption(
|
||||
"Cannot resolve blob-backed default column without a blob fetcher");
|
||||
}
|
||||
// Default column is a blob reference, so resolve it into the output value.
|
||||
bool resolved = false;
|
||||
status = WideColumnSerialization::GetValueOfDefaultColumnResolvingBlobs(
|
||||
@@ -140,14 +144,28 @@ Status GetContext::SaveWideColumnEntityToColumns(const Slice& user_key,
|
||||
Cleanable* value_pinner) {
|
||||
assert(columns_ != nullptr);
|
||||
|
||||
bool has_blob_columns = false;
|
||||
Status status =
|
||||
WideColumnSerialization::HasBlobColumns(entity, has_blob_columns);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
if (!has_blob_columns) {
|
||||
return columns_->SetWideColumnValue(entity, value_pinner);
|
||||
}
|
||||
std::vector<WideColumn> entity_columns;
|
||||
std::vector<std::pair<size_t, BlobIndex>> blob_cols;
|
||||
Slice entity_ref = entity;
|
||||
Status status = WideColumnSerialization::DeserializeV2(
|
||||
entity_ref, entity_columns, blob_cols);
|
||||
status = WideColumnSerialization::DeserializeV2(entity_ref, entity_columns,
|
||||
blob_cols);
|
||||
if (status.ok()) {
|
||||
if (LIKELY(blob_cols.empty())) {
|
||||
return columns_->SetWideColumnValue(entity, value_pinner);
|
||||
// HasBlobColumns() and DeserializeV2() must agree on whether this entity
|
||||
// contains blob-valued columns.
|
||||
assert(!blob_cols.empty());
|
||||
if (blob_fetcher_ == nullptr) {
|
||||
return Status::Corruption(
|
||||
"Cannot resolve blob-backed wide-column entity without a blob "
|
||||
"fetcher");
|
||||
}
|
||||
|
||||
// TODO: Add lazy resolution support for GetEntity point lookups. This
|
||||
@@ -201,6 +219,37 @@ Status GetContext::SaveWideColumnEntityToColumns(const Slice& user_key,
|
||||
return status;
|
||||
}
|
||||
|
||||
Status GetContext::PushWideColumnEntityDefaultOperand(const Slice& user_key,
|
||||
const Slice& entity,
|
||||
Cleanable* value_pinner) {
|
||||
Slice value_of_default;
|
||||
Slice entity_ref = entity;
|
||||
Status status = WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
entity_ref, value_of_default);
|
||||
if (status.ok()) {
|
||||
push_operand(value_of_default, value_pinner);
|
||||
return status;
|
||||
}
|
||||
if (!status.IsNotSupported()) {
|
||||
return status;
|
||||
}
|
||||
if (blob_fetcher_ == nullptr) {
|
||||
return Status::Corruption(
|
||||
"Cannot resolve blob-backed default column without a blob fetcher");
|
||||
}
|
||||
|
||||
PinnableSlice resolved_default;
|
||||
bool resolved = false;
|
||||
status = WideColumnSerialization::GetValueOfDefaultColumnResolvingBlobs(
|
||||
entity, user_key, blob_fetcher_, resolved_default, resolved);
|
||||
if (status.ok()) {
|
||||
// Resolved blob values are backed by this stack-local PinnableSlice, so
|
||||
// copy them into MergeContext instead of pinning their storage.
|
||||
push_operand(Slice(resolved_default), nullptr);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
void GetContext::SaveValue(const Slice& value, SequenceNumber /*seq*/,
|
||||
Cleanable* value_pinner) {
|
||||
assert(state_ == kNotFound);
|
||||
@@ -329,7 +378,7 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
||||
const Slice& value, bool* matched,
|
||||
Status* read_status, Cleanable* value_pinner) {
|
||||
assert(matched);
|
||||
assert((state_ != kMerge && parsed_key.type != kTypeMerge) ||
|
||||
assert((State() != kMerge && parsed_key.type != kTypeMerge) ||
|
||||
merge_context_ != nullptr);
|
||||
if (ucmp_->EqualWithoutTimestamp(parsed_key.user_key, user_key_)) {
|
||||
*matched = true;
|
||||
@@ -400,7 +449,7 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
||||
case kTypeValuePreferredSeqno:
|
||||
case kTypeBlobIndex:
|
||||
case kTypeWideColumnEntity:
|
||||
assert(state_ == kNotFound || state_ == kMerge);
|
||||
assert(State() == kNotFound || State() == kMerge);
|
||||
if (type == kTypeValuePreferredSeqno) {
|
||||
unpacked_value = ParsePackedValueForValue(value);
|
||||
}
|
||||
@@ -416,7 +465,7 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
||||
*is_blob_index_ = (type == kTypeBlobIndex);
|
||||
}
|
||||
|
||||
if (kNotFound == state_) {
|
||||
if (State() == kNotFound) {
|
||||
state_ = kFound;
|
||||
if (do_merge_) {
|
||||
if (type == kTypeBlobIndex && ucmp_->timestamp_size() != 0) {
|
||||
@@ -431,8 +480,10 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
||||
if (!s.ok()) {
|
||||
if (s.IsIncomplete()) {
|
||||
MarkKeyMayExist();
|
||||
*read_status = s;
|
||||
} else {
|
||||
state_ = kCorrupt;
|
||||
*read_status = s;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -456,8 +507,10 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
||||
if (!s.ok()) {
|
||||
if (s.IsIncomplete()) {
|
||||
MarkKeyMayExist();
|
||||
*read_status = s;
|
||||
} else {
|
||||
state_ = kCorrupt;
|
||||
*read_status = s;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -478,23 +531,24 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
||||
Slice blob_value(pin_val);
|
||||
push_operand(blob_value, nullptr);
|
||||
} else if (type == kTypeWideColumnEntity) {
|
||||
Slice value_copy = unpacked_value;
|
||||
Slice value_of_default;
|
||||
|
||||
if (!WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
value_copy, value_of_default)
|
||||
.ok()) {
|
||||
state_ = kCorrupt;
|
||||
const Status s = PushWideColumnEntityDefaultOperand(
|
||||
parsed_key.user_key, unpacked_value, value_pinner);
|
||||
if (!s.ok()) {
|
||||
if (s.IsIncomplete()) {
|
||||
MarkKeyMayExist();
|
||||
*read_status = s;
|
||||
} else {
|
||||
state_ = kCorrupt;
|
||||
*read_status = s;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
push_operand(value_of_default, value_pinner);
|
||||
} else {
|
||||
assert(type == kTypeValue || type == kTypeValuePreferredSeqno);
|
||||
push_operand(unpacked_value, value_pinner);
|
||||
}
|
||||
}
|
||||
} else if (kMerge == state_) {
|
||||
} else if (State() == kMerge) {
|
||||
assert(merge_operator_ != nullptr);
|
||||
if (type == kTypeBlobIndex) {
|
||||
PinnableSlice pin_val;
|
||||
@@ -505,7 +559,11 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
||||
Slice blob_value(pin_val);
|
||||
state_ = kFound;
|
||||
if (do_merge_) {
|
||||
MergeWithPlainBaseValue(blob_value);
|
||||
const Status s = MergeWithPlainBaseValue(blob_value);
|
||||
if (!s.ok()) {
|
||||
*read_status = s;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// It means this function is called as part of DB GetMergeOperands
|
||||
// API and the current value should be part of
|
||||
@@ -516,29 +574,38 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
||||
state_ = kFound;
|
||||
|
||||
if (do_merge_) {
|
||||
MergeWithWideColumnBaseValue(unpacked_value);
|
||||
const Status s = MergeWithWideColumnBaseValue(unpacked_value);
|
||||
if (!s.ok()) {
|
||||
*read_status = s;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// It means this function is called as part of DB GetMergeOperands
|
||||
// API and the current value should be part of
|
||||
// merge_context_->operand_list
|
||||
Slice value_copy = unpacked_value;
|
||||
Slice value_of_default;
|
||||
|
||||
if (!WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
value_copy, value_of_default)
|
||||
.ok()) {
|
||||
state_ = kCorrupt;
|
||||
const Status s = PushWideColumnEntityDefaultOperand(
|
||||
parsed_key.user_key, unpacked_value, value_pinner);
|
||||
if (!s.ok()) {
|
||||
if (s.IsIncomplete()) {
|
||||
MarkKeyMayExist();
|
||||
*read_status = s;
|
||||
} else {
|
||||
state_ = kCorrupt;
|
||||
*read_status = s;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
push_operand(value_of_default, value_pinner);
|
||||
}
|
||||
} else {
|
||||
assert(type == kTypeValue || type == kTypeValuePreferredSeqno);
|
||||
|
||||
state_ = kFound;
|
||||
if (do_merge_) {
|
||||
MergeWithPlainBaseValue(unpacked_value);
|
||||
const Status s = MergeWithPlainBaseValue(unpacked_value);
|
||||
if (!s.ok()) {
|
||||
*read_status = s;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// It means this function is called as part of DB GetMergeOperands
|
||||
// API and the current value should be part of
|
||||
@@ -555,13 +622,17 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
||||
case kTypeRangeDeletion:
|
||||
// TODO(noetzli): Verify correctness once merge of single-deletes
|
||||
// is supported
|
||||
assert(state_ == kNotFound || state_ == kMerge);
|
||||
if (kNotFound == state_) {
|
||||
assert(State() == kNotFound || State() == kMerge);
|
||||
if (State() == kNotFound) {
|
||||
state_ = kDeleted;
|
||||
} else if (kMerge == state_) {
|
||||
} else if (State() == kMerge) {
|
||||
state_ = kFound;
|
||||
if (do_merge_) {
|
||||
MergeWithNoBaseValue();
|
||||
const Status s = MergeWithNoBaseValue();
|
||||
if (!s.ok()) {
|
||||
*read_status = s;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// If do_merge_ = false then the current value shouldn't be part of
|
||||
// merge_context_->operand_list
|
||||
@@ -569,7 +640,7 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
||||
return false;
|
||||
|
||||
case kTypeMerge:
|
||||
assert(state_ == kNotFound || state_ == kMerge);
|
||||
assert(State() == kNotFound || State() == kMerge);
|
||||
state_ = kMerge;
|
||||
// value_pinner is not set from plain_table_reader.cc for example.
|
||||
push_operand(value, value_pinner);
|
||||
@@ -579,7 +650,11 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
||||
merge_operator_->ShouldMerge(
|
||||
merge_context_->GetOperandsDirectionBackward())) {
|
||||
state_ = kFound;
|
||||
MergeWithNoBaseValue();
|
||||
const Status s = MergeWithNoBaseValue();
|
||||
if (!s.ok()) {
|
||||
*read_status = s;
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (merge_context_->get_merge_operands_options != nullptr &&
|
||||
@@ -597,26 +672,27 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
||||
}
|
||||
}
|
||||
|
||||
// state_ could be Corrupt, merge or notfound
|
||||
// State() could be Corrupt, merge or notfound
|
||||
return false;
|
||||
}
|
||||
|
||||
void GetContext::PostprocessMerge(const Status& merge_status) {
|
||||
Status GetContext::PostprocessMerge(const Status& merge_status) {
|
||||
if (!merge_status.ok()) {
|
||||
if (merge_status.subcode() == Status::SubCode::kMergeOperatorFailed) {
|
||||
state_ = kMergeOperatorFailed;
|
||||
} else {
|
||||
state_ = kCorrupt;
|
||||
}
|
||||
return;
|
||||
return merge_status;
|
||||
}
|
||||
|
||||
if (LIKELY(pinnable_val_ != nullptr)) {
|
||||
pinnable_val_->PinSelf();
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void GetContext::MergeWithNoBaseValue() {
|
||||
Status GetContext::MergeWithNoBaseValue() {
|
||||
assert(do_merge_);
|
||||
assert(pinnable_val_ || columns_);
|
||||
assert(!pinnable_val_ || !columns_);
|
||||
@@ -628,10 +704,10 @@ void GetContext::MergeWithNoBaseValue() {
|
||||
merge_context_->GetOperands(), logger_, statistics_, clock_,
|
||||
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
|
||||
pinnable_val_ ? pinnable_val_->GetSelf() : nullptr, columns_);
|
||||
PostprocessMerge(s);
|
||||
return PostprocessMerge(s);
|
||||
}
|
||||
|
||||
void GetContext::MergeWithPlainBaseValue(const Slice& value) {
|
||||
Status GetContext::MergeWithPlainBaseValue(const Slice& value) {
|
||||
assert(do_merge_);
|
||||
assert(pinnable_val_ || columns_);
|
||||
assert(!pinnable_val_ || !columns_);
|
||||
@@ -643,10 +719,10 @@ void GetContext::MergeWithPlainBaseValue(const Slice& value) {
|
||||
merge_context_->GetOperands(), logger_, statistics_, clock_,
|
||||
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
|
||||
pinnable_val_ ? pinnable_val_->GetSelf() : nullptr, columns_);
|
||||
PostprocessMerge(s);
|
||||
return PostprocessMerge(s);
|
||||
}
|
||||
|
||||
void GetContext::MergeWithWideColumnBaseValue(const Slice& entity) {
|
||||
Status GetContext::MergeWithWideColumnBaseValue(const Slice& entity) {
|
||||
assert(do_merge_);
|
||||
assert(pinnable_val_ || columns_);
|
||||
assert(!pinnable_val_ || !columns_);
|
||||
@@ -655,16 +731,16 @@ void GetContext::MergeWithWideColumnBaseValue(const Slice& entity) {
|
||||
// supports V1 format.
|
||||
std::string resolved_entity;
|
||||
Slice effective_entity;
|
||||
const Status s_resolve = WideColumnSerialization::ResolveEntityForMerge(
|
||||
Status s_resolve = WideColumnSerialization::ResolveEntityForMerge(
|
||||
entity, user_key_, blob_fetcher_, nullptr /* prefetch_buffers */,
|
||||
resolved_entity, effective_entity);
|
||||
if (!s_resolve.ok()) {
|
||||
if (s_resolve.IsIncomplete()) {
|
||||
MarkKeyMayExist();
|
||||
return;
|
||||
return s_resolve;
|
||||
}
|
||||
state_ = kCorrupt;
|
||||
return;
|
||||
return s_resolve;
|
||||
}
|
||||
|
||||
// `op_failure_scope` (an output parameter) is not provided (set to nullptr)
|
||||
@@ -674,7 +750,7 @@ void GetContext::MergeWithWideColumnBaseValue(const Slice& entity) {
|
||||
merge_context_->GetOperands(), logger_, statistics_, clock_,
|
||||
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
|
||||
pinnable_val_ ? pinnable_val_->GetSelf() : nullptr, columns_);
|
||||
PostprocessMerge(s);
|
||||
return PostprocessMerge(s);
|
||||
}
|
||||
|
||||
bool GetContext::GetBlobValue(const Slice& user_key, const Slice& blob_index,
|
||||
@@ -741,11 +817,16 @@ Status replayGetContextLog(const Slice& replay_log, const Slice& user_key,
|
||||
|
||||
(void)ret;
|
||||
|
||||
Status read_status;
|
||||
get_context->SaveValue(ikey, value, &dont_care, &read_status, value_pinner);
|
||||
Status read_status = Status::OK();
|
||||
const bool keep_replaying = get_context->SaveValue(
|
||||
ikey, value, &dont_care, &read_status, value_pinner);
|
||||
if (!read_status.ok()) {
|
||||
return read_status;
|
||||
}
|
||||
if (!keep_replaying) {
|
||||
// SaveValue() reached a terminal state for this row-cache replay.
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
+12
-5
@@ -7,6 +7,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "db/read_callback.h"
|
||||
#include "rocksdb/status.h"
|
||||
#include "rocksdb/types.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -19,7 +20,6 @@ class MergeOperator;
|
||||
class PinnableWideColumns;
|
||||
class PinnedIteratorsManager;
|
||||
class Statistics;
|
||||
class Status;
|
||||
class SystemClock;
|
||||
struct ParsedInternalKey;
|
||||
|
||||
@@ -121,6 +121,10 @@ class GetContext {
|
||||
PinnedIteratorsManager* _pinned_iters_mgr = nullptr,
|
||||
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
|
||||
uint64_t tracing_get_id = 0, BlobFetcher* blob_fetcher = nullptr);
|
||||
// emplace-only; default construction and move assignment are intentionally
|
||||
// disabled.
|
||||
GetContext(GetContext&&) noexcept = default;
|
||||
GetContext& operator=(GetContext&&) noexcept = delete;
|
||||
|
||||
GetContext() = delete;
|
||||
|
||||
@@ -207,16 +211,19 @@ class GetContext {
|
||||
Status SaveWideColumnEntityToColumns(const Slice& user_key,
|
||||
const Slice& entity,
|
||||
Cleanable* value_pinner);
|
||||
Status PushWideColumnEntityDefaultOperand(const Slice& user_key,
|
||||
const Slice& entity,
|
||||
Cleanable* value_pinner);
|
||||
|
||||
// Helper method that postprocesses the results of merge operations, e.g. it
|
||||
// sets the state correctly upon merge errors.
|
||||
void PostprocessMerge(const Status& merge_status);
|
||||
Status PostprocessMerge(const Status& merge_status);
|
||||
|
||||
// The following methods perform the actual merge operation for the
|
||||
// no base value/plain base value/wide-column base value cases.
|
||||
void MergeWithNoBaseValue();
|
||||
void MergeWithPlainBaseValue(const Slice& value);
|
||||
void MergeWithWideColumnBaseValue(const Slice& entity);
|
||||
Status MergeWithNoBaseValue();
|
||||
Status MergeWithPlainBaseValue(const Slice& value);
|
||||
Status MergeWithWideColumnBaseValue(const Slice& entity);
|
||||
|
||||
bool GetBlobValue(const Slice& user_key, const Slice& blob_index,
|
||||
PinnableSlice* blob_value, Status* read_status);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/file_checksum.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "rocksdb/utilities/types_util.h"
|
||||
#include "table/get_context.h"
|
||||
#include "table/table_builder.h"
|
||||
#include "table/table_iterator.h"
|
||||
@@ -157,6 +158,8 @@ std::vector<Status> SstFileReader::MultiGet(
|
||||
statuses[i] = Status::MergeInProgress();
|
||||
break;
|
||||
case GetContext::kCorrupt:
|
||||
statuses[i] = Status::Corruption();
|
||||
break;
|
||||
case GetContext::kUnexpectedBlobIndex:
|
||||
case GetContext::kMergeOperatorFailed:
|
||||
statuses[i] = Status::Corruption();
|
||||
@@ -218,6 +221,8 @@ Status SstFileReader::Get(const ReadOptions& roptions, const Slice& key,
|
||||
status = Status::MergeInProgress();
|
||||
break;
|
||||
case GetContext::kCorrupt:
|
||||
status = Status::Corruption();
|
||||
break;
|
||||
case GetContext::kUnexpectedBlobIndex:
|
||||
case GetContext::kMergeOperatorFailed:
|
||||
status = Status::Corruption();
|
||||
@@ -272,6 +277,11 @@ std::unique_ptr<Iterator> SstFileReader::NewTableIterator() {
|
||||
return std::make_unique<TableIterator>(internal_iter);
|
||||
}
|
||||
|
||||
Status SstFileReader::ParseTableIteratorKey(const Slice& raw_table_key,
|
||||
ParsedEntryInfo* parsed_key) const {
|
||||
return ParseEntry(raw_table_key, rep_->options.comparator, parsed_key);
|
||||
}
|
||||
|
||||
std::shared_ptr<const TableProperties> SstFileReader::GetTableProperties()
|
||||
const {
|
||||
return rep_->table_reader->GetTableProperties();
|
||||
|
||||
@@ -47,8 +47,9 @@ class SstFileReaderTest : public testing::Test {
|
||||
}
|
||||
|
||||
~SstFileReaderTest() {
|
||||
Status s = env_->DeleteFile(sst_name_);
|
||||
EXPECT_OK(s);
|
||||
if (env_->FileExists(sst_name_).ok()) {
|
||||
EXPECT_OK(env_->DeleteFile(sst_name_));
|
||||
}
|
||||
}
|
||||
|
||||
void CreateFile(const std::string& file_name,
|
||||
@@ -118,6 +119,105 @@ TEST_F(SstFileReaderTest, Basic) {
|
||||
CreateFileAndCheck(keys);
|
||||
}
|
||||
|
||||
TEST_F(SstFileReaderTest, ParseTableIteratorKey) {
|
||||
// Verify that callers of the raw table iterator can decode key metadata
|
||||
// through the public SstFileReader API instead of duplicating dbformat.h.
|
||||
std::vector<std::string> keys = {EncodeAsString(0), EncodeAsString(1),
|
||||
EncodeAsString(2)};
|
||||
CreateFile(sst_name_, keys);
|
||||
|
||||
SstFileReader reader(options_);
|
||||
ASSERT_OK(reader.Open(sst_name_));
|
||||
std::unique_ptr<Iterator> iter = reader.NewTableIterator();
|
||||
ASSERT_NE(iter, nullptr);
|
||||
iter->SeekToFirst();
|
||||
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ParsedEntryInfo parsed_key;
|
||||
ASSERT_OK(reader.ParseTableIteratorKey(iter->key(), &parsed_key));
|
||||
EXPECT_EQ(parsed_key.user_key, keys[0]);
|
||||
EXPECT_EQ(parsed_key.sequence, 0);
|
||||
EXPECT_EQ(parsed_key.type, kEntryPut);
|
||||
iter->Next();
|
||||
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_OK(reader.ParseTableIteratorKey(iter->key(), &parsed_key));
|
||||
EXPECT_EQ(parsed_key.user_key, keys[1]);
|
||||
EXPECT_EQ(parsed_key.sequence, 0);
|
||||
EXPECT_EQ(parsed_key.type, kEntryMerge);
|
||||
iter->Next();
|
||||
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_OK(reader.ParseTableIteratorKey(iter->key(), &parsed_key));
|
||||
EXPECT_EQ(parsed_key.user_key, keys[2]);
|
||||
EXPECT_EQ(parsed_key.sequence, 0);
|
||||
EXPECT_EQ(parsed_key.type, kEntryDelete);
|
||||
iter->Next();
|
||||
|
||||
EXPECT_FALSE(iter->Valid());
|
||||
}
|
||||
|
||||
TEST_F(SstFileReaderTest, ParseTableIteratorKeyFromDbGeneratedSst) {
|
||||
// Verify that the parser decodes real DB sequence numbers, not only the
|
||||
// zero sequence numbers generated by SstFileWriter-created external files.
|
||||
Options options = options_;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
std::string db_name = test::PerThreadDBPath("parse_table_iterator_key_db");
|
||||
std::unique_ptr<DB> db;
|
||||
ASSERT_OK(DB::Open(options, db_name, &db));
|
||||
|
||||
std::vector<std::string> keys = {EncodeAsString(0), EncodeAsString(1)};
|
||||
ASSERT_OK(db->Put(WriteOptions(), keys[0], "value0"));
|
||||
ASSERT_OK(db->Put(WriteOptions(), keys[1], "value1"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
|
||||
std::vector<std::string> live_files;
|
||||
uint64_t manifest_file_size = 0;
|
||||
ASSERT_OK(db->GetLiveFiles(live_files, &manifest_file_size));
|
||||
std::string flushed_file;
|
||||
for (const auto& live_file : live_files) {
|
||||
if (live_file.substr(live_file.size() - 4) == ".sst") {
|
||||
ASSERT_TRUE(flushed_file.empty()) << flushed_file << " " << live_file;
|
||||
flushed_file = db_name + live_file;
|
||||
}
|
||||
}
|
||||
ASSERT_FALSE(flushed_file.empty());
|
||||
db.reset();
|
||||
|
||||
SstFileReader reader(options_);
|
||||
ASSERT_OK(reader.Open(flushed_file));
|
||||
std::unique_ptr<Iterator> iter = reader.NewTableIterator();
|
||||
ASSERT_NE(iter, nullptr);
|
||||
iter->SeekToFirst();
|
||||
|
||||
for (size_t i = 0; i < keys.size(); ++i) {
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ParsedEntryInfo parsed_key;
|
||||
ASSERT_OK(reader.ParseTableIteratorKey(iter->key(), &parsed_key));
|
||||
EXPECT_EQ(parsed_key.user_key, keys[i]);
|
||||
EXPECT_EQ(parsed_key.sequence, i + 1);
|
||||
EXPECT_EQ(parsed_key.type, kEntryPut);
|
||||
iter->Next();
|
||||
}
|
||||
|
||||
EXPECT_FALSE(iter->Valid());
|
||||
|
||||
ASSERT_OK(DestroyDB(db_name, options));
|
||||
}
|
||||
|
||||
TEST_F(SstFileReaderTest, ParseTableIteratorKeyRejectsShortKey) {
|
||||
// Verify that invalid raw table keys fail through the public parser before
|
||||
// callers can accidentally interpret a missing internal-key trailer.
|
||||
CreateFile(sst_name_,
|
||||
{EncodeAsString(0), EncodeAsString(1), EncodeAsString(2)});
|
||||
SstFileReader reader(options_);
|
||||
ParsedEntryInfo parsed_key;
|
||||
|
||||
const Status status = reader.ParseTableIteratorKey("short", &parsed_key);
|
||||
EXPECT_TRUE(status.IsInvalidArgument()) << status.ToString();
|
||||
}
|
||||
|
||||
TEST_F(SstFileReaderTest, Uint64Comparator) {
|
||||
options_.comparator = test::Uint64Comparator();
|
||||
std::vector<std::string> keys;
|
||||
@@ -864,6 +964,47 @@ TEST_P(SstFileReaderTableGetTest, Basic) {
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(SstFileReaderTableGetTest, BlobBackedWideColumnDefaultWithoutFetcher) {
|
||||
// Goal: cover the SstFileReader path where GetContext has no BlobFetcher.
|
||||
// The test writes a blob-backed wide-column entity, compacts it so the SST
|
||||
// stores blob references, and then verifies SstFileReader reports a clean
|
||||
// Corruption status instead of dereferencing a null BlobFetcher.
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 50;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string key = "blob_backed_entity";
|
||||
const std::string default_value(100, 'd');
|
||||
WideColumns columns{{kDefaultWideColumnName, default_value},
|
||||
{"meta", "inline"}};
|
||||
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
std::vector<LiveFileMetaData> files;
|
||||
dbfull()->GetLiveFilesMetaData(&files);
|
||||
ASSERT_EQ(files.size(), 1);
|
||||
std::string file_name = files[0].directory + "/" + files[0].relative_filename;
|
||||
|
||||
SstFileReader reader(options);
|
||||
ASSERT_OK(reader.Open(file_name));
|
||||
|
||||
std::vector<Slice> keys = {key};
|
||||
std::vector<std::string> values;
|
||||
auto statuses = DoGet(reader, keys, &values);
|
||||
|
||||
ASSERT_TRUE(statuses[0].IsCorruption()) << statuses[0].ToString();
|
||||
ASSERT_NE(statuses[0].ToString().find("blob fetcher"), std::string::npos)
|
||||
<< statuses[0].ToString();
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(SingleAndMulti, SstFileReaderTableGetTest,
|
||||
testing::Bool());
|
||||
|
||||
|
||||
+3
-3
@@ -8633,7 +8633,7 @@ TEST_P(UserDefinedIndexTest, ValueTypeMappingViaDBFlush) {
|
||||
// ValueTypes by writing various operation types via the DB API, flushing,
|
||||
// and inspecting what the TestUserDefinedIndexBuilder received.
|
||||
if (is_reverse_comparator_) {
|
||||
// Skip for reverse comparator — the key ordering makes this test
|
||||
// Skip for reverse comparator -- the key ordering makes this test
|
||||
// unnecessarily complex and the mapping logic is comparator-independent.
|
||||
ROCKSDB_GTEST_BYPASS("Skipped for reverse comparator");
|
||||
return;
|
||||
@@ -8735,7 +8735,7 @@ TEST_P(UserDefinedIndexTest, CompactionWithSnapshotsAndUDI) {
|
||||
ASSERT_OK(db->Delete(WriteOptions(), "key_bb"));
|
||||
ASSERT_OK(db->Flush(FlushOptions()));
|
||||
|
||||
// Compact L0 → L1. With the snapshot held, both versions of key_aa
|
||||
// Compact L0 -> L1. With the snapshot held, both versions of key_aa
|
||||
// and the delete tombstone for key_bb must be preserved in the compaction
|
||||
// output. The UDI builder receives multiple entries for key_aa.
|
||||
ASSERT_OK(db->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
@@ -8747,7 +8747,7 @@ TEST_P(UserDefinedIndexTest, CompactionWithSnapshotsAndUDI) {
|
||||
const auto& log = user_defined_index_factory->key_type_log_;
|
||||
ASSERT_FALSE(log.empty());
|
||||
|
||||
// Count total occurrences of key_aa across all builders — at least 4:
|
||||
// Count total occurrences of key_aa across all builders -- at least 4:
|
||||
// flush1 (v1) + flush2 (v2) + compaction (v2, v1).
|
||||
int key_aa_count = 0;
|
||||
int key_bb_count = 0;
|
||||
|
||||
+19
-3
@@ -30,6 +30,10 @@ _NO_SPACE_SUBSTRINGS = (
|
||||
"enospc",
|
||||
)
|
||||
_OUTPUT_PATH_RE = re.compile(r"(/[^\s]+)")
|
||||
_TSAN_OPTIONS_ENV_VAR = "TSAN_OPTIONS"
|
||||
_TSAN_SUPPRESSIONS_FILE = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "tsan_suppressions.txt")
|
||||
)
|
||||
|
||||
|
||||
def get_random_seed(override):
|
||||
@@ -51,6 +55,16 @@ def quote_arg_for_display(arg):
|
||||
return f"{flag}={shlex.quote(value)}"
|
||||
|
||||
|
||||
def stress_cmd_env():
|
||||
env = os.environ.copy()
|
||||
if (
|
||||
_TSAN_OPTIONS_ENV_VAR not in env
|
||||
and os.path.exists(_TSAN_SUPPRESSIONS_FILE)
|
||||
):
|
||||
env[_TSAN_OPTIONS_ENV_VAR] = "suppressions=" + _TSAN_SUPPRESSIONS_FILE
|
||||
return env
|
||||
|
||||
|
||||
def early_argument_parsing_before_main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
@@ -1905,7 +1919,9 @@ def diagnostic_paths(finalized_params):
|
||||
|
||||
|
||||
def execute_cmd(cmd, timeout=None, timeout_pstack=False):
|
||||
child = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
child = subprocess.Popen(
|
||||
cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, env=stress_cmd_env()
|
||||
)
|
||||
print(
|
||||
"Running db_stress with pid=%d: %s\n\n"
|
||||
% (child.pid, " ".join(quote_arg_for_display(arg) for arg in cmd))
|
||||
@@ -1920,7 +1936,7 @@ def execute_cmd(cmd, timeout=None, timeout_pstack=False):
|
||||
hit_timeout = True
|
||||
if timeout_pstack:
|
||||
os.system("pstack %d" % pid)
|
||||
child.terminate() # SIGTERM — triggers TerminationHandler
|
||||
child.terminate() # SIGTERM -- triggers TerminationHandler
|
||||
try:
|
||||
outs, errs = child.communicate(timeout=3)
|
||||
print("TERMINATED %d\n" % child.pid)
|
||||
@@ -2006,7 +2022,7 @@ def cleanup_after_success(dbname):
|
||||
if parts[0] in ["--env_uri", "--fs_uri"]:
|
||||
cleanup_cmd_parts.append(arg)
|
||||
print("Running DB cleanup command - %s\n" % " ".join(cleanup_cmd_parts))
|
||||
ret = subprocess.call(cleanup_cmd_parts)
|
||||
ret = subprocess.call(cleanup_cmd_parts, env=stress_cmd_env())
|
||||
if ret != 0:
|
||||
print("ERROR: DB cleanup returned error %d\n" % ret)
|
||||
sys.exit(2)
|
||||
|
||||
@@ -16,6 +16,7 @@ import unittest
|
||||
_DB_CRASHTEST_PATH = os.path.join(os.path.dirname(__file__), "db_crashtest.py")
|
||||
_TEST_DIR_ENV_VAR = "TEST_TMPDIR"
|
||||
_TEST_EXPECTED_DIR_ENV_VAR = "TEST_TMPDIR_EXPECTED"
|
||||
_TSAN_OPTIONS_ENV_VAR = "TSAN_OPTIONS"
|
||||
|
||||
|
||||
def load_db_crashtest_module():
|
||||
@@ -40,6 +41,7 @@ class DBCrashTestTest(unittest.TestCase):
|
||||
)
|
||||
self.old_test_tmpdir = os.environ.get(_TEST_DIR_ENV_VAR)
|
||||
self.old_test_expected_tmpdir = os.environ.get(_TEST_EXPECTED_DIR_ENV_VAR)
|
||||
self.old_tsan_options = os.environ.get(_TSAN_OPTIONS_ENV_VAR)
|
||||
os.environ[_TEST_DIR_ENV_VAR] = self.test_tmpdir
|
||||
os.environ.pop(_TEST_EXPECTED_DIR_ENV_VAR, None)
|
||||
|
||||
@@ -54,6 +56,11 @@ class DBCrashTestTest(unittest.TestCase):
|
||||
else:
|
||||
os.environ[_TEST_EXPECTED_DIR_ENV_VAR] = self.old_test_expected_tmpdir
|
||||
|
||||
if self.old_tsan_options is None:
|
||||
os.environ.pop(_TSAN_OPTIONS_ENV_VAR, None)
|
||||
else:
|
||||
os.environ[_TSAN_OPTIONS_ENV_VAR] = self.old_tsan_options
|
||||
|
||||
shutil.rmtree(self.test_tmpdir)
|
||||
|
||||
def load_db_crashtest(self):
|
||||
@@ -66,6 +73,28 @@ class DBCrashTestTest(unittest.TestCase):
|
||||
params.update(overrides)
|
||||
return params
|
||||
|
||||
def test_stress_cmd_env_defaults_tsan_suppressions(self):
|
||||
os.environ.pop(_TSAN_OPTIONS_ENV_VAR, None)
|
||||
db_crashtest = self.load_db_crashtest()
|
||||
|
||||
env = db_crashtest.stress_cmd_env()
|
||||
|
||||
self.assertEqual(
|
||||
"suppressions="
|
||||
+ os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "tsan_suppressions.txt")
|
||||
),
|
||||
env[_TSAN_OPTIONS_ENV_VAR],
|
||||
)
|
||||
|
||||
def test_stress_cmd_env_preserves_tsan_options(self):
|
||||
os.environ[_TSAN_OPTIONS_ENV_VAR] = "halt_on_error=1"
|
||||
db_crashtest = self.load_db_crashtest()
|
||||
|
||||
env = db_crashtest.stress_cmd_env()
|
||||
|
||||
self.assertEqual("halt_on_error=1", env[_TSAN_OPTIONS_ENV_VAR])
|
||||
|
||||
def test_setup_expected_values_dir_preserves_existing_contents(self):
|
||||
os.makedirs(self.expected_dir)
|
||||
marker = os.path.join(self.expected_dir, "marker")
|
||||
|
||||
@@ -205,7 +205,7 @@ def collect_changed_lines(repo_root, diff_base_arg=None):
|
||||
)
|
||||
merge_into(all_changed, parse_diff_for_changed_lines(diff_staged))
|
||||
|
||||
# Untracked files — treat every line as changed
|
||||
# Untracked files -- treat every line as changed
|
||||
untracked_out, _ = run_cmd(
|
||||
["git", "ls-files", "--others", "--exclude-standard"], cwd=repo_root
|
||||
)
|
||||
@@ -510,7 +510,7 @@ def main():
|
||||
jobs = args.jobs or os.cpu_count() or 4
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 1 — detect changes
|
||||
# Step 1 -- detect changes
|
||||
# ------------------------------------------------------------------
|
||||
log("=" * 70)
|
||||
log("Step 1: Detecting changes")
|
||||
@@ -529,7 +529,7 @@ def main():
|
||||
log(f" {f} ({len(changed_lines[f])} lines)")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 2 — select compilable files present in compile_commands.json
|
||||
# Step 2 -- select compilable files present in compile_commands.json
|
||||
# ------------------------------------------------------------------
|
||||
db_files = load_compile_db(compile_db_path, repo_root)
|
||||
cc_changed = sorted(
|
||||
@@ -549,7 +549,7 @@ def main():
|
||||
log("=" * 70)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 3 — run clang-tidy in parallel via ThreadPoolExecutor
|
||||
# Step 3 -- run clang-tidy in parallel via ThreadPoolExecutor
|
||||
# ------------------------------------------------------------------
|
||||
all_raw_output = []
|
||||
all_filtered = []
|
||||
@@ -603,7 +603,7 @@ def main():
|
||||
log(f"\nFull clang-tidy output saved to {args.output}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 4 — report filtered results
|
||||
# Step 4 -- report filtered results
|
||||
# ------------------------------------------------------------------
|
||||
log(f"\n{'=' * 70}")
|
||||
log(f"Step 3: Results (wall time {wall_time:.1f}s)")
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# ThreadSanitizer suppressions for known third-party false positives.
|
||||
#
|
||||
# Use with:
|
||||
# TSAN_OPTIONS="suppressions=/path/to/rocksdb/tools/tsan_suppressions.txt"
|
||||
#
|
||||
# If the final application already has a TSAN suppressions file, merge these
|
||||
# entries into that application-owned file.
|
||||
#
|
||||
# liburing's io_uring_mmap() uses __sys_mmap, which bypasses TSAN's mmap
|
||||
# interceptor, then immediately reads from the mapped memory in
|
||||
# io_uring_setup_ring_pointers(). When the kernel reuses a virtual address that
|
||||
# was previously mmap'd through libc and tracked by TSAN, TSAN can report a race
|
||||
# between the old mapping's write and the new io_uring read. RocksDB annotates
|
||||
# the io_uring mappings after io_uring_queue_init() returns, but that is too
|
||||
# late for reads that happen inside liburing during initialization.
|
||||
#
|
||||
# Revisit these suppressions when RocksDB upgrades to a liburing version that no
|
||||
# longer uses the raw mmap syscall for io_uring_mmap().
|
||||
race:io_uring_mmap
|
||||
race:io_uring_setup_ring_pointers
|
||||
@@ -0,0 +1 @@
|
||||
WriteCommitted TransactionDB now matches WritePrepared and WriteUnprepared compaction filtering in both single- and two-write-queue modes: a compaction filter's FilterMergeOperand will not be invoked on merge operands at or below the latest published sequence number.
|
||||
@@ -0,0 +1,3 @@
|
||||
Fixed blob-backed wide-column merge reads to preserve correct status
|
||||
propagation and resolution across memtable, read-only, and secondary DB
|
||||
paths.
|
||||
@@ -0,0 +1 @@
|
||||
Fixed a bug where `DB::GetCreationTimeOfOldestFile()` could return inaccurate results instead of the real creation time when called shortly after opening a legacy DB (one whose manifest lacks `file_creation_time`) with `open_files_async = true`. The API now waits for background SST file loading to complete only when needed; modern DBs are unaffected.
|
||||
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user