mirror of
https://github.com/facebook/rocksdb.git
synced 2026-07-07 14:47:40 +08:00
Fix GetContext status propagation and blob-backed wide-column merge operands (#14640)
Summary: - propagate lower-level read and merge failures through `GetContext` via `read_status`, so `Get` and `GetEntity` preserve the original error instead of synthesizing `Corruption` when blob-backed reads or merge resolution fail - teach `GetMergeOperands` to resolve blob-backed default columns from wide-column entities, covering both the direct base-value path and the merge-plus-base path - add regression coverage for blob-read IO errors during `Get`/`GetEntity` merge resolution and for `GetMergeOperands` on blob-backed wide-column entities - fix the `DBFlushTest.MemPurgeCorrectLogNumberAndSSTFileCreation` test race by waiting for flush callbacks and cleaning up sync points ## Testing - `make db_blob_basic_test -j14` - `/usr/bin/perl -e 'alarm shift; exec ARGV' 60 ./db_blob_basic_test --gtest_filter='DBBlobBasicTest/DBBlobBasicIOErrorTest.GetBlob_IOError/*:DBBlobBasicTest/DBBlobBasicIOErrorTest.GetEntityMergeWithBlobBaseIOError/*'` ## Task T265824017, T265415808 Pull Request resolved: https://github.com/facebook/rocksdb/pull/14640 Reviewed By: anand1976 Differential Revision: D101690700 Pulled By: xingbowang fbshipit-source-id: 2b6fc357b37a01efa72a2d54dcff55be8992f42a
This commit is contained in:
committed by
meta-codesync[bot]
parent
46ce1a03a9
commit
4707775ae9
@@ -786,13 +786,19 @@ Status BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Status s = version != nullptr ? Status::Corruption("Invalid blob file number")
|
|
||||||
: Status::NotFound();
|
|
||||||
|
|
||||||
if (blob_file_cache == nullptr) {
|
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;
|
CacheHandleGuard<BlobFileReader> reader;
|
||||||
s = blob_file_cache->GetBlobFileReader(read_options, blob_idx.file_number(),
|
s = blob_file_cache->GetBlobFileReader(read_options, blob_idx.file_number(),
|
||||||
&reader,
|
&reader,
|
||||||
|
|||||||
@@ -1620,6 +1620,47 @@ TEST_P(DBBlobBasicIOErrorTest, GetBlob_IOError) {
|
|||||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
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) {
|
TEST_P(DBBlobBasicIOErrorMultiGetTest, MultiGetBlobs_IOError) {
|
||||||
Options options = GetDefaultOptions();
|
Options options = GetDefaultOptions();
|
||||||
options.env = fault_injection_env_.get();
|
options.env = fault_injection_env_.get();
|
||||||
|
|||||||
@@ -128,6 +128,16 @@ class DBBlobIndexTest : public DBTestBase {
|
|||||||
columns, s, is_blob_index, value_found);
|
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 GetTestOptions() {
|
||||||
Options options;
|
Options options;
|
||||||
options.env = CurrentOptions().env;
|
options.env = CurrentOptions().env;
|
||||||
@@ -271,6 +281,55 @@ TEST_F(DBBlobIndexTest,
|
|||||||
ASSERT_EQ(static_cast<char>(BlobIndex::Type::kUnknown), blob_index.front());
|
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 {
|
class PlainBlobValueFilterV3 : public CompactionFilter {
|
||||||
public:
|
public:
|
||||||
PlainBlobValueFilterV3(std::atomic<int>* filter_call_count,
|
PlainBlobValueFilterV3(std::atomic<int>* filter_call_count,
|
||||||
|
|||||||
@@ -1852,6 +1852,8 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ASSERT_OK(WaitForFlushCallbacks());
|
||||||
|
|
||||||
// Check that there was at least one mempurge
|
// Check that there was at least one mempurge
|
||||||
uint32_t expected_min_mempurge_count = 1;
|
uint32_t expected_min_mempurge_count = 1;
|
||||||
// Check that there was no SST files created during flush.
|
// Check that there was no SST files created during flush.
|
||||||
@@ -1872,6 +1874,8 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
|
|||||||
ASSERT_EQ(Get(key), value);
|
ASSERT_EQ(Get(key), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ASSERT_OK(WaitForFlushCallbacks());
|
||||||
|
|
||||||
// Check that there was at least one SST files created during flush.
|
// Check that there was at least one SST files created during flush.
|
||||||
expected_sst_count = 1;
|
expected_sst_count = 1;
|
||||||
EXPECT_GE(sst_count.load(), expected_sst_count);
|
EXPECT_GE(sst_count.load(), expected_sst_count);
|
||||||
@@ -1891,6 +1895,9 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
|
|||||||
// Extra check of database consistency.
|
// Extra check of database consistency.
|
||||||
ASSERT_EQ(Get(key), value);
|
ASSERT_EQ(Get(key), value);
|
||||||
|
|
||||||
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||||
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||||
|
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+190
-18
@@ -30,6 +30,7 @@
|
|||||||
|
|
||||||
#include "db/arena_wrapped_db_iter.h"
|
#include "db/arena_wrapped_db_iter.h"
|
||||||
#include "db/attribute_group_iterator_impl.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_file_partition_manager.h"
|
||||||
#include "db/blob/blob_index.h"
|
#include "db/blob/blob_index.h"
|
||||||
#include "db/builder.h"
|
#include "db/builder.h"
|
||||||
@@ -2871,6 +2872,148 @@ Status DBImpl::ResolveDirectWriteWideColumns(const ReadOptions& read_options,
|
|||||||
return status;
|
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(
|
bool DBImpl::MaybeResolveDirectWriteValue(
|
||||||
const ReadOptions& read_options, const Slice& key,
|
const ReadOptions& read_options, const Slice& key,
|
||||||
bool resolve_direct_write_value, const Version* current,
|
bool resolve_direct_write_value, const Version* current,
|
||||||
@@ -3086,6 +3229,14 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
|
|||||||
}
|
}
|
||||||
const bool resolve_direct_write_value =
|
const bool resolve_direct_write_value =
|
||||||
partition_mgr != nullptr && (is_blob_ptr == &is_blob_index);
|
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;
|
std::string blob_lookup_key_storage;
|
||||||
auto get_blob_lookup_key = [&]() -> Slice {
|
auto get_blob_lookup_key = [&]() -> Slice {
|
||||||
return GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage);
|
return GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage);
|
||||||
@@ -3113,19 +3264,21 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
|
|||||||
get_impl_options.columns, timestamp, &s, &merge_context,
|
get_impl_options.columns, timestamp, &s, &merge_context,
|
||||||
&max_covering_tombstone_seq, read_options,
|
&max_covering_tombstone_seq, read_options,
|
||||||
false /* immutable_memtable */,
|
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;
|
done = true;
|
||||||
maybe_resolve_memtable_value();
|
maybe_resolve_memtable_value();
|
||||||
|
|
||||||
RecordTick(stats_, MEMTABLE_HIT);
|
RecordTick(stats_, MEMTABLE_HIT);
|
||||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||||
sv->imm->Get(
|
sv->imm->Get(lkey,
|
||||||
lkey,
|
get_impl_options.value
|
||||||
get_impl_options.value ? get_impl_options.value->GetSelf()
|
? get_impl_options.value->GetSelf()
|
||||||
: nullptr,
|
: nullptr,
|
||||||
get_impl_options.columns, timestamp, &s, &merge_context,
|
get_impl_options.columns, timestamp, &s,
|
||||||
&max_covering_tombstone_seq, read_options,
|
&merge_context, &max_covering_tombstone_seq,
|
||||||
get_impl_options.callback, is_blob_ptr)) {
|
read_options, get_impl_options.callback,
|
||||||
|
is_blob_ptr, memtable_blob_fetcher_ptr)) {
|
||||||
done = true;
|
done = true;
|
||||||
maybe_resolve_memtable_value();
|
maybe_resolve_memtable_value();
|
||||||
|
|
||||||
@@ -3137,14 +3290,14 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
|
|||||||
if (sv->mem->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr,
|
if (sv->mem->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr,
|
||||||
/*timestamp=*/nullptr, &s, &merge_context,
|
/*timestamp=*/nullptr, &s, &merge_context,
|
||||||
&max_covering_tombstone_seq, read_options,
|
&max_covering_tombstone_seq, read_options,
|
||||||
false /* immutable_memtable */, nullptr, nullptr,
|
false /* immutable_memtable */, nullptr, nullptr, false,
|
||||||
false)) {
|
memtable_blob_fetcher_ptr)) {
|
||||||
done = true;
|
done = true;
|
||||||
RecordTick(stats_, MEMTABLE_HIT);
|
RecordTick(stats_, MEMTABLE_HIT);
|
||||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||||
sv->imm->GetMergeOperands(lkey, &s, &merge_context,
|
sv->imm->GetMergeOperands(
|
||||||
&max_covering_tombstone_seq,
|
lkey, &s, &merge_context, &max_covering_tombstone_seq,
|
||||||
read_options)) {
|
read_options, memtable_blob_fetcher_ptr)) {
|
||||||
done = true;
|
done = true;
|
||||||
RecordTick(stats_, MEMTABLE_HIT);
|
RecordTick(stats_, MEMTABLE_HIT);
|
||||||
}
|
}
|
||||||
@@ -3840,6 +3993,14 @@ Status DBImpl::MultiGetImpl(
|
|||||||
(*sorted_keys)[start_key]->column_family);
|
(*sorted_keys)[start_key]->column_family);
|
||||||
ColumnFamilyData* cfd = cfh->cfd();
|
ColumnFamilyData* cfd = cfh->cfd();
|
||||||
auto* partition_mgr = cfd->blob_partition_manager();
|
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
|
// Clear the timestamps for returning results so that we can distinguish
|
||||||
// between tombstone or key that has never been written
|
// between tombstone or key that has never been written
|
||||||
for (size_t i = start_key; i < start_key + num_keys; ++i) {
|
for (size_t i = start_key; i < start_key + num_keys; ++i) {
|
||||||
@@ -3886,9 +4047,11 @@ Status DBImpl::MultiGetImpl(
|
|||||||
has_unpersisted_data_.load(std::memory_order_relaxed));
|
has_unpersisted_data_.load(std::memory_order_relaxed));
|
||||||
if (!skip_memtable) {
|
if (!skip_memtable) {
|
||||||
super_version->mem->MultiGet(read_options, &range, callback,
|
super_version->mem->MultiGet(read_options, &range, callback,
|
||||||
false /* immutable_memtable */);
|
false /* immutable_memtable */,
|
||||||
|
memtable_blob_fetcher_ptr);
|
||||||
if (!range.empty()) {
|
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()) {
|
if (!range.empty()) {
|
||||||
uint64_t left = range.KeysLeft();
|
uint64_t left = range.KeysLeft();
|
||||||
@@ -6323,12 +6486,20 @@ Status DBImpl::GetLatestSequenceForKey(
|
|||||||
|
|
||||||
*seq = kMaxSequenceNumber;
|
*seq = kMaxSequenceNumber;
|
||||||
*found_record_for_key = false;
|
*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
|
// Check if there is a record for this key in the latest memtable
|
||||||
sv->mem->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr, timestamp, &s,
|
sv->mem->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr, timestamp, &s,
|
||||||
&merge_context, &max_covering_tombstone_seq, seq, read_options,
|
&merge_context, &max_covering_tombstone_seq, seq, read_options,
|
||||||
false /* immutable_memtable */, nullptr /*read_callback*/,
|
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())) {
|
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
|
||||||
// unexpected error reading memtable.
|
// unexpected error reading memtable.
|
||||||
@@ -6361,7 +6532,8 @@ Status DBImpl::GetLatestSequenceForKey(
|
|||||||
// Check if there is a record for this key in the immutable memtables
|
// Check if there is a record for this key in the immutable memtables
|
||||||
sv->imm->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr, timestamp, &s,
|
sv->imm->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr, timestamp, &s,
|
||||||
&merge_context, &max_covering_tombstone_seq, seq, read_options,
|
&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())) {
|
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
|
||||||
// unexpected error reading memtable.
|
// unexpected error reading memtable.
|
||||||
@@ -6394,7 +6566,7 @@ Status DBImpl::GetLatestSequenceForKey(
|
|||||||
sv->imm->GetFromHistory(lkey, /*value=*/nullptr, /*columns=*/nullptr,
|
sv->imm->GetFromHistory(lkey, /*value=*/nullptr, /*columns=*/nullptr,
|
||||||
timestamp, &s, &merge_context,
|
timestamp, &s, &merge_context,
|
||||||
&max_covering_tombstone_seq, seq, read_options,
|
&max_covering_tombstone_seq, seq, read_options,
|
||||||
is_blob_index);
|
is_blob_index, memtable_blob_fetcher_ptr);
|
||||||
|
|
||||||
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
|
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
|
||||||
// unexpected error reading memtable.
|
// unexpected error reading memtable.
|
||||||
|
|||||||
@@ -1875,6 +1875,7 @@ class DBImpl : public DB {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
friend class DB;
|
friend class DB;
|
||||||
|
friend class DBImplReadOnly;
|
||||||
friend class DBImplSecondary;
|
friend class DBImplSecondary;
|
||||||
friend class ErrorHandler;
|
friend class ErrorHandler;
|
||||||
friend class InternalStats;
|
friend class InternalStats;
|
||||||
@@ -2952,6 +2953,29 @@ class DBImpl : public DB {
|
|||||||
bool resolve_direct_write_value, const Version* current,
|
bool resolve_direct_write_value, const Version* current,
|
||||||
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns,
|
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns,
|
||||||
Status* s, bool* is_blob_index, bool* value_found = nullptr);
|
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,
|
template <typename IterType, typename ImplType,
|
||||||
typename ErrorIteratorFuncType>
|
typename ErrorIteratorFuncType>
|
||||||
|
|||||||
@@ -295,7 +295,8 @@ Status DB::OpenAsFollower(
|
|||||||
impl->versions_.reset(new ReactiveVersionSet(
|
impl->versions_.reset(new ReactiveVersionSet(
|
||||||
dbname, &impl->immutable_db_options_, impl->mutable_db_options_,
|
dbname, &impl->immutable_db_options_, impl->mutable_db_options_,
|
||||||
impl->file_options_, impl->table_cache_.get(),
|
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(
|
impl->column_family_memtables_.reset(
|
||||||
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
|
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
|
||||||
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
|
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
|
|
||||||
#include "db/db_impl/db_impl_readonly.h"
|
#include "db/db_impl/db_impl_readonly.h"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
#include "db/arena_wrapped_db_iter.h"
|
#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/compacted_db_impl.h"
|
||||||
#include "db/db_impl/db_impl.h"
|
#include "db/db_impl/db_impl.h"
|
||||||
#include "db/manifest_ops.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();
|
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||||
assert(ucmp);
|
assert(ucmp);
|
||||||
std::string* ts =
|
|
||||||
ucmp->timestamp_size() > 0 ? get_impl_options.timestamp : nullptr;
|
|
||||||
SequenceNumber snapshot = versions_->LastSequence();
|
SequenceNumber snapshot = versions_->LastSequence();
|
||||||
GetWithTimestampReadCallback read_cb(snapshot);
|
GetWithTimestampReadCallback read_cb(snapshot);
|
||||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||||
get_impl_options.column_family);
|
get_impl_options.column_family);
|
||||||
auto cfd = cfh->cfd();
|
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_) {
|
if (tracer_) {
|
||||||
InstrumentedMutexLock lock(&trace_mutex_);
|
InstrumentedMutexLock lock(&trace_mutex_);
|
||||||
if (tracer_) {
|
if (tracer_) {
|
||||||
@@ -95,6 +110,18 @@ Status DBImplReadOnly::GetImpl(const ReadOptions& read_options,
|
|||||||
SequenceNumber max_covering_tombstone_seq = 0;
|
SequenceNumber max_covering_tombstone_seq = 0;
|
||||||
LookupKey lkey(key, snapshot, read_options.timestamp);
|
LookupKey lkey(key, snapshot, read_options.timestamp);
|
||||||
PERF_TIMER_STOP(get_snapshot_time);
|
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
|
// Look up starts here
|
||||||
if (super_version->mem->Get(
|
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.value ? get_impl_options.value->GetSelf() : nullptr,
|
||||||
get_impl_options.columns, ts, &s, &merge_context,
|
get_impl_options.columns, ts, &s, &merge_context,
|
||||||
&max_covering_tombstone_seq, read_options,
|
&max_covering_tombstone_seq, read_options,
|
||||||
false /* immutable_memtable */, &read_cb,
|
false /* immutable_memtable */, &read_cb, is_blob_ptr,
|
||||||
/*is_blob_index=*/nullptr, /*do_merge=*/get_impl_options.get_value)) {
|
/*do_merge=*/get_impl_options.get_value, memtable_blob_fetcher_ptr)) {
|
||||||
if (get_impl_options.value) {
|
DBImpl::PostprocessMemtableValueRead(
|
||||||
get_impl_options.value->PinSelf();
|
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);
|
RecordTick(stats_, MEMTABLE_HIT);
|
||||||
} else {
|
} else {
|
||||||
PERF_TIMER_GUARD(get_from_output_files_time);
|
PERF_TIMER_GUARD(get_from_output_files_time);
|
||||||
|
|||||||
@@ -6,8 +6,10 @@
|
|||||||
#include "db/db_impl/db_impl_secondary.h"
|
#include "db/db_impl/db_impl_secondary.h"
|
||||||
|
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
#include "db/arena_wrapped_db_iter.h"
|
#include "db/arena_wrapped_db_iter.h"
|
||||||
|
#include "db/blob/blob_fetcher.h"
|
||||||
#include "db/log_reader.h"
|
#include "db/log_reader.h"
|
||||||
#include "db/log_writer.h"
|
#include "db/log_writer.h"
|
||||||
#include "db/merge_context.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();
|
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||||
assert(ucmp);
|
assert(ucmp);
|
||||||
std::string* ts =
|
|
||||||
ucmp->timestamp_size() > 0 ? get_impl_options.timestamp : nullptr;
|
|
||||||
SequenceNumber snapshot = versions_->LastSequence();
|
SequenceNumber snapshot = versions_->LastSequence();
|
||||||
GetWithTimestampReadCallback read_cb(snapshot);
|
GetWithTimestampReadCallback read_cb(snapshot);
|
||||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||||
get_impl_options.column_family);
|
get_impl_options.column_family);
|
||||||
auto cfd = cfh->cfd();
|
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_) {
|
if (tracer_) {
|
||||||
InstrumentedMutexLock lock(&trace_mutex_);
|
InstrumentedMutexLock lock(&trace_mutex_);
|
||||||
if (tracer_) {
|
if (tracer_) {
|
||||||
@@ -395,6 +409,18 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
|||||||
LookupKey lkey(key, snapshot, read_options.timestamp);
|
LookupKey lkey(key, snapshot, read_options.timestamp);
|
||||||
PERF_TIMER_STOP(get_snapshot_time);
|
PERF_TIMER_STOP(get_snapshot_time);
|
||||||
bool done = false;
|
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
|
// Look up starts here
|
||||||
if (get_impl_options.get_value) {
|
if (get_impl_options.get_value) {
|
||||||
@@ -404,12 +430,14 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
|||||||
: nullptr,
|
: nullptr,
|
||||||
get_impl_options.columns, ts, &s, &merge_context,
|
get_impl_options.columns, ts, &s, &merge_context,
|
||||||
&max_covering_tombstone_seq, read_options,
|
&max_covering_tombstone_seq, read_options,
|
||||||
false /* immutable_memtable */, &read_cb,
|
false /* immutable_memtable */, &read_cb, is_blob_ptr,
|
||||||
/*is_blob_index=*/nullptr, /*do_merge=*/true)) {
|
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
|
||||||
done = true;
|
done = true;
|
||||||
if (get_impl_options.value) {
|
DBImpl::PostprocessMemtableValueRead(
|
||||||
get_impl_options.value->PinSelf();
|
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);
|
RecordTick(stats_, MEMTABLE_HIT);
|
||||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||||
super_version->imm->Get(
|
super_version->imm->Get(
|
||||||
@@ -417,11 +445,14 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
|||||||
get_impl_options.value ? get_impl_options.value->GetSelf()
|
get_impl_options.value ? get_impl_options.value->GetSelf()
|
||||||
: nullptr,
|
: nullptr,
|
||||||
get_impl_options.columns, ts, &s, &merge_context,
|
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;
|
done = true;
|
||||||
if (get_impl_options.value) {
|
DBImpl::PostprocessMemtableValueRead(
|
||||||
get_impl_options.value->PinSelf();
|
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);
|
RecordTick(stats_, MEMTABLE_HIT);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -433,13 +464,14 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
|||||||
get_impl_options.columns, ts, &s, &merge_context,
|
get_impl_options.columns, ts, &s, &merge_context,
|
||||||
&max_covering_tombstone_seq, read_options,
|
&max_covering_tombstone_seq, read_options,
|
||||||
false /* immutable_memtable */, &read_cb,
|
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;
|
done = true;
|
||||||
RecordTick(stats_, MEMTABLE_HIT);
|
RecordTick(stats_, MEMTABLE_HIT);
|
||||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||||
super_version->imm->GetMergeOperands(lkey, &s, &merge_context,
|
super_version->imm->GetMergeOperands(
|
||||||
&max_covering_tombstone_seq,
|
lkey, &s, &merge_context, &max_covering_tombstone_seq,
|
||||||
read_options)) {
|
read_options, memtable_blob_fetcher_ptr)) {
|
||||||
done = true;
|
done = true;
|
||||||
RecordTick(stats_, MEMTABLE_HIT);
|
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,
|
ts, &s, &merge_context, &max_covering_tombstone_seq, &pinned_iters_mgr,
|
||||||
/*value_found*/ nullptr,
|
/*value_found*/ nullptr,
|
||||||
/*key_exists*/ nullptr, /*seq*/ nullptr, &read_cb, /*is_blob*/ 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);
|
RecordTick(stats_, MEMTABLE_MISS);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -777,7 +809,8 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
|
|||||||
impl->versions_.reset(new ReactiveVersionSet(
|
impl->versions_.reset(new ReactiveVersionSet(
|
||||||
dbname, &impl->immutable_db_options_, impl->mutable_db_options_,
|
dbname, &impl->immutable_db_options_, impl->mutable_db_options_,
|
||||||
impl->file_options_, impl->table_cache_.get(),
|
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(
|
impl->column_family_memtables_.reset(
|
||||||
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
|
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
|
||||||
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
|
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
|
||||||
|
|||||||
+195
-2
@@ -7,9 +7,12 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// 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.
|
// 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_impl/db_impl_secondary.h"
|
||||||
#include "db/db_test_util.h"
|
#include "db/db_test_util.h"
|
||||||
#include "db/db_with_timestamp_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 "port/stack_trace.h"
|
||||||
#include "rocksdb/utilities/transaction_db.h"
|
#include "rocksdb/utilities/transaction_db.h"
|
||||||
#include "test_util/sync_point.h"
|
#include "test_util/sync_point.h"
|
||||||
@@ -357,8 +360,7 @@ TEST_F(DBSecondaryTest, GetMergeOperands) {
|
|||||||
const Status s = db_secondary_->GetMergeOperands(
|
const Status s = db_secondary_->GetMergeOperands(
|
||||||
ReadOptions(), cfh, "k1", values.data(), &merge_operands_info,
|
ReadOptions(), cfh, "k1", values.data(), &merge_operands_info,
|
||||||
&number_of_operands);
|
&number_of_operands);
|
||||||
ASSERT_NOK(s);
|
ASSERT_OK(s);
|
||||||
ASSERT_TRUE(s.IsMergeInProgress());
|
|
||||||
|
|
||||||
ASSERT_EQ(number_of_operands, 4);
|
ASSERT_EQ(number_of_operands, 4);
|
||||||
ASSERT_EQ(values[0].ToString(), "v1");
|
ASSERT_EQ(values[0].ToString(), "v1");
|
||||||
@@ -367,6 +369,197 @@ TEST_F(DBSecondaryTest, GetMergeOperands) {
|
|||||||
ASSERT_EQ(values[3].ToString(), "v4");
|
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) {
|
TEST_F(DBSecondaryTest, InternalCompactionCompactedFiles) {
|
||||||
Options options;
|
Options options;
|
||||||
options.env = env_;
|
options.env = env_;
|
||||||
|
|||||||
@@ -7510,6 +7510,65 @@ TEST_F(DBTest2, GetLatestSeqAndTsForKey) {
|
|||||||
ASSERT_EQ(0, options.statistics->getTickerCount(GET_HIT_L0));
|
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)
|
#if defined(ZSTD)
|
||||||
TEST_F(DBTest2, ZSTDChecksum) {
|
TEST_F(DBTest2, ZSTDChecksum) {
|
||||||
// Verify that corruption during decompression is caught.
|
// Verify that corruption during decompression is caught.
|
||||||
|
|||||||
+80
-22
@@ -15,6 +15,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
|
#include "db/blob/blob_fetcher.h"
|
||||||
#include "db/blob/blob_file_partition_manager.h"
|
#include "db/blob/blob_file_partition_manager.h"
|
||||||
#include "db/blob/blob_index.h"
|
#include "db/blob/blob_index.h"
|
||||||
#include "db/dbformat.h"
|
#include "db/dbformat.h"
|
||||||
@@ -70,6 +71,67 @@ Status GetDefaultColumnBlobIndexSlice(Slice entity, Slice* blob_index_slice) {
|
|||||||
return status;
|
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
|
} // namespace
|
||||||
|
|
||||||
ImmutableMemTableOptions::ImmutableMemTableOptions(
|
ImmutableMemTableOptions::ImmutableMemTableOptions(
|
||||||
@@ -1254,6 +1316,7 @@ struct Saver {
|
|||||||
bool inplace_update_support;
|
bool inplace_update_support;
|
||||||
bool do_merge;
|
bool do_merge;
|
||||||
SystemClock* clock;
|
SystemClock* clock;
|
||||||
|
const BlobFetcher* blob_fetcher;
|
||||||
|
|
||||||
ReadCallback* callback_;
|
ReadCallback* callback_;
|
||||||
bool* is_blob_index;
|
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
|
// Preserve the value with the goal of returning it as part of
|
||||||
// raw merge operands to the user
|
// raw merge operands to the user
|
||||||
|
|
||||||
Slice value_of_default;
|
*(s->status) = PushWideColumnEntityDefaultOperand(
|
||||||
*(s->status) = WideColumnSerialization::GetValueOfDefaultColumn(
|
s->key->user_key(), v, merge_context,
|
||||||
v, value_of_default);
|
s->inplace_update_support == false /* operand_pinned */,
|
||||||
|
s->blob_fetcher);
|
||||||
if (s->status->ok()) {
|
|
||||||
merge_context->PushOperand(
|
|
||||||
value_of_default,
|
|
||||||
s->inplace_update_support == false /* operand_pinned */);
|
|
||||||
}
|
|
||||||
} else if (*(s->merge_in_progress)) {
|
} else if (*(s->merge_in_progress)) {
|
||||||
assert(s->do_merge);
|
assert(s->do_merge);
|
||||||
|
|
||||||
if (s->value || s->columns) {
|
if (s->value || s->columns) {
|
||||||
// `op_failure_scope` (an output parameter) is not provided (set
|
*(s->status) = MergeWithWideColumnEntityBaseValue(
|
||||||
// to nullptr) since a failure must be propagated regardless of
|
s->key->user_key(), v, merge_operator, merge_context, s->logger,
|
||||||
// its value.
|
s->statistics, s->clock, s->value, s->columns, s->blob_fetcher);
|
||||||
*(s->status) = MergeHelper::TimedFullMerge(
|
|
||||||
merge_operator, s->key->user_key(), MergeHelper::kWideBaseValue,
|
|
||||||
v, merge_context->GetOperands(), s->logger, s->statistics,
|
|
||||||
s->clock, /* update_num_ops_stats */ true,
|
|
||||||
/* op_failure_scope */ nullptr, s->value, s->columns);
|
|
||||||
}
|
}
|
||||||
} else if (s->value) {
|
} else if (s->value) {
|
||||||
Slice value_of_default;
|
Slice value_of_default;
|
||||||
@@ -1520,7 +1573,8 @@ bool MemTable::Get(const LookupKey& key, std::string* value,
|
|||||||
SequenceNumber* max_covering_tombstone_seq,
|
SequenceNumber* max_covering_tombstone_seq,
|
||||||
SequenceNumber* seq, const ReadOptions& read_opts,
|
SequenceNumber* seq, const ReadOptions& read_opts,
|
||||||
bool immutable_memtable, ReadCallback* callback,
|
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
|
// The sequence number is updated synchronously in version_set.h
|
||||||
if (IsEmpty()) {
|
if (IsEmpty()) {
|
||||||
// Avoiding recording stats for speed.
|
// 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,
|
GetFromTable(key, *max_covering_tombstone_seq, do_merge, callback,
|
||||||
is_blob_index, value, columns, timestamp, s, merge_context,
|
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
|
// 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,
|
PinnableWideColumns* columns,
|
||||||
std::string* timestamp, Status* s,
|
std::string* timestamp, Status* s,
|
||||||
MergeContext* merge_context, SequenceNumber* seq,
|
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 saver;
|
||||||
saver.status = s;
|
saver.status = s;
|
||||||
saver.found_final_value = found_final_value;
|
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.inplace_update_support = moptions_.inplace_update_support;
|
||||||
saver.statistics = moptions_.statistics;
|
saver.statistics = moptions_.statistics;
|
||||||
saver.clock = clock_;
|
saver.clock = clock_;
|
||||||
|
saver.blob_fetcher = blob_fetcher;
|
||||||
saver.callback_ = callback;
|
saver.callback_ = callback;
|
||||||
saver.is_blob_index = is_blob_index;
|
saver.is_blob_index = is_blob_index;
|
||||||
saver.do_merge = do_merge;
|
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,
|
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
|
// The sequence number is updated synchronously in version_set.h
|
||||||
if (IsEmpty()) {
|
if (IsEmpty()) {
|
||||||
// Avoiding recording stats for speed.
|
// 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.inplace_update_support = moptions_.inplace_update_support;
|
||||||
saver.statistics = moptions_.statistics;
|
saver.statistics = moptions_.statistics;
|
||||||
saver.clock = clock_;
|
saver.clock = clock_;
|
||||||
|
saver.blob_fetcher = blob_fetcher;
|
||||||
saver.callback_ = callback;
|
saver.callback_ = callback;
|
||||||
saver.is_blob_index = &iter->is_blob_index;
|
saver.is_blob_index = &iter->is_blob_index;
|
||||||
saver.do_merge = true;
|
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->lkey), iter->max_covering_tombstone_seq, true, callback,
|
||||||
&iter->is_blob_index, iter->value ? iter->value->GetSelf() : nullptr,
|
&iter->is_blob_index, iter->value ? iter->value->GetSelf() : nullptr,
|
||||||
iter->columns, iter->timestamp, iter->s, &(iter->merge_context),
|
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 (!found_final_value && merge_in_progress) {
|
||||||
if (iter->s->ok()) {
|
if (iter->s->ok()) {
|
||||||
|
|||||||
+13
-7
@@ -39,6 +39,7 @@
|
|||||||
|
|
||||||
namespace ROCKSDB_NAMESPACE {
|
namespace ROCKSDB_NAMESPACE {
|
||||||
|
|
||||||
|
class BlobFetcher;
|
||||||
class BlobFilePartitionManager;
|
class BlobFilePartitionManager;
|
||||||
struct FlushJobInfo;
|
struct FlushJobInfo;
|
||||||
class Mutex;
|
class Mutex;
|
||||||
@@ -236,25 +237,27 @@ class ReadOnlyMemTable {
|
|||||||
SequenceNumber* max_covering_tombstone_seq,
|
SequenceNumber* max_covering_tombstone_seq,
|
||||||
SequenceNumber* seq, const ReadOptions& read_opts,
|
SequenceNumber* seq, const ReadOptions& read_opts,
|
||||||
bool immutable_memtable, ReadCallback* callback = nullptr,
|
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,
|
bool Get(const LookupKey& key, std::string* value,
|
||||||
PinnableWideColumns* columns, std::string* timestamp, Status* s,
|
PinnableWideColumns* columns, std::string* timestamp, Status* s,
|
||||||
MergeContext* merge_context,
|
MergeContext* merge_context,
|
||||||
SequenceNumber* max_covering_tombstone_seq,
|
SequenceNumber* max_covering_tombstone_seq,
|
||||||
const ReadOptions& read_opts, bool immutable_memtable,
|
const ReadOptions& read_opts, bool immutable_memtable,
|
||||||
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
|
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
|
||||||
bool do_merge = true) {
|
bool do_merge = true, const BlobFetcher* blob_fetcher = nullptr) {
|
||||||
SequenceNumber seq;
|
SequenceNumber seq;
|
||||||
return Get(key, value, columns, timestamp, s, merge_context,
|
return Get(key, value, columns, timestamp, s, merge_context,
|
||||||
max_covering_tombstone_seq, &seq, read_opts, immutable_memtable,
|
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
|
// @param immutable_memtable Whether this memtable is immutable. Used
|
||||||
// internally by NewRangeTombstoneIterator(). See comment above
|
// internally by NewRangeTombstoneIterator(). See comment above
|
||||||
// NewRangeTombstoneIterator() for more detail.
|
// NewRangeTombstoneIterator() for more detail.
|
||||||
virtual void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
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.
|
// Get total number of entries in the mem table.
|
||||||
// REQUIRES: external synchronization to prevent simultaneous
|
// REQUIRES: external synchronization to prevent simultaneous
|
||||||
@@ -688,10 +691,12 @@ class MemTable final : public ReadOnlyMemTable {
|
|||||||
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
||||||
const ReadOptions& read_opts, bool immutable_memtable,
|
const ReadOptions& read_opts, bool immutable_memtable,
|
||||||
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
|
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,
|
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
|
// 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
|
// 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* value, PinnableWideColumns* columns,
|
||||||
std::string* timestamp, Status* s,
|
std::string* timestamp, Status* s,
|
||||||
MergeContext* merge_context, SequenceNumber* seq,
|
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.,
|
// 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
|
// 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,
|
MergeContext* merge_context,
|
||||||
SequenceNumber* max_covering_tombstone_seq,
|
SequenceNumber* max_covering_tombstone_seq,
|
||||||
SequenceNumber* seq, const ReadOptions& read_opts,
|
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,
|
return GetFromList(&memlist_, key, value, columns, timestamp, s,
|
||||||
merge_context, max_covering_tombstone_seq, seq, read_opts,
|
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,
|
void MemTableListVersion::MultiGet(const ReadOptions& read_options,
|
||||||
MultiGetRange* range,
|
MultiGetRange* range, ReadCallback* callback,
|
||||||
ReadCallback* callback) {
|
const BlobFetcher* blob_fetcher) {
|
||||||
for (auto memtable : memlist_) {
|
for (auto memtable : memlist_) {
|
||||||
memtable->MultiGet(read_options, range, callback,
|
memtable->MultiGet(read_options, range, callback,
|
||||||
true /* immutable_memtable */);
|
true /* immutable_memtable */, blob_fetcher);
|
||||||
if (range->empty()) {
|
if (range->empty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -128,12 +129,13 @@ void MemTableListVersion::MultiGet(const ReadOptions& read_options,
|
|||||||
|
|
||||||
bool MemTableListVersion::GetMergeOperands(
|
bool MemTableListVersion::GetMergeOperands(
|
||||||
const LookupKey& key, Status* s, MergeContext* merge_context,
|
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_) {
|
for (ReadOnlyMemTable* memtable : memlist_) {
|
||||||
bool done = memtable->Get(
|
bool done = memtable->Get(
|
||||||
key, /*value=*/nullptr, /*columns=*/nullptr, /*timestamp=*/nullptr, s,
|
key, /*value=*/nullptr, /*columns=*/nullptr, /*timestamp=*/nullptr, s,
|
||||||
merge_context, max_covering_tombstone_seq, read_opts,
|
merge_context, max_covering_tombstone_seq, read_opts,
|
||||||
true /* immutable_memtable */, nullptr, nullptr, false);
|
true /* immutable_memtable */, nullptr, nullptr, false, blob_fetcher);
|
||||||
if (done) {
|
if (done) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -145,10 +147,11 @@ bool MemTableListVersion::GetFromHistory(
|
|||||||
const LookupKey& key, std::string* value, PinnableWideColumns* columns,
|
const LookupKey& key, std::string* value, PinnableWideColumns* columns,
|
||||||
std::string* timestamp, Status* s, MergeContext* merge_context,
|
std::string* timestamp, Status* s, MergeContext* merge_context,
|
||||||
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
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,
|
return GetFromList(&memlist_history_, key, value, columns, timestamp, s,
|
||||||
merge_context, max_covering_tombstone_seq, seq, read_opts,
|
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(
|
bool MemTableListVersion::GetFromList(
|
||||||
@@ -156,17 +159,18 @@ bool MemTableListVersion::GetFromList(
|
|||||||
std::string* value, PinnableWideColumns* columns, std::string* timestamp,
|
std::string* value, PinnableWideColumns* columns, std::string* timestamp,
|
||||||
Status* s, MergeContext* merge_context,
|
Status* s, MergeContext* merge_context,
|
||||||
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
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;
|
*seq = kMaxSequenceNumber;
|
||||||
|
|
||||||
for (auto& memtable : *list) {
|
for (auto& memtable : *list) {
|
||||||
assert(memtable->IsFragmentedRangeTombstonesConstructed());
|
assert(memtable->IsFragmentedRangeTombstonesConstructed());
|
||||||
SequenceNumber current_seq = kMaxSequenceNumber;
|
SequenceNumber current_seq = kMaxSequenceNumber;
|
||||||
|
|
||||||
bool done =
|
bool done = memtable->Get(key, value, columns, timestamp, s, merge_context,
|
||||||
memtable->Get(key, value, columns, timestamp, s, merge_context,
|
max_covering_tombstone_seq, ¤t_seq,
|
||||||
max_covering_tombstone_seq, ¤t_seq, read_opts,
|
read_opts, true /* immutable_memtable */,
|
||||||
true /* immutable_memtable */, callback, is_blob_index);
|
callback, is_blob_index, true, blob_fetcher);
|
||||||
if (*seq == kMaxSequenceNumber) {
|
if (*seq == kMaxSequenceNumber) {
|
||||||
// Store the most recent sequence number of any operation on this key.
|
// 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
|
// Since we only care about the most recent change, we only need to
|
||||||
|
|||||||
+16
-9
@@ -61,29 +61,33 @@ class MemTableListVersion {
|
|||||||
MergeContext* merge_context,
|
MergeContext* merge_context,
|
||||||
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
||||||
const ReadOptions& read_opts, ReadCallback* callback = nullptr,
|
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,
|
bool Get(const LookupKey& key, std::string* value,
|
||||||
PinnableWideColumns* columns, std::string* timestamp, Status* s,
|
PinnableWideColumns* columns, std::string* timestamp, Status* s,
|
||||||
MergeContext* merge_context,
|
MergeContext* merge_context,
|
||||||
SequenceNumber* max_covering_tombstone_seq,
|
SequenceNumber* max_covering_tombstone_seq,
|
||||||
const ReadOptions& read_opts, ReadCallback* callback = nullptr,
|
const ReadOptions& read_opts, ReadCallback* callback = nullptr,
|
||||||
bool* is_blob_index = nullptr) {
|
bool* is_blob_index = nullptr,
|
||||||
|
const BlobFetcher* blob_fetcher = nullptr) {
|
||||||
SequenceNumber seq;
|
SequenceNumber seq;
|
||||||
return Get(key, value, columns, timestamp, s, merge_context,
|
return Get(key, value, columns, timestamp, s, merge_context,
|
||||||
max_covering_tombstone_seq, &seq, read_opts, callback,
|
max_covering_tombstone_seq, &seq, read_opts, callback,
|
||||||
is_blob_index);
|
is_blob_index, blob_fetcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
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
|
// Returns all the merge operands corresponding to the key by searching all
|
||||||
// memtables starting from the most recent one.
|
// memtables starting from the most recent one.
|
||||||
bool GetMergeOperands(const LookupKey& key, Status* s,
|
bool GetMergeOperands(const LookupKey& key, Status* s,
|
||||||
MergeContext* merge_context,
|
MergeContext* merge_context,
|
||||||
SequenceNumber* max_covering_tombstone_seq,
|
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
|
// Similar to Get(), but searches the Memtable history of memtables that
|
||||||
// have already been flushed. Should only be used from in-memory only
|
// have already been flushed. Should only be used from in-memory only
|
||||||
@@ -94,17 +98,19 @@ class MemTableListVersion {
|
|||||||
Status* s, MergeContext* merge_context,
|
Status* s, MergeContext* merge_context,
|
||||||
SequenceNumber* max_covering_tombstone_seq,
|
SequenceNumber* max_covering_tombstone_seq,
|
||||||
SequenceNumber* seq, const ReadOptions& read_opts,
|
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,
|
bool GetFromHistory(const LookupKey& key, std::string* value,
|
||||||
PinnableWideColumns* columns, std::string* timestamp,
|
PinnableWideColumns* columns, std::string* timestamp,
|
||||||
Status* s, MergeContext* merge_context,
|
Status* s, MergeContext* merge_context,
|
||||||
SequenceNumber* max_covering_tombstone_seq,
|
SequenceNumber* max_covering_tombstone_seq,
|
||||||
const ReadOptions& read_opts,
|
const ReadOptions& read_opts,
|
||||||
bool* is_blob_index = nullptr) {
|
bool* is_blob_index = nullptr,
|
||||||
|
const BlobFetcher* blob_fetcher = nullptr) {
|
||||||
SequenceNumber seq;
|
SequenceNumber seq;
|
||||||
return GetFromHistory(key, value, columns, timestamp, s, merge_context,
|
return GetFromHistory(key, value, columns, timestamp, s, merge_context,
|
||||||
max_covering_tombstone_seq, &seq, read_opts,
|
max_covering_tombstone_seq, &seq, read_opts,
|
||||||
is_blob_index);
|
is_blob_index, blob_fetcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
Status AddRangeTombstoneIterators(const ReadOptions& read_opts, Arena* arena,
|
Status AddRangeTombstoneIterators(const ReadOptions& read_opts, Arena* arena,
|
||||||
@@ -188,7 +194,8 @@ class MemTableListVersion {
|
|||||||
SequenceNumber* max_covering_tombstone_seq,
|
SequenceNumber* max_covering_tombstone_seq,
|
||||||
SequenceNumber* seq, const ReadOptions& read_opts,
|
SequenceNumber* seq, const ReadOptions& read_opts,
|
||||||
ReadCallback* callback = nullptr,
|
ReadCallback* callback = nullptr,
|
||||||
bool* is_blob_index = nullptr);
|
bool* is_blob_index = nullptr,
|
||||||
|
const BlobFetcher* blob_fetcher = nullptr);
|
||||||
|
|
||||||
void AddMemTable(ReadOnlyMemTable* m);
|
void AddMemTable(ReadOnlyMemTable* m);
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -8163,11 +8163,12 @@ ReactiveVersionSet::ReactiveVersionSet(
|
|||||||
const MutableDBOptions& mutable_db_options,
|
const MutableDBOptions& mutable_db_options,
|
||||||
const FileOptions& _file_options, Cache* table_cache,
|
const FileOptions& _file_options, Cache* table_cache,
|
||||||
WriteBufferManager* write_buffer_manager, WriteController* write_controller,
|
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,
|
: VersionSet(dbname, imm_db_options, mutable_db_options, _file_options,
|
||||||
table_cache, write_buffer_manager, write_controller,
|
table_cache, write_buffer_manager, write_controller,
|
||||||
/*block_cache_tracer=*/nullptr, io_tracer, /*db_id*/ "",
|
/*block_cache_tracer=*/nullptr, io_tracer, db_id,
|
||||||
/*db_session_id*/ "", /*daily_offpeak_time_utc*/ "",
|
db_session_id, /*daily_offpeak_time_utc*/ "",
|
||||||
/*error_handler=*/nullptr, /*unchanging=*/false) {}
|
/*error_handler=*/nullptr, /*unchanging=*/false) {}
|
||||||
|
|
||||||
ReactiveVersionSet::~ReactiveVersionSet() = default;
|
ReactiveVersionSet::~ReactiveVersionSet() = default;
|
||||||
|
|||||||
+3
-1
@@ -1898,7 +1898,9 @@ class ReactiveVersionSet : public VersionSet {
|
|||||||
const FileOptions& _file_options, Cache* table_cache,
|
const FileOptions& _file_options, Cache* table_cache,
|
||||||
WriteBufferManager* write_buffer_manager,
|
WriteBufferManager* write_buffer_manager,
|
||||||
WriteController* write_controller,
|
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;
|
~ReactiveVersionSet() override;
|
||||||
|
|
||||||
|
|||||||
@@ -1203,7 +1203,7 @@ class VersionSetTestBase {
|
|||||||
reactive_versions_ = std::make_shared<ReactiveVersionSet>(
|
reactive_versions_ = std::make_shared<ReactiveVersionSet>(
|
||||||
dbname_, &imm_db_options_, mutable_db_options_, env_options_,
|
dbname_, &imm_db_options_, mutable_db_options_, env_options_,
|
||||||
table_cache_.get(), &write_buffer_manager_, &write_controller_,
|
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_,
|
imm_db_options_.db_paths.emplace_back(dbname_,
|
||||||
std::numeric_limits<uint64_t>::max());
|
std::numeric_limits<uint64_t>::max());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ class DBWideBasicTest : public DBTestBase {
|
|||||||
return wide_column_test_util::GetOptionsForBlobTest(GetDefaultOptions());
|
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.
|
// Helper: runs the EntityBlobAfterFlush test logic with the given options.
|
||||||
void RunEntityBlobAfterFlush(const Options& 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) {
|
void DBWideBasicTest::RunEntityBlobAfterFlush(const Options& options) {
|
||||||
Reopen(options);
|
Reopen(options);
|
||||||
|
|
||||||
@@ -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_F(DBWideBasicTest, MergeEntityWithBlobColumnsNoDefault) {
|
||||||
// Test: Merge on a V2 entity without a default column. The merge result
|
// 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
|
// should produce a valid entity with all original columns plus a default
|
||||||
|
|||||||
@@ -153,6 +153,8 @@ class WideColumnSerialization {
|
|||||||
// value() is initially the serialized BlobIndex bytes from the entity.
|
// value() is initially the serialized BlobIndex bytes from the entity.
|
||||||
// blob_columns: receives (column_index, blob_index) pairs identifying which
|
// blob_columns: receives (column_index, blob_index) pairs identifying which
|
||||||
// entries in `columns` are blob references and their decoded BlobIndex data.
|
// 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(
|
static Status DeserializeV2(
|
||||||
Slice& input, std::vector<WideColumn>& columns,
|
Slice& input, std::vector<WideColumn>& columns,
|
||||||
std::vector<std::pair<size_t, BlobIndex>>& blob_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.
|
// Check if the serialized entity has any blob column references.
|
||||||
// Sets *has_blob_columns to true if version >= 2 and at least one column
|
// Sets *has_blob_columns to true if version >= 2 and at least one column
|
||||||
// has blob type; false otherwise.
|
// 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.
|
// Returns Status::Corruption on decode errors.
|
||||||
static Status HasBlobColumns(const Slice& input, bool& has_blob_columns);
|
static Status HasBlobColumns(const Slice& input, bool& has_blob_columns);
|
||||||
|
|
||||||
|
|||||||
@@ -48,13 +48,18 @@ bool WBWIMemTable::Get(const LookupKey& key, std::string* value,
|
|||||||
SequenceNumber* max_covering_tombstone_seq,
|
SequenceNumber* max_covering_tombstone_seq,
|
||||||
SequenceNumber* out_seq, const ReadOptions&,
|
SequenceNumber* out_seq, const ReadOptions&,
|
||||||
bool immutable_memtable, ReadCallback* callback,
|
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());
|
assert(s->ok() || s->IsMergeInProgress());
|
||||||
(void)immutable_memtable;
|
(void)immutable_memtable;
|
||||||
(void)timestamp;
|
(void)timestamp;
|
||||||
(void)columns;
|
(void)columns;
|
||||||
|
(void)blob_fetcher;
|
||||||
assert(immutable_memtable);
|
assert(immutable_memtable);
|
||||||
assert(!timestamp); // TODO: support UDT
|
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_.upper_bound != kMaxSequenceNumber);
|
||||||
assert(assigned_seqno_.lower_bound != kMaxSequenceNumber);
|
assert(assigned_seqno_.lower_bound != kMaxSequenceNumber);
|
||||||
// WBWI does not support DeleteRange yet.
|
// 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,
|
void WBWIMemTable::MultiGet(const ReadOptions& read_options,
|
||||||
MultiGetRange* range, ReadCallback* callback,
|
MultiGetRange* range, ReadCallback* callback,
|
||||||
bool immutable_memtable) {
|
bool immutable_memtable,
|
||||||
|
const BlobFetcher* blob_fetcher) {
|
||||||
(void)immutable_memtable;
|
(void)immutable_memtable;
|
||||||
|
(void)blob_fetcher;
|
||||||
// Should only be used as immutable memtable.
|
// Should only be used as immutable memtable.
|
||||||
assert(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().
|
// TODO: reuse the InternalIterator created in Get().
|
||||||
for (auto iter = range->begin(); iter != range->end(); ++iter) {
|
for (auto iter = range->begin(); iter != range->end(); ++iter) {
|
||||||
SequenceNumber dummy_seq = 0;
|
SequenceNumber dummy_seq = 0;
|
||||||
|
|||||||
@@ -134,10 +134,12 @@ class WBWIMemTable final : public ReadOnlyMemTable {
|
|||||||
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
|
||||||
const ReadOptions& read_opts, bool immutable_memtable,
|
const ReadOptions& read_opts, bool immutable_memtable,
|
||||||
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
|
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,
|
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_; }
|
uint64_t NumEntries() const override { return num_entries_; }
|
||||||
|
|
||||||
|
|||||||
+130
-49
@@ -127,6 +127,10 @@ Status GetContext::SaveWideColumnEntityToPinnable(const Slice& user_key,
|
|||||||
pinnable_val_->PinSelf(value_of_default);
|
pinnable_val_->PinSelf(value_of_default);
|
||||||
}
|
}
|
||||||
} else if (status.IsNotSupported()) {
|
} 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.
|
// Default column is a blob reference, so resolve it into the output value.
|
||||||
bool resolved = false;
|
bool resolved = false;
|
||||||
status = WideColumnSerialization::GetValueOfDefaultColumnResolvingBlobs(
|
status = WideColumnSerialization::GetValueOfDefaultColumnResolvingBlobs(
|
||||||
@@ -140,14 +144,28 @@ Status GetContext::SaveWideColumnEntityToColumns(const Slice& user_key,
|
|||||||
Cleanable* value_pinner) {
|
Cleanable* value_pinner) {
|
||||||
assert(columns_ != nullptr);
|
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<WideColumn> entity_columns;
|
||||||
std::vector<std::pair<size_t, BlobIndex>> blob_cols;
|
std::vector<std::pair<size_t, BlobIndex>> blob_cols;
|
||||||
Slice entity_ref = entity;
|
Slice entity_ref = entity;
|
||||||
Status status = WideColumnSerialization::DeserializeV2(
|
status = WideColumnSerialization::DeserializeV2(entity_ref, entity_columns,
|
||||||
entity_ref, entity_columns, blob_cols);
|
blob_cols);
|
||||||
if (status.ok()) {
|
if (status.ok()) {
|
||||||
if (LIKELY(blob_cols.empty())) {
|
// HasBlobColumns() and DeserializeV2() must agree on whether this entity
|
||||||
return columns_->SetWideColumnValue(entity, value_pinner);
|
// 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
|
// TODO: Add lazy resolution support for GetEntity point lookups. This
|
||||||
@@ -201,6 +219,37 @@ Status GetContext::SaveWideColumnEntityToColumns(const Slice& user_key,
|
|||||||
return status;
|
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*/,
|
void GetContext::SaveValue(const Slice& value, SequenceNumber /*seq*/,
|
||||||
Cleanable* value_pinner) {
|
Cleanable* value_pinner) {
|
||||||
assert(state_ == kNotFound);
|
assert(state_ == kNotFound);
|
||||||
@@ -329,7 +378,7 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
|||||||
const Slice& value, bool* matched,
|
const Slice& value, bool* matched,
|
||||||
Status* read_status, Cleanable* value_pinner) {
|
Status* read_status, Cleanable* value_pinner) {
|
||||||
assert(matched);
|
assert(matched);
|
||||||
assert((state_ != kMerge && parsed_key.type != kTypeMerge) ||
|
assert((State() != kMerge && parsed_key.type != kTypeMerge) ||
|
||||||
merge_context_ != nullptr);
|
merge_context_ != nullptr);
|
||||||
if (ucmp_->EqualWithoutTimestamp(parsed_key.user_key, user_key_)) {
|
if (ucmp_->EqualWithoutTimestamp(parsed_key.user_key, user_key_)) {
|
||||||
*matched = true;
|
*matched = true;
|
||||||
@@ -400,7 +449,7 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
|||||||
case kTypeValuePreferredSeqno:
|
case kTypeValuePreferredSeqno:
|
||||||
case kTypeBlobIndex:
|
case kTypeBlobIndex:
|
||||||
case kTypeWideColumnEntity:
|
case kTypeWideColumnEntity:
|
||||||
assert(state_ == kNotFound || state_ == kMerge);
|
assert(State() == kNotFound || State() == kMerge);
|
||||||
if (type == kTypeValuePreferredSeqno) {
|
if (type == kTypeValuePreferredSeqno) {
|
||||||
unpacked_value = ParsePackedValueForValue(value);
|
unpacked_value = ParsePackedValueForValue(value);
|
||||||
}
|
}
|
||||||
@@ -416,7 +465,7 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
|||||||
*is_blob_index_ = (type == kTypeBlobIndex);
|
*is_blob_index_ = (type == kTypeBlobIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kNotFound == state_) {
|
if (State() == kNotFound) {
|
||||||
state_ = kFound;
|
state_ = kFound;
|
||||||
if (do_merge_) {
|
if (do_merge_) {
|
||||||
if (type == kTypeBlobIndex && ucmp_->timestamp_size() != 0) {
|
if (type == kTypeBlobIndex && ucmp_->timestamp_size() != 0) {
|
||||||
@@ -431,8 +480,10 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
|||||||
if (!s.ok()) {
|
if (!s.ok()) {
|
||||||
if (s.IsIncomplete()) {
|
if (s.IsIncomplete()) {
|
||||||
MarkKeyMayExist();
|
MarkKeyMayExist();
|
||||||
|
*read_status = s;
|
||||||
} else {
|
} else {
|
||||||
state_ = kCorrupt;
|
state_ = kCorrupt;
|
||||||
|
*read_status = s;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -456,8 +507,10 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
|||||||
if (!s.ok()) {
|
if (!s.ok()) {
|
||||||
if (s.IsIncomplete()) {
|
if (s.IsIncomplete()) {
|
||||||
MarkKeyMayExist();
|
MarkKeyMayExist();
|
||||||
|
*read_status = s;
|
||||||
} else {
|
} else {
|
||||||
state_ = kCorrupt;
|
state_ = kCorrupt;
|
||||||
|
*read_status = s;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -478,23 +531,24 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
|||||||
Slice blob_value(pin_val);
|
Slice blob_value(pin_val);
|
||||||
push_operand(blob_value, nullptr);
|
push_operand(blob_value, nullptr);
|
||||||
} else if (type == kTypeWideColumnEntity) {
|
} else if (type == kTypeWideColumnEntity) {
|
||||||
Slice value_copy = unpacked_value;
|
const Status s = PushWideColumnEntityDefaultOperand(
|
||||||
Slice value_of_default;
|
parsed_key.user_key, unpacked_value, value_pinner);
|
||||||
|
if (!s.ok()) {
|
||||||
if (!WideColumnSerialization::GetValueOfDefaultColumn(
|
if (s.IsIncomplete()) {
|
||||||
value_copy, value_of_default)
|
MarkKeyMayExist();
|
||||||
.ok()) {
|
*read_status = s;
|
||||||
state_ = kCorrupt;
|
} else {
|
||||||
|
state_ = kCorrupt;
|
||||||
|
*read_status = s;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
push_operand(value_of_default, value_pinner);
|
|
||||||
} else {
|
} else {
|
||||||
assert(type == kTypeValue || type == kTypeValuePreferredSeqno);
|
assert(type == kTypeValue || type == kTypeValuePreferredSeqno);
|
||||||
push_operand(unpacked_value, value_pinner);
|
push_operand(unpacked_value, value_pinner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (kMerge == state_) {
|
} else if (State() == kMerge) {
|
||||||
assert(merge_operator_ != nullptr);
|
assert(merge_operator_ != nullptr);
|
||||||
if (type == kTypeBlobIndex) {
|
if (type == kTypeBlobIndex) {
|
||||||
PinnableSlice pin_val;
|
PinnableSlice pin_val;
|
||||||
@@ -505,7 +559,11 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
|||||||
Slice blob_value(pin_val);
|
Slice blob_value(pin_val);
|
||||||
state_ = kFound;
|
state_ = kFound;
|
||||||
if (do_merge_) {
|
if (do_merge_) {
|
||||||
MergeWithPlainBaseValue(blob_value);
|
const Status s = MergeWithPlainBaseValue(blob_value);
|
||||||
|
if (!s.ok()) {
|
||||||
|
*read_status = s;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// It means this function is called as part of DB GetMergeOperands
|
// It means this function is called as part of DB GetMergeOperands
|
||||||
// API and the current value should be part of
|
// API and the current value should be part of
|
||||||
@@ -516,29 +574,38 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
|||||||
state_ = kFound;
|
state_ = kFound;
|
||||||
|
|
||||||
if (do_merge_) {
|
if (do_merge_) {
|
||||||
MergeWithWideColumnBaseValue(unpacked_value);
|
const Status s = MergeWithWideColumnBaseValue(unpacked_value);
|
||||||
|
if (!s.ok()) {
|
||||||
|
*read_status = s;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// It means this function is called as part of DB GetMergeOperands
|
// It means this function is called as part of DB GetMergeOperands
|
||||||
// API and the current value should be part of
|
// API and the current value should be part of
|
||||||
// merge_context_->operand_list
|
// merge_context_->operand_list
|
||||||
Slice value_copy = unpacked_value;
|
const Status s = PushWideColumnEntityDefaultOperand(
|
||||||
Slice value_of_default;
|
parsed_key.user_key, unpacked_value, value_pinner);
|
||||||
|
if (!s.ok()) {
|
||||||
if (!WideColumnSerialization::GetValueOfDefaultColumn(
|
if (s.IsIncomplete()) {
|
||||||
value_copy, value_of_default)
|
MarkKeyMayExist();
|
||||||
.ok()) {
|
*read_status = s;
|
||||||
state_ = kCorrupt;
|
} else {
|
||||||
|
state_ = kCorrupt;
|
||||||
|
*read_status = s;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
push_operand(value_of_default, value_pinner);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
assert(type == kTypeValue || type == kTypeValuePreferredSeqno);
|
assert(type == kTypeValue || type == kTypeValuePreferredSeqno);
|
||||||
|
|
||||||
state_ = kFound;
|
state_ = kFound;
|
||||||
if (do_merge_) {
|
if (do_merge_) {
|
||||||
MergeWithPlainBaseValue(unpacked_value);
|
const Status s = MergeWithPlainBaseValue(unpacked_value);
|
||||||
|
if (!s.ok()) {
|
||||||
|
*read_status = s;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// It means this function is called as part of DB GetMergeOperands
|
// It means this function is called as part of DB GetMergeOperands
|
||||||
// API and the current value should be part of
|
// API and the current value should be part of
|
||||||
@@ -555,13 +622,17 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
|||||||
case kTypeRangeDeletion:
|
case kTypeRangeDeletion:
|
||||||
// TODO(noetzli): Verify correctness once merge of single-deletes
|
// TODO(noetzli): Verify correctness once merge of single-deletes
|
||||||
// is supported
|
// is supported
|
||||||
assert(state_ == kNotFound || state_ == kMerge);
|
assert(State() == kNotFound || State() == kMerge);
|
||||||
if (kNotFound == state_) {
|
if (State() == kNotFound) {
|
||||||
state_ = kDeleted;
|
state_ = kDeleted;
|
||||||
} else if (kMerge == state_) {
|
} else if (State() == kMerge) {
|
||||||
state_ = kFound;
|
state_ = kFound;
|
||||||
if (do_merge_) {
|
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
|
// If do_merge_ = false then the current value shouldn't be part of
|
||||||
// merge_context_->operand_list
|
// merge_context_->operand_list
|
||||||
@@ -569,7 +640,7 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
case kTypeMerge:
|
case kTypeMerge:
|
||||||
assert(state_ == kNotFound || state_ == kMerge);
|
assert(State() == kNotFound || State() == kMerge);
|
||||||
state_ = kMerge;
|
state_ = kMerge;
|
||||||
// value_pinner is not set from plain_table_reader.cc for example.
|
// value_pinner is not set from plain_table_reader.cc for example.
|
||||||
push_operand(value, value_pinner);
|
push_operand(value, value_pinner);
|
||||||
@@ -579,7 +650,11 @@ bool GetContext::SaveValue(const ParsedInternalKey& parsed_key,
|
|||||||
merge_operator_->ShouldMerge(
|
merge_operator_->ShouldMerge(
|
||||||
merge_context_->GetOperandsDirectionBackward())) {
|
merge_context_->GetOperandsDirectionBackward())) {
|
||||||
state_ = kFound;
|
state_ = kFound;
|
||||||
MergeWithNoBaseValue();
|
const Status s = MergeWithNoBaseValue();
|
||||||
|
if (!s.ok()) {
|
||||||
|
*read_status = s;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (merge_context_->get_merge_operands_options != nullptr &&
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetContext::PostprocessMerge(const Status& merge_status) {
|
Status GetContext::PostprocessMerge(const Status& merge_status) {
|
||||||
if (!merge_status.ok()) {
|
if (!merge_status.ok()) {
|
||||||
if (merge_status.subcode() == Status::SubCode::kMergeOperatorFailed) {
|
if (merge_status.subcode() == Status::SubCode::kMergeOperatorFailed) {
|
||||||
state_ = kMergeOperatorFailed;
|
state_ = kMergeOperatorFailed;
|
||||||
} else {
|
} else {
|
||||||
state_ = kCorrupt;
|
state_ = kCorrupt;
|
||||||
}
|
}
|
||||||
return;
|
return merge_status;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (LIKELY(pinnable_val_ != nullptr)) {
|
if (LIKELY(pinnable_val_ != nullptr)) {
|
||||||
pinnable_val_->PinSelf();
|
pinnable_val_->PinSelf();
|
||||||
}
|
}
|
||||||
|
return Status::OK();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetContext::MergeWithNoBaseValue() {
|
Status GetContext::MergeWithNoBaseValue() {
|
||||||
assert(do_merge_);
|
assert(do_merge_);
|
||||||
assert(pinnable_val_ || columns_);
|
assert(pinnable_val_ || columns_);
|
||||||
assert(!pinnable_val_ || !columns_);
|
assert(!pinnable_val_ || !columns_);
|
||||||
@@ -628,10 +704,10 @@ void GetContext::MergeWithNoBaseValue() {
|
|||||||
merge_context_->GetOperands(), logger_, statistics_, clock_,
|
merge_context_->GetOperands(), logger_, statistics_, clock_,
|
||||||
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
|
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
|
||||||
pinnable_val_ ? pinnable_val_->GetSelf() : nullptr, columns_);
|
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(do_merge_);
|
||||||
assert(pinnable_val_ || columns_);
|
assert(pinnable_val_ || columns_);
|
||||||
assert(!pinnable_val_ || !columns_);
|
assert(!pinnable_val_ || !columns_);
|
||||||
@@ -643,10 +719,10 @@ void GetContext::MergeWithPlainBaseValue(const Slice& value) {
|
|||||||
merge_context_->GetOperands(), logger_, statistics_, clock_,
|
merge_context_->GetOperands(), logger_, statistics_, clock_,
|
||||||
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
|
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
|
||||||
pinnable_val_ ? pinnable_val_->GetSelf() : nullptr, columns_);
|
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(do_merge_);
|
||||||
assert(pinnable_val_ || columns_);
|
assert(pinnable_val_ || columns_);
|
||||||
assert(!pinnable_val_ || !columns_);
|
assert(!pinnable_val_ || !columns_);
|
||||||
@@ -655,16 +731,16 @@ void GetContext::MergeWithWideColumnBaseValue(const Slice& entity) {
|
|||||||
// supports V1 format.
|
// supports V1 format.
|
||||||
std::string resolved_entity;
|
std::string resolved_entity;
|
||||||
Slice effective_entity;
|
Slice effective_entity;
|
||||||
const Status s_resolve = WideColumnSerialization::ResolveEntityForMerge(
|
Status s_resolve = WideColumnSerialization::ResolveEntityForMerge(
|
||||||
entity, user_key_, blob_fetcher_, nullptr /* prefetch_buffers */,
|
entity, user_key_, blob_fetcher_, nullptr /* prefetch_buffers */,
|
||||||
resolved_entity, effective_entity);
|
resolved_entity, effective_entity);
|
||||||
if (!s_resolve.ok()) {
|
if (!s_resolve.ok()) {
|
||||||
if (s_resolve.IsIncomplete()) {
|
if (s_resolve.IsIncomplete()) {
|
||||||
MarkKeyMayExist();
|
MarkKeyMayExist();
|
||||||
return;
|
return s_resolve;
|
||||||
}
|
}
|
||||||
state_ = kCorrupt;
|
state_ = kCorrupt;
|
||||||
return;
|
return s_resolve;
|
||||||
}
|
}
|
||||||
|
|
||||||
// `op_failure_scope` (an output parameter) is not provided (set to nullptr)
|
// `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_,
|
merge_context_->GetOperands(), logger_, statistics_, clock_,
|
||||||
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
|
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
|
||||||
pinnable_val_ ? pinnable_val_->GetSelf() : nullptr, columns_);
|
pinnable_val_ ? pinnable_val_->GetSelf() : nullptr, columns_);
|
||||||
PostprocessMerge(s);
|
return PostprocessMerge(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GetContext::GetBlobValue(const Slice& user_key, const Slice& blob_index,
|
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;
|
(void)ret;
|
||||||
|
|
||||||
Status read_status;
|
Status read_status = Status::OK();
|
||||||
get_context->SaveValue(ikey, value, &dont_care, &read_status, value_pinner);
|
const bool keep_replaying = get_context->SaveValue(
|
||||||
|
ikey, value, &dont_care, &read_status, value_pinner);
|
||||||
if (!read_status.ok()) {
|
if (!read_status.ok()) {
|
||||||
return read_status;
|
return read_status;
|
||||||
}
|
}
|
||||||
|
if (!keep_replaying) {
|
||||||
|
// SaveValue() reached a terminal state for this row-cache replay.
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Status::OK();
|
return Status::OK();
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-5
@@ -7,6 +7,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "db/read_callback.h"
|
#include "db/read_callback.h"
|
||||||
|
#include "rocksdb/status.h"
|
||||||
#include "rocksdb/types.h"
|
#include "rocksdb/types.h"
|
||||||
|
|
||||||
namespace ROCKSDB_NAMESPACE {
|
namespace ROCKSDB_NAMESPACE {
|
||||||
@@ -19,7 +20,6 @@ class MergeOperator;
|
|||||||
class PinnableWideColumns;
|
class PinnableWideColumns;
|
||||||
class PinnedIteratorsManager;
|
class PinnedIteratorsManager;
|
||||||
class Statistics;
|
class Statistics;
|
||||||
class Status;
|
|
||||||
class SystemClock;
|
class SystemClock;
|
||||||
struct ParsedInternalKey;
|
struct ParsedInternalKey;
|
||||||
|
|
||||||
@@ -121,6 +121,10 @@ class GetContext {
|
|||||||
PinnedIteratorsManager* _pinned_iters_mgr = nullptr,
|
PinnedIteratorsManager* _pinned_iters_mgr = nullptr,
|
||||||
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
|
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
|
||||||
uint64_t tracing_get_id = 0, BlobFetcher* blob_fetcher = 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;
|
GetContext() = delete;
|
||||||
|
|
||||||
@@ -207,16 +211,19 @@ class GetContext {
|
|||||||
Status SaveWideColumnEntityToColumns(const Slice& user_key,
|
Status SaveWideColumnEntityToColumns(const Slice& user_key,
|
||||||
const Slice& entity,
|
const Slice& entity,
|
||||||
Cleanable* value_pinner);
|
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
|
// Helper method that postprocesses the results of merge operations, e.g. it
|
||||||
// sets the state correctly upon merge errors.
|
// 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
|
// The following methods perform the actual merge operation for the
|
||||||
// no base value/plain base value/wide-column base value cases.
|
// no base value/plain base value/wide-column base value cases.
|
||||||
void MergeWithNoBaseValue();
|
Status MergeWithNoBaseValue();
|
||||||
void MergeWithPlainBaseValue(const Slice& value);
|
Status MergeWithPlainBaseValue(const Slice& value);
|
||||||
void MergeWithWideColumnBaseValue(const Slice& entity);
|
Status MergeWithWideColumnBaseValue(const Slice& entity);
|
||||||
|
|
||||||
bool GetBlobValue(const Slice& user_key, const Slice& blob_index,
|
bool GetBlobValue(const Slice& user_key, const Slice& blob_index,
|
||||||
PinnableSlice* blob_value, Status* read_status);
|
PinnableSlice* blob_value, Status* read_status);
|
||||||
|
|||||||
@@ -157,6 +157,8 @@ std::vector<Status> SstFileReader::MultiGet(
|
|||||||
statuses[i] = Status::MergeInProgress();
|
statuses[i] = Status::MergeInProgress();
|
||||||
break;
|
break;
|
||||||
case GetContext::kCorrupt:
|
case GetContext::kCorrupt:
|
||||||
|
statuses[i] = Status::Corruption();
|
||||||
|
break;
|
||||||
case GetContext::kUnexpectedBlobIndex:
|
case GetContext::kUnexpectedBlobIndex:
|
||||||
case GetContext::kMergeOperatorFailed:
|
case GetContext::kMergeOperatorFailed:
|
||||||
statuses[i] = Status::Corruption();
|
statuses[i] = Status::Corruption();
|
||||||
@@ -218,6 +220,8 @@ Status SstFileReader::Get(const ReadOptions& roptions, const Slice& key,
|
|||||||
status = Status::MergeInProgress();
|
status = Status::MergeInProgress();
|
||||||
break;
|
break;
|
||||||
case GetContext::kCorrupt:
|
case GetContext::kCorrupt:
|
||||||
|
status = Status::Corruption();
|
||||||
|
break;
|
||||||
case GetContext::kUnexpectedBlobIndex:
|
case GetContext::kUnexpectedBlobIndex:
|
||||||
case GetContext::kMergeOperatorFailed:
|
case GetContext::kMergeOperatorFailed:
|
||||||
status = Status::Corruption();
|
status = Status::Corruption();
|
||||||
|
|||||||
@@ -864,6 +864,47 @@ TEST_P(SstFileReaderTableGetTest, Basic) {
|
|||||||
Close();
|
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,
|
INSTANTIATE_TEST_CASE_P(SingleAndMulti, SstFileReaderTableGetTest,
|
||||||
testing::Bool());
|
testing::Bool());
|
||||||
|
|
||||||
|
|||||||
@@ -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 merge reads against wide-column/blob-backed base values to preserve precise failure statuses, including `GetMergeOperands()` and direct-write memtable reads.
|
||||||
Reference in New Issue
Block a user