mirror of
https://github.com/facebook/rocksdb.git
synced 2026-07-07 14:47:40 +08:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c628788abd | |||
| 56a54b99f2 | |||
| 116dd52be4 | |||
| c8ff39be33 | |||
| b01951ed91 | |||
| aa44f959a6 | |||
| 4a42da5759 | |||
| 0435a9fc31 | |||
| 01393464ea | |||
| 97a152b841 | |||
| 537ccd2fbe | |||
| 86e421fba9 | |||
| c5201abc4a | |||
| 4eda1b79fa | |||
| 030f7c4a7b | |||
| 6c60c8f3f3 | |||
| 2660754dbc | |||
| 94a867c845 | |||
| 1b5474c8e1 | |||
| b135fa7a8b |
+34
@@ -1,6 +1,40 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 9.3.2 (08/02/2024)
|
||||
### Bug Fixes
|
||||
* *Make DestroyDB supports slow deletion when it's configured in `SstFileManager`. The slow deletion is subject to the configured `rate_bytes_per_sec`, but not subject to the `max_trash_db_ratio`.
|
||||
* Fix a race condition in pessimistic transactions that could allow multiple transactions with the same name to be registered simultaneously, resulting in a crash or other unpredictable behavior.
|
||||
|
||||
## 9.3.1 (05/25/2024)
|
||||
### Bug Fixes
|
||||
* [internal only] Build script improvement
|
||||
|
||||
## 9.3.0 (05/17/2024)
|
||||
### New Features
|
||||
* Optimistic transactions and pessimistic transactions with the WriteCommitted policy now support the `GetEntity` API.
|
||||
* Added new `Iterator` property, "rocksdb.iterator.is-value-pinned", for checking whether the `Slice` returned by `Iterator::value()` can be used until the `Iterator` is destroyed.
|
||||
* Optimistic transactions and WriteCommitted pessimistic transactions now support the `MultiGetEntity` API.
|
||||
* Optimistic transactions and pessimistic transactions with the WriteCommitted policy now support the `PutEntity` API. Support for read APIs and other write policies (WritePrepared, WriteUnprepared) will be added later.
|
||||
|
||||
### Public API Changes
|
||||
* Exposed block based metadata cache options via C API
|
||||
* Exposed compaction pri via c api.
|
||||
* Add a kAdmPolicyAllowAll option to TieredAdmissionPolicy that admits all blocks evicted from the primary block cache into the compressed secondary cache.
|
||||
|
||||
### Behavior Changes
|
||||
* CompactRange() with change_level=true on a CF with FIFO compaction will return Status::NotSupported().
|
||||
* External file ingestion with FIFO compaction will always ingest to L0.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a bug for databases using `DBOptions::allow_2pc == true` (all `TransactionDB`s except `OptimisticTransactionDB`) that have exactly one column family. Due to a missing WAL sync, attempting to open the DB could have returned a `Status::Corruption` with a message like "SST file is ahead of WALs".
|
||||
* Fix a bug in CreateColumnFamilyWithImport() where if multiple CFs are imported, we were not resetting files' epoch number and L0 files can have overlapping key range but the same epoch number.
|
||||
* Fixed race conditions when `ColumnFamilyOptions::inplace_update_support == true` between user overwrites and reads on the same key.
|
||||
* Fix a bug where `CompactFiles()` can compact files of range conflict with other ongoing compactions' when `preclude_last_level_data_seconds > 0` is used
|
||||
* Fixed a false positive `Status::Corruption` reported when reopening a DB that used `DBOptions::recycle_log_file_num > 0` and `DBOptions::wal_compression != kNoCompression`.
|
||||
* While WAL is locked with LockWAL(), some operations like Flush() and IngestExternalFile() are now blocked as they should have been.
|
||||
* Fixed a bug causing stale memory access when using the TieredSecondaryCache with an NVM secondary cache, and a file system that supports return an FS allocated buffer for MultiRead (FSSupportedOps::kFSBuffer is set).
|
||||
|
||||
## 9.2.0 (05/01/2024)
|
||||
### New Features
|
||||
* Added two options `deadline` and `max_size_bytes` for CacheDumper to exit early
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# This file is a Facebook-specific integration for buck builds, so can
|
||||
# only be validated by Facebook employees.
|
||||
load("//rocks/buckifier:defs.bzl", "cpp_library_wrapper","rocks_cpp_library_wrapper","cpp_binary_wrapper","cpp_unittest_wrapper","fancy_bench_wrapper","add_c_test_wrapper")
|
||||
load("@fbcode_macros//build_defs:export_files.bzl", "export_file")
|
||||
|
||||
|
||||
cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
|
||||
@@ -7,6 +7,7 @@ rocksdb_target_header_template = """# This file \100generated by:
|
||||
# This file is a Facebook-specific integration for buck builds, so can
|
||||
# only be validated by Facebook employees.
|
||||
load("//rocks/buckifier:defs.bzl", "cpp_library_wrapper","rocks_cpp_library_wrapper","cpp_binary_wrapper","cpp_unittest_wrapper","fancy_bench_wrapper","add_c_test_wrapper")
|
||||
load("@fbcode_macros//build_defs:export_files.bzl", "export_file")
|
||||
|
||||
"""
|
||||
|
||||
|
||||
Vendored
+105
@@ -873,6 +873,111 @@ TEST_P(DBTieredAdmPolicyTest, CompressedCacheAdmission) {
|
||||
Destroy(options);
|
||||
}
|
||||
|
||||
TEST_F(DBTieredSecondaryCacheTest, FSBufferTest) {
|
||||
class WrapFS : public FileSystemWrapper {
|
||||
public:
|
||||
explicit WrapFS(const std::shared_ptr<FileSystem>& _target)
|
||||
: FileSystemWrapper(_target) {}
|
||||
~WrapFS() override {}
|
||||
const char* Name() const override { return "WrapFS"; }
|
||||
|
||||
IOStatus NewRandomAccessFile(const std::string& fname,
|
||||
const FileOptions& opts,
|
||||
std::unique_ptr<FSRandomAccessFile>* result,
|
||||
IODebugContext* dbg) override {
|
||||
class WrappedRandomAccessFile : public FSRandomAccessFileOwnerWrapper {
|
||||
public:
|
||||
explicit WrappedRandomAccessFile(
|
||||
std::unique_ptr<FSRandomAccessFile>& file)
|
||||
: FSRandomAccessFileOwnerWrapper(std::move(file)) {}
|
||||
|
||||
IOStatus MultiRead(FSReadRequest* reqs, size_t num_reqs,
|
||||
const IOOptions& options,
|
||||
IODebugContext* dbg) override {
|
||||
for (size_t i = 0; i < num_reqs; ++i) {
|
||||
FSReadRequest& req = reqs[i];
|
||||
FSAllocationPtr buffer(new char[req.len], [](void* ptr) {
|
||||
delete[] static_cast<char*>(ptr);
|
||||
});
|
||||
req.fs_scratch = std::move(buffer);
|
||||
req.status = Read(req.offset, req.len, options, &req.result,
|
||||
static_cast<char*>(req.fs_scratch.get()), dbg);
|
||||
}
|
||||
return IOStatus::OK();
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
IOStatus s = target()->NewRandomAccessFile(fname, opts, &file, dbg);
|
||||
EXPECT_OK(s);
|
||||
result->reset(new WrappedRandomAccessFile(file));
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
void SupportedOps(int64_t& supported_ops) override {
|
||||
supported_ops = 1 << FSSupportedOps::kAsyncIO;
|
||||
supported_ops |= 1 << FSSupportedOps::kFSBuffer;
|
||||
}
|
||||
};
|
||||
|
||||
if (!LZ4_Supported()) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<WrapFS> wrap_fs =
|
||||
std::make_shared<WrapFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> wrap_env(new CompositeEnvWrapper(env_, wrap_fs));
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.block_cache = NewCache(250 * 1024, 20 * 1024, 256 * 1024,
|
||||
TieredAdmissionPolicy::kAdmPolicyAuto,
|
||||
/*ready_before_wait=*/true);
|
||||
table_options.block_size = 4 * 1024;
|
||||
table_options.cache_index_and_filter_blocks = false;
|
||||
Options options = GetDefaultOptions();
|
||||
options.create_if_missing = true;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
options.statistics = CreateDBStatistics();
|
||||
options.env = wrap_env.get();
|
||||
|
||||
options.paranoid_file_checks = false;
|
||||
DestroyAndReopen(options);
|
||||
Random rnd(301);
|
||||
const int N = 256;
|
||||
for (int i = 0; i < N; i++) {
|
||||
std::string p_v;
|
||||
test::CompressibleString(&rnd, 0.5, 1007, &p_v);
|
||||
ASSERT_OK(Put(Key(i), p_v));
|
||||
}
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::vector<std::string> keys;
|
||||
std::vector<std::string> values;
|
||||
|
||||
keys.push_back(Key(0));
|
||||
keys.push_back(Key(4));
|
||||
keys.push_back(Key(8));
|
||||
values = MultiGet(keys, /*snapshot=*/nullptr, /*async=*/true);
|
||||
ASSERT_EQ(values.size(), keys.size());
|
||||
for (const auto& value : values) {
|
||||
ASSERT_EQ(1007, value.size());
|
||||
}
|
||||
ASSERT_EQ(nvm_sec_cache()->num_insert_saved(), 3u);
|
||||
ASSERT_EQ(nvm_sec_cache()->num_misses(), 3u);
|
||||
ASSERT_EQ(nvm_sec_cache()->num_hits(), 0u);
|
||||
|
||||
std::string v = Get(Key(12));
|
||||
ASSERT_EQ(1007, v.size());
|
||||
ASSERT_EQ(nvm_sec_cache()->num_insert_saved(), 4u);
|
||||
ASSERT_EQ(nvm_sec_cache()->num_misses(), 4u);
|
||||
ASSERT_EQ(options.statistics->getTickerCount(BLOCK_CACHE_MISS), 4u);
|
||||
|
||||
Close();
|
||||
Destroy(options);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
DBTieredAdmPolicyTest, DBTieredAdmPolicyTest,
|
||||
::testing::Values(TieredAdmissionPolicy::kAdmPolicyAuto,
|
||||
|
||||
@@ -538,6 +538,7 @@ ColumnFamilyData::ColumnFamilyData(
|
||||
refs_(0),
|
||||
initialized_(false),
|
||||
dropped_(false),
|
||||
flush_skip_reschedule_(false),
|
||||
internal_comparator_(cf_options.comparator),
|
||||
initial_cf_options_(SanitizeOptions(db_options, cf_options)),
|
||||
ioptions_(db_options, initial_cf_options_),
|
||||
@@ -1607,6 +1608,19 @@ FSDirectory* ColumnFamilyData::GetDataDir(size_t path_id) const {
|
||||
return data_dirs_[path_id].get();
|
||||
}
|
||||
|
||||
void ColumnFamilyData::SetFlushSkipReschedule() {
|
||||
const Comparator* ucmp = user_comparator();
|
||||
const size_t ts_sz = ucmp->timestamp_size();
|
||||
if (ts_sz == 0 || ioptions_.persist_user_defined_timestamps) {
|
||||
return;
|
||||
}
|
||||
flush_skip_reschedule_.store(true);
|
||||
}
|
||||
|
||||
bool ColumnFamilyData::GetAndClearFlushSkipReschedule() {
|
||||
return flush_skip_reschedule_.exchange(false);
|
||||
}
|
||||
|
||||
bool ColumnFamilyData::ShouldPostponeFlushToRetainUDT(
|
||||
uint64_t max_memtable_id) {
|
||||
const Comparator* ucmp = user_comparator();
|
||||
|
||||
@@ -329,6 +329,10 @@ class ColumnFamilyData {
|
||||
void SetDropped();
|
||||
bool IsDropped() const { return dropped_.load(std::memory_order_relaxed); }
|
||||
|
||||
void SetFlushSkipReschedule();
|
||||
|
||||
bool GetAndClearFlushSkipReschedule();
|
||||
|
||||
// thread-safe
|
||||
int NumberLevels() const { return ioptions_.num_levels; }
|
||||
|
||||
@@ -568,6 +572,10 @@ class ColumnFamilyData {
|
||||
// of its files (if missing)
|
||||
void RecoverEpochNumbers();
|
||||
|
||||
int GetUnflushedMemTableCountForWriteStallCheck() const {
|
||||
return (mem_->IsEmpty() ? 0 : 1) + imm_.NumNotFlushed();
|
||||
}
|
||||
|
||||
private:
|
||||
friend class ColumnFamilySet;
|
||||
ColumnFamilyData(uint32_t id, const std::string& name,
|
||||
@@ -592,6 +600,15 @@ class ColumnFamilyData {
|
||||
std::atomic<bool> initialized_;
|
||||
std::atomic<bool> dropped_; // true if client dropped it
|
||||
|
||||
// When user-defined timestamps in memtable only feature is enabled, this
|
||||
// flag indicates a successfully requested flush that should
|
||||
// skip being rescheduled and haven't undergone the rescheduling check yet.
|
||||
// This flag is cleared when a check skips rescheduling a FlushRequest.
|
||||
// With this flag, automatic flushes in regular cases can continue to
|
||||
// retain UDTs by getting rescheduled as usual while manual flushes and
|
||||
// error recovery flushes will proceed without getting rescheduled.
|
||||
std::atomic<bool> flush_skip_reschedule_;
|
||||
|
||||
const InternalKeyComparator internal_comparator_;
|
||||
InternalTblPropCollFactories internal_tbl_prop_coll_factories_;
|
||||
|
||||
|
||||
+228
-60
@@ -23,6 +23,7 @@
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/iterator.h"
|
||||
#include "rocksdb/listener.h"
|
||||
#include "rocksdb/utilities/object_registry.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testharness.h"
|
||||
@@ -34,6 +35,13 @@
|
||||
#include "utilities/merge_operators.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
namespace {
|
||||
std::string EncodeAsUint64(uint64_t number) {
|
||||
std::string result;
|
||||
PutFixed64(&result, number);
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
static const int kValueSize = 1000;
|
||||
|
||||
@@ -3603,7 +3611,9 @@ TEST(ColumnFamilyTest, ValidateMemtableKVChecksumOption) {
|
||||
}
|
||||
|
||||
// Tests the flushing behavior of a column family to retain user-defined
|
||||
// timestamp when `persist_user_defined_timestamp` is false.
|
||||
// timestamp when `persist_user_defined_timestamp` is false. The behavior of
|
||||
// auto flush is it makes some effort to retain user-defined timestamps while
|
||||
// the behavior of manual flush is that it skips retaining UDTs.
|
||||
class ColumnFamilyRetainUDTTest : public ColumnFamilyTestBase {
|
||||
public:
|
||||
ColumnFamilyRetainUDTTest() : ColumnFamilyTestBase(kLatestFormatVersion) {}
|
||||
@@ -3621,6 +3631,27 @@ class ColumnFamilyRetainUDTTest : public ColumnFamilyTestBase {
|
||||
return db_->Put(WriteOptions(), handles_[cf], Slice(key), Slice(ts),
|
||||
Slice(value));
|
||||
}
|
||||
|
||||
std::string Get(int cf, const std::string& key, const std::string& read_ts) {
|
||||
ReadOptions ropts;
|
||||
Slice timestamp = read_ts;
|
||||
ropts.timestamp = ×tamp;
|
||||
std::string value;
|
||||
Status s = db_->Get(ropts, handles_[cf], Slice(key), &value);
|
||||
if (s.IsNotFound()) {
|
||||
return "NOT_FOUND";
|
||||
} else if (s.ok()) {
|
||||
return value;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void CheckEffectiveCutoffTime(uint64_t expected_cutoff) {
|
||||
std::string effective_full_history_ts_low;
|
||||
EXPECT_OK(
|
||||
db_->GetFullHistoryTsLow(handles_[0], &effective_full_history_ts_low));
|
||||
EXPECT_EQ(EncodeAsUint64(expected_cutoff), effective_full_history_ts_low);
|
||||
}
|
||||
};
|
||||
|
||||
class TestTsComparator : public Comparator {
|
||||
@@ -3664,7 +3695,9 @@ TEST_F(ColumnFamilyRetainUDTTest, SanityCheck) {
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(ColumnFamilyRetainUDTTest, FullHistoryTsLowNotSet) {
|
||||
class AutoFlushRetainUDTTest : public ColumnFamilyRetainUDTTest {};
|
||||
|
||||
TEST_F(AutoFlushRetainUDTTest, FullHistoryTsLowNotSet) {
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::BackgroundFlush:CheckFlushRequest:cb", [&](void* arg) {
|
||||
ASSERT_NE(nullptr, arg);
|
||||
@@ -3674,28 +3707,22 @@ TEST_F(ColumnFamilyRetainUDTTest, FullHistoryTsLowNotSet) {
|
||||
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
Open();
|
||||
std::string write_ts;
|
||||
PutFixed64(&write_ts, 1);
|
||||
ASSERT_OK(Put(0, "foo", write_ts, "v1"));
|
||||
// No `full_history_ts_low` explicitly set by user, flush is continued
|
||||
ASSERT_OK(Put(0, "foo", EncodeAsUint64(1), "v1"));
|
||||
// No `full_history_ts_low` explicitly set by user, auto flush is continued
|
||||
// without checking if its UDTs expired.
|
||||
ASSERT_OK(Flush(0));
|
||||
ASSERT_OK(dbfull()->TEST_SwitchWAL());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
// After flush, `full_history_ts_low` should be automatically advanced to
|
||||
// the effective cutoff timestamp: write_ts + 1
|
||||
std::string cutoff_ts;
|
||||
PutFixed64(&cutoff_ts, 2);
|
||||
std::string effective_full_history_ts_low;
|
||||
ASSERT_OK(
|
||||
db_->GetFullHistoryTsLow(handles_[0], &effective_full_history_ts_low));
|
||||
ASSERT_EQ(cutoff_ts, effective_full_history_ts_low);
|
||||
CheckEffectiveCutoffTime(2);
|
||||
Close();
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(ColumnFamilyRetainUDTTest, AllKeysExpired) {
|
||||
TEST_F(AutoFlushRetainUDTTest, AllKeysExpired) {
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::BackgroundFlush:CheckFlushRequest:cb", [&](void* arg) {
|
||||
ASSERT_NE(nullptr, arg);
|
||||
@@ -3705,27 +3732,22 @@ TEST_F(ColumnFamilyRetainUDTTest, AllKeysExpired) {
|
||||
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
Open();
|
||||
std::string write_ts;
|
||||
PutFixed64(&write_ts, 1);
|
||||
ASSERT_OK(Put(0, "foo", write_ts, "v1"));
|
||||
std::string cutoff_ts;
|
||||
PutFixed64(&cutoff_ts, 3);
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(handles_[0], cutoff_ts));
|
||||
// All keys expired w.r.t the configured `full_history_ts_low`, flush continue
|
||||
// without the need for a re-schedule.
|
||||
ASSERT_OK(Flush(0));
|
||||
ASSERT_OK(Put(0, "foo", EncodeAsUint64(1), "v1"));
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(handles_[0], EncodeAsUint64(3)));
|
||||
// All keys expired w.r.t the configured `full_history_ts_low`, auto flush
|
||||
// continue without the need for a re-schedule.
|
||||
ASSERT_OK(dbfull()->TEST_SwitchWAL());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
// `full_history_ts_low` stays unchanged after flush.
|
||||
std::string effective_full_history_ts_low;
|
||||
ASSERT_OK(
|
||||
db_->GetFullHistoryTsLow(handles_[0], &effective_full_history_ts_low));
|
||||
ASSERT_EQ(cutoff_ts, effective_full_history_ts_low);
|
||||
CheckEffectiveCutoffTime(3);
|
||||
Close();
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
TEST_F(ColumnFamilyRetainUDTTest, NotAllKeysExpiredFlushToAvoidWriteStall) {
|
||||
|
||||
TEST_F(AutoFlushRetainUDTTest, NotAllKeysExpiredFlushToAvoidWriteStall) {
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::BackgroundFlush:CheckFlushRequest:cb", [&](void* arg) {
|
||||
ASSERT_NE(nullptr, arg);
|
||||
@@ -3735,72 +3757,218 @@ TEST_F(ColumnFamilyRetainUDTTest, NotAllKeysExpiredFlushToAvoidWriteStall) {
|
||||
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
Open();
|
||||
std::string cutoff_ts;
|
||||
std::string write_ts;
|
||||
PutFixed64(&write_ts, 1);
|
||||
ASSERT_OK(Put(0, "foo", write_ts, "v1"));
|
||||
PutFixed64(&cutoff_ts, 1);
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(handles_[0], cutoff_ts));
|
||||
ASSERT_OK(Put(0, "foo", EncodeAsUint64(1), "v1"));
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(handles_[0], EncodeAsUint64(1)));
|
||||
ASSERT_OK(db_->SetOptions(handles_[0], {{"max_write_buffer_number", "1"}}));
|
||||
// Not all keys expired, but flush is continued without a re-schedule because
|
||||
// of risk of write stall.
|
||||
ASSERT_OK(Flush(0));
|
||||
// Not all keys expired, but auto flush is continued without a re-schedule
|
||||
// because of risk of write stall.
|
||||
ASSERT_OK(dbfull()->TEST_SwitchWAL());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
// After flush, `full_history_ts_low` should be automatically advanced to
|
||||
// the effective cutoff timestamp: write_ts + 1
|
||||
std::string effective_full_history_ts_low;
|
||||
ASSERT_OK(
|
||||
db_->GetFullHistoryTsLow(handles_[0], &effective_full_history_ts_low));
|
||||
|
||||
cutoff_ts.clear();
|
||||
PutFixed64(&cutoff_ts, 2);
|
||||
ASSERT_EQ(cutoff_ts, effective_full_history_ts_low);
|
||||
CheckEffectiveCutoffTime(2);
|
||||
Close();
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(ColumnFamilyRetainUDTTest, NotAllKeysExpiredFlushRescheduled) {
|
||||
std::string cutoff_ts;
|
||||
TEST_F(AutoFlushRetainUDTTest, NotAllKeysExpiredFlushRescheduled) {
|
||||
std::atomic<int> local_counter{1};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::AfterRetainUDTReschedule:cb", [&](void* /*arg*/) {
|
||||
// Increasing full_history_ts_low so all keys expired after the initial
|
||||
// FlushRequest is rescheduled
|
||||
cutoff_ts.clear();
|
||||
PutFixed64(&cutoff_ts, 3);
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(handles_[0], cutoff_ts));
|
||||
ASSERT_OK(
|
||||
db_->IncreaseFullHistoryTsLow(handles_[0], EncodeAsUint64(3)));
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::BackgroundFlush:CheckFlushRequest:cb", [&](void* arg) {
|
||||
ASSERT_NE(nullptr, arg);
|
||||
auto reschedule_count = *static_cast<int*>(arg);
|
||||
ASSERT_EQ(2, reschedule_count);
|
||||
local_counter.fetch_add(1);
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Open();
|
||||
std::string write_ts;
|
||||
PutFixed64(&write_ts, 1);
|
||||
ASSERT_OK(Put(0, "foo", write_ts, "v1"));
|
||||
PutFixed64(&cutoff_ts, 1);
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(handles_[0], cutoff_ts));
|
||||
ASSERT_OK(Put(0, "foo", EncodeAsUint64(1), "v1"));
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(handles_[0], EncodeAsUint64(1)));
|
||||
// Not all keys expired, and there is no risk of write stall. Flush is
|
||||
// rescheduled. The actual flush happens after `full_history_ts_low` is
|
||||
// increased to mark all keys expired.
|
||||
ASSERT_OK(Flush(0));
|
||||
ASSERT_OK(dbfull()->TEST_SwitchWAL());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
// Make sure callback is not skipped.
|
||||
ASSERT_EQ(2, local_counter);
|
||||
|
||||
std::string effective_full_history_ts_low;
|
||||
ASSERT_OK(
|
||||
db_->GetFullHistoryTsLow(handles_[0], &effective_full_history_ts_low));
|
||||
// `full_history_ts_low` stays unchanged.
|
||||
ASSERT_EQ(cutoff_ts, effective_full_history_ts_low);
|
||||
CheckEffectiveCutoffTime(3);
|
||||
Close();
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
class ManualFlushSkipRetainUDTTest : public ColumnFamilyRetainUDTTest {
|
||||
public:
|
||||
// Write an entry with timestamp that is not expired w.r.t cutoff timestamp,
|
||||
// and make sure automatic flush would be rescheduled to retain UDT.
|
||||
void CheckAutomaticFlushRetainUDT(uint64_t write_ts) {
|
||||
std::atomic<int> local_counter{1};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::AfterRetainUDTReschedule:cb", [&](void* /*arg*/) {
|
||||
// Increasing full_history_ts_low so all keys expired after the
|
||||
// initial FlushRequest is rescheduled
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(
|
||||
handles_[0], EncodeAsUint64(write_ts + 1)));
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::BackgroundFlush:CheckFlushRequest:cb", [&](void* arg) {
|
||||
ASSERT_NE(nullptr, arg);
|
||||
auto reschedule_count = *static_cast<int*>(arg);
|
||||
ASSERT_EQ(2, reschedule_count);
|
||||
local_counter.fetch_add(1);
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
EXPECT_OK(Put(0, "foo", EncodeAsUint64(write_ts),
|
||||
"foo" + std::to_string(write_ts)));
|
||||
EXPECT_OK(dbfull()->TEST_SwitchWAL());
|
||||
EXPECT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
// Make sure callback is not skipped.
|
||||
EXPECT_EQ(2, local_counter);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ManualFlushSkipRetainUDTTest, ManualFlush) {
|
||||
Open();
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(handles_[0], EncodeAsUint64(0)));
|
||||
|
||||
// Manual flush proceeds without trying to retain UDT.
|
||||
ASSERT_OK(Put(0, "foo", EncodeAsUint64(1), "v1"));
|
||||
ASSERT_OK(Flush(0));
|
||||
CheckEffectiveCutoffTime(2);
|
||||
CheckAutomaticFlushRetainUDT(3);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(ManualFlushSkipRetainUDTTest, ManualCompaction) {
|
||||
Open();
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(handles_[0], EncodeAsUint64(0)));
|
||||
|
||||
// Manual compaction proceeds without trying to retain UDT.
|
||||
ASSERT_OK(Put(0, "foo", EncodeAsUint64(1), "v2"));
|
||||
ASSERT_OK(
|
||||
db_->CompactRange(CompactRangeOptions(), handles_[0], nullptr, nullptr));
|
||||
CheckEffectiveCutoffTime(2);
|
||||
CheckAutomaticFlushRetainUDT(3);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(ManualFlushSkipRetainUDTTest, BulkLoading) {
|
||||
Open();
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(handles_[0], EncodeAsUint64(0)));
|
||||
ASSERT_OK(Put(0, "foo", EncodeAsUint64(1), "v1"));
|
||||
|
||||
// Test flush behavior in bulk loading scenarios.
|
||||
Options options(db_options_, column_family_options_);
|
||||
std::string sst_files_dir = dbname_ + "/sst_files/";
|
||||
ASSERT_OK(DestroyDir(env_, sst_files_dir));
|
||||
ASSERT_OK(env_->CreateDir(sst_files_dir));
|
||||
SstFileWriter sst_file_writer(EnvOptions(), options);
|
||||
std::string file1 = sst_files_dir + "file1.sst";
|
||||
ASSERT_OK(sst_file_writer.Open(file1));
|
||||
ASSERT_OK(sst_file_writer.Put("foo", EncodeAsUint64(0), "v2"));
|
||||
ExternalSstFileInfo file1_info;
|
||||
ASSERT_OK(sst_file_writer.Finish(&file1_info));
|
||||
|
||||
// Bulk loading in UDT mode doesn't support external file key range overlap
|
||||
// with DB key range.
|
||||
ASSERT_TRUE(db_->IngestExternalFile({file1}, IngestExternalFileOptions())
|
||||
.IsInvalidArgument());
|
||||
|
||||
std::string file2 = sst_files_dir + "file2.sst";
|
||||
ASSERT_OK(sst_file_writer.Open(file2));
|
||||
ASSERT_OK(sst_file_writer.Put("bar", EncodeAsUint64(0), "val"));
|
||||
ExternalSstFileInfo file2_info;
|
||||
ASSERT_OK(sst_file_writer.Finish(&file2_info));
|
||||
// A successful bulk loading, and it doesn't trigger any flush. As a result
|
||||
// the effective cutoff timestamp is also unchanged.
|
||||
ASSERT_OK(db_->IngestExternalFile({file2}, IngestExternalFileOptions()));
|
||||
|
||||
ASSERT_EQ(Get(0, "foo", EncodeAsUint64(1)), "v1");
|
||||
ASSERT_EQ(Get(0, "bar", EncodeAsUint64(0)), "val");
|
||||
CheckEffectiveCutoffTime(0);
|
||||
CheckAutomaticFlushRetainUDT(1);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(ManualFlushSkipRetainUDTTest, AutomaticFlushQueued) {
|
||||
Open();
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(handles_[0], EncodeAsUint64(0)));
|
||||
|
||||
ASSERT_OK(Put(0, "foo", EncodeAsUint64(1), "v1"));
|
||||
ASSERT_OK(dbfull()->TEST_SwitchWAL());
|
||||
CheckEffectiveCutoffTime(0);
|
||||
|
||||
// Default `max_write_buffer_number=2` used, writing another memtable can get
|
||||
// automatic flush to proceed because of memory pressure. Not doing that so
|
||||
// we can test automatic flush gets to proceed because of an ongoing manual
|
||||
// flush attempt.
|
||||
ASSERT_OK(Flush(0));
|
||||
CheckEffectiveCutoffTime(2);
|
||||
CheckAutomaticFlushRetainUDT(3);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(ManualFlushSkipRetainUDTTest, ConcurrentManualFlushes) {
|
||||
Open();
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(handles_[0], EncodeAsUint64(0)));
|
||||
|
||||
std::vector<ROCKSDB_NAMESPACE::port::Thread> manual_flush_tds;
|
||||
std::atomic<int> next_ts{0};
|
||||
std::mutex mtx;
|
||||
std::condition_variable cv;
|
||||
|
||||
auto manual_flush = [&](int write_ts) {
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mtx);
|
||||
cv.wait(lock,
|
||||
[&write_ts, &next_ts] { return write_ts == next_ts.load(); });
|
||||
ASSERT_OK(Put(0, "foo" + std::to_string(write_ts),
|
||||
EncodeAsUint64(write_ts),
|
||||
"val_" + std::to_string(write_ts)));
|
||||
next_ts.fetch_add(1);
|
||||
cv.notify_all();
|
||||
}
|
||||
if (write_ts % 2 == 0) {
|
||||
ASSERT_OK(Flush(0));
|
||||
} else {
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), handles_[0], nullptr,
|
||||
nullptr));
|
||||
}
|
||||
};
|
||||
|
||||
for (int write_ts = 0; write_ts < 10; write_ts++) {
|
||||
manual_flush_tds.emplace_back(manual_flush, write_ts);
|
||||
}
|
||||
|
||||
for (auto& td : manual_flush_tds) {
|
||||
td.join();
|
||||
}
|
||||
|
||||
CheckEffectiveCutoffTime(10);
|
||||
CheckAutomaticFlushRetainUDT(11);
|
||||
Close();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+12
-6
@@ -229,12 +229,18 @@ Status DBImpl::GetLiveFilesStorageInfo(
|
||||
// metadata.
|
||||
mutex_.Lock();
|
||||
if (flush_memtable) {
|
||||
Status status = FlushForGetLiveFiles();
|
||||
if (!status.ok()) {
|
||||
mutex_.Unlock();
|
||||
ROCKS_LOG_ERROR(immutable_db_options_.info_log, "Cannot Flush data %s\n",
|
||||
status.ToString().c_str());
|
||||
return status;
|
||||
bool wal_locked = lock_wal_count_ > 0;
|
||||
if (wal_locked) {
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"Can't FlushForGetLiveFiles while WAL is locked");
|
||||
} else {
|
||||
Status status = FlushForGetLiveFiles();
|
||||
if (!status.ok()) {
|
||||
mutex_.Unlock();
|
||||
ROCKS_LOG_ERROR(immutable_db_options_.info_log,
|
||||
"Cannot Flush data %s\n", status.ToString().c_str());
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+35
-21
@@ -17,6 +17,7 @@
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
@@ -5163,6 +5164,14 @@ Status DestroyDB(const std::string& dbname, const Options& options,
|
||||
Env* env = soptions.env;
|
||||
std::vector<std::string> filenames;
|
||||
bool wal_in_db_path = soptions.IsWalDirSameAsDBPath();
|
||||
auto sfm = static_cast_with_check<SstFileManagerImpl>(
|
||||
options.sst_file_manager.get());
|
||||
// Allocate a separate trash bucket to be used by all the to be deleted
|
||||
// files, so we can later wait for this bucket to be empty before return.
|
||||
std::optional<int32_t> bucket;
|
||||
if (sfm) {
|
||||
bucket = sfm->NewTrashBucket();
|
||||
}
|
||||
|
||||
// Reset the logger because it holds a handle to the
|
||||
// log file and prevents cleanup and directory removal
|
||||
@@ -5174,6 +5183,7 @@ Status DestroyDB(const std::string& dbname, const Options& options,
|
||||
/*IODebugContext*=*/nullptr)
|
||||
.PermitUncheckedError();
|
||||
|
||||
std::set<std::string> paths_to_delete;
|
||||
FileLock* lock;
|
||||
const std::string lockname = LockFileName(dbname);
|
||||
Status result = env->LockFile(lockname, &lock);
|
||||
@@ -5190,10 +5200,9 @@ Status DestroyDB(const std::string& dbname, const Options& options,
|
||||
del = DestroyDB(path_to_delete, options);
|
||||
} else if (type == kTableFile || type == kWalFile ||
|
||||
type == kBlobFile) {
|
||||
del = DeleteDBFile(
|
||||
&soptions, path_to_delete, dbname,
|
||||
/*force_bg=*/false,
|
||||
/*force_fg=*/(type == kWalFile) ? !wal_in_db_path : false);
|
||||
del = DeleteUnaccountedDBFile(&soptions, path_to_delete, dbname,
|
||||
/*force_bg=*/false,
|
||||
/*force_fg=*/false, bucket);
|
||||
} else {
|
||||
del = env->DeleteFile(path_to_delete);
|
||||
}
|
||||
@@ -5202,6 +5211,7 @@ Status DestroyDB(const std::string& dbname, const Options& options,
|
||||
}
|
||||
}
|
||||
}
|
||||
paths_to_delete.insert(dbname);
|
||||
|
||||
std::set<std::string> paths;
|
||||
for (const DbPath& db_path : options.db_paths) {
|
||||
@@ -5223,18 +5233,19 @@ Status DestroyDB(const std::string& dbname, const Options& options,
|
||||
(type == kTableFile ||
|
||||
type == kBlobFile)) { // Lock file will be deleted at end
|
||||
std::string file_path = path + "/" + fname;
|
||||
Status del = DeleteDBFile(&soptions, file_path, dbname,
|
||||
/*force_bg=*/false, /*force_fg=*/false);
|
||||
Status del = DeleteUnaccountedDBFile(&soptions, file_path, dbname,
|
||||
/*force_bg=*/false,
|
||||
/*force_fg=*/false, bucket);
|
||||
if (!del.ok() && result.ok()) {
|
||||
result = del;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Should we return an error if we cannot delete the directory?
|
||||
env->DeleteDir(path).PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
|
||||
paths_to_delete.merge(paths);
|
||||
|
||||
std::vector<std::string> walDirFiles;
|
||||
std::string archivedir = ArchivalDirectory(dbname);
|
||||
bool wal_dir_exists = false;
|
||||
@@ -5258,46 +5269,49 @@ Status DestroyDB(const std::string& dbname, const Options& options,
|
||||
// Delete archival files.
|
||||
for (const auto& file : archiveFiles) {
|
||||
if (ParseFileName(file, &number, &type) && type == kWalFile) {
|
||||
Status del =
|
||||
DeleteDBFile(&soptions, archivedir + "/" + file, archivedir,
|
||||
/*force_bg=*/false, /*force_fg=*/!wal_in_db_path);
|
||||
Status del = DeleteUnaccountedDBFile(
|
||||
&soptions, archivedir + "/" + file, archivedir,
|
||||
/*force_bg=*/false, /*force_fg=*/!wal_in_db_path, bucket);
|
||||
if (!del.ok() && result.ok()) {
|
||||
result = del;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ignore error in case dir contains other files
|
||||
env->DeleteDir(archivedir).PermitUncheckedError();
|
||||
paths_to_delete.insert(archivedir);
|
||||
}
|
||||
|
||||
// Delete log files in the WAL dir
|
||||
if (wal_dir_exists) {
|
||||
for (const auto& file : walDirFiles) {
|
||||
if (ParseFileName(file, &number, &type) && type == kWalFile) {
|
||||
Status del =
|
||||
DeleteDBFile(&soptions, LogFileName(soptions.wal_dir, number),
|
||||
soptions.wal_dir, /*force_bg=*/false,
|
||||
/*force_fg=*/!wal_in_db_path);
|
||||
Status del = DeleteUnaccountedDBFile(
|
||||
&soptions, LogFileName(soptions.wal_dir, number),
|
||||
soptions.wal_dir, /*force_bg=*/false,
|
||||
/*force_fg=*/!wal_in_db_path, bucket);
|
||||
if (!del.ok() && result.ok()) {
|
||||
result = del;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ignore error in case dir contains other files
|
||||
env->DeleteDir(soptions.wal_dir).PermitUncheckedError();
|
||||
paths_to_delete.insert(soptions.wal_dir);
|
||||
}
|
||||
|
||||
// Ignore error since state is already gone
|
||||
env->UnlockFile(lock).PermitUncheckedError();
|
||||
env->DeleteFile(lockname).PermitUncheckedError();
|
||||
|
||||
// Make sure trash files are all cleared before return.
|
||||
if (sfm && bucket.has_value()) {
|
||||
sfm->WaitForEmptyTrashBucket(bucket.value());
|
||||
}
|
||||
// sst_file_manager holds a ref to the logger. Make sure the logger is
|
||||
// gone before trying to remove the directory.
|
||||
soptions.sst_file_manager.reset();
|
||||
|
||||
// Ignore error in case dir contains other files
|
||||
env->DeleteDir(dbname).PermitUncheckedError();
|
||||
;
|
||||
for (const auto& path_to_delete : paths_to_delete) {
|
||||
env->DeleteDir(path_to_delete).PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+21
-10
@@ -1211,6 +1211,8 @@ class DBImpl : public DB {
|
||||
return logs_.back().number;
|
||||
}
|
||||
|
||||
void TEST_DeleteObsoleteFiles();
|
||||
|
||||
const std::unordered_set<uint64_t>& TEST_GetFilesGrabbedForPurge() const {
|
||||
return files_grabbed_for_purge_;
|
||||
}
|
||||
@@ -1447,6 +1449,9 @@ class DBImpl : public DB {
|
||||
Status RenameTempFileToOptionsFile(const std::string& file_name);
|
||||
Status DeleteObsoleteOptionsFiles();
|
||||
|
||||
void NotifyOnManualFlushScheduled(autovector<ColumnFamilyData*> cfds,
|
||||
FlushReason flush_reason);
|
||||
|
||||
void NotifyOnFlushBegin(ColumnFamilyData* cfd, FileMetaData* file_meta,
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
int job_id, FlushReason flush_reason);
|
||||
@@ -2054,17 +2059,22 @@ class DBImpl : public DB {
|
||||
mutex_.Lock();
|
||||
}
|
||||
|
||||
if (!immutable_db_options_.unordered_write) {
|
||||
// Then the writes are finished before the next write group starts
|
||||
return;
|
||||
if (immutable_db_options_.unordered_write) {
|
||||
// Wait for the ones who already wrote to the WAL to finish their
|
||||
// memtable write.
|
||||
if (pending_memtable_writes_.load() != 0) {
|
||||
// XXX: suspicious wait while holding DB mutex?
|
||||
std::unique_lock<std::mutex> guard(switch_mutex_);
|
||||
switch_cv_.wait(guard,
|
||||
[&] { return pending_memtable_writes_.load() == 0; });
|
||||
}
|
||||
} else {
|
||||
// (Writes are finished before the next write group starts.)
|
||||
}
|
||||
|
||||
// Wait for the ones who already wrote to the WAL to finish their
|
||||
// memtable write.
|
||||
if (pending_memtable_writes_.load() != 0) {
|
||||
std::unique_lock<std::mutex> guard(switch_mutex_);
|
||||
switch_cv_.wait(guard,
|
||||
[&] { return pending_memtable_writes_.load() == 0; });
|
||||
// Wait for any LockWAL to clear
|
||||
while (lock_wal_count_ > 0) {
|
||||
bg_cv_.Wait();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2174,7 +2184,8 @@ class DBImpl : public DB {
|
||||
void GenerateFlushRequest(const autovector<ColumnFamilyData*>& cfds,
|
||||
FlushReason flush_reason, FlushRequest* req);
|
||||
|
||||
void SchedulePendingFlush(const FlushRequest& req);
|
||||
// Returns true if `req` is successfully enqueued.
|
||||
bool SchedulePendingFlush(const FlushRequest& req);
|
||||
|
||||
void SchedulePendingCompaction(ColumnFamilyData* cfd);
|
||||
void SchedulePendingPurge(std::string fname, std::string dir_to_sync,
|
||||
|
||||
@@ -87,6 +87,9 @@ bool DBImpl::ShouldRescheduleFlushRequestToRetainUDT(
|
||||
mutex_.AssertHeld();
|
||||
assert(flush_req.cfd_to_max_mem_id_to_persist.size() == 1);
|
||||
ColumnFamilyData* cfd = flush_req.cfd_to_max_mem_id_to_persist.begin()->first;
|
||||
if (cfd->GetAndClearFlushSkipReschedule()) {
|
||||
return false;
|
||||
}
|
||||
uint64_t max_memtable_id =
|
||||
flush_req.cfd_to_max_mem_id_to_persist.begin()->second;
|
||||
if (cfd->IsDropped() ||
|
||||
@@ -98,15 +101,20 @@ bool DBImpl::ShouldRescheduleFlushRequestToRetainUDT(
|
||||
// alleviated if we continue with the flush instead of postponing it.
|
||||
const auto& mutable_cf_options = *cfd->GetLatestMutableCFOptions();
|
||||
|
||||
// Taking the status of the active Memtable into consideration so that we are
|
||||
// not just checking if DB is currently already in write stall mode.
|
||||
int mem_to_flush = cfd->mem()->ApproximateMemoryUsageFast() >=
|
||||
cfd->mem()->write_buffer_size() / 2
|
||||
? 1
|
||||
: 0;
|
||||
// Use the same criteria as WaitUntilFlushWouldNotStallWrites does w.r.t
|
||||
// defining what a write stall is about to happen means. If this uses a
|
||||
// stricter criteria, for example, a write stall is about to happen if the
|
||||
// last memtable is 10% full, there is a possibility that manual flush could
|
||||
// be waiting in `WaitUntilFlushWouldNotStallWrites` with the incorrect
|
||||
// expectation that others will clear up the excessive memtables and
|
||||
// eventually let it proceed. The others in this case won't start clearing
|
||||
// until the last memtable is 10% full. To avoid that scenario, the criteria
|
||||
// this uses should be the same or less strict than
|
||||
// `WaitUntilFlushWouldNotStallWrites` does.
|
||||
WriteStallCondition write_stall =
|
||||
ColumnFamilyData::GetWriteStallConditionAndCause(
|
||||
cfd->imm()->NumNotFlushed() + mem_to_flush, /*num_l0_files=*/0,
|
||||
cfd->GetUnflushedMemTableCountForWriteStallCheck(),
|
||||
/*num_l0_files=*/0,
|
||||
/*num_compaction_needed_bytes=*/0, mutable_cf_options,
|
||||
*cfd->ioptions())
|
||||
.first;
|
||||
@@ -2310,6 +2318,23 @@ void DBImpl::GenerateFlushRequest(const autovector<ColumnFamilyData*>& cfds,
|
||||
}
|
||||
}
|
||||
|
||||
void DBImpl::NotifyOnManualFlushScheduled(autovector<ColumnFamilyData*> cfds,
|
||||
FlushReason flush_reason) {
|
||||
if (immutable_db_options_.listeners.size() == 0U) {
|
||||
return;
|
||||
}
|
||||
if (shutting_down_.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
std::vector<ManualFlushInfo> info;
|
||||
for (ColumnFamilyData* cfd : cfds) {
|
||||
info.push_back({cfd->GetID(), cfd->GetName(), flush_reason});
|
||||
}
|
||||
for (const auto& listener : immutable_db_options_.listeners) {
|
||||
listener->OnManualFlushScheduled(this, info);
|
||||
}
|
||||
}
|
||||
|
||||
Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
|
||||
const FlushOptions& flush_options,
|
||||
FlushReason flush_reason,
|
||||
@@ -2414,7 +2439,14 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
|
||||
}
|
||||
}
|
||||
for (const auto& req : flush_reqs) {
|
||||
SchedulePendingFlush(req);
|
||||
assert(req.cfd_to_max_mem_id_to_persist.size() == 1);
|
||||
ColumnFamilyData* loop_cfd =
|
||||
req.cfd_to_max_mem_id_to_persist.begin()->first;
|
||||
bool already_queued_for_flush = loop_cfd->queued_for_flush();
|
||||
bool flush_req_enqueued = SchedulePendingFlush(req);
|
||||
if (already_queued_for_flush || flush_req_enqueued) {
|
||||
loop_cfd->SetFlushSkipReschedule();
|
||||
}
|
||||
}
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
}
|
||||
@@ -2426,6 +2458,8 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NotifyOnManualFlushScheduled({cfd}, flush_reason);
|
||||
TEST_SYNC_POINT("DBImpl::FlushMemTable:AfterScheduleFlush");
|
||||
TEST_SYNC_POINT("DBImpl::FlushMemTable:BeforeWaitForBgFlush");
|
||||
if (s.ok() && flush_options.wait) {
|
||||
@@ -2570,6 +2604,7 @@ Status DBImpl::AtomicFlushMemTables(
|
||||
}
|
||||
}
|
||||
}
|
||||
NotifyOnManualFlushScheduled(cfds, flush_reason);
|
||||
TEST_SYNC_POINT("DBImpl::AtomicFlushMemTables:AfterScheduleFlush");
|
||||
TEST_SYNC_POINT("DBImpl::AtomicFlushMemTables:BeforeWaitForBgFlush");
|
||||
if (s.ok() && flush_options.wait) {
|
||||
@@ -2627,7 +2662,9 @@ Status DBImpl::RetryFlushesForErrorRecovery(FlushReason flush_reason,
|
||||
flush_reason,
|
||||
{{cfd,
|
||||
std::numeric_limits<uint64_t>::max() /* max_mem_id_to_persist */}}};
|
||||
SchedulePendingFlush(flush_req);
|
||||
if (SchedulePendingFlush(flush_req)) {
|
||||
cfd->SetFlushSkipReschedule();
|
||||
};
|
||||
}
|
||||
}
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
@@ -2715,13 +2752,13 @@ Status DBImpl::WaitUntilFlushWouldNotStallWrites(ColumnFamilyData* cfd,
|
||||
// mode due to pending compaction bytes, but that's less common
|
||||
// No extra immutable Memtable will be created if the current Memtable is
|
||||
// empty.
|
||||
int mem_to_flush = cfd->mem()->IsEmpty() ? 0 : 1;
|
||||
write_stall_condition = ColumnFamilyData::GetWriteStallConditionAndCause(
|
||||
cfd->imm()->NumNotFlushed() + mem_to_flush,
|
||||
vstorage->l0_delay_trigger_count() + 1,
|
||||
vstorage->estimated_compaction_needed_bytes(),
|
||||
mutable_cf_options, *cfd->ioptions())
|
||||
.first;
|
||||
write_stall_condition =
|
||||
ColumnFamilyData::GetWriteStallConditionAndCause(
|
||||
cfd->GetUnflushedMemTableCountForWriteStallCheck(),
|
||||
vstorage->l0_delay_trigger_count() + 1,
|
||||
vstorage->estimated_compaction_needed_bytes(), mutable_cf_options,
|
||||
*cfd->ioptions())
|
||||
.first;
|
||||
} while (write_stall_condition != WriteStallCondition::kNormal);
|
||||
}
|
||||
return Status::OK();
|
||||
@@ -3033,13 +3070,14 @@ ColumnFamilyData* DBImpl::PickCompactionFromQueue(
|
||||
return cfd;
|
||||
}
|
||||
|
||||
void DBImpl::SchedulePendingFlush(const FlushRequest& flush_req) {
|
||||
bool DBImpl::SchedulePendingFlush(const FlushRequest& flush_req) {
|
||||
mutex_.AssertHeld();
|
||||
bool enqueued = false;
|
||||
if (reject_new_background_jobs_) {
|
||||
return;
|
||||
return enqueued;
|
||||
}
|
||||
if (flush_req.cfd_to_max_mem_id_to_persist.empty()) {
|
||||
return;
|
||||
return enqueued;
|
||||
}
|
||||
if (!immutable_db_options_.atomic_flush) {
|
||||
// For the non-atomic flush case, we never schedule multiple column
|
||||
@@ -3054,6 +3092,7 @@ void DBImpl::SchedulePendingFlush(const FlushRequest& flush_req) {
|
||||
cfd->set_queued_for_flush(true);
|
||||
++unscheduled_flushes_;
|
||||
flush_queue_.push_back(flush_req);
|
||||
enqueued = true;
|
||||
}
|
||||
} else {
|
||||
for (auto& iter : flush_req.cfd_to_max_mem_id_to_persist) {
|
||||
@@ -3062,7 +3101,9 @@ void DBImpl::SchedulePendingFlush(const FlushRequest& flush_req) {
|
||||
}
|
||||
++unscheduled_flushes_;
|
||||
flush_queue_.push_back(flush_req);
|
||||
enqueued = true;
|
||||
}
|
||||
return enqueued;
|
||||
}
|
||||
|
||||
void DBImpl::SchedulePendingCompaction(ColumnFamilyData* cfd) {
|
||||
|
||||
@@ -314,6 +314,11 @@ const autovector<uint64_t>& DBImpl::TEST_GetFilesToQuarantine() const {
|
||||
return error_handler_.GetFilesToQuarantine();
|
||||
}
|
||||
|
||||
void DBImpl::TEST_DeleteObsoleteFiles() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
DeleteObsoleteFiles();
|
||||
}
|
||||
|
||||
size_t DBImpl::TEST_EstimateInMemoryStatsHistorySize() const {
|
||||
InstrumentedMutexLock l(&const_cast<DBImpl*>(this)->stats_history_mutex_);
|
||||
return EstimateInMemoryStatsHistorySize();
|
||||
|
||||
@@ -2156,6 +2156,8 @@ void DBImpl::NotifyOnMemTableSealed(ColumnFamilyData* /*cfd*/,
|
||||
// two_write_queues_ is true (This is to simplify the reasoning.)
|
||||
Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context) {
|
||||
mutex_.AssertHeld();
|
||||
assert(lock_wal_count_ == 0);
|
||||
|
||||
// TODO: plumb Env::IOActivity, Env::IOPriority
|
||||
const ReadOptions read_options;
|
||||
const WriteOptions write_options;
|
||||
@@ -2199,6 +2201,11 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context) {
|
||||
memtable_info.earliest_seqno = cfd->mem()->GetEarliestSequenceNumber();
|
||||
memtable_info.num_entries = cfd->mem()->num_entries();
|
||||
memtable_info.num_deletes = cfd->mem()->num_deletes();
|
||||
if (!cfd->ioptions()->persist_user_defined_timestamps &&
|
||||
cfd->user_comparator()->timestamp_size() > 0) {
|
||||
const Slice& newest_udt = cfd->mem()->GetNewestUDT();
|
||||
memtable_info.newest_udt.assign(newest_udt.data(), newest_udt.size());
|
||||
}
|
||||
// Log this later after lock release. It may be outdated, e.g., if background
|
||||
// flush happens before logging, but that should be ok.
|
||||
int num_imm_unflushed = cfd->imm()->NumNotFlushed();
|
||||
|
||||
+53
-1
@@ -507,6 +507,23 @@ TEST_F(DBSSTTest, DBWithSstFileManagerForBlobFiles) {
|
||||
ASSERT_EQ(files_deleted, 0);
|
||||
ASSERT_EQ(files_scheduled_to_delete, 0);
|
||||
Close();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"SstFileManagerImpl::ScheduleUnaccountedFileDeletion", [&](void* arg) {
|
||||
assert(arg);
|
||||
const std::string* const file_path =
|
||||
static_cast<const std::string*>(arg);
|
||||
if (EndsWith(*file_path, ".blob")) {
|
||||
++files_scheduled_to_delete;
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::OnDeleteFile", [&](void* arg) {
|
||||
const std::string* const file_path =
|
||||
static_cast<const std::string*>(arg);
|
||||
if (EndsWith(*file_path, ".blob")) {
|
||||
files_deleted++;
|
||||
}
|
||||
});
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
ASSERT_EQ(files_deleted, blob_files.size());
|
||||
ASSERT_EQ(files_scheduled_to_delete, blob_files.size());
|
||||
@@ -649,6 +666,23 @@ TEST_F(DBSSTTest, DBWithSstFileManagerForBlobFilesWithGC) {
|
||||
}
|
||||
|
||||
Close();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"SstFileManagerImpl::ScheduleUnaccountedFileDeletion", [&](void* arg) {
|
||||
assert(arg);
|
||||
const std::string* const file_path =
|
||||
static_cast<const std::string*>(arg);
|
||||
if (EndsWith(*file_path, ".blob")) {
|
||||
++files_scheduled_to_delete;
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::OnDeleteFile", [&](void* arg) {
|
||||
const std::string* const file_path =
|
||||
static_cast<const std::string*>(arg);
|
||||
if (EndsWith(*file_path, ".blob")) {
|
||||
files_deleted++;
|
||||
}
|
||||
});
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
sfm->WaitForEmptyTrash();
|
||||
ASSERT_EQ(files_deleted, 5);
|
||||
@@ -887,7 +921,7 @@ TEST_P(DBWALTestWithParam, WALTrashCleanupOnOpen) {
|
||||
// before restarting the DB.
|
||||
// We have to set this on the 2nd to last file for it to delay deletion
|
||||
// on the last file. (Quirk of DeleteScheduler::BackgroundEmptyTrash())
|
||||
options.sst_file_manager->SetDeleteRateBytesPerSecond(1);
|
||||
options.sst_file_manager->SetDeleteRateBytesPerSecond(1024 * 1024);
|
||||
}
|
||||
ASSERT_OK(Put("Key2", DummyString(1024, v)));
|
||||
ASSERT_OK(Put("Key3", DummyString(1024, v)));
|
||||
@@ -1902,6 +1936,24 @@ TEST_F(DBSSTTest, DBWithSFMForBlobFilesAtomicFlush) {
|
||||
ASSERT_EQ(files_deleted, 1);
|
||||
|
||||
Close();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"SstFileManagerImpl::ScheduleUnaccountedFileDeletion", [&](void* arg) {
|
||||
assert(arg);
|
||||
const std::string* const file_path =
|
||||
static_cast<const std::string*>(arg);
|
||||
if (EndsWith(*file_path, ".blob")) {
|
||||
++files_scheduled_to_delete;
|
||||
}
|
||||
});
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::OnDeleteFile", [&](void* arg) {
|
||||
const std::string* const file_path =
|
||||
static_cast<const std::string*>(arg);
|
||||
if (EndsWith(*file_path, ".blob")) {
|
||||
files_deleted++;
|
||||
}
|
||||
});
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
|
||||
ASSERT_EQ(files_scheduled_to_delete, 4);
|
||||
|
||||
+83
-22
@@ -666,11 +666,25 @@ TEST_P(DBWriteTest, LockWALInEffect) {
|
||||
// try the 1st WAL created during open
|
||||
ASSERT_OK(Put("key0", "value"));
|
||||
ASSERT_NE(options.manual_wal_flush, dbfull()->WALBufferIsEmpty());
|
||||
|
||||
ASSERT_OK(db_->LockWAL());
|
||||
|
||||
ASSERT_TRUE(dbfull()->WALBufferIsEmpty());
|
||||
uint64_t wal_num = dbfull()->TEST_GetCurrentLogNumber();
|
||||
// Manual flush with wait=false should abruptly fail with TryAgain
|
||||
FlushOptions flush_opts;
|
||||
flush_opts.wait = false;
|
||||
for (bool allow_write_stall : {true, false}) {
|
||||
flush_opts.allow_write_stall = allow_write_stall;
|
||||
ASSERT_TRUE(db_->Flush(flush_opts).IsTryAgain());
|
||||
}
|
||||
ASSERT_EQ(wal_num, dbfull()->TEST_GetCurrentLogNumber());
|
||||
|
||||
ASSERT_OK(db_->UnlockWAL());
|
||||
// try the 2nd wal created during SwitchWAL
|
||||
|
||||
// try the 2nd wal created during SwitchWAL (not locked this time)
|
||||
ASSERT_OK(dbfull()->TEST_SwitchWAL());
|
||||
ASSERT_NE(wal_num, dbfull()->TEST_GetCurrentLogNumber());
|
||||
ASSERT_OK(Put("key1", "value"));
|
||||
ASSERT_NE(options.manual_wal_flush, dbfull()->WALBufferIsEmpty());
|
||||
ASSERT_OK(db_->LockWAL());
|
||||
@@ -709,21 +723,57 @@ TEST_P(DBWriteTest, LockWALInEffect) {
|
||||
}
|
||||
|
||||
TEST_P(DBWriteTest, LockWALConcurrentRecursive) {
|
||||
// This is a micro-stress test of LockWAL and concurrency handling.
|
||||
// It is considered the most convenient way to balance functional
|
||||
// coverage and reproducibility (vs. the two extremes of (a) unit tests
|
||||
// tailored to specific interleavings and (b) db_stress)
|
||||
Options options = GetOptions();
|
||||
Reopen(options);
|
||||
ASSERT_OK(Put("k1", "val"));
|
||||
ASSERT_OK(Put("k1", "k1_orig"));
|
||||
ASSERT_OK(db_->LockWAL()); // 0 -> 1
|
||||
auto frozen_seqno = db_->GetLatestSequenceNumber();
|
||||
std::atomic<bool> t1_completed{false};
|
||||
port::Thread t1{[&]() {
|
||||
// Won't finish until WAL unlocked
|
||||
ASSERT_OK(Put("k1", "val2"));
|
||||
t1_completed = true;
|
||||
|
||||
std::string ingest_file = dbname_ + "/external.sst";
|
||||
{
|
||||
SstFileWriter sst_file_writer(EnvOptions(), options);
|
||||
ASSERT_OK(sst_file_writer.Open(ingest_file));
|
||||
ASSERT_OK(sst_file_writer.Put("k2", "k2_val"));
|
||||
ExternalSstFileInfo external_info;
|
||||
ASSERT_OK(sst_file_writer.Finish(&external_info));
|
||||
}
|
||||
AcqRelAtomic<bool> parallel_ingest_completed{false};
|
||||
port::Thread parallel_ingest{[&]() {
|
||||
IngestExternalFileOptions ingest_opts;
|
||||
ingest_opts.move_files = true; // faster than copy
|
||||
// Shouldn't finish until WAL unlocked
|
||||
ASSERT_OK(db_->IngestExternalFile({ingest_file}, ingest_opts));
|
||||
parallel_ingest_completed.Store(true);
|
||||
}};
|
||||
|
||||
AcqRelAtomic<bool> flush_completed{false};
|
||||
port::Thread parallel_flush{[&]() {
|
||||
FlushOptions flush_opts;
|
||||
// NB: Flush with wait=false case is tested above in LockWALInEffect
|
||||
flush_opts.wait = true;
|
||||
// allow_write_stall = true blocks in fewer cases
|
||||
flush_opts.allow_write_stall = true;
|
||||
// Shouldn't finish until WAL unlocked
|
||||
ASSERT_OK(db_->Flush(flush_opts));
|
||||
flush_completed.Store(true);
|
||||
}};
|
||||
|
||||
AcqRelAtomic<bool> parallel_put_completed{false};
|
||||
port::Thread parallel_put{[&]() {
|
||||
// This can make certain failure scenarios more likely:
|
||||
// sleep(1);
|
||||
// Shouldn't finish until WAL unlocked
|
||||
ASSERT_OK(Put("k1", "k1_mod"));
|
||||
parallel_put_completed.Store(true);
|
||||
}};
|
||||
|
||||
ASSERT_OK(db_->LockWAL()); // 1 -> 2
|
||||
// Read-only ops are OK
|
||||
ASSERT_EQ(Get("k1"), "val");
|
||||
ASSERT_EQ(Get("k1"), "k1_orig");
|
||||
{
|
||||
std::vector<LiveFileStorageInfo> files;
|
||||
LiveFilesStorageInfoOptions lf_opts;
|
||||
@@ -732,29 +782,35 @@ TEST_P(DBWriteTest, LockWALConcurrentRecursive) {
|
||||
ASSERT_OK(db_->GetLiveFilesStorageInfo({lf_opts}, &files));
|
||||
}
|
||||
|
||||
port::Thread t2{[&]() {
|
||||
port::Thread parallel_lock_wal{[&]() {
|
||||
ASSERT_OK(db_->LockWAL()); // 2 -> 3 or 1 -> 2
|
||||
}};
|
||||
|
||||
ASSERT_OK(db_->UnlockWAL()); // 2 -> 1 or 3 -> 2
|
||||
// Give t1 an extra chance to jump in case of bug
|
||||
// Give parallel_put an extra chance to jump in case of bug
|
||||
std::this_thread::yield();
|
||||
t2.join();
|
||||
ASSERT_FALSE(t1_completed.load());
|
||||
parallel_lock_wal.join();
|
||||
ASSERT_FALSE(parallel_put_completed.Load());
|
||||
ASSERT_FALSE(parallel_ingest_completed.Load());
|
||||
ASSERT_FALSE(flush_completed.Load());
|
||||
|
||||
// Should now have 2 outstanding LockWAL
|
||||
ASSERT_EQ(Get("k1"), "val");
|
||||
ASSERT_EQ(Get("k1"), "k1_orig");
|
||||
|
||||
ASSERT_OK(db_->UnlockWAL()); // 2 -> 1
|
||||
|
||||
ASSERT_FALSE(t1_completed.load());
|
||||
ASSERT_EQ(Get("k1"), "val");
|
||||
ASSERT_FALSE(parallel_put_completed.Load());
|
||||
ASSERT_FALSE(parallel_ingest_completed.Load());
|
||||
ASSERT_FALSE(flush_completed.Load());
|
||||
|
||||
ASSERT_EQ(Get("k1"), "k1_orig");
|
||||
ASSERT_EQ(Get("k2"), "NOT_FOUND");
|
||||
ASSERT_EQ(frozen_seqno, db_->GetLatestSequenceNumber());
|
||||
|
||||
// Ensure final Unlock is concurrency safe and extra Unlock is safe but
|
||||
// non-OK
|
||||
std::atomic<int> unlock_ok{0};
|
||||
port::Thread t3{[&]() {
|
||||
port::Thread parallel_stuff{[&]() {
|
||||
if (db_->UnlockWAL().ok()) {
|
||||
unlock_ok++;
|
||||
}
|
||||
@@ -767,18 +823,23 @@ TEST_P(DBWriteTest, LockWALConcurrentRecursive) {
|
||||
if (db_->UnlockWAL().ok()) {
|
||||
unlock_ok++;
|
||||
}
|
||||
t3.join();
|
||||
parallel_stuff.join();
|
||||
|
||||
// There was one extra unlock, so just one non-ok
|
||||
ASSERT_EQ(unlock_ok.load(), 2);
|
||||
|
||||
// Write can proceed
|
||||
t1.join();
|
||||
ASSERT_TRUE(t1_completed.load());
|
||||
ASSERT_EQ(Get("k1"), "val2");
|
||||
parallel_put.join();
|
||||
ASSERT_TRUE(parallel_put_completed.Load());
|
||||
ASSERT_EQ(Get("k1"), "k1_mod");
|
||||
parallel_ingest.join();
|
||||
ASSERT_TRUE(parallel_ingest_completed.Load());
|
||||
ASSERT_EQ(Get("k2"), "k2_val");
|
||||
parallel_flush.join();
|
||||
ASSERT_TRUE(flush_completed.Load());
|
||||
// And new writes
|
||||
ASSERT_OK(Put("k2", "val"));
|
||||
ASSERT_EQ(Get("k2"), "val");
|
||||
ASSERT_OK(Put("k3", "val"));
|
||||
ASSERT_EQ(Get("k3"), "val");
|
||||
}
|
||||
|
||||
TEST_P(DBWriteTest, ConcurrentlyDisabledWAL) {
|
||||
|
||||
@@ -674,10 +674,6 @@ TEST_F(ExternalSSTFileBasicTest, NoCopy) {
|
||||
ASSERT_EQ(file3_info.smallest_key, Key(110));
|
||||
ASSERT_EQ(file3_info.largest_key, Key(124));
|
||||
|
||||
ASSERT_OK(dbfull()->LockWAL());
|
||||
// TODO(FIXME): should not allow file ingestion.
|
||||
// With below line, ingestion will block, without it, ingestion can go
|
||||
// through. ASSERT_OK(dbfull()->Put(WriteOptions(), "vo", "vo"));
|
||||
s = DeprecatedAddFile({file1}, true /* move file */);
|
||||
ASSERT_OK(s) << s.ToString();
|
||||
ASSERT_EQ(Status::NotFound(), env_->FileExists(file1));
|
||||
|
||||
@@ -342,8 +342,7 @@ Status ExternalSstFileIngestionJob::NeedsFlush(bool* flush_needed,
|
||||
autovector<UserKeyRange> ranges;
|
||||
ranges.reserve(n);
|
||||
for (const IngestedFileInfo& file_to_ingest : files_to_ingest_) {
|
||||
ranges.emplace_back(file_to_ingest.smallest_internal_key.user_key(),
|
||||
file_to_ingest.largest_internal_key.user_key());
|
||||
ranges.emplace_back(file_to_ingest.start_ukey, file_to_ingest.limit_ukey);
|
||||
}
|
||||
Status status = cfd_->RangesOverlapWithMemtables(
|
||||
ranges, super_version, db_options_.allow_data_in_errors, flush_needed);
|
||||
@@ -930,6 +929,17 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
}
|
||||
}
|
||||
|
||||
const size_t ts_sz = ucmp->timestamp_size();
|
||||
Slice smallest = file_to_ingest->smallest_internal_key.user_key();
|
||||
Slice largest = file_to_ingest->largest_internal_key.user_key();
|
||||
if (ts_sz > 0) {
|
||||
AppendUserKeyWithMaxTimestamp(&file_to_ingest->start_ukey, smallest, ts_sz);
|
||||
AppendUserKeyWithMinTimestamp(&file_to_ingest->limit_ukey, largest, ts_sz);
|
||||
} else {
|
||||
file_to_ingest->start_ukey.assign(smallest.data(), smallest.size());
|
||||
file_to_ingest->limit_ukey.assign(largest.data(), largest.size());
|
||||
}
|
||||
|
||||
auto s =
|
||||
GetSstInternalUniqueId(file_to_ingest->table_properties.db_id,
|
||||
file_to_ingest->table_properties.db_session_id,
|
||||
@@ -982,9 +992,8 @@ Status ExternalSstFileIngestionJob::AssignLevelAndSeqnoForIngestedFile(
|
||||
if (lvl > 0 && lvl < vstorage->base_level()) {
|
||||
continue;
|
||||
}
|
||||
if (cfd_->RangeOverlapWithCompaction(
|
||||
file_to_ingest->smallest_internal_key.user_key(),
|
||||
file_to_ingest->largest_internal_key.user_key(), lvl)) {
|
||||
if (cfd_->RangeOverlapWithCompaction(file_to_ingest->start_ukey,
|
||||
file_to_ingest->limit_ukey, lvl)) {
|
||||
// We must use L0 or any level higher than `lvl` to be able to overwrite
|
||||
// the compaction output keys that we overlap with in this level, We also
|
||||
// need to assign this file a seqno to overwrite the compaction output
|
||||
@@ -994,9 +1003,8 @@ Status ExternalSstFileIngestionJob::AssignLevelAndSeqnoForIngestedFile(
|
||||
} else if (vstorage->NumLevelFiles(lvl) > 0) {
|
||||
bool overlap_with_level = false;
|
||||
status = sv->current->OverlapWithLevelIterator(
|
||||
ro, env_options_, file_to_ingest->smallest_internal_key.user_key(),
|
||||
file_to_ingest->largest_internal_key.user_key(), lvl,
|
||||
&overlap_with_level);
|
||||
ro, env_options_, file_to_ingest->start_ukey,
|
||||
file_to_ingest->limit_ukey, lvl, &overlap_with_level);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
@@ -1165,9 +1173,8 @@ bool ExternalSstFileIngestionJob::IngestedFileFitInLevel(
|
||||
}
|
||||
|
||||
auto* vstorage = cfd_->current()->storage_info();
|
||||
Slice file_smallest_user_key(
|
||||
file_to_ingest->smallest_internal_key.user_key());
|
||||
Slice file_largest_user_key(file_to_ingest->largest_internal_key.user_key());
|
||||
Slice file_smallest_user_key(file_to_ingest->start_ukey);
|
||||
Slice file_largest_user_key(file_to_ingest->limit_ukey);
|
||||
|
||||
if (vstorage->OverlapInLevel(level, &file_smallest_user_key,
|
||||
&file_largest_user_key)) {
|
||||
|
||||
@@ -32,6 +32,17 @@ struct IngestedFileInfo {
|
||||
InternalKey smallest_internal_key;
|
||||
// Largest internal key in external file
|
||||
InternalKey largest_internal_key;
|
||||
// NOTE: use below two fields for all `*Overlap*` types of checks instead of
|
||||
// smallest_internal_key.user_key() and largest_internal_key.user_key().
|
||||
// The smallest / largest user key contained in the file for key range checks.
|
||||
// These could be different from smallest_internal_key.user_key(), and
|
||||
// largest_internal_key.user_key() when user-defined timestamps are enabled,
|
||||
// because the check is about making sure the user key without timestamps part
|
||||
// does not overlap. To achieve that, the smallest user key will be updated
|
||||
// with the maximum timestamp while the largest user key will be updated with
|
||||
// the min timestamp. It's otherwise the same.
|
||||
std::string start_ukey;
|
||||
std::string limit_ukey;
|
||||
// Sequence number for keys in external file
|
||||
SequenceNumber original_seqno;
|
||||
// Offset of the global sequence number field in the file, will
|
||||
|
||||
@@ -3358,11 +3358,60 @@ TEST_F(ExternalSSTFileWithTimestampTest, Basic) {
|
||||
|
||||
ASSERT_OK(IngestExternalUDTFile({file4}));
|
||||
|
||||
for (int k = 200; k < 250; k++) {
|
||||
VerifyValueAndTs(Key(k), EncodeAsUint64(k), Key(k) + "_val",
|
||||
EncodeAsUint64(k));
|
||||
}
|
||||
|
||||
// In UDT mode, any external file that can be successfully ingested also
|
||||
// should not overlap with the db. As a result, they can always get the
|
||||
// seq 0 assigned.
|
||||
ASSERT_EQ(db_->GetLatestSequenceNumber(), seq_num_before_ingestion);
|
||||
|
||||
// file5.sst (Key(200), ts = 199)
|
||||
// While DB has (Key(200), ts = 200) => user key without timestamp overlaps
|
||||
std::string file5 = sst_files_dir_ + "file5.sst";
|
||||
ASSERT_OK(sst_file_writer.Open(file5));
|
||||
ASSERT_OK(
|
||||
sst_file_writer.Put(Key(200), EncodeAsUint64(199), Key(200) + "_val"));
|
||||
|
||||
ExternalSstFileInfo file5_info;
|
||||
ASSERT_OK(sst_file_writer.Finish(&file5_info));
|
||||
ASSERT_TRUE(IngestExternalUDTFile({file5}).IsInvalidArgument());
|
||||
|
||||
// file6.sst (Key(200), ts = 201)
|
||||
// While DB has (Key(200), ts = 200) => user key without timestamp overlaps
|
||||
std::string file6 = sst_files_dir_ + "file6.sst";
|
||||
ASSERT_OK(sst_file_writer.Open(file6));
|
||||
ASSERT_OK(
|
||||
sst_file_writer.Put(Key(200), EncodeAsUint64(201), Key(0) + "_val"));
|
||||
|
||||
ExternalSstFileInfo file6_info;
|
||||
ASSERT_OK(sst_file_writer.Finish(&file6_info));
|
||||
ASSERT_TRUE(IngestExternalUDTFile({file6}).IsInvalidArgument());
|
||||
|
||||
// Check memtable overlap.
|
||||
ASSERT_OK(dbfull()->Put(WriteOptions(), Key(250), EncodeAsUint64(250),
|
||||
Key(250) + "_val"));
|
||||
|
||||
std::string file7 = sst_files_dir_ + "file7.sst";
|
||||
ASSERT_OK(sst_file_writer.Open(file7));
|
||||
ASSERT_OK(
|
||||
sst_file_writer.Put(Key(250), EncodeAsUint64(249), Key(250) + "_val2"));
|
||||
|
||||
ExternalSstFileInfo file7_info;
|
||||
ASSERT_OK(sst_file_writer.Finish(&file7_info));
|
||||
ASSERT_TRUE(IngestExternalUDTFile({file7}).IsInvalidArgument());
|
||||
|
||||
std::string file8 = sst_files_dir_ + "file8.sst";
|
||||
ASSERT_OK(sst_file_writer.Open(file8));
|
||||
ASSERT_OK(
|
||||
sst_file_writer.Put(Key(250), EncodeAsUint64(251), Key(250) + "_val3"));
|
||||
|
||||
ExternalSstFileInfo file8_info;
|
||||
ASSERT_OK(sst_file_writer.Finish(&file8_info));
|
||||
ASSERT_TRUE(IngestExternalUDTFile({file8}).IsInvalidArgument());
|
||||
|
||||
DestroyAndRecreateExternalSSTFilesDir();
|
||||
} while (ChangeOptions(kSkipPlainTable | kSkipFIFOCompaction |
|
||||
kRangeDelSkipConfigs));
|
||||
|
||||
+9
-4
@@ -487,9 +487,11 @@ unsigned int Reader::ReadPhysicalRecord(Slice* result, size_t* drop_size,
|
||||
type == kRecyclableUserDefinedTimestampSizeType);
|
||||
if (is_recyclable_type) {
|
||||
header_size = kRecyclableHeaderSize;
|
||||
if (end_of_buffer_offset_ - buffer_.size() == 0) {
|
||||
recycled_ = true;
|
||||
if (first_record_read_ && !recycled_) {
|
||||
// A recycled log should have started with a recycled record
|
||||
return kBadRecord;
|
||||
}
|
||||
recycled_ = true;
|
||||
// We need enough for the larger header
|
||||
if (buffer_.size() < static_cast<size_t>(kRecyclableHeaderSize)) {
|
||||
int r = kEof;
|
||||
@@ -867,9 +869,12 @@ bool FragmentBufferedReader::TryReadFragment(
|
||||
int header_size = kHeaderSize;
|
||||
if ((type >= kRecyclableFullType && type <= kRecyclableLastType) ||
|
||||
type == kRecyclableUserDefinedTimestampSizeType) {
|
||||
if (end_of_buffer_offset_ - buffer_.size() == 0) {
|
||||
recycled_ = true;
|
||||
if (first_record_read_ && !recycled_) {
|
||||
// A recycled log should have started with a recycled record
|
||||
*fragment_type_or_err = kBadRecord;
|
||||
return true;
|
||||
}
|
||||
recycled_ = true;
|
||||
header_size = kRecyclableHeaderSize;
|
||||
while (buffer_.size() < static_cast<size_t>(kRecyclableHeaderSize)) {
|
||||
size_t old_size = buffer_.size();
|
||||
|
||||
@@ -1157,6 +1157,42 @@ TEST_P(CompressionLogTest, AlignedFragmentation) {
|
||||
ASSERT_EQ("EOF", Read());
|
||||
}
|
||||
|
||||
TEST_P(CompressionLogTest, ChecksumMismatch) {
|
||||
const CompressionType kCompressionType = std::get<2>(GetParam());
|
||||
const bool kCompressionEnabled = kCompressionType != kNoCompression;
|
||||
const bool kRecyclableLog = (std::get<0>(GetParam()) != 0);
|
||||
if (!StreamingCompressionTypeSupported(kCompressionType)) {
|
||||
ROCKSDB_GTEST_SKIP("Test requires support for compression type");
|
||||
return;
|
||||
}
|
||||
ASSERT_OK(SetupTestEnv());
|
||||
|
||||
Write("foooooo");
|
||||
int header_len;
|
||||
if (kRecyclableLog) {
|
||||
header_len = kRecyclableHeaderSize;
|
||||
} else {
|
||||
header_len = kHeaderSize;
|
||||
}
|
||||
int compression_record_len;
|
||||
if (kCompressionEnabled) {
|
||||
compression_record_len = header_len + 4;
|
||||
} else {
|
||||
compression_record_len = 0;
|
||||
}
|
||||
IncrementByte(compression_record_len + header_len /* offset */,
|
||||
14 /* delta */);
|
||||
|
||||
ASSERT_EQ("EOF", Read());
|
||||
if (!kRecyclableLog) {
|
||||
ASSERT_GT(DroppedBytes(), 0U);
|
||||
ASSERT_EQ("OK", MatchError("checksum mismatch"));
|
||||
} else {
|
||||
ASSERT_EQ(0U, DroppedBytes());
|
||||
ASSERT_EQ("", ReportMessage());
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
Compression, CompressionLogTest,
|
||||
::testing::Combine(::testing::Values(0, 1), ::testing::Bool(),
|
||||
|
||||
+124
-64
@@ -928,15 +928,19 @@ Status WriteBatch::Put(ColumnFamilyHandle* column_family, const Slice& key,
|
||||
}
|
||||
|
||||
if (0 == ts_sz) {
|
||||
return WriteBatchInternal::Put(this, cf_id, key, value);
|
||||
s = WriteBatchInternal::Put(this, cf_id, key, value);
|
||||
} else {
|
||||
needs_in_place_update_ts_ = true;
|
||||
has_key_with_ts_ = true;
|
||||
std::string dummy_ts(ts_sz, '\0');
|
||||
std::array<Slice, 2> key_with_ts{{key, dummy_ts}};
|
||||
s = WriteBatchInternal::Put(this, cf_id, SliceParts(key_with_ts.data(), 2),
|
||||
SliceParts(&value, 1));
|
||||
}
|
||||
|
||||
needs_in_place_update_ts_ = true;
|
||||
has_key_with_ts_ = true;
|
||||
std::string dummy_ts(ts_sz, '\0');
|
||||
std::array<Slice, 2> key_with_ts{{key, dummy_ts}};
|
||||
return WriteBatchInternal::Put(this, cf_id, SliceParts(key_with_ts.data(), 2),
|
||||
SliceParts(&value, 1));
|
||||
if (s.ok()) {
|
||||
MaybeTrackTimestampSize(cf_id, ts_sz);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status WriteBatch::TimedPut(ColumnFamilyHandle* column_family, const Slice& key,
|
||||
@@ -961,7 +965,7 @@ Status WriteBatch::TimedPut(ColumnFamilyHandle* column_family, const Slice& key,
|
||||
|
||||
Status WriteBatch::Put(ColumnFamilyHandle* column_family, const Slice& key,
|
||||
const Slice& ts, const Slice& value) {
|
||||
const Status s = CheckColumnFamilyTimestampSize(column_family, ts);
|
||||
Status s = CheckColumnFamilyTimestampSize(column_family, ts);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -969,8 +973,12 @@ Status WriteBatch::Put(ColumnFamilyHandle* column_family, const Slice& key,
|
||||
assert(column_family);
|
||||
uint32_t cf_id = column_family->GetID();
|
||||
std::array<Slice, 2> key_with_ts{{key, ts}};
|
||||
return WriteBatchInternal::Put(this, cf_id, SliceParts(key_with_ts.data(), 2),
|
||||
SliceParts(&value, 1));
|
||||
s = WriteBatchInternal::Put(this, cf_id, SliceParts(key_with_ts.data(), 2),
|
||||
SliceParts(&value, 1));
|
||||
if (s.ok()) {
|
||||
MaybeTrackTimestampSize(cf_id, ts.size());
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status WriteBatchInternal::CheckSlicePartsLength(const SliceParts& key,
|
||||
@@ -1038,7 +1046,11 @@ Status WriteBatch::Put(ColumnFamilyHandle* column_family, const SliceParts& key,
|
||||
}
|
||||
|
||||
if (ts_sz == 0) {
|
||||
return WriteBatchInternal::Put(this, cf_id, key, value);
|
||||
s = WriteBatchInternal::Put(this, cf_id, key, value);
|
||||
if (s.ok()) {
|
||||
MaybeTrackTimestampSize(cf_id, ts_sz);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
return Status::InvalidArgument(
|
||||
@@ -1245,20 +1257,24 @@ Status WriteBatch::Delete(ColumnFamilyHandle* column_family, const Slice& key) {
|
||||
}
|
||||
|
||||
if (0 == ts_sz) {
|
||||
return WriteBatchInternal::Delete(this, cf_id, key);
|
||||
s = WriteBatchInternal::Delete(this, cf_id, key);
|
||||
} else {
|
||||
needs_in_place_update_ts_ = true;
|
||||
has_key_with_ts_ = true;
|
||||
std::string dummy_ts(ts_sz, '\0');
|
||||
std::array<Slice, 2> key_with_ts{{key, dummy_ts}};
|
||||
s = WriteBatchInternal::Delete(this, cf_id,
|
||||
SliceParts(key_with_ts.data(), 2));
|
||||
}
|
||||
|
||||
needs_in_place_update_ts_ = true;
|
||||
has_key_with_ts_ = true;
|
||||
std::string dummy_ts(ts_sz, '\0');
|
||||
std::array<Slice, 2> key_with_ts{{key, dummy_ts}};
|
||||
return WriteBatchInternal::Delete(this, cf_id,
|
||||
SliceParts(key_with_ts.data(), 2));
|
||||
if (s.ok()) {
|
||||
MaybeTrackTimestampSize(cf_id, ts_sz);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status WriteBatch::Delete(ColumnFamilyHandle* column_family, const Slice& key,
|
||||
const Slice& ts) {
|
||||
const Status s = CheckColumnFamilyTimestampSize(column_family, ts);
|
||||
Status s = CheckColumnFamilyTimestampSize(column_family, ts);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -1266,8 +1282,12 @@ Status WriteBatch::Delete(ColumnFamilyHandle* column_family, const Slice& key,
|
||||
has_key_with_ts_ = true;
|
||||
uint32_t cf_id = column_family->GetID();
|
||||
std::array<Slice, 2> key_with_ts{{key, ts}};
|
||||
return WriteBatchInternal::Delete(this, cf_id,
|
||||
SliceParts(key_with_ts.data(), 2));
|
||||
s = WriteBatchInternal::Delete(this, cf_id,
|
||||
SliceParts(key_with_ts.data(), 2));
|
||||
if (s.ok()) {
|
||||
MaybeTrackTimestampSize(cf_id, ts.size());
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status WriteBatchInternal::Delete(WriteBatch* b, uint32_t column_family_id,
|
||||
@@ -1312,7 +1332,11 @@ Status WriteBatch::Delete(ColumnFamilyHandle* column_family,
|
||||
}
|
||||
|
||||
if (0 == ts_sz) {
|
||||
return WriteBatchInternal::Delete(this, cf_id, key);
|
||||
s = WriteBatchInternal::Delete(this, cf_id, key);
|
||||
if (s.ok()) {
|
||||
MaybeTrackTimestampSize(cf_id, ts_sz);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
return Status::InvalidArgument(
|
||||
@@ -1360,20 +1384,24 @@ Status WriteBatch::SingleDelete(ColumnFamilyHandle* column_family,
|
||||
}
|
||||
|
||||
if (0 == ts_sz) {
|
||||
return WriteBatchInternal::SingleDelete(this, cf_id, key);
|
||||
s = WriteBatchInternal::SingleDelete(this, cf_id, key);
|
||||
} else {
|
||||
needs_in_place_update_ts_ = true;
|
||||
has_key_with_ts_ = true;
|
||||
std::string dummy_ts(ts_sz, '\0');
|
||||
std::array<Slice, 2> key_with_ts{{key, dummy_ts}};
|
||||
s = WriteBatchInternal::SingleDelete(this, cf_id,
|
||||
SliceParts(key_with_ts.data(), 2));
|
||||
}
|
||||
|
||||
needs_in_place_update_ts_ = true;
|
||||
has_key_with_ts_ = true;
|
||||
std::string dummy_ts(ts_sz, '\0');
|
||||
std::array<Slice, 2> key_with_ts{{key, dummy_ts}};
|
||||
return WriteBatchInternal::SingleDelete(this, cf_id,
|
||||
SliceParts(key_with_ts.data(), 2));
|
||||
if (s.ok()) {
|
||||
MaybeTrackTimestampSize(cf_id, ts_sz);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status WriteBatch::SingleDelete(ColumnFamilyHandle* column_family,
|
||||
const Slice& key, const Slice& ts) {
|
||||
const Status s = CheckColumnFamilyTimestampSize(column_family, ts);
|
||||
Status s = CheckColumnFamilyTimestampSize(column_family, ts);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -1381,8 +1409,12 @@ Status WriteBatch::SingleDelete(ColumnFamilyHandle* column_family,
|
||||
assert(column_family);
|
||||
uint32_t cf_id = column_family->GetID();
|
||||
std::array<Slice, 2> key_with_ts{{key, ts}};
|
||||
return WriteBatchInternal::SingleDelete(this, cf_id,
|
||||
SliceParts(key_with_ts.data(), 2));
|
||||
s = WriteBatchInternal::SingleDelete(this, cf_id,
|
||||
SliceParts(key_with_ts.data(), 2));
|
||||
if (s.ok()) {
|
||||
MaybeTrackTimestampSize(cf_id, ts.size());
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status WriteBatchInternal::SingleDelete(WriteBatch* b,
|
||||
@@ -1429,7 +1461,11 @@ Status WriteBatch::SingleDelete(ColumnFamilyHandle* column_family,
|
||||
}
|
||||
|
||||
if (0 == ts_sz) {
|
||||
return WriteBatchInternal::SingleDelete(this, cf_id, key);
|
||||
s = WriteBatchInternal::SingleDelete(this, cf_id, key);
|
||||
if (s.ok()) {
|
||||
MaybeTrackTimestampSize(cf_id, ts_sz);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
return Status::InvalidArgument(
|
||||
@@ -1479,23 +1515,27 @@ Status WriteBatch::DeleteRange(ColumnFamilyHandle* column_family,
|
||||
}
|
||||
|
||||
if (0 == ts_sz) {
|
||||
return WriteBatchInternal::DeleteRange(this, cf_id, begin_key, end_key);
|
||||
s = WriteBatchInternal::DeleteRange(this, cf_id, begin_key, end_key);
|
||||
} else {
|
||||
needs_in_place_update_ts_ = true;
|
||||
has_key_with_ts_ = true;
|
||||
std::string dummy_ts(ts_sz, '\0');
|
||||
std::array<Slice, 2> begin_key_with_ts{{begin_key, dummy_ts}};
|
||||
std::array<Slice, 2> end_key_with_ts{{end_key, dummy_ts}};
|
||||
s = WriteBatchInternal::DeleteRange(this, cf_id,
|
||||
SliceParts(begin_key_with_ts.data(), 2),
|
||||
SliceParts(end_key_with_ts.data(), 2));
|
||||
}
|
||||
|
||||
needs_in_place_update_ts_ = true;
|
||||
has_key_with_ts_ = true;
|
||||
std::string dummy_ts(ts_sz, '\0');
|
||||
std::array<Slice, 2> begin_key_with_ts{{begin_key, dummy_ts}};
|
||||
std::array<Slice, 2> end_key_with_ts{{end_key, dummy_ts}};
|
||||
return WriteBatchInternal::DeleteRange(
|
||||
this, cf_id, SliceParts(begin_key_with_ts.data(), 2),
|
||||
SliceParts(end_key_with_ts.data(), 2));
|
||||
if (s.ok()) {
|
||||
MaybeTrackTimestampSize(cf_id, ts_sz);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status WriteBatch::DeleteRange(ColumnFamilyHandle* column_family,
|
||||
const Slice& begin_key, const Slice& end_key,
|
||||
const Slice& ts) {
|
||||
const Status s = CheckColumnFamilyTimestampSize(column_family, ts);
|
||||
Status s = CheckColumnFamilyTimestampSize(column_family, ts);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -1504,9 +1544,13 @@ Status WriteBatch::DeleteRange(ColumnFamilyHandle* column_family,
|
||||
uint32_t cf_id = column_family->GetID();
|
||||
std::array<Slice, 2> key_with_ts{{begin_key, ts}};
|
||||
std::array<Slice, 2> end_key_with_ts{{end_key, ts}};
|
||||
return WriteBatchInternal::DeleteRange(this, cf_id,
|
||||
SliceParts(key_with_ts.data(), 2),
|
||||
SliceParts(end_key_with_ts.data(), 2));
|
||||
s = WriteBatchInternal::DeleteRange(this, cf_id,
|
||||
SliceParts(key_with_ts.data(), 2),
|
||||
SliceParts(end_key_with_ts.data(), 2));
|
||||
if (s.ok()) {
|
||||
MaybeTrackTimestampSize(cf_id, ts.size());
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status WriteBatchInternal::DeleteRange(WriteBatch* b, uint32_t column_family_id,
|
||||
@@ -1553,7 +1597,11 @@ Status WriteBatch::DeleteRange(ColumnFamilyHandle* column_family,
|
||||
}
|
||||
|
||||
if (0 == ts_sz) {
|
||||
return WriteBatchInternal::DeleteRange(this, cf_id, begin_key, end_key);
|
||||
s = WriteBatchInternal::DeleteRange(this, cf_id, begin_key, end_key);
|
||||
if (s.ok()) {
|
||||
MaybeTrackTimestampSize(cf_id, ts_sz);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
return Status::InvalidArgument(
|
||||
@@ -1607,21 +1655,25 @@ Status WriteBatch::Merge(ColumnFamilyHandle* column_family, const Slice& key,
|
||||
}
|
||||
|
||||
if (0 == ts_sz) {
|
||||
return WriteBatchInternal::Merge(this, cf_id, key, value);
|
||||
s = WriteBatchInternal::Merge(this, cf_id, key, value);
|
||||
} else {
|
||||
needs_in_place_update_ts_ = true;
|
||||
has_key_with_ts_ = true;
|
||||
std::string dummy_ts(ts_sz, '\0');
|
||||
std::array<Slice, 2> key_with_ts{{key, dummy_ts}};
|
||||
|
||||
s = WriteBatchInternal::Merge(
|
||||
this, cf_id, SliceParts(key_with_ts.data(), 2), SliceParts(&value, 1));
|
||||
}
|
||||
|
||||
needs_in_place_update_ts_ = true;
|
||||
has_key_with_ts_ = true;
|
||||
std::string dummy_ts(ts_sz, '\0');
|
||||
std::array<Slice, 2> key_with_ts{{key, dummy_ts}};
|
||||
|
||||
return WriteBatchInternal::Merge(
|
||||
this, cf_id, SliceParts(key_with_ts.data(), 2), SliceParts(&value, 1));
|
||||
if (s.ok()) {
|
||||
MaybeTrackTimestampSize(cf_id, ts_sz);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status WriteBatch::Merge(ColumnFamilyHandle* column_family, const Slice& key,
|
||||
const Slice& ts, const Slice& value) {
|
||||
const Status s = CheckColumnFamilyTimestampSize(column_family, ts);
|
||||
Status s = CheckColumnFamilyTimestampSize(column_family, ts);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -1629,8 +1681,12 @@ Status WriteBatch::Merge(ColumnFamilyHandle* column_family, const Slice& key,
|
||||
assert(column_family);
|
||||
uint32_t cf_id = column_family->GetID();
|
||||
std::array<Slice, 2> key_with_ts{{key, ts}};
|
||||
return WriteBatchInternal::Merge(
|
||||
this, cf_id, SliceParts(key_with_ts.data(), 2), SliceParts(&value, 1));
|
||||
s = WriteBatchInternal::Merge(this, cf_id, SliceParts(key_with_ts.data(), 2),
|
||||
SliceParts(&value, 1));
|
||||
if (s.ok()) {
|
||||
MaybeTrackTimestampSize(cf_id, ts.size());
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status WriteBatchInternal::Merge(WriteBatch* b, uint32_t column_family_id,
|
||||
@@ -1679,7 +1735,11 @@ Status WriteBatch::Merge(ColumnFamilyHandle* column_family,
|
||||
}
|
||||
|
||||
if (0 == ts_sz) {
|
||||
return WriteBatchInternal::Merge(this, cf_id, key, value);
|
||||
s = WriteBatchInternal::Merge(this, cf_id, key, value);
|
||||
if (s.ok()) {
|
||||
MaybeTrackTimestampSize(cf_id, ts_sz);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
return Status::InvalidArgument(
|
||||
|
||||
@@ -958,12 +958,48 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "LockWAL() failed: %s\n", s.ToString().c_str());
|
||||
} else {
|
||||
// Verify no writes during LockWAL
|
||||
auto old_seqno = db_->GetLatestSequenceNumber();
|
||||
// Yield for a while
|
||||
do {
|
||||
std::this_thread::yield();
|
||||
} while (thread->rand.OneIn(2));
|
||||
// Latest seqno should not have changed
|
||||
// And also that WAL is not changed during LockWAL()
|
||||
std::unique_ptr<LogFile> old_wal;
|
||||
s = db_->GetCurrentWalFile(&old_wal);
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "GetCurrentWalFile() failed: %s\n",
|
||||
s.ToString().c_str());
|
||||
} else {
|
||||
// Yield for a while
|
||||
do {
|
||||
std::this_thread::yield();
|
||||
} while (thread->rand.OneIn(2));
|
||||
// Current WAL and size should not have changed
|
||||
std::unique_ptr<LogFile> new_wal;
|
||||
s = db_->GetCurrentWalFile(&new_wal);
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "GetCurrentWalFile() failed: %s\n",
|
||||
s.ToString().c_str());
|
||||
} else {
|
||||
if (old_wal->LogNumber() != new_wal->LogNumber()) {
|
||||
fprintf(stderr,
|
||||
"Failed: WAL number changed during LockWAL(): %" PRIu64
|
||||
" to %" PRIu64 "\n",
|
||||
old_wal->LogNumber(), new_wal->LogNumber());
|
||||
}
|
||||
// FIXME: FaultInjectionTestFS does not report file sizes that
|
||||
// reflect what has been flushed. Either that needs to be fixed
|
||||
// or GetSortedWals/GetLiveWalFile need to stop relying on
|
||||
// asking the FS for sizes.
|
||||
if (!fault_fs_guard &&
|
||||
old_wal->SizeFileBytes() != new_wal->SizeFileBytes()) {
|
||||
fprintf(stderr,
|
||||
"Failed: WAL %" PRIu64
|
||||
" size changed during LockWAL(): %" PRIu64
|
||||
" to %" PRIu64 "\n",
|
||||
old_wal->LogNumber(), old_wal->SizeFileBytes(),
|
||||
new_wal->SizeFileBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Verify no writes during LockWAL
|
||||
auto new_seqno = db_->GetLatestSequenceNumber();
|
||||
if (old_seqno != new_seqno) {
|
||||
fprintf(
|
||||
@@ -971,6 +1007,7 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
"Failure: latest seqno changed from %u to %u with WAL locked\n",
|
||||
(unsigned)old_seqno, (unsigned)new_seqno);
|
||||
}
|
||||
// Verification done. Now unlock WAL
|
||||
s = db_->UnlockWAL();
|
||||
if (!s.ok()) {
|
||||
fprintf(stderr, "UnlockWAL() failed: %s\n", s.ToString().c_str());
|
||||
@@ -2468,6 +2505,7 @@ void StressTest::TestPromoteL0(ThreadState* thread,
|
||||
|
||||
Status StressTest::TestFlush(const std::vector<int>& rand_column_families) {
|
||||
FlushOptions flush_opts;
|
||||
assert(flush_opts.wait);
|
||||
if (FLAGS_atomic_flush) {
|
||||
return db_->Flush(flush_opts, column_families_);
|
||||
}
|
||||
|
||||
+124
-23
@@ -31,6 +31,7 @@ DeleteScheduler::DeleteScheduler(SystemClock* clock, FileSystem* fs,
|
||||
total_trash_size_(0),
|
||||
rate_bytes_per_sec_(rate_bytes_per_sec),
|
||||
pending_files_(0),
|
||||
next_trash_bucket_(0),
|
||||
bytes_max_delete_chunk_(bytes_max_delete_chunk),
|
||||
closing_(false),
|
||||
cv_(&mu_),
|
||||
@@ -66,10 +67,8 @@ Status DeleteScheduler::DeleteFile(const std::string& file_path,
|
||||
total_trash_size_.load() > total_size * max_trash_db_ratio_.load())) {
|
||||
// Rate limiting is disabled or trash size makes up more than
|
||||
// max_trash_db_ratio_ (default 25%) of the total DB size
|
||||
TEST_SYNC_POINT("DeleteScheduler::DeleteFile");
|
||||
Status s = fs_->DeleteFile(file_path, IOOptions(), nullptr);
|
||||
Status s = DeleteFileImmediately(file_path, /*accounted=*/true);
|
||||
if (s.ok()) {
|
||||
s = sst_file_manager_->OnDeleteFile(file_path);
|
||||
ROCKS_LOG_INFO(info_log_,
|
||||
"Deleted file %s immediately, rate_bytes_per_sec %" PRIi64
|
||||
", total_trash_size %" PRIu64 ", total_size %" PRIi64
|
||||
@@ -77,15 +76,57 @@ Status DeleteScheduler::DeleteFile(const std::string& file_path,
|
||||
file_path.c_str(), rate_bytes_per_sec_.load(),
|
||||
total_trash_size_.load(), total_size,
|
||||
max_trash_db_ratio_.load());
|
||||
InstrumentedMutexLock l(&mu_);
|
||||
RecordTick(stats_.get(), FILES_DELETED_IMMEDIATELY);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
return AddFileToDeletionQueue(file_path, dir_to_sync, /*bucket=*/std::nullopt,
|
||||
/*accounted=*/true);
|
||||
}
|
||||
|
||||
Status DeleteScheduler::DeleteUnaccountedFile(const std::string& file_path,
|
||||
const std::string& dir_to_sync,
|
||||
const bool force_bg,
|
||||
std::optional<int32_t> bucket) {
|
||||
uint64_t num_hard_links = 1;
|
||||
fs_->NumFileLinks(file_path, IOOptions(), &num_hard_links, nullptr)
|
||||
.PermitUncheckedError();
|
||||
|
||||
// We can tolerate rare races where we might immediately delete both links
|
||||
// to a file.
|
||||
if (rate_bytes_per_sec_.load() <= 0 || (!force_bg && num_hard_links > 1)) {
|
||||
Status s = DeleteFileImmediately(file_path, /*accounted=*/false);
|
||||
if (s.ok()) {
|
||||
ROCKS_LOG_INFO(info_log_,
|
||||
"Deleted file %s immediately, rate_bytes_per_sec %" PRIi64,
|
||||
file_path.c_str(), rate_bytes_per_sec_.load());
|
||||
}
|
||||
return s;
|
||||
}
|
||||
return AddFileToDeletionQueue(file_path, dir_to_sync, bucket,
|
||||
/*accounted=*/false);
|
||||
}
|
||||
|
||||
Status DeleteScheduler::DeleteFileImmediately(const std::string& file_path,
|
||||
bool accounted) {
|
||||
TEST_SYNC_POINT("DeleteScheduler::DeleteFile");
|
||||
TEST_SYNC_POINT_CALLBACK("DeleteScheduler::DeleteFile::cb",
|
||||
const_cast<std::string*>(&file_path));
|
||||
Status s = fs_->DeleteFile(file_path, IOOptions(), nullptr);
|
||||
if (s.ok()) {
|
||||
s = OnDeleteFile(file_path, accounted);
|
||||
InstrumentedMutexLock l(&mu_);
|
||||
RecordTick(stats_.get(), FILES_DELETED_IMMEDIATELY);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status DeleteScheduler::AddFileToDeletionQueue(const std::string& file_path,
|
||||
const std::string& dir_to_sync,
|
||||
std::optional<int32_t> bucket,
|
||||
bool accounted) {
|
||||
// Move file to trash
|
||||
std::string trash_file;
|
||||
Status s = MarkAsTrash(file_path, &trash_file);
|
||||
Status s = MarkAsTrash(file_path, accounted, &trash_file);
|
||||
ROCKS_LOG_INFO(info_log_, "Mark file: %s as trash -- %s", trash_file.c_str(),
|
||||
s.ToString().c_str());
|
||||
|
||||
@@ -94,7 +135,7 @@ Status DeleteScheduler::DeleteFile(const std::string& file_path,
|
||||
file_path.c_str(), s.ToString().c_str());
|
||||
s = fs_->DeleteFile(file_path, IOOptions(), nullptr);
|
||||
if (s.ok()) {
|
||||
s = sst_file_manager_->OnDeleteFile(file_path);
|
||||
s = OnDeleteFile(file_path, accounted);
|
||||
ROCKS_LOG_INFO(info_log_, "Deleted file %s immediately",
|
||||
trash_file.c_str());
|
||||
InstrumentedMutexLock l(&mu_);
|
||||
@@ -104,11 +145,13 @@ Status DeleteScheduler::DeleteFile(const std::string& file_path,
|
||||
}
|
||||
|
||||
// Update the total trash size
|
||||
uint64_t trash_file_size = 0;
|
||||
IOStatus io_s =
|
||||
fs_->GetFileSize(trash_file, IOOptions(), &trash_file_size, nullptr);
|
||||
if (io_s.ok()) {
|
||||
total_trash_size_.fetch_add(trash_file_size);
|
||||
if (accounted) {
|
||||
uint64_t trash_file_size = 0;
|
||||
IOStatus io_s =
|
||||
fs_->GetFileSize(trash_file, IOOptions(), &trash_file_size, nullptr);
|
||||
if (io_s.ok()) {
|
||||
total_trash_size_.fetch_add(trash_file_size);
|
||||
}
|
||||
}
|
||||
//**TODO: What should we do if we failed to
|
||||
// get the file size?
|
||||
@@ -117,8 +160,15 @@ Status DeleteScheduler::DeleteFile(const std::string& file_path,
|
||||
{
|
||||
InstrumentedMutexLock l(&mu_);
|
||||
RecordTick(stats_.get(), FILES_MARKED_TRASH);
|
||||
queue_.emplace(trash_file, dir_to_sync);
|
||||
queue_.emplace(trash_file, dir_to_sync, accounted, bucket);
|
||||
pending_files_++;
|
||||
if (bucket.has_value()) {
|
||||
auto iter = pending_files_in_buckets_.find(bucket.value());
|
||||
assert(iter != pending_files_in_buckets_.end());
|
||||
if (iter != pending_files_in_buckets_.end()) {
|
||||
iter->second++;
|
||||
}
|
||||
}
|
||||
if (pending_files_ == 1) {
|
||||
cv_.SignalAll();
|
||||
}
|
||||
@@ -177,7 +227,7 @@ Status DeleteScheduler::CleanupDirectory(Env* env, SstFileManagerImpl* sfm,
|
||||
}
|
||||
|
||||
Status DeleteScheduler::MarkAsTrash(const std::string& file_path,
|
||||
std::string* trash_file) {
|
||||
bool accounted, std::string* trash_file) {
|
||||
// Sanity check of the path
|
||||
size_t idx = file_path.rfind('/');
|
||||
if (idx == std::string::npos || idx == file_path.size() - 1) {
|
||||
@@ -211,7 +261,7 @@ Status DeleteScheduler::MarkAsTrash(const std::string& file_path,
|
||||
}
|
||||
cnt++;
|
||||
}
|
||||
if (s.ok()) {
|
||||
if (s.ok() && accounted) {
|
||||
s = sst_file_manager_->OnMoveFile(file_path, *trash_file);
|
||||
}
|
||||
return s;
|
||||
@@ -235,6 +285,8 @@ void DeleteScheduler::BackgroundEmptyTrash() {
|
||||
uint64_t total_deleted_bytes = 0;
|
||||
int64_t current_delete_rate = rate_bytes_per_sec_.load();
|
||||
while (!queue_.empty() && !closing_) {
|
||||
// Satisfy static analysis.
|
||||
std::optional<int32_t> bucket = std::nullopt;
|
||||
if (current_delete_rate != rate_bytes_per_sec_.load()) {
|
||||
// User changed the delete rate
|
||||
current_delete_rate = rate_bytes_per_sec_.load();
|
||||
@@ -247,14 +299,17 @@ void DeleteScheduler::BackgroundEmptyTrash() {
|
||||
// Get new file to delete
|
||||
const FileAndDir& fad = queue_.front();
|
||||
std::string path_in_trash = fad.fname;
|
||||
std::string dir_to_sync = fad.dir;
|
||||
bool accounted = fad.accounted;
|
||||
bucket = fad.bucket;
|
||||
|
||||
// We don't need to hold the lock while deleting the file
|
||||
mu_.Unlock();
|
||||
uint64_t deleted_bytes = 0;
|
||||
bool is_complete = true;
|
||||
// Delete file from trash and update total_penlty value
|
||||
Status s =
|
||||
DeleteTrashFile(path_in_trash, fad.dir, &deleted_bytes, &is_complete);
|
||||
Status s = DeleteTrashFile(path_in_trash, dir_to_sync, accounted,
|
||||
&deleted_bytes, &is_complete);
|
||||
total_deleted_bytes += deleted_bytes;
|
||||
mu_.Lock();
|
||||
if (is_complete) {
|
||||
@@ -288,12 +343,20 @@ void DeleteScheduler::BackgroundEmptyTrash() {
|
||||
TEST_SYNC_POINT_CALLBACK("DeleteScheduler::BackgroundEmptyTrash:Wait",
|
||||
&total_penalty);
|
||||
|
||||
int32_t pending_files_in_bucket = std::numeric_limits<int32_t>::max();
|
||||
if (is_complete) {
|
||||
pending_files_--;
|
||||
if (bucket.has_value()) {
|
||||
auto iter = pending_files_in_buckets_.find(bucket.value());
|
||||
assert(iter != pending_files_in_buckets_.end());
|
||||
if (iter != pending_files_in_buckets_.end()) {
|
||||
pending_files_in_bucket = iter->second--;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pending_files_ == 0) {
|
||||
// Unblock WaitForEmptyTrash since there are no more files waiting
|
||||
// to be deleted
|
||||
if (pending_files_ == 0 || pending_files_in_bucket == 0) {
|
||||
// Unblock WaitForEmptyTrash or WaitForEmptyTrashBucket since there are
|
||||
// no more files waiting to be deleted
|
||||
cv_.SignalAll();
|
||||
}
|
||||
}
|
||||
@@ -302,12 +365,14 @@ void DeleteScheduler::BackgroundEmptyTrash() {
|
||||
|
||||
Status DeleteScheduler::DeleteTrashFile(const std::string& path_in_trash,
|
||||
const std::string& dir_to_sync,
|
||||
uint64_t* deleted_bytes,
|
||||
bool accounted, uint64_t* deleted_bytes,
|
||||
bool* is_complete) {
|
||||
uint64_t file_size;
|
||||
Status s = fs_->GetFileSize(path_in_trash, IOOptions(), &file_size, nullptr);
|
||||
*is_complete = true;
|
||||
TEST_SYNC_POINT("DeleteScheduler::DeleteTrashFile:DeleteFile");
|
||||
TEST_SYNC_POINT_CALLBACK("DeleteScheduler::DeleteTrashFile::cb",
|
||||
const_cast<std::string*>(&path_in_trash));
|
||||
if (s.ok()) {
|
||||
bool need_full_delete = true;
|
||||
if (bytes_max_delete_chunk_ != 0 && file_size > bytes_max_delete_chunk_) {
|
||||
@@ -374,7 +439,7 @@ Status DeleteScheduler::DeleteTrashFile(const std::string& path_in_trash,
|
||||
}
|
||||
if (s.ok()) {
|
||||
*deleted_bytes = file_size;
|
||||
s = sst_file_manager_->OnDeleteFile(path_in_trash);
|
||||
s = OnDeleteFile(path_in_trash, accounted);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,12 +449,24 @@ Status DeleteScheduler::DeleteTrashFile(const std::string& path_in_trash,
|
||||
path_in_trash.c_str(), s.ToString().c_str());
|
||||
*deleted_bytes = 0;
|
||||
} else {
|
||||
total_trash_size_.fetch_sub(*deleted_bytes);
|
||||
if (accounted) {
|
||||
total_trash_size_.fetch_sub(*deleted_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
Status DeleteScheduler::OnDeleteFile(const std::string& file_path,
|
||||
bool accounted) {
|
||||
if (accounted) {
|
||||
return sst_file_manager_->OnDeleteFile(file_path);
|
||||
}
|
||||
TEST_SYNC_POINT_CALLBACK("DeleteScheduler::OnDeleteFile",
|
||||
const_cast<std::string*>(&file_path));
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void DeleteScheduler::WaitForEmptyTrash() {
|
||||
InstrumentedMutexLock l(&mu_);
|
||||
while (pending_files_ > 0 && !closing_) {
|
||||
@@ -397,6 +474,30 @@ void DeleteScheduler::WaitForEmptyTrash() {
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<int32_t> DeleteScheduler::NewTrashBucket() {
|
||||
if (rate_bytes_per_sec_.load() <= 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
InstrumentedMutexLock l(&mu_);
|
||||
int32_t bucket_number = next_trash_bucket_++;
|
||||
pending_files_in_buckets_.emplace(bucket_number, 0);
|
||||
return bucket_number;
|
||||
}
|
||||
|
||||
void DeleteScheduler::WaitForEmptyTrashBucket(int32_t bucket) {
|
||||
InstrumentedMutexLock l(&mu_);
|
||||
if (bucket >= next_trash_bucket_) {
|
||||
return;
|
||||
}
|
||||
auto iter = pending_files_in_buckets_.find(bucket);
|
||||
while (iter != pending_files_in_buckets_.end() && iter->second > 0 &&
|
||||
!closing_) {
|
||||
cv_.Wait();
|
||||
iter = pending_files_in_buckets_.find(bucket);
|
||||
}
|
||||
pending_files_in_buckets_.erase(bucket);
|
||||
}
|
||||
|
||||
void DeleteScheduler::MaybeCreateBackgroundThread() {
|
||||
if (bg_thread_ == nullptr && rate_bytes_per_sec_.load() > 0) {
|
||||
bg_thread_.reset(
|
||||
|
||||
+61
-8
@@ -7,6 +7,7 @@
|
||||
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
@@ -48,16 +49,45 @@ class DeleteScheduler {
|
||||
MaybeCreateBackgroundThread();
|
||||
}
|
||||
|
||||
// Mark file as trash directory and schedule its deletion. If force_bg is
|
||||
// set, it forces the file to always be deleted in the background thread,
|
||||
// except when rate limiting is disabled
|
||||
// Delete an accounted file that is tracked by `SstFileManager` and should be
|
||||
// tracked by this `DeleteScheduler` when it's deleted.
|
||||
// The file is deleted immediately if slow deletion is disabled. If force_bg
|
||||
// is not set and trash to db size ratio exceeded the configured threshold,
|
||||
// it is immediately deleted too. In all other cases, the file will be moved
|
||||
// to a trash directory and scheduled for deletion by a background thread.
|
||||
Status DeleteFile(const std::string& fname, const std::string& dir_to_sync,
|
||||
const bool force_bg = false);
|
||||
|
||||
// Wait for all files being deleteing in the background to finish or for
|
||||
// Delete an unaccounted file that is not tracked by `SstFileManager` and
|
||||
// should not be tracked by this `DeleteScheduler` when it's deleted.
|
||||
// The file is deleted immediately if slow deletion is disabled. If force_bg
|
||||
// is not set and the file have more than 1 hard link, it is immediately
|
||||
// deleted too. In all other cases, the file will be moved to a trash
|
||||
// directory and scheduled for deletion by a background thread.
|
||||
// This API also supports assign a file to a specified bucket created by
|
||||
// `NewTrashBucket` when delete files in the background. So the caller can
|
||||
// wait for a specific bucket to be empty by checking the
|
||||
// `WaitForEmptyTrashBucket` API.
|
||||
Status DeleteUnaccountedFile(const std::string& file_path,
|
||||
const std::string& dir_to_sync,
|
||||
const bool force_bg = false,
|
||||
std::optional<int32_t> bucket = std::nullopt);
|
||||
|
||||
// Wait for all files being deleted in the background to finish or for
|
||||
// destructor to be called.
|
||||
void WaitForEmptyTrash();
|
||||
|
||||
// Creates a new trash bucket. A bucket is only created and returned when slow
|
||||
// deletion is enabled.
|
||||
// For each bucket that is created, the user should also call
|
||||
// `WaitForEmptyTrashBucket` after scheduling file deletions to make sure the
|
||||
// trash files are all cleared.
|
||||
std::optional<int32_t> NewTrashBucket();
|
||||
|
||||
// Wait for all the files in the specified bucket to be deleted in the
|
||||
// background or for the destructor to be called.
|
||||
void WaitForEmptyTrashBucket(int32_t bucket);
|
||||
|
||||
// Return a map containing errors that happened in BackgroundEmptyTrash
|
||||
// file_path => error status
|
||||
std::map<std::string, Status> GetBackgroundErrors();
|
||||
@@ -87,12 +117,21 @@ class DeleteScheduler {
|
||||
}
|
||||
|
||||
private:
|
||||
Status MarkAsTrash(const std::string& file_path, std::string* path_in_trash);
|
||||
Status DeleteFileImmediately(const std::string& file_path, bool accounted);
|
||||
|
||||
Status AddFileToDeletionQueue(const std::string& file_path,
|
||||
const std::string& dir_to_sync,
|
||||
std::optional<int32_t> bucket, bool accounted);
|
||||
|
||||
Status MarkAsTrash(const std::string& file_path, bool accounted,
|
||||
std::string* path_in_trash);
|
||||
|
||||
Status DeleteTrashFile(const std::string& path_in_trash,
|
||||
const std::string& dir_to_sync,
|
||||
const std::string& dir_to_sync, bool accounted,
|
||||
uint64_t* deleted_bytes, bool* is_complete);
|
||||
|
||||
Status OnDeleteFile(const std::string& file_path, bool accounted);
|
||||
|
||||
void BackgroundEmptyTrash();
|
||||
|
||||
void MaybeCreateBackgroundThread();
|
||||
@@ -104,19 +143,28 @@ class DeleteScheduler {
|
||||
std::atomic<uint64_t> total_trash_size_;
|
||||
// Maximum number of bytes that should be deleted per second
|
||||
std::atomic<int64_t> rate_bytes_per_sec_;
|
||||
// Mutex to protect queue_, pending_files_, bg_errors_, closing_, stats_
|
||||
// Mutex to protect queue_, pending_files_, next_trash_bucket_,
|
||||
// pending_files_in_buckets_, bg_errors_, closing_, stats_
|
||||
InstrumentedMutex mu_;
|
||||
|
||||
struct FileAndDir {
|
||||
FileAndDir(const std::string& f, const std::string& d) : fname(f), dir(d) {}
|
||||
FileAndDir(const std::string& _fname, const std::string& _dir,
|
||||
bool _accounted, std::optional<int32_t> _bucket)
|
||||
: fname(_fname), dir(_dir), accounted(_accounted), bucket(_bucket) {}
|
||||
std::string fname;
|
||||
std::string dir; // empty will be skipped.
|
||||
bool accounted;
|
||||
std::optional<int32_t> bucket;
|
||||
};
|
||||
|
||||
// Queue of trash files that need to be deleted
|
||||
std::queue<FileAndDir> queue_;
|
||||
// Number of trash files that are waiting to be deleted
|
||||
int32_t pending_files_;
|
||||
// Next trash bucket that can be created
|
||||
int32_t next_trash_bucket_;
|
||||
// A mapping from trash bucket to number of pending files in the bucket
|
||||
std::map<int32_t, int32_t> pending_files_in_buckets_;
|
||||
uint64_t bytes_max_delete_chunk_;
|
||||
// Errors that happened in BackgroundEmptyTrash (file_path => error)
|
||||
std::map<std::string, Status> bg_errors_;
|
||||
@@ -127,6 +175,7 @@ class DeleteScheduler {
|
||||
// Condition variable signaled in these conditions
|
||||
// - pending_files_ value change from 0 => 1
|
||||
// - pending_files_ value change from 1 => 0
|
||||
// - a value in pending_files_in_buckets change from 1 => 0
|
||||
// - closing_ value is set to true
|
||||
InstrumentedCondVar cv_;
|
||||
// Background thread running BackgroundEmptyTrash
|
||||
@@ -138,6 +187,10 @@ class DeleteScheduler {
|
||||
// If the trash size constitutes for more than this fraction of the total DB
|
||||
// size we will start deleting new files passed to DeleteScheduler
|
||||
// immediately
|
||||
// Unaccounted files passed for deletion will not cause change in
|
||||
// total_trash_size_ or affect the DeleteScheduler::total_trash_size_ over
|
||||
// SstFileManager::total_size_ ratio. Their slow deletion is not subject to
|
||||
// this configured threshold either.
|
||||
std::atomic<double> max_trash_db_ratio_;
|
||||
static const uint64_t kMicrosInSecond = 1000 * 1000LL;
|
||||
std::shared_ptr<Statistics> stats_;
|
||||
|
||||
@@ -78,7 +78,7 @@ class DeleteSchedulerTest : public testing::Test {
|
||||
}
|
||||
|
||||
std::string NewDummyFile(const std::string& file_name, uint64_t size = 1024,
|
||||
size_t dummy_files_dirs_idx = 0) {
|
||||
size_t dummy_files_dirs_idx = 0, bool track = true) {
|
||||
std::string file_path =
|
||||
dummy_files_dirs_[dummy_files_dirs_idx] + "/" + file_name;
|
||||
std::unique_ptr<WritableFile> f;
|
||||
@@ -86,7 +86,9 @@ class DeleteSchedulerTest : public testing::Test {
|
||||
std::string data(size, 'A');
|
||||
EXPECT_OK(f->Append(data));
|
||||
EXPECT_OK(f->Close());
|
||||
EXPECT_OK(sst_file_mgr_->OnAddFile(file_path));
|
||||
if (track) {
|
||||
EXPECT_OK(sst_file_mgr_->OnAddFile(file_path));
|
||||
}
|
||||
return file_path;
|
||||
}
|
||||
|
||||
@@ -353,6 +355,8 @@ TEST_F(DeleteSchedulerTest, DisableRateLimiting) {
|
||||
ASSERT_EQ(num_files,
|
||||
stats_->getAndResetTickerCount(FILES_DELETED_IMMEDIATELY));
|
||||
|
||||
ASSERT_FALSE(delete_scheduler_->NewTrashBucket().has_value());
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
@@ -718,6 +722,141 @@ TEST_F(DeleteSchedulerTest, IsTrashCheck) {
|
||||
ASSERT_FALSE(DeleteScheduler::IsTrashFile("abc.trashx"));
|
||||
}
|
||||
|
||||
TEST_F(DeleteSchedulerTest, DeleteAccountedAndUnaccountedFiles) {
|
||||
rate_bytes_per_sec_ = 1024 * 1024; // 1 MB / s
|
||||
NewDeleteScheduler();
|
||||
|
||||
// Create 100 files, every file is 1 KB
|
||||
int num_files = 100; // 100 files
|
||||
uint64_t file_size = 1024; // 1 KB as a file size
|
||||
std::vector<std::string> generated_files;
|
||||
for (int i = 0; i < num_files; i++) {
|
||||
std::string file_name = "file" + std::to_string(i) + ".data";
|
||||
generated_files.push_back(NewDummyFile(file_name, file_size,
|
||||
/*dummy_files_dirs_idx*/ 0,
|
||||
/*track=*/false));
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_files; i++) {
|
||||
if (i % 2) {
|
||||
ASSERT_OK(sst_file_mgr_->OnAddFile(generated_files[i], file_size));
|
||||
ASSERT_OK(delete_scheduler_->DeleteFile(generated_files[i], ""));
|
||||
} else {
|
||||
ASSERT_OK(
|
||||
delete_scheduler_->DeleteUnaccountedFile(generated_files[i], ""));
|
||||
}
|
||||
}
|
||||
|
||||
delete_scheduler_->WaitForEmptyTrash();
|
||||
ASSERT_EQ(0, delete_scheduler_->GetTotalTrashSize());
|
||||
ASSERT_EQ(0, sst_file_mgr_->GetTotalSize());
|
||||
}
|
||||
|
||||
TEST_F(DeleteSchedulerTest, ConcurrentlyDeleteUnaccountedFilesInBuckets) {
|
||||
int bg_delete_file = 0;
|
||||
int fg_delete_file = 0;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::DeleteTrashFile:DeleteFile",
|
||||
[&](void* /*arg*/) { bg_delete_file++; });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::DeleteFile", [&](void* /*arg*/) { fg_delete_file++; });
|
||||
rate_bytes_per_sec_ = 1024 * 1024; // 1 MB / s
|
||||
NewDeleteScheduler();
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
// Create 1000 files, every file is 1 KB
|
||||
int num_files = 1000;
|
||||
uint64_t file_size = 1024; // 1 KB as a file size
|
||||
std::vector<std::string> generated_files;
|
||||
for (int i = 0; i < num_files; i++) {
|
||||
std::string file_name = "file" + std::to_string(i) + ".data";
|
||||
generated_files.push_back(NewDummyFile(file_name, file_size,
|
||||
/*dummy_files_dirs_idx*/ 0,
|
||||
/*track=*/false));
|
||||
}
|
||||
// Concurrently delete files in different buckets and check all the buckets
|
||||
// are empty.
|
||||
int thread_cnt = 10;
|
||||
int files_per_thread = 100;
|
||||
std::atomic<int> thread_num(0);
|
||||
std::vector<port::Thread> threads;
|
||||
std::function<void()> delete_thread = [&]() {
|
||||
std::optional<int32_t> bucket = delete_scheduler_->NewTrashBucket();
|
||||
ASSERT_TRUE(bucket.has_value());
|
||||
int idx = thread_num.fetch_add(1);
|
||||
int range_start = idx * files_per_thread;
|
||||
int range_end = range_start + files_per_thread;
|
||||
for (int j = range_start; j < range_end; j++) {
|
||||
ASSERT_OK(delete_scheduler_->DeleteUnaccountedFile(
|
||||
generated_files[j], "", /*false_bg=*/false, bucket));
|
||||
}
|
||||
delete_scheduler_->WaitForEmptyTrashBucket(bucket.value());
|
||||
};
|
||||
|
||||
for (int i = 0; i < thread_cnt; i++) {
|
||||
threads.emplace_back(delete_thread);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < threads.size(); i++) {
|
||||
threads[i].join();
|
||||
}
|
||||
|
||||
ASSERT_EQ(0, delete_scheduler_->GetTotalTrashSize());
|
||||
ASSERT_EQ(0, stats_->getAndResetTickerCount(FILES_DELETED_IMMEDIATELY));
|
||||
ASSERT_EQ(1000, stats_->getAndResetTickerCount(FILES_MARKED_TRASH));
|
||||
ASSERT_EQ(0, fg_delete_file);
|
||||
ASSERT_EQ(1000, bg_delete_file);
|
||||
|
||||
// OK to re check an already empty bucket
|
||||
delete_scheduler_->WaitForEmptyTrashBucket(9);
|
||||
// Invalid bucket return too.
|
||||
delete_scheduler_->WaitForEmptyTrashBucket(100);
|
||||
std::optional<int32_t> next_bucket = delete_scheduler_->NewTrashBucket();
|
||||
ASSERT_TRUE(next_bucket.has_value());
|
||||
ASSERT_EQ(10, next_bucket.value());
|
||||
delete_scheduler_->WaitForEmptyTrashBucket(10);
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
TEST_F(DeleteSchedulerTest,
|
||||
ImmediatelyDeleteUnaccountedFilesWithRemainingLinks) {
|
||||
int bg_delete_file = 0;
|
||||
int fg_delete_file = 0;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::DeleteTrashFile:DeleteFile",
|
||||
[&](void* /*arg*/) { bg_delete_file++; });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::DeleteFile", [&](void* /*arg*/) { fg_delete_file++; });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
rate_bytes_per_sec_ = 1024 * 1024; // 1 MB / sec
|
||||
NewDeleteScheduler();
|
||||
|
||||
std::string file1 = NewDummyFile("data_1", 500 * 1024,
|
||||
/*dummy_files_dirs_idx*/ 0, /*track=*/false);
|
||||
std::string file2 = NewDummyFile("data_2", 100 * 1024,
|
||||
/*dummy_files_dirs_idx*/ 0, /*track=*/false);
|
||||
|
||||
ASSERT_OK(env_->LinkFile(file1, dummy_files_dirs_[0] + "/data_1b"));
|
||||
ASSERT_OK(env_->LinkFile(file2, dummy_files_dirs_[0] + "/data_2b"));
|
||||
|
||||
// Should delete in 4 batch if there is no hardlink
|
||||
ASSERT_OK(
|
||||
delete_scheduler_->DeleteUnaccountedFile(file1, "", /*force_bg=*/false));
|
||||
ASSERT_OK(
|
||||
delete_scheduler_->DeleteUnaccountedFile(file2, "", /*force_bg=*/false));
|
||||
|
||||
delete_scheduler_->WaitForEmptyTrash();
|
||||
|
||||
ASSERT_EQ(0, delete_scheduler_->GetTotalTrashSize());
|
||||
ASSERT_EQ(0, bg_delete_file);
|
||||
ASSERT_EQ(2, fg_delete_file);
|
||||
ASSERT_EQ(0, stats_->getAndResetTickerCount(FILES_MARKED_TRASH));
|
||||
ASSERT_EQ(2, stats_->getAndResetTickerCount(FILES_DELETED_IMMEDIATELY));
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+17
-2
@@ -122,8 +122,8 @@ IOStatus CreateFile(FileSystem* fs, const std::string& destination,
|
||||
Status DeleteDBFile(const ImmutableDBOptions* db_options,
|
||||
const std::string& fname, const std::string& dir_to_sync,
|
||||
const bool force_bg, const bool force_fg) {
|
||||
SstFileManagerImpl* sfm =
|
||||
static_cast<SstFileManagerImpl*>(db_options->sst_file_manager.get());
|
||||
SstFileManagerImpl* sfm = static_cast_with_check<SstFileManagerImpl>(
|
||||
db_options->sst_file_manager.get());
|
||||
if (sfm && !force_fg) {
|
||||
return sfm->ScheduleFileDeletion(fname, dir_to_sync, force_bg);
|
||||
} else {
|
||||
@@ -131,6 +131,21 @@ Status DeleteDBFile(const ImmutableDBOptions* db_options,
|
||||
}
|
||||
}
|
||||
|
||||
Status DeleteUnaccountedDBFile(const ImmutableDBOptions* db_options,
|
||||
const std::string& fname,
|
||||
const std::string& dir_to_sync,
|
||||
const bool force_bg, const bool force_fg,
|
||||
std::optional<int32_t> bucket) {
|
||||
SstFileManagerImpl* sfm = static_cast_with_check<SstFileManagerImpl>(
|
||||
db_options->sst_file_manager.get());
|
||||
if (sfm && !force_fg) {
|
||||
return sfm->ScheduleUnaccountedFileDeletion(fname, dir_to_sync, force_bg,
|
||||
bucket);
|
||||
} else {
|
||||
return db_options->env->DeleteFile(fname);
|
||||
}
|
||||
}
|
||||
|
||||
// requested_checksum_func_name brings the function name of the checksum
|
||||
// generator in checksum_factory. Empty string is permitted, in which case the
|
||||
// name of the generator created by the factory is unchecked. When
|
||||
|
||||
@@ -55,6 +55,16 @@ Status DeleteDBFile(const ImmutableDBOptions* db_options,
|
||||
const std::string& fname, const std::string& path_to_sync,
|
||||
const bool force_bg, const bool force_fg);
|
||||
|
||||
// Delete an unaccounted DB file that is not tracked by SstFileManager and will
|
||||
// not be tracked by its DeleteScheduler when getting deleted.
|
||||
// If a legitimate bucket is provided and this file is scheduled for slow
|
||||
// deletion, it will be assigned to the specified trash bucket.
|
||||
Status DeleteUnaccountedDBFile(const ImmutableDBOptions* db_options,
|
||||
const std::string& fname,
|
||||
const std::string& dir_to_sync,
|
||||
const bool force_bg, const bool force_fg,
|
||||
std::optional<int32_t> bucket);
|
||||
|
||||
// TODO(hx235): pass the whole DBOptions intead of its individual fields
|
||||
IOStatus GenerateOneFileChecksum(
|
||||
FileSystem* fs, const std::string& file_path,
|
||||
|
||||
@@ -421,10 +421,28 @@ Status SstFileManagerImpl::ScheduleFileDeletion(const std::string& file_path,
|
||||
return delete_scheduler_.DeleteFile(file_path, path_to_sync, force_bg);
|
||||
}
|
||||
|
||||
Status SstFileManagerImpl::ScheduleUnaccountedFileDeletion(
|
||||
const std::string& file_path, const std::string& dir_to_sync,
|
||||
const bool force_bg, std::optional<int32_t> bucket) {
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"SstFileManagerImpl::ScheduleUnaccountedFileDeletion",
|
||||
const_cast<std::string*>(&file_path));
|
||||
return delete_scheduler_.DeleteUnaccountedFile(file_path, dir_to_sync,
|
||||
force_bg, bucket);
|
||||
}
|
||||
|
||||
void SstFileManagerImpl::WaitForEmptyTrash() {
|
||||
delete_scheduler_.WaitForEmptyTrash();
|
||||
}
|
||||
|
||||
std::optional<int32_t> SstFileManagerImpl::NewTrashBucket() {
|
||||
return delete_scheduler_.NewTrashBucket();
|
||||
}
|
||||
|
||||
void SstFileManagerImpl::WaitForEmptyTrashBucket(int32_t bucket) {
|
||||
delete_scheduler_.WaitForEmptyTrashBucket(bucket);
|
||||
}
|
||||
|
||||
void SstFileManagerImpl::OnAddFileImpl(const std::string& file_path,
|
||||
uint64_t file_size) {
|
||||
auto tracked_file = tracked_files_.find(file_path);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "db/compaction/compaction.h"
|
||||
@@ -118,17 +118,40 @@ class SstFileManagerImpl : public SstFileManager {
|
||||
// not guaranteed
|
||||
bool CancelErrorRecovery(ErrorHandler* db);
|
||||
|
||||
// Mark file as trash and schedule it's deletion. If force_bg is set, it
|
||||
// Mark a file as trash and schedule its deletion. If force_bg is set, it
|
||||
// forces the file to be deleting in the background regardless of DB size,
|
||||
// except when rate limited delete is disabled
|
||||
// except when rate limited delete is disabled.
|
||||
virtual Status ScheduleFileDeletion(const std::string& file_path,
|
||||
const std::string& dir_to_sync,
|
||||
const bool force_bg = false);
|
||||
|
||||
// Wait for all files being deleteing in the background to finish or for
|
||||
// Delete an unaccounted file. The file is deleted immediately if slow
|
||||
// deletion is disabled. A file with more than 1 hard links will be deleted
|
||||
// immediately unless force_bg is set. In other cases, files will be scheduled
|
||||
// for slow deletion, and assigned to the specified bucket if a legitimate one
|
||||
// is provided. A legitimate bucket is one that is created with the
|
||||
// `NewTrashBucket` API, and for which `WaitForEmptyTrashBucket` hasn't been
|
||||
// called yet.
|
||||
virtual Status ScheduleUnaccountedFileDeletion(
|
||||
const std::string& file_path, const std::string& dir_to_sync,
|
||||
const bool force_bg = false,
|
||||
std::optional<int32_t> bucket = std::nullopt);
|
||||
|
||||
// Wait for all files being deleted in the background to finish or for
|
||||
// destructor to be called.
|
||||
virtual void WaitForEmptyTrash();
|
||||
|
||||
// Creates a new trash bucket. A legitimate bucket is only created and
|
||||
// returned when slow deletion is enabled.
|
||||
// For each bucket that is created and used, the user should also call
|
||||
// `WaitForEmptyTrashBucket` after scheduling file deletions to make sure all
|
||||
// the trash files are cleared.
|
||||
std::optional<int32_t> NewTrashBucket();
|
||||
|
||||
// Wait for all the files in the specified bucket to be deleted in the
|
||||
// background or for destructor to be called.
|
||||
virtual void WaitForEmptyTrashBucket(int32_t bucket);
|
||||
|
||||
DeleteScheduler* delete_scheduler() { return &delete_scheduler_; }
|
||||
|
||||
// Stop the error recovery background thread. This should be called only
|
||||
|
||||
@@ -1037,8 +1037,10 @@ struct AdvancedColumnFamilyOptions {
|
||||
// When setting this flag to `false`, users should also call
|
||||
// `DB::IncreaseFullHistoryTsLow` to set a cutoff timestamp for flush. RocksDB
|
||||
// refrains from flushing a memtable with data still above
|
||||
// the cutoff timestamp with best effort. If this cutoff timestamp is not set,
|
||||
// flushing continues normally.
|
||||
// the cutoff timestamp with best effort. One limitation of this best effort
|
||||
// is that when `max_write_buffer_number` is equal to or smaller than 2,
|
||||
// RocksDB will not attempt to retain user-defined timestamps, all flush jobs
|
||||
// continue normally.
|
||||
//
|
||||
// Users can do user-defined
|
||||
// multi-versioned read above the cutoff timestamp. When users try to read
|
||||
|
||||
@@ -1679,8 +1679,8 @@ class DB {
|
||||
// Freezes the logical state of the DB (by stopping writes), and if WAL is
|
||||
// enabled, ensures that state has been flushed to DB files (as in
|
||||
// FlushWAL()). This can be used for taking a Checkpoint at a known DB
|
||||
// state, though the user must use options to insure no DB flush is invoked
|
||||
// in this frozen state. Other operations allowed on a "read only" DB should
|
||||
// state, though while the WAL is locked, flushes as part of CreateCheckpoint
|
||||
// and simiar are skipped. Other operations allowed on a "read only" DB should
|
||||
// work while frozen. Each LockWAL() call that returns OK must eventually be
|
||||
// followed by a corresponding call to UnlockWAL(). Where supported, non-OK
|
||||
// status is generally only possible with some kind of corruption or I/O
|
||||
|
||||
@@ -328,6 +328,15 @@ struct BlobFileGarbageInfo : public BlobFileInfo {
|
||||
uint64_t garbage_blob_bytes;
|
||||
};
|
||||
|
||||
struct ManualFlushInfo {
|
||||
// the id of the column family
|
||||
uint32_t cf_id;
|
||||
// the name of the column family
|
||||
std::string cf_name;
|
||||
// Reason that triggered this manual flush
|
||||
FlushReason flush_reason;
|
||||
};
|
||||
|
||||
struct FlushJobInfo {
|
||||
// the id of the column family
|
||||
uint32_t cf_id;
|
||||
@@ -492,6 +501,10 @@ struct MemTableInfo {
|
||||
uint64_t num_entries;
|
||||
// Total number of deletes in memtable
|
||||
uint64_t num_deletes;
|
||||
|
||||
// The newest user-defined timestamps in the memtable. Note this field is
|
||||
// only populated when `persist_user_defined_timestamps` is false.
|
||||
std::string newest_udt;
|
||||
};
|
||||
|
||||
struct ExternalFileIngestionInfo {
|
||||
@@ -595,6 +608,14 @@ class EventListener : public Customizable {
|
||||
virtual void OnFlushBegin(DB* /*db*/,
|
||||
const FlushJobInfo& /*flush_job_info*/) {}
|
||||
|
||||
// A callback function to RocksDB which will be called after a manual flush
|
||||
// is scheduled. The default implementation is no-op.
|
||||
// The size of the `manual_flush_info` vector should only be bigger than 1 if
|
||||
// the DB enables atomic flush and has more than 1 column families. Its size
|
||||
// should be 1 in all other cases.
|
||||
virtual void OnManualFlushScheduled(
|
||||
DB* /*db*/, const std::vector<ManualFlushInfo>& /*manual_flush_info*/) {}
|
||||
|
||||
// A callback function for RocksDB which will be called whenever
|
||||
// a SST file is deleted. Different from OnCompactionCompleted and
|
||||
// OnFlushCompleted, this callback is designed for external logging
|
||||
|
||||
@@ -160,6 +160,7 @@ class LDBCommand {
|
||||
DB* db_;
|
||||
DBWithTTL* db_ttl_;
|
||||
std::map<std::string, ColumnFamilyHandle*> cf_handles_;
|
||||
std::map<uint32_t, const Comparator*> ucmps_;
|
||||
|
||||
/**
|
||||
* true implies that this command can work if the db is opened in read-only
|
||||
@@ -224,17 +225,19 @@ class LDBCommand {
|
||||
ColumnFamilyHandle* GetCfHandle();
|
||||
|
||||
static std::string PrintKeyValue(const std::string& key,
|
||||
const std::string& timestamp,
|
||||
const std::string& value, bool is_key_hex,
|
||||
bool is_value_hex);
|
||||
bool is_value_hex, const Comparator* ucmp);
|
||||
|
||||
static std::string PrintKeyValue(const std::string& key,
|
||||
const std::string& value, bool is_hex);
|
||||
const std::string& timestamp,
|
||||
const std::string& value, bool is_hex,
|
||||
const Comparator* ucmp);
|
||||
|
||||
static std::string PrintKeyValueOrWideColumns(const Slice& key,
|
||||
const Slice& value,
|
||||
const WideColumns& wide_columns,
|
||||
bool is_key_hex,
|
||||
bool is_value_hex);
|
||||
static std::string PrintKeyValueOrWideColumns(
|
||||
const Slice& key, const Slice& timestamp, const Slice& value,
|
||||
const WideColumns& wide_columns, bool is_key_hex, bool is_value_hex,
|
||||
const Comparator* ucmp);
|
||||
|
||||
/**
|
||||
* Return true if the specified flag is present in the specified flags vector
|
||||
|
||||
@@ -235,6 +235,11 @@ struct TransactionDBOptions {
|
||||
const Slice& /*key*/)>
|
||||
rollback_deletion_type_callback;
|
||||
|
||||
// A flag to control for the whole DB whether user-defined timestamp based
|
||||
// validation are enabled when applicable. Only WriteCommittedTxn support
|
||||
// user-defined timestamps so this option only applies in this case.
|
||||
bool enable_udt_validation = true;
|
||||
|
||||
private:
|
||||
// 128 entries
|
||||
// Should the default value change, please also update wp_snapshot_cache_bits
|
||||
@@ -318,6 +323,22 @@ struct TransactionOptions {
|
||||
// description. If a negative value is specified, then the default value from
|
||||
// TransactionDBOptions is used.
|
||||
int64_t write_batch_flush_threshold = -1;
|
||||
|
||||
// DO NOT USE.
|
||||
// This is only a temporary option dedicated for MyRocks that will soon be
|
||||
// removed.
|
||||
// In normal use cases, meta info like column family's timestamp size is
|
||||
// tracked at the transaction layer, so it's not necessary and even
|
||||
// detrimental to track such info inside the internal WriteBatch because it
|
||||
// may let anti-patterns like bypassing Transaction write APIs and directly
|
||||
// write to its internal `WriteBatch` retrieved like this:
|
||||
// https://github.com/facebook/mysql-5.6/blob/fb-mysql-8.0.32/storage/rocksdb/ha_rocksdb.cc#L4949-L4950
|
||||
// Setting this option to true will keep aforementioned use case continue to
|
||||
// work before it's refactored out.
|
||||
// When this flag is enabled, we also intentionally only track the timestamp
|
||||
// size in APIs that MyRocks currently are using, including Put, Merge, Delete
|
||||
// DeleteRange, SingleDelete.
|
||||
bool write_batch_track_timestamp_size = false;
|
||||
};
|
||||
|
||||
// The per-write optimizations that do not involve transactions. TransactionDB
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// minor or major version number planned for release.
|
||||
#define ROCKSDB_MAJOR 9
|
||||
#define ROCKSDB_MINOR 3
|
||||
#define ROCKSDB_PATCH 0
|
||||
#define ROCKSDB_PATCH 2
|
||||
|
||||
// Do not use these. We made the mistake of declaring macros starting with
|
||||
// double underscore. Now we have to live with our choice. We'll deprecate these
|
||||
|
||||
@@ -437,6 +437,30 @@ class WriteBatch : public WriteBatchBase {
|
||||
Status UpdateTimestamps(const Slice& ts,
|
||||
std::function<size_t(uint32_t /*cf*/)> ts_sz_func);
|
||||
|
||||
// TODO: remove these internal APIs after MyRocks refactor to not directly
|
||||
// write to a `WriteBatch` retrieved from `Transaction` via
|
||||
// `Transaction::GetWriteBatch`.
|
||||
|
||||
void SetTrackTimestampSize(bool track_timestamp_size) {
|
||||
track_timestamp_size_ = track_timestamp_size;
|
||||
}
|
||||
|
||||
inline void MaybeTrackTimestampSize(uint32_t column_family_id, size_t ts_sz) {
|
||||
if (!track_timestamp_size_) {
|
||||
return;
|
||||
}
|
||||
auto iter = cf_id_to_ts_sz_.find(column_family_id);
|
||||
if (iter == cf_id_to_ts_sz_.end()) {
|
||||
cf_id_to_ts_sz_.emplace(column_family_id, ts_sz);
|
||||
}
|
||||
}
|
||||
|
||||
// Return a mapping from column family id to timestamp size of all the column
|
||||
// families involved in this WriteBatch.
|
||||
const std::unordered_map<uint32_t, size_t>& GetColumnFamilyToTimestampSize() {
|
||||
return cf_id_to_ts_sz_;
|
||||
}
|
||||
|
||||
// Verify the per-key-value checksums of this write batch.
|
||||
// Corruption status will be returned if the verification fails.
|
||||
// If this write batch does not have per-key-value checksum,
|
||||
@@ -511,6 +535,10 @@ class WriteBatch : public WriteBatchBase {
|
||||
|
||||
size_t default_cf_ts_sz_ = 0;
|
||||
|
||||
bool track_timestamp_size_ = false;
|
||||
|
||||
std::unordered_map<uint32_t, size_t> cf_id_to_ts_sz_;
|
||||
|
||||
protected:
|
||||
std::string rep_; // See comment in write_batch.cc for the format of rep_
|
||||
};
|
||||
|
||||
+8
-10
@@ -241,7 +241,7 @@ inline void BlockFetcher::GetBlockContents() {
|
||||
// Read a block from the file and verify its checksum. Upon return, io_status_
|
||||
// will be updated with the status of the read, and slice_ will be updated
|
||||
// with a pointer to the data.
|
||||
void BlockFetcher::ReadBlock(bool retry, FSAllocationPtr& fs_buf) {
|
||||
void BlockFetcher::ReadBlock(bool retry) {
|
||||
FSReadRequest read_req;
|
||||
IOOptions opts;
|
||||
io_status_ = file_->PrepareIOOptions(read_options_, opts);
|
||||
@@ -336,7 +336,7 @@ void BlockFetcher::ReadBlock(bool retry, FSAllocationPtr& fs_buf) {
|
||||
|
||||
if (io_status_.ok()) {
|
||||
InsertCompressedBlockToPersistentCacheIfNeeded();
|
||||
fs_buf = std::move(read_req.fs_scratch);
|
||||
fs_buf_ = std::move(read_req.fs_scratch);
|
||||
} else {
|
||||
ReleaseFileSystemProvidedBuffer(&read_req);
|
||||
direct_io_buf_.reset();
|
||||
@@ -347,7 +347,6 @@ void BlockFetcher::ReadBlock(bool retry, FSAllocationPtr& fs_buf) {
|
||||
}
|
||||
|
||||
IOStatus BlockFetcher::ReadBlockContents() {
|
||||
FSAllocationPtr fs_buf;
|
||||
if (TryGetUncompressBlockFromPersistentCache()) {
|
||||
compression_type_ = kNoCompression;
|
||||
#ifndef NDEBUG
|
||||
@@ -360,15 +359,15 @@ IOStatus BlockFetcher::ReadBlockContents() {
|
||||
return io_status_;
|
||||
}
|
||||
} else if (!TryGetSerializedBlockFromPersistentCache()) {
|
||||
ReadBlock(/*retry =*/false, fs_buf);
|
||||
ReadBlock(/*retry =*/false);
|
||||
// If the file system supports retry after corruption, then try to
|
||||
// re-read the block and see if it succeeds.
|
||||
if (io_status_.IsCorruption() && retry_corrupt_read_) {
|
||||
assert(!fs_buf);
|
||||
ReadBlock(/*retry=*/true, fs_buf);
|
||||
assert(!fs_buf_);
|
||||
ReadBlock(/*retry=*/true);
|
||||
}
|
||||
if (!io_status_.ok()) {
|
||||
assert(!fs_buf);
|
||||
assert(!fs_buf_);
|
||||
return io_status_;
|
||||
}
|
||||
}
|
||||
@@ -417,16 +416,15 @@ IOStatus BlockFetcher::ReadAsyncBlockContents() {
|
||||
return io_s;
|
||||
}
|
||||
if (io_s.ok()) {
|
||||
FSAllocationPtr fs_buf;
|
||||
// Data Block is already in prefetch.
|
||||
got_from_prefetch_buffer_ = true;
|
||||
ProcessTrailerIfPresent();
|
||||
if (io_status_.IsCorruption() && retry_corrupt_read_) {
|
||||
got_from_prefetch_buffer_ = false;
|
||||
ReadBlock(/*retry = */ true, fs_buf);
|
||||
ReadBlock(/*retry = */ true);
|
||||
}
|
||||
if (!io_status_.ok()) {
|
||||
assert(!fs_buf);
|
||||
assert(!fs_buf_);
|
||||
return io_status_;
|
||||
}
|
||||
used_buf_ = const_cast<char*>(slice_.data());
|
||||
|
||||
@@ -137,6 +137,7 @@ class BlockFetcher {
|
||||
bool for_compaction_ = false;
|
||||
bool use_fs_scratch_ = false;
|
||||
bool retry_corrupt_read_ = false;
|
||||
FSAllocationPtr fs_buf_;
|
||||
|
||||
// return true if found
|
||||
bool TryGetUncompressBlockFromPersistentCache();
|
||||
@@ -152,7 +153,7 @@ class BlockFetcher {
|
||||
void InsertCompressedBlockToPersistentCacheIfNeeded();
|
||||
void InsertUncompressedBlockToPersistentCacheIfNeeded();
|
||||
void ProcessTrailerIfPresent();
|
||||
void ReadBlock(bool retry, FSAllocationPtr& fs_buf);
|
||||
void ReadBlock(bool retry);
|
||||
|
||||
void ReleaseFileSystemProvidedBuffer(FSReadRequest* read_req) {
|
||||
if (use_fs_scratch_) {
|
||||
|
||||
@@ -864,12 +864,6 @@ def finalize_and_sanitize(src_params):
|
||||
elif (dest_params.get("use_put_entity_one_in") > 1 and
|
||||
dest_params.get("use_timed_put_one_in") == 1):
|
||||
dest_params["use_timed_put_one_in"] = 3
|
||||
# TODO: re-enable this combination.
|
||||
if dest_params.get("lock_wal_one_in") != 0 and dest_params["ingest_external_file_one_in"] != 0:
|
||||
if random.choice([0, 1]) == 0:
|
||||
dest_params["ingest_external_file_one_in"] = 0
|
||||
else:
|
||||
dest_params["lock_wal_one_in"] = 0
|
||||
return dest_params
|
||||
|
||||
def gen_cmd_params(args):
|
||||
|
||||
+187
-45
@@ -45,6 +45,7 @@
|
||||
#include "util/file_checksum_helper.h"
|
||||
#include "util/stderr_logger.h"
|
||||
#include "util/string_util.h"
|
||||
#include "util/write_batch_util.h"
|
||||
#include "utilities/blob_db/blob_dump_tool.h"
|
||||
#include "utilities/merge_operators.h"
|
||||
#include "utilities/ttl/db_ttl_impl.h"
|
||||
@@ -115,6 +116,7 @@ namespace {
|
||||
|
||||
void DumpWalFile(Options options, std::string wal_file, bool print_header,
|
||||
bool print_values, bool is_write_committed,
|
||||
const std::map<uint32_t, const Comparator*>& ucmps,
|
||||
LDBCommandExecuteResult* exec_state);
|
||||
|
||||
void DumpSstFile(Options options, std::string filename, bool output_hex,
|
||||
@@ -503,6 +505,7 @@ void LDBCommand::OpenDB() {
|
||||
bool found_cf_name = false;
|
||||
for (size_t i = 0; i < handles_opened.size(); i++) {
|
||||
cf_handles_[column_families_[i].name] = handles_opened[i];
|
||||
ucmps_[handles_opened[i]->GetID()] = handles_opened[i]->GetComparator();
|
||||
if (column_family_name_ == column_families_[i].name) {
|
||||
found_cf_name = true;
|
||||
}
|
||||
@@ -512,6 +515,8 @@ void LDBCommand::OpenDB() {
|
||||
"Non-existing column family " + column_family_name_);
|
||||
CloseDB();
|
||||
}
|
||||
ColumnFamilyHandle* default_cf = db_->DefaultColumnFamily();
|
||||
ucmps_[default_cf->GetID()] = default_cf->GetComparator();
|
||||
} else {
|
||||
// We successfully opened DB in single column family mode.
|
||||
assert(column_families_.empty());
|
||||
@@ -520,6 +525,8 @@ void LDBCommand::OpenDB() {
|
||||
"Non-existing column family " + column_family_name_);
|
||||
CloseDB();
|
||||
}
|
||||
ColumnFamilyHandle* default_cf = db_->DefaultColumnFamily();
|
||||
ucmps_[default_cf->GetID()] = default_cf->GetComparator();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1146,27 +1153,36 @@ std::string LDBCommand::StringToHex(const std::string& str) {
|
||||
}
|
||||
|
||||
std::string LDBCommand::PrintKeyValue(const std::string& key,
|
||||
const std::string& timestamp,
|
||||
const std::string& value, bool is_key_hex,
|
||||
bool is_value_hex) {
|
||||
bool is_value_hex,
|
||||
const Comparator* ucmp) {
|
||||
std::string result;
|
||||
result.append(is_key_hex ? StringToHex(key) : key);
|
||||
if (!timestamp.empty()) {
|
||||
result.append("|timestamp:");
|
||||
result.append(ucmp->TimestampToString(timestamp));
|
||||
}
|
||||
result.append(DELIM);
|
||||
result.append(is_value_hex ? StringToHex(value) : value);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string LDBCommand::PrintKeyValue(const std::string& key,
|
||||
const std::string& value, bool is_hex) {
|
||||
return PrintKeyValue(key, value, is_hex, is_hex);
|
||||
const std::string& timestamp,
|
||||
const std::string& value, bool is_hex,
|
||||
const Comparator* ucmp) {
|
||||
return PrintKeyValue(key, timestamp, value, is_hex, is_hex, ucmp);
|
||||
}
|
||||
|
||||
std::string LDBCommand::PrintKeyValueOrWideColumns(
|
||||
const Slice& key, const Slice& value, const WideColumns& wide_columns,
|
||||
bool is_key_hex, bool is_value_hex) {
|
||||
const Slice& key, const Slice& timestamp, const Slice& value,
|
||||
const WideColumns& wide_columns, bool is_key_hex, bool is_value_hex,
|
||||
const Comparator* ucmp) {
|
||||
if (wide_columns.empty() ||
|
||||
WideColumnsHelper::HasDefaultColumnOnly(wide_columns)) {
|
||||
return PrintKeyValue(key.ToString(), value.ToString(), is_key_hex,
|
||||
is_value_hex);
|
||||
return PrintKeyValue(key.ToString(), timestamp.ToString(), value.ToString(),
|
||||
is_key_hex, is_value_hex, ucmp);
|
||||
}
|
||||
/*
|
||||
// Sample plaintext output (first column is kDefaultWideColumnName)
|
||||
@@ -1177,9 +1193,10 @@ std::string LDBCommand::PrintKeyValueOrWideColumns(
|
||||
*/
|
||||
std::ostringstream oss;
|
||||
WideColumnsHelper::DumpWideColumns(wide_columns, oss, is_value_hex);
|
||||
return PrintKeyValue(key.ToString(), oss.str().c_str(), is_key_hex,
|
||||
false); // is_value_hex_ is already honored in oss.
|
||||
// avoid double-hexing it.
|
||||
return PrintKeyValue(key.ToString(), timestamp.ToString(), oss.str().c_str(),
|
||||
is_key_hex, false,
|
||||
ucmp); // is_value_hex_ is already honored in oss.
|
||||
// avoid double-hexing it.
|
||||
}
|
||||
|
||||
std::string LDBCommand::HelpRangeCmdArgs() {
|
||||
@@ -1929,10 +1946,12 @@ void InternalDumpCommand::DoCommand() {
|
||||
assert(GetExecuteState().IsFailed());
|
||||
return;
|
||||
}
|
||||
|
||||
ColumnFamilyHandle* cfh = GetCfHandle();
|
||||
const Comparator* ucmp = cfh->GetComparator();
|
||||
size_t ts_sz = ucmp->timestamp_size();
|
||||
if (print_stats_) {
|
||||
std::string stats;
|
||||
if (db_->GetProperty(GetCfHandle(), "rocksdb.stats", &stats)) {
|
||||
if (db_->GetProperty(cfh, "rocksdb.stats", &stats)) {
|
||||
fprintf(stdout, "%s\n", stats.c_str());
|
||||
}
|
||||
}
|
||||
@@ -1954,7 +1973,11 @@ void InternalDumpCommand::DoCommand() {
|
||||
for (auto& key_version : key_versions) {
|
||||
ValueType value_type = static_cast<ValueType>(key_version.type);
|
||||
InternalKey ikey(key_version.user_key, key_version.sequence, value_type);
|
||||
if (has_to_ && ikey.user_key() == to_) {
|
||||
Slice user_key_without_ts = ikey.user_key();
|
||||
if (ts_sz > 0) {
|
||||
user_key_without_ts.remove_suffix(ts_sz);
|
||||
}
|
||||
if (has_to_ && ucmp->Compare(user_key_without_ts, to_) == 0) {
|
||||
// GetAllKeyVersions() includes keys with user key `to_`, but idump has
|
||||
// traditionally excluded such keys.
|
||||
break;
|
||||
@@ -1990,7 +2013,7 @@ void InternalDumpCommand::DoCommand() {
|
||||
}
|
||||
|
||||
if (!count_only_ && !count_delim_) {
|
||||
std::string key = ikey.DebugString(is_key_hex_);
|
||||
std::string key = ikey.DebugString(is_key_hex_, ucmp);
|
||||
Slice value(key_version.value);
|
||||
if (!decode_blob_index_ || value_type != kTypeBlobIndex) {
|
||||
if (value_type == kTypeWideColumnEntity) {
|
||||
@@ -2166,7 +2189,7 @@ void DBDumperCommand::DoCommand() {
|
||||
// TODO(myabandeh): allow configuring is_write_commited
|
||||
DumpWalFile(options_, path_, /* print_header_ */ true,
|
||||
/* print_values_ */ true, true /* is_write_commited */,
|
||||
&exec_state_);
|
||||
ucmps_, &exec_state_);
|
||||
break;
|
||||
case kTableFile:
|
||||
DumpSstFile(options_, path_, is_key_hex_, /* show_properties */ true,
|
||||
@@ -2206,8 +2229,16 @@ void DBDumperCommand::DoDumpCommand() {
|
||||
|
||||
// Setup key iterator
|
||||
ReadOptions scan_read_opts;
|
||||
Slice read_timestamp;
|
||||
ColumnFamilyHandle* cfh = GetCfHandle();
|
||||
const Comparator* ucmp = cfh->GetComparator();
|
||||
size_t ts_sz = ucmp->timestamp_size();
|
||||
if (ucmp->timestamp_size() > 0) {
|
||||
read_timestamp = ucmp->GetMaxTimestamp();
|
||||
scan_read_opts.timestamp = &read_timestamp;
|
||||
}
|
||||
scan_read_opts.total_order_seek = true;
|
||||
Iterator* iter = db_->NewIterator(scan_read_opts, GetCfHandle());
|
||||
Iterator* iter = db_->NewIterator(scan_read_opts, cfh);
|
||||
Status st = iter->status();
|
||||
if (!st.ok()) {
|
||||
exec_state_ =
|
||||
@@ -2262,7 +2293,7 @@ void DBDumperCommand::DoDumpCommand() {
|
||||
for (; iter->Valid(); iter->Next()) {
|
||||
int rawtime = 0;
|
||||
// If end marker was specified, we stop before it
|
||||
if (!null_to_ && (iter->key().ToString() >= to_)) {
|
||||
if (!null_to_ && ucmp->Compare(iter->key(), to_) >= 0) {
|
||||
break;
|
||||
}
|
||||
// Terminate if maximum number of keys have been dumped
|
||||
@@ -2316,11 +2347,14 @@ void DBDumperCommand::DoDumpCommand() {
|
||||
// (TODO) TTL Iterator does not support wide columns yet.
|
||||
std::string str =
|
||||
is_db_ttl_
|
||||
? PrintKeyValue(iter->key().ToString(), iter->value().ToString(),
|
||||
is_key_hex_, is_value_hex_)
|
||||
: PrintKeyValueOrWideColumns(iter->key(), iter->value(),
|
||||
iter->columns(), is_key_hex_,
|
||||
is_value_hex_);
|
||||
? PrintKeyValue(iter->key().ToString(),
|
||||
ts_sz == 0 ? "" : iter->timestamp().ToString(),
|
||||
iter->value().ToString(), is_key_hex_,
|
||||
is_value_hex_, ucmp)
|
||||
: PrintKeyValueOrWideColumns(
|
||||
iter->key(), ts_sz == 0 ? "" : iter->timestamp().ToString(),
|
||||
iter->value(), iter->columns(), is_key_hex_, is_value_hex_,
|
||||
ucmp);
|
||||
fprintf(stdout, "%s\n", str.c_str());
|
||||
}
|
||||
}
|
||||
@@ -2641,14 +2675,16 @@ struct StdErrReporter : public log::Reader::Reporter {
|
||||
class InMemoryHandler : public WriteBatch::Handler {
|
||||
public:
|
||||
InMemoryHandler(std::stringstream& row, bool print_values,
|
||||
bool write_after_commit = false)
|
||||
bool write_after_commit,
|
||||
const std::map<uint32_t, const Comparator*>& ucmps)
|
||||
: Handler(),
|
||||
row_(row),
|
||||
print_values_(print_values),
|
||||
write_after_commit_(write_after_commit) {}
|
||||
write_after_commit_(write_after_commit),
|
||||
ucmps_(ucmps) {}
|
||||
|
||||
void commonPutMerge(const Slice& key, const Slice& value) {
|
||||
std::string k = LDBCommand::StringToHex(key.ToString());
|
||||
void commonPutMerge(uint32_t cf, const Slice& key, const Slice& value) {
|
||||
std::string k = PrintKey(cf, key);
|
||||
if (print_values_) {
|
||||
std::string v = LDBCommand::StringToHex(value.ToString());
|
||||
row_ << k << " : ";
|
||||
@@ -2660,14 +2696,13 @@ class InMemoryHandler : public WriteBatch::Handler {
|
||||
|
||||
Status PutCF(uint32_t cf, const Slice& key, const Slice& value) override {
|
||||
row_ << "PUT(" << cf << ") : ";
|
||||
commonPutMerge(key, value);
|
||||
commonPutMerge(cf, key, value);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status PutEntityCF(uint32_t cf, const Slice& key,
|
||||
const Slice& value) override {
|
||||
row_ << "PUT_ENTITY(" << cf << ") : ";
|
||||
std::string k = LDBCommand::StringToHex(key.ToString());
|
||||
row_ << "PUT_ENTITY(" << cf << ") : " << PrintKey(cf, key);
|
||||
if (print_values_) {
|
||||
return WideColumnsHelper::DumpSliceAsWideColumns(value, row_, true);
|
||||
}
|
||||
@@ -2676,7 +2711,7 @@ class InMemoryHandler : public WriteBatch::Handler {
|
||||
|
||||
Status MergeCF(uint32_t cf, const Slice& key, const Slice& value) override {
|
||||
row_ << "MERGE(" << cf << ") : ";
|
||||
commonPutMerge(key, value);
|
||||
commonPutMerge(cf, key, value);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -2687,21 +2722,21 @@ class InMemoryHandler : public WriteBatch::Handler {
|
||||
|
||||
Status DeleteCF(uint32_t cf, const Slice& key) override {
|
||||
row_ << "DELETE(" << cf << ") : ";
|
||||
row_ << LDBCommand::StringToHex(key.ToString()) << " ";
|
||||
row_ << PrintKey(cf, key) << " ";
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status SingleDeleteCF(uint32_t cf, const Slice& key) override {
|
||||
row_ << "SINGLE_DELETE(" << cf << ") : ";
|
||||
row_ << LDBCommand::StringToHex(key.ToString()) << " ";
|
||||
row_ << PrintKey(cf, key) << " ";
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status DeleteRangeCF(uint32_t cf, const Slice& begin_key,
|
||||
const Slice& end_key) override {
|
||||
row_ << "DELETE_RANGE(" << cf << ") : ";
|
||||
row_ << LDBCommand::StringToHex(begin_key.ToString()) << " ";
|
||||
row_ << LDBCommand::StringToHex(end_key.ToString()) << " ";
|
||||
row_ << PrintKey(cf, begin_key) << " ";
|
||||
row_ << PrintKey(cf, end_key) << " ";
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -2746,13 +2781,37 @@ class InMemoryHandler : public WriteBatch::Handler {
|
||||
}
|
||||
|
||||
private:
|
||||
std::string PrintKey(uint32_t cf, const Slice& key) {
|
||||
auto ucmp_iter = ucmps_.find(cf);
|
||||
if (ucmp_iter == ucmps_.end()) {
|
||||
// Fallback to default print slice as hex
|
||||
return LDBCommand::StringToHex(key.ToString());
|
||||
}
|
||||
size_t ts_sz = ucmp_iter->second->timestamp_size();
|
||||
if (ts_sz == 0) {
|
||||
return LDBCommand::StringToHex(key.ToString());
|
||||
} else {
|
||||
// This could happen if there is corruption or undetected comparator
|
||||
// change.
|
||||
if (key.size() < ts_sz) {
|
||||
return "CORRUPT KEY";
|
||||
}
|
||||
Slice user_key_without_ts = key;
|
||||
user_key_without_ts.remove_suffix(ts_sz);
|
||||
Slice ts = Slice(key.data() + key.size() - ts_sz, ts_sz);
|
||||
return LDBCommand::StringToHex(user_key_without_ts.ToString()) +
|
||||
"|timestamp:" + ucmp_iter->second->TimestampToString(ts);
|
||||
}
|
||||
}
|
||||
std::stringstream& row_;
|
||||
bool print_values_;
|
||||
bool write_after_commit_;
|
||||
const std::map<uint32_t, const Comparator*> ucmps_;
|
||||
};
|
||||
|
||||
void DumpWalFile(Options options, std::string wal_file, bool print_header,
|
||||
bool print_values, bool is_write_committed,
|
||||
const std::map<uint32_t, const Comparator*>& ucmps,
|
||||
LDBCommandExecuteResult* exec_state) {
|
||||
const auto& fs = options.env->GetFileSystem();
|
||||
FileOptions soptions(options);
|
||||
@@ -2773,6 +2832,12 @@ void DumpWalFile(Options options, std::string wal_file, bool print_header,
|
||||
uint64_t log_number;
|
||||
FileType type;
|
||||
|
||||
// Comparators are available and will be used for formatting user key if DB
|
||||
// is opened for this dump wal operation.
|
||||
UnorderedMap<uint32_t, size_t> running_ts_sz;
|
||||
for (const auto& [cf_id, ucmp] : ucmps) {
|
||||
running_ts_sz.emplace(cf_id, ucmp->timestamp_size());
|
||||
}
|
||||
// we need the log number, but ParseFilename expects dbname/NNN.log.
|
||||
std::string sanitized = wal_file;
|
||||
size_t lastslash = sanitized.rfind('/');
|
||||
@@ -2785,6 +2850,7 @@ void DumpWalFile(Options options, std::string wal_file, bool print_header,
|
||||
}
|
||||
log::Reader reader(options.info_log, std::move(wal_file_reader), &reporter,
|
||||
true /* checksum */, log_number);
|
||||
std::unordered_set<uint32_t> encountered_cf_ids;
|
||||
std::string scratch;
|
||||
WriteBatch batch;
|
||||
Slice record;
|
||||
@@ -2813,11 +2879,51 @@ void DumpWalFile(Options options, std::string wal_file, bool print_header,
|
||||
}
|
||||
break;
|
||||
}
|
||||
const UnorderedMap<uint32_t, size_t> recorded_ts_sz =
|
||||
reader.GetRecordedTimestampSize();
|
||||
if (!running_ts_sz.empty()) {
|
||||
status = HandleWriteBatchTimestampSizeDifference(
|
||||
&batch, running_ts_sz, recorded_ts_sz,
|
||||
TimestampSizeConsistencyMode::kVerifyConsistency,
|
||||
/*new_batch=*/nullptr);
|
||||
if (!status.ok()) {
|
||||
std::stringstream oss;
|
||||
oss << "Format for user keys in WAL file is inconsistent with the "
|
||||
"comparator used to open the DB. Timestamp size recorded in "
|
||||
"WAL vs specified by "
|
||||
"comparator: {";
|
||||
bool first_cf = true;
|
||||
for (const auto& [cf_id, ts_sz] : running_ts_sz) {
|
||||
if (first_cf) {
|
||||
first_cf = false;
|
||||
} else {
|
||||
oss << ", ";
|
||||
}
|
||||
auto record_ts_iter = recorded_ts_sz.find(cf_id);
|
||||
size_t ts_sz_in_wal = (record_ts_iter == recorded_ts_sz.end())
|
||||
? 0
|
||||
: record_ts_iter->second;
|
||||
oss << "(cf_id: " << cf_id << ", [recorded: " << ts_sz_in_wal
|
||||
<< ", comparator: " << ts_sz << "])";
|
||||
}
|
||||
oss << "}";
|
||||
if (exec_state) {
|
||||
*exec_state = LDBCommandExecuteResult::Failed(oss.str());
|
||||
} else {
|
||||
std::cerr << oss.str() << std::endl;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
row << WriteBatchInternal::Sequence(&batch) << ",";
|
||||
row << WriteBatchInternal::Count(&batch) << ",";
|
||||
row << WriteBatchInternal::ByteSize(&batch) << ",";
|
||||
row << reader.LastRecordOffset() << ",";
|
||||
InMemoryHandler handler(row, print_values, is_write_committed);
|
||||
ColumnFamilyCollector cf_collector;
|
||||
status = batch.Iterate(&cf_collector);
|
||||
auto cf_ids = cf_collector.column_families();
|
||||
encountered_cf_ids.insert(cf_ids.begin(), cf_ids.end());
|
||||
InMemoryHandler handler(row, print_values, is_write_committed, ucmps);
|
||||
status = batch.Iterate(&handler);
|
||||
if (!status.ok()) {
|
||||
if (exec_state) {
|
||||
@@ -2832,6 +2938,29 @@ void DumpWalFile(Options options, std::string wal_file, bool print_header,
|
||||
}
|
||||
std::cout << row.str();
|
||||
}
|
||||
|
||||
std::stringstream cf_ids_oss;
|
||||
bool empty_cfs = true;
|
||||
for (uint32_t cf_id : encountered_cf_ids) {
|
||||
if (ucmps.find(cf_id) == ucmps.end()) {
|
||||
if (empty_cfs) {
|
||||
cf_ids_oss << "[";
|
||||
empty_cfs = false;
|
||||
} else {
|
||||
cf_ids_oss << ",";
|
||||
}
|
||||
cf_ids_oss << cf_id;
|
||||
}
|
||||
}
|
||||
if (!empty_cfs) {
|
||||
cf_ids_oss << "]";
|
||||
std::cout
|
||||
<< "(Column family id: " << cf_ids_oss.str()
|
||||
<< " contained in WAL are not opened in DB. Applied default "
|
||||
"hex formatting for user key. Specify --db=<db_path> to "
|
||||
"open DB for better user key formatting if it contains timestamp.)"
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2847,7 +2976,7 @@ WALDumperCommand::WALDumperCommand(
|
||||
const std::map<std::string, std::string>& options,
|
||||
const std::vector<std::string>& flags)
|
||||
: LDBCommand(options, flags, true,
|
||||
BuildCmdLineOptions({ARG_WAL_FILE, ARG_WRITE_COMMITTED,
|
||||
BuildCmdLineOptions({ARG_WAL_FILE, ARG_DB, ARG_WRITE_COMMITTED,
|
||||
ARG_PRINT_HEADER, ARG_PRINT_VALUE})),
|
||||
print_header_(false),
|
||||
print_values_(false),
|
||||
@@ -2867,12 +2996,17 @@ WALDumperCommand::WALDumperCommand(
|
||||
exec_state_ = LDBCommandExecuteResult::Failed("Argument " + ARG_WAL_FILE +
|
||||
" must be specified.");
|
||||
}
|
||||
|
||||
if (!db_path_.empty()) {
|
||||
no_db_open_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void WALDumperCommand::Help(std::string& ret) {
|
||||
ret.append(" ");
|
||||
ret.append(WALDumperCommand::Name());
|
||||
ret.append(" --" + ARG_WAL_FILE + "=<write_ahead_log_file_path>");
|
||||
ret.append(" [--" + ARG_DB + "=<db_path>]");
|
||||
ret.append(" [--" + ARG_PRINT_HEADER + "] ");
|
||||
ret.append(" [--" + ARG_PRINT_VALUE + "] ");
|
||||
ret.append(" [--" + ARG_WRITE_COMMITTED + "=true|false] ");
|
||||
@@ -2882,7 +3016,7 @@ void WALDumperCommand::Help(std::string& ret) {
|
||||
void WALDumperCommand::DoCommand() {
|
||||
PrepareOptions();
|
||||
DumpWalFile(options_, wal_file_, print_header_, print_values_,
|
||||
is_write_committed_, &exec_state_);
|
||||
is_write_committed_, ucmps_, &exec_state_);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -3362,6 +3496,8 @@ void ScanCommand::DoCommand() {
|
||||
int num_keys_scanned = 0;
|
||||
ReadOptions scan_read_opts;
|
||||
ColumnFamilyHandle* cfh = GetCfHandle();
|
||||
const Comparator* ucmp = cfh->GetComparator();
|
||||
size_t ts_sz = ucmp->timestamp_size();
|
||||
Slice read_timestamp;
|
||||
Status st = MaybePopulateReadTimestamp(cfh, scan_read_opts, &read_timestamp);
|
||||
if (!st.ok()) {
|
||||
@@ -3418,12 +3554,15 @@ void ScanCommand::DoCommand() {
|
||||
}
|
||||
fprintf(stdout, "%s\n", key_str.c_str());
|
||||
} else {
|
||||
std::string str = is_db_ttl_ ? PrintKeyValue(it->key().ToString(),
|
||||
it->value().ToString(),
|
||||
is_key_hex_, is_value_hex_)
|
||||
: PrintKeyValueOrWideColumns(
|
||||
it->key(), it->value(), it->columns(),
|
||||
is_key_hex_, is_value_hex_);
|
||||
std::string str =
|
||||
is_db_ttl_
|
||||
? PrintKeyValue(it->key().ToString(),
|
||||
ts_sz == 0 ? "" : it->timestamp().ToString(),
|
||||
it->value().ToString(), is_key_hex_,
|
||||
is_value_hex_, ucmp)
|
||||
: PrintKeyValueOrWideColumns(
|
||||
it->key(), ts_sz == 0 ? "" : it->timestamp(), it->value(),
|
||||
it->columns(), is_key_hex_, is_value_hex_, ucmp);
|
||||
fprintf(stdout, "%s\n", str.c_str());
|
||||
}
|
||||
|
||||
@@ -3761,8 +3900,11 @@ void DBQuerierCommand::DoCommand() {
|
||||
key = (is_key_hex_ ? HexToString(tokens[1]) : tokens[1]);
|
||||
s = db_->Get(read_options, GetCfHandle(), Slice(key), &value);
|
||||
if (s.ok()) {
|
||||
// TODO: add read timestamp support in querier
|
||||
fprintf(stdout, "%s\n",
|
||||
PrintKeyValue(key, value, is_key_hex_, is_value_hex_).c_str());
|
||||
PrintKeyValue(key, "", value, is_key_hex_, is_value_hex_,
|
||||
GetCfHandle()->GetComparator())
|
||||
.c_str());
|
||||
} else {
|
||||
if (s.IsNotFound()) {
|
||||
fprintf(stdout, "Not found %s\n", tokens[1].c_str());
|
||||
@@ -4222,7 +4364,7 @@ void DBFileDumperCommand::DoCommand() {
|
||||
std::cout << filename << std::endl;
|
||||
// TODO(myabandeh): allow configuring is_write_commited
|
||||
DumpWalFile(options_, filename, true, true, true /* is_write_commited */,
|
||||
&exec_state_);
|
||||
ucmps_, &exec_state_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,7 +369,7 @@ class WALDumperCommand : public LDBCommand {
|
||||
const std::map<std::string, std::string>& options,
|
||||
const std::vector<std::string>& flags);
|
||||
|
||||
bool NoDBOpen() override { return true; }
|
||||
bool NoDBOpen() override { return no_db_open_; }
|
||||
|
||||
static void Help(std::string& ret);
|
||||
|
||||
@@ -380,6 +380,7 @@ class WALDumperCommand : public LDBCommand {
|
||||
std::string wal_file_;
|
||||
bool print_values_;
|
||||
bool is_write_committed_; // default will be set to true
|
||||
bool no_db_open_ = true;
|
||||
|
||||
static const std::string ARG_WAL_FILE;
|
||||
static const std::string ARG_WRITE_COMMITTED;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
* CompactRange() with change_level=true on a CF with FIFO compaction will return Status::NotSupported().
|
||||
@@ -1 +0,0 @@
|
||||
* External file ingestion with FIFO compaction will always ingest to L0.
|
||||
@@ -1 +0,0 @@
|
||||
Fix a bug where `CompactFiles()` can compact files of range conflict with other ongoing compactions' when `preclude_last_level_data_seconds > 0` is used
|
||||
@@ -1 +0,0 @@
|
||||
* Fixed a bug for databases using `DBOptions::allow_2pc == true` (all `TransactionDB`s except `OptimisticTransactionDB`) that have exactly one column family. Due to a missing WAL sync, attempting to open the DB could have returned a `Status::Corruption` with a message like "SST file is ahead of WALs".
|
||||
@@ -1 +0,0 @@
|
||||
* Fix a bug in CreateColumnFamilyWithImport() where if multiple CFs are imported, we were not resetting files' epoch number and L0 files can have overlapping key range but the same epoch number.
|
||||
@@ -1 +0,0 @@
|
||||
* Fixed race conditions when `ColumnFamilyOptions::inplace_update_support == true` between user overwrites and reads on the same key.
|
||||
@@ -1 +0,0 @@
|
||||
Optimistic transactions and pessimistic transactions with the WriteCommitted policy now support the `GetEntity` API.
|
||||
@@ -1 +0,0 @@
|
||||
* Added new `Iterator` property, "rocksdb.iterator.is-value-pinned", for checking whether the `Slice` returned by `Iterator::value()` can be used until the `Iterator` is destroyed.
|
||||
@@ -1 +0,0 @@
|
||||
Optimistic transactions and WriteCommitted pessimistic transactions now support the `MultiGetEntity` API.
|
||||
@@ -1 +0,0 @@
|
||||
Optimistic transactions and pessimistic transactions with the WriteCommitted policy now support the `PutEntity` API. Support for read APIs and other write policies (WritePrepared, WriteUnprepared) will be added later.
|
||||
-1
@@ -1 +0,0 @@
|
||||
Exposed block based metadata cache options via C API
|
||||
@@ -1 +0,0 @@
|
||||
Exposed compaction pri via c api.
|
||||
@@ -1 +0,0 @@
|
||||
Add a kAdmPolicyAllowAll option to TieredAdmissionPolicy that admits all blocks evicted from the primary block cache into the compressed secondary cache.
|
||||
@@ -158,6 +158,39 @@ Status TimestampRecoveryHandler::PutCF(uint32_t cf, const Slice& key,
|
||||
return WriteBatchInternal::Put(new_batch_.get(), cf, new_key, value);
|
||||
}
|
||||
|
||||
Status TimestampRecoveryHandler::PutEntityCF(uint32_t cf, const Slice& key,
|
||||
const Slice& entity) {
|
||||
std::string new_key_buf;
|
||||
Slice new_key;
|
||||
Status status = TimestampRecoveryHandler::ReconcileTimestampDiscrepancy(
|
||||
cf, key, &new_key_buf, &new_key);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
Slice entity_copy = entity;
|
||||
WideColumns columns;
|
||||
if (!WideColumnSerialization::Deserialize(entity_copy, columns).ok()) {
|
||||
return Status::Corruption("Unable to deserialize entity",
|
||||
entity.ToString(/* hex */ true));
|
||||
}
|
||||
|
||||
return WriteBatchInternal::PutEntity(new_batch_.get(), cf, new_key, columns);
|
||||
}
|
||||
|
||||
Status TimestampRecoveryHandler::TimedPutCF(uint32_t cf, const Slice& key,
|
||||
const Slice& value,
|
||||
uint64_t write_time) {
|
||||
std::string new_key_buf;
|
||||
Slice new_key;
|
||||
Status status =
|
||||
ReconcileTimestampDiscrepancy(cf, key, &new_key_buf, &new_key);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
return WriteBatchInternal::TimedPut(new_batch_.get(), cf, new_key, value,
|
||||
write_time);
|
||||
}
|
||||
|
||||
Status TimestampRecoveryHandler::DeleteCF(uint32_t cf, const Slice& key) {
|
||||
std::string new_key_buf;
|
||||
Slice new_key;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "db/wide/wide_column_serialization.h"
|
||||
#include "db/write_batch_internal.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/status.h"
|
||||
@@ -116,6 +117,12 @@ class TimestampRecoveryHandler : public WriteBatch::Handler {
|
||||
|
||||
Status PutCF(uint32_t cf, const Slice& key, const Slice& value) override;
|
||||
|
||||
Status PutEntityCF(uint32_t cf, const Slice& key,
|
||||
const Slice& entity) override;
|
||||
|
||||
Status TimedPutCF(uint32_t cf, const Slice& key, const Slice& value,
|
||||
uint64_t write_time) override;
|
||||
|
||||
Status DeleteCF(uint32_t cf, const Slice& key) override;
|
||||
|
||||
Status SingleDeleteCF(uint32_t cf, const Slice& key) override;
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace ROCKSDB_NAMESPACE {
|
||||
namespace {
|
||||
static const std::string kTestKeyWithoutTs = "key";
|
||||
static const std::string kValuePlaceHolder = "value";
|
||||
static const uint64_t kWriteUnixTime = 100;
|
||||
} // namespace
|
||||
|
||||
class HandleTimestampSizeDifferenceTest : public testing::Test {
|
||||
@@ -38,6 +39,34 @@ class HandleTimestampSizeDifferenceTest : public testing::Test {
|
||||
return AddKey(cf, key);
|
||||
}
|
||||
|
||||
Status TimedPutCF(uint32_t cf, const Slice& key, const Slice& value,
|
||||
uint64_t write_unix_time) override {
|
||||
if (value.compare(kValuePlaceHolder) != 0) {
|
||||
return Status::InvalidArgument();
|
||||
}
|
||||
if (write_unix_time != kWriteUnixTime) {
|
||||
return Status::InvalidArgument();
|
||||
}
|
||||
return AddKey(cf, key);
|
||||
}
|
||||
|
||||
Status PutEntityCF(uint32_t cf, const Slice& key,
|
||||
const Slice& entity) override {
|
||||
Slice entity_copy = entity;
|
||||
WideColumns columns;
|
||||
Status s = WideColumnSerialization::Deserialize(entity_copy, columns);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (columns.size() != 1) {
|
||||
return Status::InvalidArgument();
|
||||
}
|
||||
if (columns[0].value().compare(kValuePlaceHolder) != 0) {
|
||||
return Status::InvalidArgument();
|
||||
}
|
||||
return AddKey(cf, key);
|
||||
}
|
||||
|
||||
Status DeleteCF(uint32_t cf, const Slice& key) override {
|
||||
return AddKey(cf, key);
|
||||
}
|
||||
@@ -117,6 +146,10 @@ class HandleTimestampSizeDifferenceTest : public testing::Test {
|
||||
WriteBatchInternal::Merge(batch, cf_id, key, kValuePlaceHolder));
|
||||
ASSERT_OK(WriteBatchInternal::PutBlobIndex(batch, cf_id, key,
|
||||
kValuePlaceHolder));
|
||||
ASSERT_OK(WriteBatchInternal::TimedPut(
|
||||
batch, cf_id, key, kValuePlaceHolder, kWriteUnixTime));
|
||||
WideColumns columns{{kDefaultWideColumnName, kValuePlaceHolder}};
|
||||
ASSERT_OK(WriteBatchInternal::PutEntity(batch, cf_id, key, columns));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,11 @@ class ColumnFamilyCollector : public WriteBatch::Handler {
|
||||
return AddColumnFamilyId(column_family_id);
|
||||
}
|
||||
|
||||
Status PutEntityCF(uint32_t column_family_id, const Slice&,
|
||||
const Slice&) override {
|
||||
return AddColumnFamilyId(column_family_id);
|
||||
}
|
||||
|
||||
Status TimedPutCF(uint32_t column_family_id, const Slice&, const Slice&,
|
||||
uint64_t) override {
|
||||
return AddColumnFamilyId(column_family_id);
|
||||
|
||||
@@ -760,7 +760,7 @@ TEST_F(BlobDBTest, SstFileManager) {
|
||||
// run the same test for Get(), MultiGet() and Iterator each.
|
||||
std::shared_ptr<SstFileManager> sst_file_manager(
|
||||
NewSstFileManager(mock_env_.get()));
|
||||
sst_file_manager->SetDeleteRateBytesPerSecond(1);
|
||||
sst_file_manager->SetDeleteRateBytesPerSecond(1024 * 1024);
|
||||
SstFileManagerImpl *sfm =
|
||||
static_cast<SstFileManagerImpl *>(sst_file_manager.get());
|
||||
|
||||
@@ -818,7 +818,7 @@ TEST_F(BlobDBTest, SstFileManagerRestart) {
|
||||
// run the same test for Get(), MultiGet() and Iterator each.
|
||||
std::shared_ptr<SstFileManager> sst_file_manager(
|
||||
NewSstFileManager(mock_env_.get()));
|
||||
sst_file_manager->SetDeleteRateBytesPerSecond(1);
|
||||
sst_file_manager->SetDeleteRateBytesPerSecond(1024 * 1024);
|
||||
SstFileManagerImpl *sfm =
|
||||
static_cast<SstFileManagerImpl *>(sst_file_manager.get());
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "rocksdb/sst_file_manager.h"
|
||||
#include "rocksdb/utilities/transaction_db.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testharness.h"
|
||||
@@ -253,6 +255,13 @@ class CheckpointTest : public testing::Test {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int NumTableFilesAtLevel(int level) {
|
||||
std::string property;
|
||||
EXPECT_TRUE(db_->GetProperty(
|
||||
"rocksdb.num-files-at-level" + std::to_string(level), &property));
|
||||
return atoi(property.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(CheckpointTest, GetSnapshotLink) {
|
||||
@@ -975,6 +984,100 @@ TEST_F(CheckpointTest, PutRaceWithCheckpointTrackedWalSync) {
|
||||
Close();
|
||||
}
|
||||
|
||||
class CheckpointDestroyTest : public CheckpointTest,
|
||||
public testing::WithParamInterface<bool> {};
|
||||
|
||||
TEST_P(CheckpointDestroyTest, DisableEnableSlowDeletion) {
|
||||
bool slow_deletion = GetParam();
|
||||
Options options = CurrentOptions();
|
||||
options.num_levels = 2;
|
||||
options.disable_auto_compactions = true;
|
||||
Status s;
|
||||
options.sst_file_manager.reset(NewSstFileManager(
|
||||
options.env, options.info_log, "", slow_deletion ? 1024 * 1024 : 0,
|
||||
false /* delete_existing_trash */, &s, 1));
|
||||
ASSERT_OK(s);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("foo", "a"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("bar", "b"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ASSERT_OK(Put("bar", "val" + std::to_string(i)));
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
ASSERT_EQ(NumTableFilesAtLevel(0), 10);
|
||||
ASSERT_EQ(NumTableFilesAtLevel(1), 2);
|
||||
|
||||
Checkpoint* checkpoint;
|
||||
ASSERT_OK(Checkpoint::Create(db_, &checkpoint));
|
||||
ASSERT_OK(checkpoint->CreateCheckpoint(snapshot_name_));
|
||||
|
||||
delete checkpoint;
|
||||
checkpoint = nullptr;
|
||||
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_EQ(NumTableFilesAtLevel(0), 0);
|
||||
ASSERT_EQ(NumTableFilesAtLevel(1), 2);
|
||||
|
||||
DB* snapshot_db;
|
||||
ASSERT_OK(DB::Open(options, snapshot_name_, &snapshot_db));
|
||||
ReadOptions read_opts;
|
||||
std::string get_result;
|
||||
ASSERT_OK(snapshot_db->Get(read_opts, "foo", &get_result));
|
||||
ASSERT_EQ("a", get_result);
|
||||
ASSERT_OK(snapshot_db->Get(read_opts, "bar", &get_result));
|
||||
ASSERT_EQ("val9", get_result);
|
||||
delete snapshot_db;
|
||||
|
||||
// Make sure original obsolete files for hard linked files are all deleted.
|
||||
DBImpl* db_impl = static_cast_with_check<DBImpl>(db_);
|
||||
db_impl->TEST_DeleteObsoleteFiles();
|
||||
auto sfm = static_cast_with_check<SstFileManagerImpl>(
|
||||
options.sst_file_manager.get());
|
||||
ASSERT_NE(nullptr, sfm);
|
||||
sfm->WaitForEmptyTrash();
|
||||
// SST file 2-12 for "bar" will be compacted into one file on L1 during the
|
||||
// compaction after checkpoint is created. SST file 1 on L1: foo, seq:
|
||||
// 1 (hard links is 1 after checkpoint destroy)
|
||||
std::atomic<int> bg_delete_sst{0};
|
||||
std::atomic<int> fg_delete_sst{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::DeleteFile::cb", [&](void* arg) {
|
||||
ASSERT_NE(nullptr, arg);
|
||||
auto file_name = *static_cast<std::string*>(arg);
|
||||
if (file_name.size() >= 4 &&
|
||||
file_name.compare(file_name.size() - 4, 4, ".sst") == 0) {
|
||||
fg_delete_sst.fetch_add(1);
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DeleteScheduler::DeleteTrashFile::cb", [&](void* arg) {
|
||||
ASSERT_NE(nullptr, arg);
|
||||
auto file_name = *static_cast<std::string*>(arg);
|
||||
if (file_name.size() >= 10 &&
|
||||
file_name.compare(file_name.size() - 10, 10, ".sst.trash") == 0) {
|
||||
bg_delete_sst.fetch_add(1);
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
ASSERT_OK(DestroyDB(snapshot_name_, options));
|
||||
if (slow_deletion) {
|
||||
ASSERT_EQ(fg_delete_sst, 1);
|
||||
ASSERT_EQ(bg_delete_sst, 11);
|
||||
} else {
|
||||
ASSERT_EQ(fg_delete_sst, 12);
|
||||
}
|
||||
|
||||
ASSERT_EQ("a", Get("foo"));
|
||||
ASSERT_EQ("val9", Get("bar"));
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
INSTANTIATE_TEST_CASE_P(CheckpointDestroyTest, CheckpointDestroyTest,
|
||||
::testing::Values(true, false));
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+16
-4
@@ -85,9 +85,21 @@ Status GetAllKeyVersions(DB* db, ColumnFamilyHandle* cfh, Slice begin_key,
|
||||
ScopedArenaPtr<InternalIterator> iter(
|
||||
idb->NewInternalIterator(read_options, &arena, kMaxSequenceNumber, cfh));
|
||||
|
||||
if (!begin_key.empty()) {
|
||||
const Comparator* ucmp = icmp.user_comparator();
|
||||
size_t ts_sz = ucmp->timestamp_size();
|
||||
|
||||
Slice from_slice = begin_key;
|
||||
bool has_begin = !begin_key.empty();
|
||||
Slice end_slice = end_key;
|
||||
bool has_end = !end_key.empty();
|
||||
std::string begin_key_buf, end_key_buf;
|
||||
auto [from, end] = MaybeAddTimestampsToRange(
|
||||
has_begin ? &from_slice : nullptr, has_end ? &end_slice : nullptr, ts_sz,
|
||||
&begin_key_buf, &end_key_buf);
|
||||
if (has_begin) {
|
||||
assert(from.has_value());
|
||||
InternalKey ikey;
|
||||
ikey.SetMinPossibleForUserKey(begin_key);
|
||||
ikey.SetMinPossibleForUserKey(from.value());
|
||||
iter->Seek(ikey.Encode());
|
||||
} else {
|
||||
iter->SeekToFirst();
|
||||
@@ -102,8 +114,8 @@ Status GetAllKeyVersions(DB* db, ColumnFamilyHandle* cfh, Slice begin_key,
|
||||
return pik_status;
|
||||
}
|
||||
|
||||
if (!end_key.empty() &&
|
||||
icmp.user_comparator()->Compare(ikey.user_key, end_key) > 0) {
|
||||
if (has_end && end.has_value() &&
|
||||
icmp.user_comparator()->Compare(ikey.user_key, end.value()) > 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,8 @@ void PessimisticTransaction::Initialize(const TransactionOptions& txn_options) {
|
||||
deadlock_detect_ = txn_options.deadlock_detect;
|
||||
deadlock_detect_depth_ = txn_options.deadlock_detect_depth;
|
||||
write_batch_.SetMaxBytes(txn_options.max_write_batch_size);
|
||||
write_batch_.GetWriteBatch()->SetTrackTimestampSize(
|
||||
txn_options.write_batch_track_timestamp_size);
|
||||
skip_concurrency_control_ = txn_options.skip_concurrency_control;
|
||||
|
||||
lock_timeout_ = txn_options.lock_timeout * 1000;
|
||||
@@ -189,12 +191,9 @@ inline Status WriteCommittedTxn::GetForUpdateImpl(
|
||||
}
|
||||
}
|
||||
|
||||
if (!do_validate && kMaxTxnTimestamp != read_timestamp_) {
|
||||
return Status::InvalidArgument(
|
||||
"If do_validate is false then GetForUpdate with read_timestamp is not "
|
||||
"defined.");
|
||||
} else if (do_validate && kMaxTxnTimestamp == read_timestamp_) {
|
||||
return Status::InvalidArgument("read_timestamp must be set for validation");
|
||||
Status s = SanityCheckReadTimestamp(do_validate);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (!read_options.timestamp) {
|
||||
@@ -217,6 +216,33 @@ inline Status WriteCommittedTxn::GetForUpdateImpl(
|
||||
value, exclusive, do_validate);
|
||||
}
|
||||
|
||||
Status WriteCommittedTxn::SanityCheckReadTimestamp(bool do_validate) {
|
||||
bool enable_udt_validation =
|
||||
txn_db_impl_->GetTxnDBOptions().enable_udt_validation;
|
||||
if (!enable_udt_validation) {
|
||||
if (kMaxTxnTimestamp != read_timestamp_) {
|
||||
return Status::InvalidArgument(
|
||||
"read_timestamp is set but timestamp validation is disabled for the "
|
||||
"DB");
|
||||
}
|
||||
} else {
|
||||
if (!do_validate) {
|
||||
if (kMaxTxnTimestamp != read_timestamp_) {
|
||||
return Status::InvalidArgument(
|
||||
"If do_validate is false then GetForUpdate with read_timestamp is "
|
||||
"not "
|
||||
"defined.");
|
||||
}
|
||||
} else {
|
||||
if (kMaxTxnTimestamp == read_timestamp_) {
|
||||
return Status::InvalidArgument(
|
||||
"read_timestamp must be set for validation");
|
||||
}
|
||||
}
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status WriteCommittedTxn::PutEntityImpl(ColumnFamilyHandle* column_family,
|
||||
const Slice& key,
|
||||
const WideColumns& columns,
|
||||
@@ -442,7 +468,8 @@ Status WriteCommittedTxn::SetReadTimestampForValidation(TxnTimestamp ts) {
|
||||
}
|
||||
|
||||
Status WriteCommittedTxn::SetCommitTimestamp(TxnTimestamp ts) {
|
||||
if (read_timestamp_ < kMaxTxnTimestamp && ts <= read_timestamp_) {
|
||||
if (txn_db_impl_->GetTxnDBOptions().enable_udt_validation &&
|
||||
read_timestamp_ < kMaxTxnTimestamp && ts <= read_timestamp_) {
|
||||
return Status::InvalidArgument(
|
||||
"Cannot commit at timestamp smaller than or equal to read timestamp");
|
||||
}
|
||||
@@ -692,8 +719,16 @@ Status WriteCommittedTxn::CommitWithoutPrepareInternal() {
|
||||
EncodeFixed64(commit_ts_buf, commit_timestamp_);
|
||||
Slice commit_ts(commit_ts_buf, sizeof(commit_ts_buf));
|
||||
|
||||
Status s =
|
||||
wb->UpdateTimestamps(commit_ts, [wbwi, this](uint32_t cf) -> size_t {
|
||||
Status s = wb->UpdateTimestamps(
|
||||
commit_ts, [wb, wbwi, this](uint32_t cf) -> size_t {
|
||||
// First search through timestamp info kept inside the WriteBatch
|
||||
// in case some writes bypassed the Transaction's write APIs.
|
||||
auto cf_id_to_ts_sz = wb->GetColumnFamilyToTimestampSize();
|
||||
auto iter = cf_id_to_ts_sz.find(cf);
|
||||
if (iter != cf_id_to_ts_sz.end()) {
|
||||
size_t ts_sz = iter->second;
|
||||
return ts_sz;
|
||||
}
|
||||
auto cf_iter = cfs_with_ts_tracked_when_indexing_disabled_.find(cf);
|
||||
if (cf_iter != cfs_with_ts_tracked_when_indexing_disabled_.end()) {
|
||||
return sizeof(kMaxTxnTimestamp);
|
||||
@@ -768,16 +803,24 @@ Status WriteCommittedTxn::CommitInternal() {
|
||||
s = WriteBatchInternal::MarkCommitWithTimestamp(working_batch, name_,
|
||||
commit_ts);
|
||||
if (s.ok()) {
|
||||
s = wb->UpdateTimestamps(commit_ts, [wbwi, this](uint32_t cf) -> size_t {
|
||||
if (cfs_with_ts_tracked_when_indexing_disabled_.find(cf) !=
|
||||
cfs_with_ts_tracked_when_indexing_disabled_.end()) {
|
||||
return sizeof(kMaxTxnTimestamp);
|
||||
}
|
||||
const Comparator* ucmp =
|
||||
WriteBatchWithIndexInternal::GetUserComparator(*wbwi, cf);
|
||||
return ucmp ? ucmp->timestamp_size()
|
||||
: std::numeric_limits<size_t>::max();
|
||||
});
|
||||
s = wb->UpdateTimestamps(
|
||||
commit_ts, [wb, wbwi, this](uint32_t cf) -> size_t {
|
||||
// first search through timestamp info kept inside the WriteBatch
|
||||
// in case some writes bypassed the Transaction's write APIs.
|
||||
auto cf_id_to_ts_sz = wb->GetColumnFamilyToTimestampSize();
|
||||
auto iter = cf_id_to_ts_sz.find(cf);
|
||||
if (iter != cf_id_to_ts_sz.end()) {
|
||||
return iter->second;
|
||||
}
|
||||
if (cfs_with_ts_tracked_when_indexing_disabled_.find(cf) !=
|
||||
cfs_with_ts_tracked_when_indexing_disabled_.end()) {
|
||||
return sizeof(kMaxTxnTimestamp);
|
||||
}
|
||||
const Comparator* ucmp =
|
||||
WriteBatchWithIndexInternal::GetUserComparator(*wbwi, cf);
|
||||
return ucmp ? ucmp->timestamp_size()
|
||||
: std::numeric_limits<size_t>::max();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1151,7 +1194,10 @@ Status PessimisticTransaction::ValidateSnapshot(
|
||||
|
||||
return TransactionUtil::CheckKeyForConflicts(
|
||||
db_impl_, cfh, key.ToString(), snap_seq, ts_sz == 0 ? nullptr : &ts_buf,
|
||||
false /* cache_only */);
|
||||
false /* cache_only */,
|
||||
/* snap_checker */ nullptr,
|
||||
/* min_uncommitted */ kMaxSequenceNumber,
|
||||
txn_db_impl_->GetTxnDBOptions().enable_udt_validation);
|
||||
}
|
||||
|
||||
bool PessimisticTransaction::TryStealingLocks() {
|
||||
@@ -1171,14 +1217,15 @@ Status PessimisticTransaction::SetName(const TransactionName& name) {
|
||||
if (txn_state_ == STARTED) {
|
||||
if (name_.length()) {
|
||||
s = Status::InvalidArgument("Transaction has already been named.");
|
||||
} else if (txn_db_impl_->GetTransactionByName(name) != nullptr) {
|
||||
s = Status::InvalidArgument("Transaction name must be unique.");
|
||||
} else if (name.length() < 1 || name.length() > 512) {
|
||||
s = Status::InvalidArgument(
|
||||
"Transaction name length must be between 1 and 512 chars.");
|
||||
} else {
|
||||
name_ = name;
|
||||
txn_db_impl_->RegisterTransaction(this);
|
||||
s = txn_db_impl_->RegisterTransaction(this);
|
||||
if (!s.ok()) {
|
||||
name_.clear();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s = Status::InvalidArgument("Transaction is beyond state for naming.");
|
||||
|
||||
@@ -325,6 +325,11 @@ class WriteCommittedTxn : public PessimisticTransaction {
|
||||
|
||||
Status RollbackInternal() override;
|
||||
|
||||
// Checks if the combination of `do_validate`, the read timestamp set in
|
||||
// `read_timestamp_` and the `enable_udt_validation` flag in
|
||||
// TransactionDBOptions make sense together.
|
||||
Status SanityCheckReadTimestamp(bool do_validate);
|
||||
|
||||
// Column families that enable timestamps and whose data are written when
|
||||
// indexing_enabled_ is false. If a key is written when indexing_enabled_ is
|
||||
// true, then the corresponding column family is not added to cfs_with_ts
|
||||
|
||||
@@ -723,6 +723,11 @@ void PessimisticTransactionDB::ReinitializeTransaction(
|
||||
Transaction* PessimisticTransactionDB::GetTransactionByName(
|
||||
const TransactionName& name) {
|
||||
std::lock_guard<std::mutex> lock(name_map_mutex_);
|
||||
return GetTransactionByNameLocked(name);
|
||||
}
|
||||
|
||||
Transaction* PessimisticTransactionDB::GetTransactionByNameLocked(
|
||||
const TransactionName& name) {
|
||||
auto it = transactions_.find(name);
|
||||
if (it == transactions_.end()) {
|
||||
return nullptr;
|
||||
@@ -755,13 +760,15 @@ void PessimisticTransactionDB::SetDeadlockInfoBufferSize(uint32_t target_size) {
|
||||
lock_manager_->Resize(target_size);
|
||||
}
|
||||
|
||||
void PessimisticTransactionDB::RegisterTransaction(Transaction* txn) {
|
||||
Status PessimisticTransactionDB::RegisterTransaction(Transaction* txn) {
|
||||
assert(txn);
|
||||
assert(txn->GetName().length() > 0);
|
||||
assert(GetTransactionByName(txn->GetName()) == nullptr);
|
||||
assert(txn->GetState() == Transaction::STARTED);
|
||||
std::lock_guard<std::mutex> lock(name_map_mutex_);
|
||||
transactions_[txn->GetName()] = txn;
|
||||
if (!transactions_.insert({txn->GetName(), txn}).second) {
|
||||
return Status::InvalidArgument("Duplicate txn name " + txn->GetName());
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void PessimisticTransactionDB::UnregisterTransaction(Transaction* txn) {
|
||||
|
||||
@@ -173,7 +173,7 @@ class PessimisticTransactionDB : public TransactionDB {
|
||||
|
||||
Transaction* GetTransactionByName(const TransactionName& name) override;
|
||||
|
||||
void RegisterTransaction(Transaction* txn);
|
||||
Status RegisterTransaction(Transaction* txn);
|
||||
void UnregisterTransaction(Transaction* txn);
|
||||
|
||||
// not thread safe. current use case is during recovery (single thread)
|
||||
@@ -239,6 +239,7 @@ class PessimisticTransactionDB : public TransactionDB {
|
||||
friend class WriteUnpreparedTransactionTest_MarkLogWithPrepSection_Test;
|
||||
|
||||
Transaction* BeginInternalTransaction(const WriteOptions& options);
|
||||
Transaction* GetTransactionByNameLocked(const TransactionName& name);
|
||||
|
||||
std::shared_ptr<LockManager> lock_manager_;
|
||||
|
||||
|
||||
@@ -814,11 +814,13 @@ Status TransactionBaseImpl::RebuildFromWriteBatch(WriteBatch* src_batch) {
|
||||
}
|
||||
|
||||
Status PutCF(uint32_t cf, const Slice& key, const Slice& val) override {
|
||||
return txn_->Put(db_->GetColumnFamilyHandle(cf), key, val);
|
||||
Slice user_key = GetUserKey(cf, key);
|
||||
return txn_->Put(db_->GetColumnFamilyHandle(cf), user_key, val);
|
||||
}
|
||||
|
||||
Status PutEntityCF(uint32_t cf, const Slice& key,
|
||||
const Slice& entity) override {
|
||||
Slice user_key = GetUserKey(cf, key);
|
||||
Slice entity_copy = entity;
|
||||
WideColumns columns;
|
||||
const Status s =
|
||||
@@ -827,19 +829,22 @@ Status TransactionBaseImpl::RebuildFromWriteBatch(WriteBatch* src_batch) {
|
||||
return s;
|
||||
}
|
||||
|
||||
return txn_->PutEntity(db_->GetColumnFamilyHandle(cf), key, columns);
|
||||
return txn_->PutEntity(db_->GetColumnFamilyHandle(cf), user_key, columns);
|
||||
}
|
||||
|
||||
Status DeleteCF(uint32_t cf, const Slice& key) override {
|
||||
return txn_->Delete(db_->GetColumnFamilyHandle(cf), key);
|
||||
Slice user_key = GetUserKey(cf, key);
|
||||
return txn_->Delete(db_->GetColumnFamilyHandle(cf), user_key);
|
||||
}
|
||||
|
||||
Status SingleDeleteCF(uint32_t cf, const Slice& key) override {
|
||||
return txn_->SingleDelete(db_->GetColumnFamilyHandle(cf), key);
|
||||
Slice user_key = GetUserKey(cf, key);
|
||||
return txn_->SingleDelete(db_->GetColumnFamilyHandle(cf), user_key);
|
||||
}
|
||||
|
||||
Status MergeCF(uint32_t cf, const Slice& key, const Slice& val) override {
|
||||
return txn_->Merge(db_->GetColumnFamilyHandle(cf), key, val);
|
||||
Slice user_key = GetUserKey(cf, key);
|
||||
return txn_->Merge(db_->GetColumnFamilyHandle(cf), user_key, val);
|
||||
}
|
||||
|
||||
// this is used for reconstructing prepared transactions upon
|
||||
@@ -862,6 +867,21 @@ Status TransactionBaseImpl::RebuildFromWriteBatch(WriteBatch* src_batch) {
|
||||
Status MarkRollback(const Slice&) override {
|
||||
return Status::InvalidArgument();
|
||||
}
|
||||
size_t GetTimestampSize(uint32_t cf_id) {
|
||||
auto cfd = db_->versions_->GetColumnFamilySet()->GetColumnFamily(cf_id);
|
||||
const Comparator* ucmp = cfd->user_comparator();
|
||||
assert(ucmp);
|
||||
return ucmp->timestamp_size();
|
||||
}
|
||||
|
||||
Slice GetUserKey(uint32_t cf_id, const Slice& key) {
|
||||
size_t ts_sz = GetTimestampSize(cf_id);
|
||||
if (ts_sz == 0) {
|
||||
return key;
|
||||
}
|
||||
assert(key.size() >= ts_sz);
|
||||
return Slice(key.data(), key.size() - ts_sz);
|
||||
}
|
||||
};
|
||||
|
||||
IndexedWriteBatchBuilder copycat(this, dbimpl_);
|
||||
|
||||
@@ -21,7 +21,8 @@ namespace ROCKSDB_NAMESPACE {
|
||||
Status TransactionUtil::CheckKeyForConflicts(
|
||||
DBImpl* db_impl, ColumnFamilyHandle* column_family, const std::string& key,
|
||||
SequenceNumber snap_seq, const std::string* const read_ts, bool cache_only,
|
||||
ReadCallback* snap_checker, SequenceNumber min_uncommitted) {
|
||||
ReadCallback* snap_checker, SequenceNumber min_uncommitted,
|
||||
bool enable_udt_validation) {
|
||||
Status result;
|
||||
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(column_family);
|
||||
@@ -37,8 +38,9 @@ Status TransactionUtil::CheckKeyForConflicts(
|
||||
SequenceNumber earliest_seq =
|
||||
db_impl->GetEarliestMemTableSequenceNumber(sv, true);
|
||||
|
||||
result = CheckKey(db_impl, sv, earliest_seq, snap_seq, key, read_ts,
|
||||
cache_only, snap_checker, min_uncommitted);
|
||||
result =
|
||||
CheckKey(db_impl, sv, earliest_seq, snap_seq, key, read_ts, cache_only,
|
||||
snap_checker, min_uncommitted, enable_udt_validation);
|
||||
|
||||
db_impl->ReturnAndCleanupSuperVersion(cfd, sv);
|
||||
}
|
||||
@@ -52,7 +54,8 @@ Status TransactionUtil::CheckKey(DBImpl* db_impl, SuperVersion* sv,
|
||||
const std::string& key,
|
||||
const std::string* const read_ts,
|
||||
bool cache_only, ReadCallback* snap_checker,
|
||||
SequenceNumber min_uncommitted) {
|
||||
SequenceNumber min_uncommitted,
|
||||
bool enable_udt_validation) {
|
||||
// When `min_uncommitted` is provided, keys are not always committed
|
||||
// in sequence number order, and `snap_checker` is used to check whether
|
||||
// specific sequence number is in the database is visible to the transaction.
|
||||
@@ -130,7 +133,7 @@ Status TransactionUtil::CheckKey(DBImpl* db_impl, SuperVersion* sv,
|
||||
? snap_seq < seq
|
||||
: !snap_checker->IsVisible(seq);
|
||||
// Perform conflict checking based on timestamp if applicable.
|
||||
if (!write_conflict && read_ts != nullptr) {
|
||||
if (enable_udt_validation && !write_conflict && read_ts != nullptr) {
|
||||
ColumnFamilyData* cfd = sv->cfd;
|
||||
assert(cfd);
|
||||
const Comparator* const ucmp = cfd->user_comparator();
|
||||
|
||||
@@ -43,7 +43,8 @@ class TransactionUtil {
|
||||
const std::string& key, SequenceNumber snap_seq,
|
||||
const std::string* const ts, bool cache_only,
|
||||
ReadCallback* snap_checker = nullptr,
|
||||
SequenceNumber min_uncommitted = kMaxSequenceNumber);
|
||||
SequenceNumber min_uncommitted = kMaxSequenceNumber,
|
||||
bool enable_udt_validation = true);
|
||||
|
||||
// For each key,SequenceNumber pair tracked by the LockTracker, this function
|
||||
// will verify there have been no writes to the key in the db since that
|
||||
@@ -70,13 +71,15 @@ class TransactionUtil {
|
||||
// seq > `snap_seq`: applicable to conflict
|
||||
// `min_uncommitted` <= seq <= `snap_seq`: call `snap_checker` to determine.
|
||||
//
|
||||
// If user-defined timestamp is enabled, a write conflict is detected if an
|
||||
// operation for `key` with timestamp greater than `ts` exists.
|
||||
// If user-defined timestamp is enabled and `enable_udt_validation` is set to
|
||||
// true, a write conflict is detected if an operation for `key` with timestamp
|
||||
// greater than `ts` exists.
|
||||
static Status CheckKey(DBImpl* db_impl, SuperVersion* sv,
|
||||
SequenceNumber earliest_seq, SequenceNumber snap_seq,
|
||||
const std::string& key, const std::string* const ts,
|
||||
bool cache_only, ReadCallback* snap_checker = nullptr,
|
||||
SequenceNumber min_uncommitted = kMaxSequenceNumber);
|
||||
SequenceNumber min_uncommitted = kMaxSequenceNumber,
|
||||
bool enable_udt_validation = true);
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
@@ -130,6 +130,129 @@ void CheckKeyValueTsWithIterator(
|
||||
}
|
||||
}
|
||||
|
||||
// This is an incorrect usage of this API, supporting this should be removed
|
||||
// after MyRocks remove this pattern in a refactor.
|
||||
TEST_P(WriteCommittedTxnWithTsTest, WritesBypassTransactionAPIs) {
|
||||
options.comparator = test::BytewiseComparatorWithU64TsWrapper();
|
||||
ASSERT_OK(ReOpen());
|
||||
|
||||
const std::string test_cf_name = "test_cf";
|
||||
ColumnFamilyOptions cf_options;
|
||||
ColumnFamilyHandle* cfh = nullptr;
|
||||
assert(db);
|
||||
ASSERT_OK(db->CreateColumnFamily(cf_options, test_cf_name, &cfh));
|
||||
delete cfh;
|
||||
cfh = nullptr;
|
||||
|
||||
std::vector<ColumnFamilyDescriptor> cf_descs;
|
||||
cf_descs.emplace_back(kDefaultColumnFamilyName, options);
|
||||
cf_descs.emplace_back(test_cf_name, Options(DBOptions(), cf_options));
|
||||
options.avoid_flush_during_shutdown = true;
|
||||
ASSERT_OK(ReOpenNoDelete(cf_descs, &handles_));
|
||||
|
||||
// Write in each transaction a mixture of column families that enable
|
||||
// timestamp and disable timestamps.
|
||||
|
||||
TransactionOptions txn_opts;
|
||||
txn_opts.write_batch_track_timestamp_size = true;
|
||||
std::unique_ptr<Transaction> txn0(NewTxn(WriteOptions(), txn_opts));
|
||||
assert(txn0);
|
||||
ASSERT_OK(txn0->Put(handles_[0], "key1", "key1_val"));
|
||||
// Timestamp size info for writes like this can only be correctly tracked if
|
||||
// TransactionOptions.write_batch_track_timestamp_size is true.
|
||||
ASSERT_OK(txn0->GetWriteBatch()->GetWriteBatch()->Put(handles_[1], "foo",
|
||||
"foo_val"));
|
||||
ASSERT_OK(txn0->SetName("txn0"));
|
||||
ASSERT_OK(txn0->SetCommitTimestamp(2));
|
||||
ASSERT_OK(txn0->Prepare());
|
||||
ASSERT_OK(txn0->Commit());
|
||||
txn0.reset();
|
||||
|
||||
// For keys written from transactions that disable
|
||||
// `write_batch_track_timestamp_size`
|
||||
// The keys has incorrect behavior like:
|
||||
// *Cannot be found after commit: because transaction's UpdateTimestamp do not
|
||||
// have correct timestamp size when this write bypass transaction write APIs.
|
||||
// *Can be found again after DB restart recovers the write from WAL log:
|
||||
// because recovered transaction's UpdateTimestamp get correct timestamp size
|
||||
// info directly from VersionSet.
|
||||
// If there is a flush that persisted this transaction into sst files after
|
||||
// it's committed, the key will be forever corrupted.
|
||||
std::unique_ptr<Transaction> txn1(
|
||||
NewTxn(WriteOptions(), TransactionOptions()));
|
||||
assert(txn1);
|
||||
ASSERT_OK(txn1->Put(handles_[0], "key2", "key2_val"));
|
||||
// Writing a key with more than 8 bytes so that we can manifest the error as
|
||||
// a NotFound error instead of an issue during `WriteBatch::UpdateTimestamp`.
|
||||
ASSERT_OK(txn1->GetWriteBatch()->GetWriteBatch()->Put(
|
||||
handles_[1], "foobarbaz", "baz_val"));
|
||||
ASSERT_OK(txn1->SetName("txn1"));
|
||||
ASSERT_OK(txn1->SetCommitTimestamp(2));
|
||||
ASSERT_OK(txn1->Prepare());
|
||||
ASSERT_OK(txn1->Commit());
|
||||
txn1.reset();
|
||||
|
||||
ASSERT_OK(db->Flush(FlushOptions(), handles_[1]));
|
||||
|
||||
WriteOptions wopts;
|
||||
wopts.sync = true;
|
||||
std::unique_ptr<Transaction> txn2(NewTxn(wopts, TransactionOptions()));
|
||||
assert(txn2);
|
||||
ASSERT_OK(txn2->Put(handles_[0], "key3", "key3_val"));
|
||||
ASSERT_OK(txn2->GetWriteBatch()->GetWriteBatch()->Put(
|
||||
handles_[1], "bazbazbaz", "bazbazbaz_val"));
|
||||
ASSERT_OK(txn2->SetCommitTimestamp(2));
|
||||
ASSERT_OK(txn2->SetName("txn2"));
|
||||
ASSERT_OK(txn2->Prepare());
|
||||
ASSERT_OK(txn2->Commit());
|
||||
txn2.reset();
|
||||
|
||||
std::unique_ptr<Transaction> txn3(
|
||||
NewTxn(WriteOptions(), TransactionOptions()));
|
||||
assert(txn3);
|
||||
std::string value;
|
||||
ReadOptions ropts;
|
||||
std::string read_ts;
|
||||
Slice timestamp = EncodeU64Ts(2, &read_ts);
|
||||
ropts.timestamp = ×tamp;
|
||||
ASSERT_OK(txn3->Get(ropts, handles_[0], "key1", &value));
|
||||
ASSERT_EQ("key1_val", value);
|
||||
ASSERT_OK(txn3->Get(ropts, handles_[0], "key2", &value));
|
||||
ASSERT_EQ("key2_val", value);
|
||||
ASSERT_OK(txn3->Get(ropts, handles_[0], "key3", &value));
|
||||
ASSERT_EQ("key3_val", value);
|
||||
txn3.reset();
|
||||
|
||||
std::unique_ptr<Transaction> txn4(
|
||||
NewTxn(WriteOptions(), TransactionOptions()));
|
||||
assert(txn4);
|
||||
ASSERT_OK(txn4->Get(ReadOptions(), handles_[1], "foo", &value));
|
||||
ASSERT_EQ("foo_val", value);
|
||||
// Incorrect behavior: committed keys cannot be found
|
||||
ASSERT_TRUE(
|
||||
txn4->Get(ReadOptions(), handles_[1], "foobarbaz", &value).IsNotFound());
|
||||
ASSERT_TRUE(
|
||||
txn4->Get(ReadOptions(), handles_[1], "bazbazbaz", &value).IsNotFound());
|
||||
txn4.reset();
|
||||
|
||||
ASSERT_OK(ReOpenNoDelete(cf_descs, &handles_));
|
||||
std::unique_ptr<Transaction> txn5(
|
||||
NewTxn(WriteOptions(), TransactionOptions()));
|
||||
assert(txn5);
|
||||
ASSERT_OK(txn5->Get(ReadOptions(), handles_[1], "foo", &value));
|
||||
ASSERT_EQ("foo_val", value);
|
||||
// Incorrect behavior:
|
||||
// *unflushed key can be found after reopen replays the entries from WAL
|
||||
// (this is not suggesting using flushing as a workaround but to show a
|
||||
// possible misleading behavior)
|
||||
// *flushed key is forever corrupted.
|
||||
ASSERT_TRUE(
|
||||
txn5->Get(ReadOptions(), handles_[1], "foobarbaz", &value).IsNotFound());
|
||||
ASSERT_OK(txn5->Get(ReadOptions(), handles_[1], "bazbazbaz", &value));
|
||||
ASSERT_EQ("bazbazbaz_val", value);
|
||||
txn5.reset();
|
||||
}
|
||||
|
||||
TEST_P(WriteCommittedTxnWithTsTest, ReOpenWithTimestamp) {
|
||||
options.merge_operator = MergeOperators::CreateUInt64AddOperator();
|
||||
ASSERT_OK(ReOpenNoDelete());
|
||||
@@ -288,12 +411,25 @@ TEST_P(WriteCommittedTxnWithTsTest, RecoverFromWal) {
|
||||
|
||||
txn0.reset();
|
||||
|
||||
std::unique_ptr<Transaction> txn3(
|
||||
NewTxn(WriteOptions(), TransactionOptions()));
|
||||
assert(txn3);
|
||||
ASSERT_OK(txn3->Put(handles_[1], "baz", "baz_value"));
|
||||
ASSERT_OK(txn3->SetName("txn3"));
|
||||
ASSERT_OK(txn3->Prepare());
|
||||
txn3.reset();
|
||||
|
||||
ASSERT_OK(ReOpenNoDelete(cf_descs, &handles_));
|
||||
|
||||
{
|
||||
Transaction* recovered_txn0 = db->GetTransactionByName("txn0");
|
||||
ASSERT_OK(recovered_txn0->SetCommitTimestamp(23));
|
||||
ASSERT_OK(recovered_txn0->Commit());
|
||||
delete recovered_txn0;
|
||||
std::string value;
|
||||
Status s = GetFromDb(ReadOptions(), handles_[1], "foo", /*ts=*/23, &value);
|
||||
ASSERT_TRUE(s.IsNotFound());
|
||||
ASSERT_OK(s);
|
||||
ASSERT_EQ("foo_value", value);
|
||||
|
||||
s = db->Get(ReadOptions(), handles_[0], "bar", &value);
|
||||
ASSERT_OK(s);
|
||||
@@ -314,6 +450,9 @@ TEST_P(WriteCommittedTxnWithTsTest, RecoverFromWal) {
|
||||
s = GetFromDb(ReadOptions(), handles_[1], "key1", /*ts=*/24, &value);
|
||||
ASSERT_OK(s);
|
||||
ASSERT_EQ("value_3", value);
|
||||
|
||||
s = GetFromDb(ReadOptions(), handles_[1], "baz", /*ts=*/24, &value);
|
||||
ASSERT_TRUE(s.IsNotFound());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,6 +677,109 @@ TEST_P(WriteCommittedTxnWithTsTest, GetForUpdate) {
|
||||
txn5.reset();
|
||||
}
|
||||
|
||||
TEST_P(WriteCommittedTxnWithTsTest, GetForUpdateUdtValidationNotEnabled) {
|
||||
ASSERT_OK(ReOpenNoDelete());
|
||||
|
||||
ColumnFamilyOptions cf_options;
|
||||
cf_options.comparator = test::BytewiseComparatorWithU64TsWrapper();
|
||||
const std::string test_cf_name = "test_cf";
|
||||
ColumnFamilyHandle* cfh = nullptr;
|
||||
assert(db);
|
||||
ASSERT_OK(db->CreateColumnFamily(cf_options, test_cf_name, &cfh));
|
||||
delete cfh;
|
||||
cfh = nullptr;
|
||||
|
||||
std::vector<ColumnFamilyDescriptor> cf_descs;
|
||||
cf_descs.emplace_back(kDefaultColumnFamilyName, options);
|
||||
cf_descs.emplace_back(test_cf_name, Options(DBOptions(), cf_options));
|
||||
options.avoid_flush_during_shutdown = true;
|
||||
|
||||
txn_db_options.enable_udt_validation = false;
|
||||
ASSERT_OK(ReOpenNoDelete(cf_descs, &handles_));
|
||||
|
||||
// blind write a key/value for latter read via `GetForUpdate`.
|
||||
std::unique_ptr<Transaction> txn0(
|
||||
NewTxn(WriteOptions(), TransactionOptions()));
|
||||
ASSERT_OK(txn0->Put(handles_[1], "key", "value0"));
|
||||
ASSERT_OK(txn0->SetCommitTimestamp(20));
|
||||
ASSERT_OK(txn0->Commit());
|
||||
|
||||
// When timestamp validation is disabled across the whole DB
|
||||
// `SetReadTimestampForValidation` should not be called.
|
||||
std::unique_ptr<Transaction> txn1(
|
||||
NewTxn(WriteOptions(), TransactionOptions()));
|
||||
std::string value;
|
||||
ASSERT_OK(txn1->SetReadTimestampForValidation(21));
|
||||
ASSERT_TRUE(txn1->GetForUpdate(ReadOptions(), handles_[1], "key", &value,
|
||||
/* exclusive= */ true, /*do_validate=*/true)
|
||||
.IsInvalidArgument());
|
||||
txn1.reset();
|
||||
|
||||
// do_validate and no snapshot, no conflict checking at all
|
||||
std::unique_ptr<Transaction> txn2(
|
||||
NewTxn(WriteOptions(), TransactionOptions()));
|
||||
ASSERT_OK(txn2->GetForUpdate(ReadOptions(), handles_[1], "key", &value,
|
||||
/* exclusive= */ true, /*do_validate=*/true));
|
||||
ASSERT_OK(txn2->Put(handles_[1], "key", "value1"));
|
||||
ASSERT_OK(txn2->SetCommitTimestamp(21));
|
||||
ASSERT_OK(txn2->Commit());
|
||||
txn2.reset();
|
||||
|
||||
// do_validate and set snapshot, execute sequence number based conflict
|
||||
// checking and skip timestamp based conflict checking.
|
||||
std::unique_ptr<Transaction> txn3(
|
||||
NewTxn(WriteOptions(), TransactionOptions()));
|
||||
txn3->SetSnapshot();
|
||||
ASSERT_OK(txn3->GetForUpdate(ReadOptions(), handles_[1], "key", &value,
|
||||
/* exclusive= */ true, /*do_validate=*/true));
|
||||
ASSERT_OK(txn3->Put(handles_[1], "key", "value2"));
|
||||
ASSERT_OK(txn3->SetCommitTimestamp(22));
|
||||
ASSERT_OK(txn3->Commit());
|
||||
txn3.reset();
|
||||
|
||||
// Always check `ReadOptions.timestamp` to be consistent with the default
|
||||
// `read_timestamp_` if it's explicitly set, even if whole DB disables
|
||||
// timestamp validation.
|
||||
std::unique_ptr<Transaction> txn4(
|
||||
NewTxn(WriteOptions(), TransactionOptions()));
|
||||
ReadOptions ropts;
|
||||
std::string read_timestamp;
|
||||
Slice read_ts = EncodeU64Ts(27, &read_timestamp);
|
||||
ropts.timestamp = &read_ts;
|
||||
ASSERT_TRUE(txn4->GetForUpdate(ropts, handles_[1], "key", &value,
|
||||
/* exclusive= */ true, /*do_validate=*/true)
|
||||
.IsInvalidArgument());
|
||||
txn4.reset();
|
||||
|
||||
// Conflict of timestamps not caught when parallel transactions commit with
|
||||
// some out of order timestamps.
|
||||
std::unique_ptr<Transaction> txn5(
|
||||
db->BeginTransaction(WriteOptions(), TransactionOptions()));
|
||||
assert(txn5);
|
||||
|
||||
std::unique_ptr<Transaction> txn6(
|
||||
db->BeginTransaction(WriteOptions(), TransactionOptions()));
|
||||
assert(txn6);
|
||||
ASSERT_OK(txn6->GetForUpdate(ReadOptions(), handles_[1], "key", &value,
|
||||
/* exclusive= */ true, /*do_validate=*/true));
|
||||
ASSERT_OK(txn6->Put(handles_[1], "key", "value4"));
|
||||
ASSERT_OK(txn6->SetName("txn6"));
|
||||
ASSERT_OK(txn6->Prepare());
|
||||
ASSERT_OK(txn6->SetCommitTimestamp(24));
|
||||
ASSERT_OK(txn6->Commit());
|
||||
txn6.reset();
|
||||
|
||||
txn5->SetSnapshot();
|
||||
ASSERT_OK(txn5->GetForUpdate(ReadOptions(), handles_[1], "key", &value,
|
||||
/* exclusive= */ true, /*do_validate=*/true));
|
||||
ASSERT_OK(txn5->Put(handles_[1], "key", "value3"));
|
||||
ASSERT_OK(txn5->SetName("txn5"));
|
||||
// txn5 commits after txn6 but writes a smaller timestamp
|
||||
ASSERT_OK(txn5->SetCommitTimestamp(23));
|
||||
ASSERT_OK(txn5->Commit());
|
||||
txn5.reset();
|
||||
}
|
||||
|
||||
TEST_P(WriteCommittedTxnWithTsTest, BlindWrite) {
|
||||
ASSERT_OK(ReOpenNoDelete());
|
||||
|
||||
|
||||
@@ -524,7 +524,8 @@ Status WritePreparedTxn::ValidateSnapshot(ColumnFamilyHandle* column_family,
|
||||
// TODO(yanqin): support user-defined timestamp
|
||||
return TransactionUtil::CheckKeyForConflicts(
|
||||
db_impl_, cfh, key.ToString(), snap_seq, /*ts=*/nullptr,
|
||||
false /* cache_only */, &snap_checker, min_uncommitted);
|
||||
false /* cache_only */, &snap_checker, min_uncommitted,
|
||||
txn_db_impl_->GetTxnDBOptions().enable_udt_validation);
|
||||
}
|
||||
|
||||
void WritePreparedTxn::SetSnapshot() {
|
||||
|
||||
@@ -1076,7 +1076,8 @@ Status WriteUnpreparedTxn::ValidateSnapshot(ColumnFamilyHandle* column_family,
|
||||
// TODO(yanqin): Support user-defined timestamp.
|
||||
return TransactionUtil::CheckKeyForConflicts(
|
||||
db_impl_, cfh, key.ToString(), snap_seq, /*ts=*/nullptr,
|
||||
false /* cache_only */, &snap_checker, min_uncommitted);
|
||||
false /* cache_only */, &snap_checker, min_uncommitted,
|
||||
txn_db_impl_->GetTxnDBOptions().enable_udt_validation);
|
||||
}
|
||||
|
||||
const std::map<SequenceNumber, size_t>&
|
||||
|
||||
Reference in New Issue
Block a user