mirror of
https://github.com/facebook/rocksdb.git
synced 2026-07-07 14:47:40 +08:00
9f474a1034
Summary: BlockBuilder was somewhat inconsistent in its treatment of Slices whose size exceeds 4GB, which in a random corruption case could lead to (for example) serializing only the bottom 32 bits of a value size (uncorrupted) but appending the full multi-GB (corrupted) size. Because this is inner loop code, we don't want to pay CPU for extra conversions, data movements, or Status plumbing (already ruled out by Xingbo). In this change we make BlockBuilder more internally consistent and lift the 32-bit size requirement to callers (of a few functions specifically, for now). To ensure that's satisfied, I've added additional checks near the perimeter of RocksDB to ensure keys and values do not exceed 4GB, plus an extra random corruption or backstop check in BlockBasedTableBuilder. In detail, BlockBuilder (block_builder.cc/h): * Add API comments documenting the < 4GB assumption on Add/AddWithLastKey * Add debug assertions verifying input slice sizes < 4GB * Simplify AddWithLastKeyImpl to use uint32_t locals, reducing static_cast * Use only bottom 32 bits when appending derivative slices for consistency * Update MaybeStripTimestampFromKey to also truncate to 32-bit size * Add FIXME comments where buffer_/values_buffer_ sizes are truncated BlockBasedTableBuilder (block_based_table_builder.cc): * Add value size check (> uint32_t max) as a safety net against random corruption, since we have seen such corruptions in production WriteBatch (write_batch.cc): * Tighten existing key size checks to account for kNumInternalBytes (8-byte internal key suffix), using new kMaxWriteBatchKeySize constant * Add missing size checks to Delete, SingleDelete, and DeleteRange (both Slice and SliceParts variants) SstFileWriter (sst_file_writer.cc): * Add key and value size checks in AddImpl and DeleteRangeImpl, which bypass WriteBatch and go directly to the table builder MergeHelper (merge_helper.cc): * Add merge result size check (> uint32_t max) in both overloads of TimedFullMergeImpl, returning Corruption if exceeded * Add PartialMergeMulti result size check in MergeUntil CompactionIterator (compaction_iterator.cc): * Add size check on compaction filter output values (kChangeValue and kChangeWideColumnEntity), returning Corruption if > 4GB meta_blocks.cc: * Use Slice with uint32_t-truncated sizes in PropertyBlockBuilder::Finish to handle potentially oversized user property collector output Needed follow-up: * Check for blocks that are mis-encoded due to overflowing 32 bits for restart point offsets (and similar). See FIXME comments in block_builder.cc for why this is tricky. Pull Request resolved: https://github.com/facebook/rocksdb/pull/14461 Test Plan: New extreme-size unit tests, along with some testing infrastructure improvements: Added test::HasBigMem() (in test_util/testharness.h) which returns true when the system has ≥128GB RAM (via sysconf(_SC_PHYS_PAGES) on Linux/macOS) or when ROCKSDB_BIGMEM_TESTS is set. All extreme-size tests use HasBigMem() to skip gracefully on smaller machines rather than being permanently disabled. Where possible, tests use MemMapping::AllocateLazyZeroed() to supply large keys/values as Slices backed by anonymous mmap (cleaner with new MemMapping::AsSlice()). On Linux, read-only access to these pages maps to the shared kernel zero page, so the source data consumes no physical RAM — only the destination copy (e.g., WriteBatch::rep_) materializes, cutting peak memory roughly in half vs. std::string. Tests (all enabled, skip via HasBigMem()): ./write_batch_test --gtest_filter='*LargeKeyValueSizeLimit*' ./external_sst_file_basic_test --gtest_filter='*LargeSizeSstFileWriter*' ./merge_test --gtest_filter='*LargeMergeResultRejected*' ./merge_helper_test --gtest_filter='*LargePartialMergeResultRejected*' ./db_compaction_test --gtest_filter='*CompactionFilterLargeValueRejected*' All 5 pass (verified on a 128GB+ machine). On smaller machines, all 5 bypass cleanly with "insufficient memory for reliable continuous testing". Reviewed By: xingbowang Differential Revision: D96521899 Pulled By: pdillinger fbshipit-source-id: 70f4b5e6a23ab074d60e653fbb7ddc5edbe162ab
963 lines
31 KiB
C++
963 lines
31 KiB
C++
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
|
|
// This source code is licensed under both the GPLv2 (found in the
|
|
// COPYING file in the root directory) and Apache 2.0 License
|
|
// (found in the LICENSE.Apache file in the root directory).
|
|
//
|
|
#include <cassert>
|
|
#include <iostream>
|
|
#include <memory>
|
|
|
|
#include "db/db_impl/db_impl.h"
|
|
#include "db/dbformat.h"
|
|
#include "db/merge_helper.h"
|
|
#include "db/write_batch_internal.h"
|
|
#include "port/stack_trace.h"
|
|
#include "rocksdb/cache.h"
|
|
#include "rocksdb/comparator.h"
|
|
#include "rocksdb/db.h"
|
|
#include "rocksdb/env.h"
|
|
#include "rocksdb/merge_operator.h"
|
|
#include "rocksdb/utilities/db_ttl.h"
|
|
#include "rocksdb/wide_columns.h"
|
|
#include "test_util/testharness.h"
|
|
#include "util/cast_util.h"
|
|
#include "util/coding.h"
|
|
#include "utilities/merge_operators.h"
|
|
|
|
namespace ROCKSDB_NAMESPACE {
|
|
|
|
bool use_compression;
|
|
|
|
class MergeTest : public testing::Test {};
|
|
|
|
size_t num_merge_operator_calls;
|
|
void resetNumMergeOperatorCalls() { num_merge_operator_calls = 0; }
|
|
|
|
size_t num_partial_merge_calls;
|
|
void resetNumPartialMergeCalls() { num_partial_merge_calls = 0; }
|
|
|
|
class CountMergeOperator : public AssociativeMergeOperator {
|
|
public:
|
|
CountMergeOperator() {
|
|
mergeOperator_ = MergeOperators::CreateUInt64AddOperator();
|
|
}
|
|
|
|
bool Merge(const Slice& key, const Slice* existing_value, const Slice& value,
|
|
std::string* new_value, Logger* logger) const override {
|
|
assert(new_value->empty());
|
|
++num_merge_operator_calls;
|
|
if (existing_value == nullptr) {
|
|
new_value->assign(value.data(), value.size());
|
|
return true;
|
|
}
|
|
|
|
return mergeOperator_->PartialMerge(key, *existing_value, value, new_value,
|
|
logger);
|
|
}
|
|
|
|
bool PartialMergeMulti(const Slice& key,
|
|
const std::deque<Slice>& operand_list,
|
|
std::string* new_value,
|
|
Logger* logger) const override {
|
|
assert(new_value->empty());
|
|
++num_partial_merge_calls;
|
|
return mergeOperator_->PartialMergeMulti(key, operand_list, new_value,
|
|
logger);
|
|
}
|
|
|
|
const char* Name() const override { return "UInt64AddOperator"; }
|
|
|
|
private:
|
|
std::shared_ptr<MergeOperator> mergeOperator_;
|
|
};
|
|
|
|
class EnvMergeTest : public EnvWrapper {
|
|
public:
|
|
EnvMergeTest() : EnvWrapper(Env::Default()) {}
|
|
static const char* kClassName() { return "MergeEnv"; }
|
|
const char* Name() const override { return kClassName(); }
|
|
// ~EnvMergeTest() override {}
|
|
|
|
uint64_t NowNanos() override {
|
|
++now_nanos_count_;
|
|
return target()->NowNanos();
|
|
}
|
|
|
|
static uint64_t now_nanos_count_;
|
|
|
|
static std::unique_ptr<EnvMergeTest> singleton_;
|
|
|
|
static EnvMergeTest* GetInstance() {
|
|
if (nullptr == singleton_) {
|
|
singleton_.reset(new EnvMergeTest);
|
|
}
|
|
return singleton_.get();
|
|
}
|
|
};
|
|
|
|
uint64_t EnvMergeTest::now_nanos_count_{0};
|
|
std::unique_ptr<EnvMergeTest> EnvMergeTest::singleton_;
|
|
|
|
std::unique_ptr<DB> OpenDb(const std::string& dbname, const bool ttl = false,
|
|
const size_t max_successive_merges = 0) {
|
|
std::unique_ptr<DB> db;
|
|
Options options;
|
|
options.create_if_missing = true;
|
|
options.merge_operator = std::make_shared<CountMergeOperator>();
|
|
options.max_successive_merges = max_successive_merges;
|
|
options.env = EnvMergeTest::GetInstance();
|
|
EXPECT_OK(DestroyDB(dbname, Options()));
|
|
Status s;
|
|
if (ttl) {
|
|
DBWithTTL* db_with_ttl;
|
|
s = DBWithTTL::Open(options, dbname, &db_with_ttl);
|
|
db.reset(db_with_ttl);
|
|
} else {
|
|
s = DB::Open(options, dbname, &db);
|
|
}
|
|
EXPECT_OK(s);
|
|
assert(s.ok());
|
|
// Allowed to call NowNanos during DB creation (in GenerateRawUniqueId() for
|
|
// session ID)
|
|
EnvMergeTest::now_nanos_count_ = 0;
|
|
return db;
|
|
}
|
|
|
|
// Imagine we are maintaining a set of uint64 counters.
|
|
// Each counter has a distinct name. And we would like
|
|
// to support four high level operations:
|
|
// set, add, get and remove
|
|
// This is a quick implementation without a Merge operation.
|
|
class Counters {
|
|
protected:
|
|
UnownedPtr<DB> db_;
|
|
|
|
WriteOptions put_option_;
|
|
ReadOptions get_option_;
|
|
WriteOptions delete_option_;
|
|
|
|
uint64_t default_;
|
|
|
|
public:
|
|
explicit Counters(UnownedPtr<DB> db, uint64_t defaultCount = 0)
|
|
: db_(db),
|
|
put_option_(),
|
|
get_option_(),
|
|
delete_option_(),
|
|
default_(defaultCount) {
|
|
assert(db_);
|
|
}
|
|
|
|
virtual ~Counters() = default;
|
|
|
|
// public interface of Counters.
|
|
// All four functions return false
|
|
// if the underlying level db operation failed.
|
|
|
|
// mapped to a levedb Put
|
|
bool set(const std::string& key, uint64_t value) {
|
|
// just treat the internal rep of int64 as the string
|
|
char buf[sizeof(value)];
|
|
EncodeFixed64(buf, value);
|
|
Slice slice(buf, sizeof(value));
|
|
auto s = db_->Put(put_option_, key, slice);
|
|
|
|
if (s.ok()) {
|
|
return true;
|
|
} else {
|
|
std::cerr << s.ToString() << std::endl;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// mapped to a rocksdb Delete
|
|
bool remove(const std::string& key) {
|
|
auto s = db_->Delete(delete_option_, key);
|
|
|
|
if (s.ok()) {
|
|
return true;
|
|
} else {
|
|
std::cerr << s.ToString() << std::endl;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// mapped to a rocksdb Get
|
|
bool get(const std::string& key, uint64_t* value) {
|
|
std::string str;
|
|
auto s = db_->Get(get_option_, key, &str);
|
|
|
|
if (s.IsNotFound()) {
|
|
// return default value if not found;
|
|
*value = default_;
|
|
return true;
|
|
} else if (s.ok()) {
|
|
// deserialization
|
|
if (str.size() != sizeof(uint64_t)) {
|
|
std::cerr << "value corruption\n";
|
|
return false;
|
|
}
|
|
*value = DecodeFixed64(str.data());
|
|
return true;
|
|
} else {
|
|
std::cerr << s.ToString() << std::endl;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 'add' is implemented as get -> modify -> set
|
|
// An alternative is a single merge operation, see MergeBasedCounters
|
|
virtual bool add(const std::string& key, uint64_t value) {
|
|
uint64_t base = default_;
|
|
return get(key, &base) && set(key, base + value);
|
|
}
|
|
|
|
// convenience functions for testing
|
|
void assert_set(const std::string& key, uint64_t value) {
|
|
assert(set(key, value));
|
|
}
|
|
|
|
void assert_remove(const std::string& key) { assert(remove(key)); }
|
|
|
|
uint64_t assert_get(const std::string& key) {
|
|
uint64_t value = default_;
|
|
int result = get(key, &value);
|
|
assert(result);
|
|
if (result == 0) {
|
|
exit(1); // Disable unused variable warning.
|
|
}
|
|
return value;
|
|
}
|
|
|
|
void assert_add(const std::string& key, uint64_t value) {
|
|
int result = add(key, value);
|
|
assert(result);
|
|
if (result == 0) {
|
|
exit(1); // Disable unused variable warning.
|
|
}
|
|
}
|
|
};
|
|
|
|
// Implement 'add' directly with the new Merge operation
|
|
class MergeBasedCounters : public Counters {
|
|
private:
|
|
WriteOptions merge_option_; // for merge
|
|
|
|
public:
|
|
explicit MergeBasedCounters(UnownedPtr<DB> db, uint64_t defaultCount = 0)
|
|
: Counters(db, defaultCount), merge_option_() {}
|
|
|
|
// mapped to a rocksdb Merge operation
|
|
bool add(const std::string& key, uint64_t value) override {
|
|
char encoded[sizeof(uint64_t)];
|
|
EncodeFixed64(encoded, value);
|
|
Slice slice(encoded, sizeof(uint64_t));
|
|
auto s = db_->Merge(merge_option_, key, slice);
|
|
|
|
if (s.ok()) {
|
|
return true;
|
|
} else {
|
|
std::cerr << s.ToString() << std::endl;
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
|
|
void dumpDb(const std::unique_ptr<DB>& db) {
|
|
auto it = std::unique_ptr<Iterator>(db->NewIterator(ReadOptions()));
|
|
for (it->SeekToFirst(); it->Valid(); it->Next()) {
|
|
// uint64_t value = DecodeFixed64(it->value().data());
|
|
// std::cout << it->key().ToString() << ": " << value << std::endl;
|
|
}
|
|
assert(it->status().ok()); // Check for any errors found during the scan
|
|
}
|
|
|
|
void testCounters(Counters& counters, const std::unique_ptr<DB>& db,
|
|
bool test_compaction) {
|
|
FlushOptions o;
|
|
o.wait = true;
|
|
|
|
counters.assert_set("a", 1);
|
|
|
|
if (test_compaction) {
|
|
ASSERT_OK(db->Flush(o));
|
|
}
|
|
|
|
ASSERT_EQ(counters.assert_get("a"), 1);
|
|
|
|
counters.assert_remove("b");
|
|
|
|
// defaut value is 0 if non-existent
|
|
ASSERT_EQ(counters.assert_get("b"), 0);
|
|
|
|
counters.assert_add("a", 2);
|
|
|
|
if (test_compaction) {
|
|
ASSERT_OK(db->Flush(o));
|
|
}
|
|
|
|
// 1+2 = 3
|
|
ASSERT_EQ(counters.assert_get("a"), 3);
|
|
|
|
dumpDb(db);
|
|
|
|
// 1+...+49 = ?
|
|
uint64_t sum = 0;
|
|
for (int i = 1; i < 50; i++) {
|
|
counters.assert_add("b", i);
|
|
sum += i;
|
|
}
|
|
ASSERT_EQ(counters.assert_get("b"), sum);
|
|
|
|
dumpDb(db);
|
|
|
|
if (test_compaction) {
|
|
ASSERT_OK(db->Flush(o));
|
|
|
|
ASSERT_OK(db->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
|
|
|
dumpDb(db);
|
|
|
|
ASSERT_EQ(counters.assert_get("a"), 3);
|
|
ASSERT_EQ(counters.assert_get("b"), sum);
|
|
}
|
|
}
|
|
|
|
void testCountersWithFlushAndCompaction(Counters& counters,
|
|
const std::unique_ptr<DB>& db) {
|
|
ASSERT_OK(db->Put({}, "1", "1"));
|
|
ASSERT_OK(db->Flush(FlushOptions()));
|
|
|
|
std::atomic<int> cnt{0};
|
|
const auto get_thread_id = [&cnt]() {
|
|
thread_local int thread_id{cnt++};
|
|
return thread_id;
|
|
};
|
|
SyncPoint::GetInstance()->DisableProcessing();
|
|
SyncPoint::GetInstance()->ClearAllCallBacks();
|
|
SyncPoint::GetInstance()->SetCallBack(
|
|
"VersionSet::LogAndApply:BeforeWriterWaiting", [&](void* /*arg*/) {
|
|
int thread_id = get_thread_id();
|
|
if (1 == thread_id) {
|
|
TEST_SYNC_POINT(
|
|
"testCountersWithFlushAndCompaction::bg_compact_thread:0");
|
|
} else if (2 == thread_id) {
|
|
TEST_SYNC_POINT(
|
|
"testCountersWithFlushAndCompaction::bg_flush_thread:0");
|
|
}
|
|
});
|
|
SyncPoint::GetInstance()->SetCallBack(
|
|
"VersionSet::LogAndApply:WriteManifest", [&](void* /*arg*/) {
|
|
int thread_id = get_thread_id();
|
|
if (0 == thread_id) {
|
|
TEST_SYNC_POINT(
|
|
"testCountersWithFlushAndCompaction::set_options_thread:0");
|
|
TEST_SYNC_POINT(
|
|
"testCountersWithFlushAndCompaction::set_options_thread:1");
|
|
}
|
|
});
|
|
SyncPoint::GetInstance()->SetCallBack(
|
|
"VersionSet::LogAndApply:WakeUpAndDone", [&](void* arg) {
|
|
auto* mutex = static_cast<InstrumentedMutex*>(arg);
|
|
mutex->AssertHeld();
|
|
int thread_id = get_thread_id();
|
|
ASSERT_EQ(2, thread_id);
|
|
mutex->Unlock();
|
|
TEST_SYNC_POINT(
|
|
"testCountersWithFlushAndCompaction::bg_flush_thread:1");
|
|
TEST_SYNC_POINT(
|
|
"testCountersWithFlushAndCompaction::bg_flush_thread:2");
|
|
mutex->Lock();
|
|
});
|
|
SyncPoint::GetInstance()->LoadDependency({
|
|
{"testCountersWithFlushAndCompaction::set_options_thread:0",
|
|
"testCountersWithCompactionAndFlush:BeforeCompact"},
|
|
{"testCountersWithFlushAndCompaction::bg_compact_thread:0",
|
|
"testCountersWithFlushAndCompaction:BeforeIncCounters"},
|
|
{"testCountersWithFlushAndCompaction::bg_flush_thread:0",
|
|
"testCountersWithFlushAndCompaction::set_options_thread:1"},
|
|
{"testCountersWithFlushAndCompaction::bg_flush_thread:1",
|
|
"testCountersWithFlushAndCompaction:BeforeVerification"},
|
|
{"testCountersWithFlushAndCompaction:AfterGet",
|
|
"testCountersWithFlushAndCompaction::bg_flush_thread:2"},
|
|
});
|
|
// This test relies on old behavior of SetOptions writing to the
|
|
// manifest. Here we restore that old behavior for reproducer purposes.
|
|
// (Brief attempts to use an alternative to SetOptions failed.)
|
|
SyncPoint::GetInstance()->SetCallBack(
|
|
"DBImpl::SetOptions:dummy_edit", [&](void* arg) {
|
|
auto* dummy_edit = static_cast<VersionEdit*>(arg);
|
|
dummy_edit->Clear();
|
|
});
|
|
SyncPoint::GetInstance()->EnableProcessing();
|
|
|
|
port::Thread set_options_thread([&]() {
|
|
ASSERT_OK(static_cast_with_check<DBImpl>(db.get())->SetOptions(
|
|
{{"disable_auto_compactions", "false"}}));
|
|
});
|
|
TEST_SYNC_POINT("testCountersWithCompactionAndFlush:BeforeCompact");
|
|
port::Thread compact_thread([&]() {
|
|
ASSERT_OK(static_cast_with_check<DBImpl>(db.get())->CompactRange(
|
|
CompactRangeOptions(), db->DefaultColumnFamily(), nullptr, nullptr));
|
|
});
|
|
|
|
TEST_SYNC_POINT("testCountersWithFlushAndCompaction:BeforeIncCounters");
|
|
counters.add("test-key", 1);
|
|
|
|
FlushOptions flush_opts;
|
|
flush_opts.wait = false;
|
|
ASSERT_OK(db->Flush(flush_opts));
|
|
|
|
TEST_SYNC_POINT("testCountersWithFlushAndCompaction:BeforeVerification");
|
|
std::string expected;
|
|
PutFixed64(&expected, 1);
|
|
std::string actual;
|
|
Status s = db->Get(ReadOptions(), "test-key", &actual);
|
|
TEST_SYNC_POINT("testCountersWithFlushAndCompaction:AfterGet");
|
|
set_options_thread.join();
|
|
compact_thread.join();
|
|
ASSERT_OK(s);
|
|
ASSERT_EQ(expected, actual);
|
|
SyncPoint::GetInstance()->DisableProcessing();
|
|
SyncPoint::GetInstance()->ClearAllCallBacks();
|
|
}
|
|
|
|
void testSuccessiveMerge(Counters& counters, size_t max_num_merges,
|
|
size_t num_merges) {
|
|
counters.assert_remove("z");
|
|
uint64_t sum = 0;
|
|
|
|
for (size_t i = 1; i <= num_merges; ++i) {
|
|
resetNumMergeOperatorCalls();
|
|
counters.assert_add("z", i);
|
|
sum += i;
|
|
|
|
if (i % (max_num_merges + 1) == 0) {
|
|
ASSERT_EQ(num_merge_operator_calls, max_num_merges + 1);
|
|
} else {
|
|
ASSERT_EQ(num_merge_operator_calls, 0);
|
|
}
|
|
|
|
resetNumMergeOperatorCalls();
|
|
ASSERT_EQ(counters.assert_get("z"), sum);
|
|
ASSERT_EQ(num_merge_operator_calls, i % (max_num_merges + 1));
|
|
}
|
|
}
|
|
|
|
void testPartialMerge(Counters* counters, const std::unique_ptr<DB>& db,
|
|
size_t max_merge, size_t min_merge, size_t count) {
|
|
FlushOptions o;
|
|
o.wait = true;
|
|
|
|
// Test case 1: partial merge should be called when the number of merge
|
|
// operands exceeds the threshold.
|
|
uint64_t tmp_sum = 0;
|
|
resetNumPartialMergeCalls();
|
|
for (size_t i = 1; i <= count; i++) {
|
|
counters->assert_add("b", i);
|
|
tmp_sum += i;
|
|
}
|
|
ASSERT_OK(db->Flush(o));
|
|
ASSERT_OK(db->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
|
ASSERT_EQ(tmp_sum, counters->assert_get("b"));
|
|
if (count > max_merge) {
|
|
// in this case, FullMerge should be called instead.
|
|
ASSERT_EQ(num_partial_merge_calls, 0U);
|
|
} else {
|
|
// if count >= min_merge, then partial merge should be called once.
|
|
ASSERT_EQ((count >= min_merge), (num_partial_merge_calls == 1));
|
|
}
|
|
|
|
// Test case 2: partial merge should not be called when a put is found.
|
|
resetNumPartialMergeCalls();
|
|
tmp_sum = 0;
|
|
ASSERT_OK(db->Put(ROCKSDB_NAMESPACE::WriteOptions(), "c", "10"));
|
|
for (size_t i = 1; i <= count; i++) {
|
|
counters->assert_add("c", i);
|
|
tmp_sum += i;
|
|
}
|
|
ASSERT_OK(db->Flush(o));
|
|
ASSERT_OK(db->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
|
ASSERT_EQ(tmp_sum, counters->assert_get("c"));
|
|
ASSERT_EQ(num_partial_merge_calls, 0U);
|
|
// NowNanos was previously called in MergeHelper::FilterMerge(), which
|
|
// harmed performance.
|
|
ASSERT_EQ(EnvMergeTest::now_nanos_count_, 0U);
|
|
}
|
|
|
|
void testSingleBatchSuccessiveMerge(const std::unique_ptr<DB>& db,
|
|
size_t max_num_merges, size_t num_merges) {
|
|
ASSERT_GT(num_merges, max_num_merges);
|
|
|
|
Slice key("BatchSuccessiveMerge");
|
|
uint64_t merge_value = 1;
|
|
char buf[sizeof(merge_value)];
|
|
EncodeFixed64(buf, merge_value);
|
|
Slice merge_value_slice(buf, sizeof(merge_value));
|
|
|
|
// Create the batch
|
|
WriteBatch batch;
|
|
for (size_t i = 0; i < num_merges; ++i) {
|
|
ASSERT_OK(batch.Merge(key, merge_value_slice));
|
|
}
|
|
|
|
// Apply to memtable and count the number of merges
|
|
resetNumMergeOperatorCalls();
|
|
ASSERT_OK(db->Write(WriteOptions(), &batch));
|
|
ASSERT_EQ(
|
|
num_merge_operator_calls,
|
|
static_cast<size_t>(num_merges - (num_merges % (max_num_merges + 1))));
|
|
|
|
// Get the value
|
|
resetNumMergeOperatorCalls();
|
|
std::string get_value_str;
|
|
ASSERT_OK(db->Get(ReadOptions(), key, &get_value_str));
|
|
assert(get_value_str.size() == sizeof(uint64_t));
|
|
uint64_t get_value = DecodeFixed64(get_value_str.data());
|
|
ASSERT_EQ(get_value, num_merges * merge_value);
|
|
ASSERT_EQ(num_merge_operator_calls,
|
|
static_cast<size_t>((num_merges % (max_num_merges + 1))));
|
|
}
|
|
|
|
void runTest(const std::string& dbname, const bool use_ttl = false) {
|
|
{
|
|
auto db = OpenDb(dbname, use_ttl);
|
|
|
|
{
|
|
Counters counters(db.get(), 0);
|
|
testCounters(counters, db, true);
|
|
}
|
|
|
|
{
|
|
MergeBasedCounters counters(db.get(), 0);
|
|
testCounters(counters, db, use_compression);
|
|
}
|
|
}
|
|
|
|
ASSERT_OK(DestroyDB(dbname, Options()));
|
|
|
|
{
|
|
size_t max_merge = 5;
|
|
auto db = OpenDb(dbname, use_ttl, max_merge);
|
|
MergeBasedCounters counters(db.get(), 0);
|
|
testCounters(counters, db, use_compression);
|
|
testSuccessiveMerge(counters, max_merge, max_merge * 2);
|
|
testSingleBatchSuccessiveMerge(db, 5, 7);
|
|
ASSERT_OK(db->Close());
|
|
ASSERT_OK(DestroyDB(dbname, Options()));
|
|
}
|
|
|
|
{
|
|
size_t max_merge = 100;
|
|
// Min merge is hard-coded to 2.
|
|
uint32_t min_merge = 2;
|
|
for (uint32_t count = min_merge - 1; count <= min_merge + 1; count++) {
|
|
auto db = OpenDb(dbname, use_ttl, max_merge);
|
|
MergeBasedCounters counters(db.get(), 0);
|
|
testPartialMerge(&counters, db, max_merge, min_merge, count);
|
|
ASSERT_OK(db->Close());
|
|
ASSERT_OK(DestroyDB(dbname, Options()));
|
|
}
|
|
{
|
|
auto db = OpenDb(dbname, use_ttl, max_merge);
|
|
MergeBasedCounters counters(db.get(), 0);
|
|
testPartialMerge(&counters, db, max_merge, min_merge, min_merge * 10);
|
|
ASSERT_OK(db->Close());
|
|
ASSERT_OK(DestroyDB(dbname, Options()));
|
|
}
|
|
}
|
|
|
|
{
|
|
{
|
|
auto db = OpenDb(dbname);
|
|
MergeBasedCounters counters(db.get(), 0);
|
|
counters.add("test-key", 1);
|
|
counters.add("test-key", 1);
|
|
counters.add("test-key", 1);
|
|
ASSERT_OK(db->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
|
}
|
|
|
|
std::unique_ptr<DB> reopen_db;
|
|
ASSERT_OK(DB::Open(Options(), dbname, &reopen_db));
|
|
std::string value;
|
|
ASSERT_NOK(reopen_db->Get(ReadOptions(), "test-key", &value));
|
|
reopen_db.reset();
|
|
ASSERT_OK(DestroyDB(dbname, Options()));
|
|
}
|
|
|
|
/* Temporary remove this test
|
|
{
|
|
std::cout << "Test merge-operator not set after reopen (recovery case)\n";
|
|
{
|
|
auto db = OpenDb(dbname);
|
|
MergeBasedCounters counters(db.get(), 0);
|
|
counters.add("test-key", 1);
|
|
counters.add("test-key", 1);
|
|
counters.add("test-key", 1);
|
|
}
|
|
|
|
std::unique_ptr<DB> reopen_db;
|
|
ASSERT_TRUE(DB::Open(Options(), dbname, &reopen_db).IsInvalidArgument());
|
|
}
|
|
*/
|
|
}
|
|
|
|
TEST_F(MergeTest, MergeDbTest) {
|
|
runTest(test::PerThreadDBPath("merge_testdb"));
|
|
}
|
|
|
|
TEST_F(MergeTest, MergeDbTtlTest) {
|
|
runTest(test::PerThreadDBPath("merge_testdbttl"),
|
|
true); // Run test on TTL database
|
|
}
|
|
|
|
TEST_F(MergeTest, MergeWithCompactionAndFlush) {
|
|
const std::string dbname =
|
|
test::PerThreadDBPath("merge_with_compaction_and_flush");
|
|
{
|
|
auto db = OpenDb(dbname);
|
|
{
|
|
MergeBasedCounters counters(db.get(), 0);
|
|
testCountersWithFlushAndCompaction(counters, db);
|
|
}
|
|
}
|
|
ASSERT_OK(DestroyDB(dbname, Options()));
|
|
}
|
|
|
|
TEST_F(MergeTest, FullMergeV3FallbackNewValue) {
|
|
// Test that the default FullMergeV3 implementation correctly handles the case
|
|
// when FullMergeV2 results in a new value.
|
|
|
|
const Slice key("foo");
|
|
const MergeOperator::MergeOperationInputV3::OperandList operands{
|
|
"first", "second", "third"};
|
|
constexpr Logger* logger = nullptr;
|
|
|
|
auto append_operator =
|
|
MergeOperators::CreateStringAppendOperator(std::string());
|
|
|
|
// No existing value
|
|
{
|
|
MergeOperator::MergeOperationInputV3::ExistingValue existing_value;
|
|
const MergeOperator::MergeOperationInputV3 merge_in(
|
|
key, std::move(existing_value), operands, logger);
|
|
|
|
MergeOperator::MergeOperationOutputV3 merge_out;
|
|
|
|
ASSERT_TRUE(append_operator->FullMergeV3(merge_in, &merge_out));
|
|
|
|
const auto& result = std::get<std::string>(merge_out.new_value);
|
|
ASSERT_EQ(result, operands[0].ToString() + operands[1].ToString() +
|
|
operands[2].ToString());
|
|
}
|
|
|
|
// Plain existing value
|
|
{
|
|
const Slice plain("plain");
|
|
MergeOperator::MergeOperationInputV3::ExistingValue existing_value(plain);
|
|
const MergeOperator::MergeOperationInputV3 merge_in(
|
|
key, std::move(existing_value), operands, logger);
|
|
|
|
MergeOperator::MergeOperationOutputV3 merge_out;
|
|
|
|
ASSERT_TRUE(append_operator->FullMergeV3(merge_in, &merge_out));
|
|
|
|
const auto& result = std::get<std::string>(merge_out.new_value);
|
|
ASSERT_EQ(result, plain.ToString() + operands[0].ToString() +
|
|
operands[1].ToString() + operands[2].ToString());
|
|
}
|
|
|
|
// Wide-column existing value with default column
|
|
{
|
|
const WideColumns entity{
|
|
{kDefaultWideColumnName, "default"}, {"one", "1"}, {"two", "2"}};
|
|
MergeOperator::MergeOperationInputV3::ExistingValue existing_value(entity);
|
|
const MergeOperator::MergeOperationInputV3 merge_in(
|
|
key, std::move(existing_value), operands, logger);
|
|
|
|
MergeOperator::MergeOperationOutputV3 merge_out;
|
|
|
|
ASSERT_TRUE(append_operator->FullMergeV3(merge_in, &merge_out));
|
|
|
|
const auto& result =
|
|
std::get<MergeOperator::MergeOperationOutputV3::NewColumns>(
|
|
merge_out.new_value);
|
|
ASSERT_EQ(result.size(), entity.size());
|
|
ASSERT_EQ(result[0].first, entity[0].name());
|
|
ASSERT_EQ(result[0].second,
|
|
entity[0].value().ToString() + operands[0].ToString() +
|
|
operands[1].ToString() + operands[2].ToString());
|
|
ASSERT_EQ(result[1].first, entity[1].name());
|
|
ASSERT_EQ(result[1].second, entity[1].value());
|
|
ASSERT_EQ(result[2].first, entity[2].name());
|
|
ASSERT_EQ(result[2].second, entity[2].value());
|
|
}
|
|
|
|
// Wide-column existing value without default column
|
|
{
|
|
const WideColumns entity{{"one", "1"}, {"two", "2"}};
|
|
MergeOperator::MergeOperationInputV3::ExistingValue existing_value(entity);
|
|
const MergeOperator::MergeOperationInputV3 merge_in(
|
|
key, std::move(existing_value), operands, logger);
|
|
|
|
MergeOperator::MergeOperationOutputV3 merge_out;
|
|
|
|
ASSERT_TRUE(append_operator->FullMergeV3(merge_in, &merge_out));
|
|
|
|
const auto& result =
|
|
std::get<MergeOperator::MergeOperationOutputV3::NewColumns>(
|
|
merge_out.new_value);
|
|
ASSERT_EQ(result.size(), entity.size() + 1);
|
|
ASSERT_EQ(result[0].first, kDefaultWideColumnName);
|
|
ASSERT_EQ(result[0].second, operands[0].ToString() +
|
|
operands[1].ToString() +
|
|
operands[2].ToString());
|
|
ASSERT_EQ(result[1].first, entity[0].name());
|
|
ASSERT_EQ(result[1].second, entity[0].value());
|
|
ASSERT_EQ(result[2].first, entity[1].name());
|
|
ASSERT_EQ(result[2].second, entity[1].value());
|
|
}
|
|
}
|
|
|
|
TEST_F(MergeTest, FullMergeV3FallbackExistingOperand) {
|
|
// Test that the default FullMergeV3 implementation correctly handles the case
|
|
// when FullMergeV2 results in an existing operand.
|
|
|
|
const Slice key("foo");
|
|
const MergeOperator::MergeOperationInputV3::OperandList operands{
|
|
"first", "second", "third"};
|
|
constexpr Logger* logger = nullptr;
|
|
|
|
auto put_operator = MergeOperators::CreatePutOperator();
|
|
|
|
// No existing value
|
|
{
|
|
MergeOperator::MergeOperationInputV3::ExistingValue existing_value;
|
|
const MergeOperator::MergeOperationInputV3 merge_in(
|
|
key, std::move(existing_value), operands, logger);
|
|
|
|
MergeOperator::MergeOperationOutputV3 merge_out;
|
|
|
|
ASSERT_TRUE(put_operator->FullMergeV3(merge_in, &merge_out));
|
|
|
|
const auto& result = std::get<Slice>(merge_out.new_value);
|
|
ASSERT_EQ(result.data(), operands.back().data());
|
|
ASSERT_EQ(result.size(), operands.back().size());
|
|
}
|
|
|
|
// Plain existing value
|
|
{
|
|
const Slice plain("plain");
|
|
MergeOperator::MergeOperationInputV3::ExistingValue existing_value(plain);
|
|
const MergeOperator::MergeOperationInputV3 merge_in(
|
|
key, std::move(existing_value), operands, logger);
|
|
|
|
MergeOperator::MergeOperationOutputV3 merge_out;
|
|
|
|
ASSERT_TRUE(put_operator->FullMergeV3(merge_in, &merge_out));
|
|
|
|
const auto& result = std::get<Slice>(merge_out.new_value);
|
|
ASSERT_EQ(result.data(), operands.back().data());
|
|
ASSERT_EQ(result.size(), operands.back().size());
|
|
}
|
|
|
|
// Wide-column existing value with default column
|
|
{
|
|
const WideColumns entity{
|
|
{kDefaultWideColumnName, "default"}, {"one", "1"}, {"two", "2"}};
|
|
MergeOperator::MergeOperationInputV3::ExistingValue existing_value(entity);
|
|
const MergeOperator::MergeOperationInputV3 merge_in(
|
|
key, std::move(existing_value), operands, logger);
|
|
|
|
MergeOperator::MergeOperationOutputV3 merge_out;
|
|
|
|
ASSERT_TRUE(put_operator->FullMergeV3(merge_in, &merge_out));
|
|
|
|
const auto& result =
|
|
std::get<MergeOperator::MergeOperationOutputV3::NewColumns>(
|
|
merge_out.new_value);
|
|
ASSERT_EQ(result.size(), entity.size());
|
|
ASSERT_EQ(result[0].first, entity[0].name());
|
|
ASSERT_EQ(result[0].second, operands.back());
|
|
ASSERT_EQ(result[1].first, entity[1].name());
|
|
ASSERT_EQ(result[1].second, entity[1].value());
|
|
ASSERT_EQ(result[2].first, entity[2].name());
|
|
ASSERT_EQ(result[2].second, entity[2].value());
|
|
}
|
|
|
|
// Wide-column existing value without default column
|
|
{
|
|
const WideColumns entity{{"one", "1"}, {"two", "2"}};
|
|
MergeOperator::MergeOperationInputV3::ExistingValue existing_value(entity);
|
|
const MergeOperator::MergeOperationInputV3 merge_in(
|
|
key, std::move(existing_value), operands, logger);
|
|
|
|
MergeOperator::MergeOperationOutputV3 merge_out;
|
|
|
|
ASSERT_TRUE(put_operator->FullMergeV3(merge_in, &merge_out));
|
|
|
|
const auto& result =
|
|
std::get<MergeOperator::MergeOperationOutputV3::NewColumns>(
|
|
merge_out.new_value);
|
|
ASSERT_EQ(result.size(), entity.size() + 1);
|
|
ASSERT_EQ(result[0].first, kDefaultWideColumnName);
|
|
ASSERT_EQ(result[0].second, operands.back());
|
|
ASSERT_EQ(result[1].first, entity[0].name());
|
|
ASSERT_EQ(result[1].second, entity[0].value());
|
|
ASSERT_EQ(result[2].first, entity[1].name());
|
|
ASSERT_EQ(result[2].second, entity[1].value());
|
|
}
|
|
}
|
|
|
|
TEST_F(MergeTest, FullMergeV3FallbackFailure) {
|
|
// Test that the default FullMergeV3 implementation correctly handles the case
|
|
// when FullMergeV2 fails.
|
|
|
|
const Slice key("foo");
|
|
const MergeOperator::MergeOperationInputV3::OperandList operands{
|
|
"first", "second", "third"};
|
|
constexpr Logger* logger = nullptr;
|
|
|
|
class FailMergeOperator : public MergeOperator {
|
|
public:
|
|
bool FullMergeV2(const MergeOperationInput& /* merge_in */,
|
|
MergeOperationOutput* merge_out) const override {
|
|
assert(merge_out);
|
|
merge_out->op_failure_scope = OpFailureScope::kMustMerge;
|
|
|
|
return false;
|
|
}
|
|
|
|
const char* Name() const override { return "FailMergeOperator"; }
|
|
};
|
|
|
|
FailMergeOperator fail_operator;
|
|
|
|
// No existing value
|
|
{
|
|
MergeOperator::MergeOperationInputV3::ExistingValue existing_value;
|
|
const MergeOperator::MergeOperationInputV3 merge_in(
|
|
key, std::move(existing_value), operands, logger);
|
|
|
|
MergeOperator::MergeOperationOutputV3 merge_out;
|
|
|
|
ASSERT_FALSE(fail_operator.FullMergeV3(merge_in, &merge_out));
|
|
ASSERT_EQ(merge_out.op_failure_scope,
|
|
MergeOperator::OpFailureScope::kMustMerge);
|
|
}
|
|
|
|
// Plain existing value
|
|
{
|
|
const Slice plain("plain");
|
|
MergeOperator::MergeOperationInputV3::ExistingValue existing_value(plain);
|
|
const MergeOperator::MergeOperationInputV3 merge_in(
|
|
key, std::move(existing_value), operands, logger);
|
|
|
|
MergeOperator::MergeOperationOutputV3 merge_out;
|
|
|
|
ASSERT_FALSE(fail_operator.FullMergeV3(merge_in, &merge_out));
|
|
ASSERT_EQ(merge_out.op_failure_scope,
|
|
MergeOperator::OpFailureScope::kMustMerge);
|
|
}
|
|
|
|
// Wide-column existing value with default column
|
|
{
|
|
const WideColumns entity{
|
|
{kDefaultWideColumnName, "default"}, {"one", "1"}, {"two", "2"}};
|
|
MergeOperator::MergeOperationInputV3::ExistingValue existing_value(entity);
|
|
const MergeOperator::MergeOperationInputV3 merge_in(
|
|
key, std::move(existing_value), operands, logger);
|
|
|
|
MergeOperator::MergeOperationOutputV3 merge_out;
|
|
|
|
ASSERT_FALSE(fail_operator.FullMergeV3(merge_in, &merge_out));
|
|
ASSERT_EQ(merge_out.op_failure_scope,
|
|
MergeOperator::OpFailureScope::kMustMerge);
|
|
}
|
|
|
|
// Wide-column existing value without default column
|
|
{
|
|
const WideColumns entity{{"one", "1"}, {"two", "2"}};
|
|
MergeOperator::MergeOperationInputV3::ExistingValue existing_value(entity);
|
|
const MergeOperator::MergeOperationInputV3 merge_in(
|
|
key, std::move(existing_value), operands, logger);
|
|
|
|
MergeOperator::MergeOperationOutputV3 merge_out;
|
|
|
|
ASSERT_FALSE(fail_operator.FullMergeV3(merge_in, &merge_out));
|
|
ASSERT_EQ(merge_out.op_failure_scope,
|
|
MergeOperator::OpFailureScope::kMustMerge);
|
|
}
|
|
}
|
|
|
|
// Uses anonymous mmap (lazy-zeroed) so the large operands don't consume
|
|
// physical memory -- only the merge result does (~4GB peak).
|
|
TEST_F(MergeTest, LargeMergeResultRejected) {
|
|
if (!test::HasBigMem()) {
|
|
ROCKSDB_GTEST_BYPASS("insufficient memory for reliable continuous testing");
|
|
return;
|
|
}
|
|
|
|
// A merge operator that produces a result > 4GB by concatenating operands
|
|
class ConcatOperator : public MergeOperator {
|
|
public:
|
|
bool FullMergeV2(const MergeOperationInput& merge_in,
|
|
MergeOperationOutput* merge_out) const override {
|
|
merge_out->new_value.clear();
|
|
if (merge_in.existing_value) {
|
|
merge_out->new_value.append(merge_in.existing_value->data(),
|
|
merge_in.existing_value->size());
|
|
}
|
|
for (const auto& op : merge_in.operand_list) {
|
|
merge_out->new_value.append(op.data(), op.size());
|
|
}
|
|
return true;
|
|
}
|
|
const char* Name() const override { return "ConcatOperator"; }
|
|
};
|
|
|
|
// Use TimedFullMerge directly - two operands that sum to > 4GB
|
|
constexpr size_t kHalfSize =
|
|
(size_t{std::numeric_limits<uint32_t>::max()} + 1) / 2;
|
|
MemMapping mm1 = MemMapping::AllocateLazyZeroed(kHalfSize);
|
|
ASSERT_NE(nullptr, mm1.Get());
|
|
MemMapping mm2 = MemMapping::AllocateLazyZeroed(kHalfSize);
|
|
ASSERT_NE(nullptr, mm2.Get());
|
|
std::vector<Slice> operands{mm1.AsSlice(), mm2.AsSlice()};
|
|
std::string result;
|
|
Slice result_operand;
|
|
ValueType result_type;
|
|
|
|
ConcatOperator concat_op;
|
|
Status s = MergeHelper::TimedFullMerge(
|
|
&concat_op, "key", MergeHelper::kNoBaseValue, operands, nullptr, nullptr,
|
|
SystemClock::Default().get(), false, nullptr, &result, &result_operand,
|
|
&result_type);
|
|
ASSERT_TRUE(s.IsCorruption());
|
|
ASSERT_TRUE(s.ToString().find("4GB") != std::string::npos);
|
|
|
|
// Control: just under 4GB should be accepted
|
|
result.clear();
|
|
result.shrink_to_fit();
|
|
operands = {mm1.AsSlice(), Slice(mm2.AsSlice().data(), kHalfSize - 1)};
|
|
s = MergeHelper::TimedFullMerge(&concat_op, "key", MergeHelper::kNoBaseValue,
|
|
operands, nullptr, nullptr,
|
|
SystemClock::Default().get(), false, nullptr,
|
|
&result, &result_operand, &result_type);
|
|
ASSERT_OK(s);
|
|
ASSERT_EQ(result.size(), size_t{std::numeric_limits<uint32_t>::max()});
|
|
}
|
|
|
|
} // namespace ROCKSDB_NAMESPACE
|
|
|
|
int main(int argc, char** argv) {
|
|
ROCKSDB_NAMESPACE::use_compression = false;
|
|
if (argc > 1) {
|
|
ROCKSDB_NAMESPACE::use_compression = true;
|
|
}
|
|
|
|
ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
return RUN_ALL_TESTS();
|
|
}
|