mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-06 11:02:36 -05:00
Compare commits
23
Commits
siying-patch-8
...
7.5.fb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2452696d9 | ||
|
|
d56ad2328b | ||
|
|
540d5aae51 | ||
|
|
0e860cb42a | ||
|
|
dcd4435c9f | ||
|
|
b91628b4cb | ||
|
|
be73bf1b02 | ||
|
|
35bc98893d | ||
|
|
7c138a6358 | ||
|
|
354fa5f635 | ||
|
|
a58fae99c0 | ||
|
|
70ba7cbb28 | ||
|
|
dcc3eb3576 | ||
|
|
efdb428edc | ||
|
|
faa0f9723c | ||
|
|
7506c1a4ca | ||
|
|
fb579a221c | ||
|
|
dcb6a3be4e | ||
|
|
69a18b9bad | ||
|
|
a543773bbc | ||
|
|
00e68e7a30 | ||
|
|
a3acf2ef87 | ||
|
|
66685d6aa1 |
No files matched your search
@@ -670,6 +670,7 @@ set(SOURCES
|
||||
db/range_del_aggregator.cc
|
||||
db/range_tombstone_fragmenter.cc
|
||||
db/repair.cc
|
||||
db/seqno_to_time_mapping.cc
|
||||
db/snapshot_impl.cc
|
||||
db/table_cache.cc
|
||||
db/table_properties_collector.cc
|
||||
@@ -1296,6 +1297,7 @@ if(WITH_TESTS)
|
||||
db/perf_context_test.cc
|
||||
db/periodic_work_scheduler_test.cc
|
||||
db/plain_table_db_test.cc
|
||||
db/seqno_time_test.cc
|
||||
db/prefix_test.cc
|
||||
db/range_del_aggregator_test.cc
|
||||
db/range_tombstone_fragmenter_test.cc
|
||||
|
||||
+24
-7
@@ -1,5 +1,19 @@
|
||||
# Rocksdb Change Log
|
||||
## Unreleased
|
||||
## 7.5.4 (11/03/2022)
|
||||
* Fixed a bug where blobs read during compaction would pollute the cache.
|
||||
|
||||
## 7.5.3 (08/24/2022)
|
||||
* Fixed a regression where L0's compaction score could become very large.
|
||||
|
||||
## 7.5.2 (08/02/2022)
|
||||
### Bug Fixes
|
||||
* Fix a bug starting in 7.4.0 in which some fsync operations might be skipped in a DB after any DropColumnFamily on that DB, until it is re-opened. This can lead to data loss on power loss. (For custom FileSystem implementations, this could lead to `FSDirectory::Fsync` or `FSDirectory::Close` after the first `FSDirectory::Close`; Also, valgrind could report call to `close()` with `fd=-1`.)
|
||||
|
||||
## 7.5.1 (08/01/2022)
|
||||
### Bug Fixes
|
||||
* Fix a bug where rate_limiter_parameter is not passed into `PartitionedFilterBlockReader::GetFilterPartitionBlock`.
|
||||
|
||||
## 7.5.0 (07/15/2022)
|
||||
### New Features
|
||||
* Mempurge option flag `experimental_mempurge_threshold` is now a ColumnFamilyOptions and can now be dynamically configured using `SetOptions()`.
|
||||
* Support backward iteration when `ReadOptions::iter_start_ts` is set.
|
||||
@@ -8,6 +22,7 @@
|
||||
* User can configure the new ColumnFamilyOptions `blob_cache` to enable/disable blob caching.
|
||||
* Either sharing the backend cache with the block cache or using a completely separate cache is supported.
|
||||
* A new abstraction interface called `BlobSource` for blob read logic gives all users access to blobs, whether they are in the blob cache, secondary cache, or (remote) storage. Blobs can be potentially read both while handling user reads (`Get`, `MultiGet`, or iterator) and during compaction (while dealing with compaction filters, Merges, or garbage collection) but eventually all blob reads go through `Version::GetBlob` or, for MultiGet, `Version::MultiGetBlob` (and then get dispatched to the interface -- `BlobSource`).
|
||||
* Add experimental tiered compaction feature `AdvancedColumnFamilyOptions::preclude_last_level_data_seconds`, which makes sure the new data inserted within preclude_last_level_data_seconds won't be placed on cold tier (the feature is not complete).
|
||||
|
||||
### Public API changes
|
||||
* Add metadata related structs and functions in C API, including
|
||||
@@ -19,18 +34,25 @@
|
||||
* When using block cache strict capacity limit (`LRUCache` with `strict_capacity_limit=true`), DB operations now fail with Status code `kAborted` subcode `kMemoryLimit` (`IsMemoryLimit()`) instead of `kIncomplete` (`IsIncomplete()`) when the capacity limit is reached, because Incomplete can mean other specific things for some operations. In more detail, `Cache::Insert()` now returns the updated Status code and this usually propagates through RocksDB to the user on failure.
|
||||
* NewClockCache calls temporarily return an LRUCache (with similar characteristics as the desired ClockCache). This is because ClockCache is being replaced by a new version (the old one had unknown bugs) but this is still under development.
|
||||
* Add two functions `int ReserveThreads(int threads_to_be_reserved)` and `int ReleaseThreads(threads_to_be_released)` into `Env` class. In the default implementation, both return 0. Newly added `xxxEnv` class that inherits `Env` should implement these two functions for thread reservation/releasing features.
|
||||
* Removed Customizable support for RateLimiter and removed its CreateFromString() and Type() functions.
|
||||
|
||||
### Bug Fixes
|
||||
* Fix a bug in which backup/checkpoint can include a WAL deleted by RocksDB.
|
||||
* Fix a bug where concurrent compactions might cause unnecessary further write stalling. In some cases, this might cause write rate to drop to minimum.
|
||||
* Fix a bug in Logger where if dbname and db_log_dir are on different filesystems, dbname creation would fail wrt to db_log_dir path returning an error and fails to open the DB.
|
||||
* Fix a CPU and memory efficiency issue introduce by https://github.com/facebook/rocksdb/pull/8336 which made InternalKeyComparator configurable as an unintended side effect.
|
||||
* Fix a CPU and memory efficiency issue introduce by https://github.com/facebook/rocksdb/pull/8336 which made InternalKeyComparator configurable as an unintended side effect
|
||||
* Fix a bug where `GenericRateLimiter` could revert the bandwidth set dynamically using `SetBytesPerSecond()` when a user configures a structure enclosing it, e.g., using `GetOptionsFromString()` to configure an `Options` that references an existing `RateLimiter` object.
|
||||
|
||||
## Behavior Change
|
||||
* In leveled compaction with dynamic levelling, level multiplier is not anymore adjusted due to oversized L0. Instead, compaction score is adjusted by increasing size level target by adding incoming bytes from upper levels. This would deprioritize compactions from upper levels if more data from L0 is coming. This is to fix some unnecessary full stalling due to drastic change of level targets, while not wasting write bandwidth for compaction while writes are overloaded.
|
||||
* For track_and_verify_wals_in_manifest, revert to the original behavior before #10087: syncing of live WAL file is not tracked, and we track only the synced sizes of **closed** WALs. (PR #10330).
|
||||
* WAL compression now computes/verifies checksum during compression/decompression.
|
||||
|
||||
### Performance Improvements
|
||||
* Rather than doing total sort against all files in a level, SortFileByOverlappingRatio() to only find the top 50 files based on score. This can improve write throughput for the use cases where data is loaded in increasing key order and there are a lot of files in one LSM-tree, where applying compaction results is the bottleneck.
|
||||
* In leveled compaction, L0->L1 trivial move will allow more than one file to be moved in one compaction. This would allow L0 files to be moved down faster when data is loaded in sequential order, making slowdown or stop condition harder to hit. Also seek L0->L1 trivial move when only some files qualify.
|
||||
* In leveled compaction, try to trivial move more than one files if possible, up to 4 files or max_compaction_bytes. This is to allow higher write throughput for some use cases where data is loaded in sequential order, where appying compaction results is the bottleneck.
|
||||
|
||||
## 7.4.0 (06/19/2022)
|
||||
### Bug Fixes
|
||||
* Fixed a bug in calculating key-value integrity protection for users of in-place memtable updates. In particular, the affected users would be those who configure `protection_bytes_per_key > 0` on `WriteBatch` or `WriteOptions`, and configure `inplace_callback != nullptr`.
|
||||
@@ -85,11 +107,6 @@
|
||||
### Performance Improvements
|
||||
* When compiled with folly (Meta-internal integration; experimental in open source build), improve the locking performance (CPU efficiency) of LRUCache by using folly DistributedMutex in place of standard mutex.
|
||||
|
||||
### Performance Improvements
|
||||
* Rather than doing total sort against all files in a level, SortFileByOverlappingRatio() to only find the top 50 files based on score. This can improve write throughput for the use cases where data is loaded in increasing key order and there are a lot of files in one LSM-tree, where applying compaction results is the bottleneck.
|
||||
* In leveled compaction, L0->L1 trivial move will allow more than one file to be moved in one compaction. This would allow L0 files to be moved down faster when data is loaded in sequential order, making slowdown or stop condition harder to hit. Also seek L0->L1 trivial move when only some files qualify.
|
||||
* In leveled compaction, try to trivial move more than one files if possible, up to 4 files or max_compaction_bytes. This is to allow higher write throughput for some use cases where data is loaded in sequential order, where appying compaction results is the bottleneck.
|
||||
|
||||
## 7.3.0 (05/20/2022)
|
||||
### Bug Fixes
|
||||
* Fixed a bug where manual flush would block forever even though flush options had wait=false.
|
||||
|
||||
@@ -1504,6 +1504,9 @@ db_table_properties_test: $(OBJ_DIR)/db/db_table_properties_test.o $(TEST_LIBRAR
|
||||
log_write_bench: $(OBJ_DIR)/util/log_write_bench.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK) $(PROFILING_FLAGS)
|
||||
|
||||
seqno_time_test: $(OBJ_DIR)/db/seqno_time_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
plain_table_db_test: $(OBJ_DIR)/db/plain_table_db_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/range_del_aggregator.cc",
|
||||
"db/range_tombstone_fragmenter.cc",
|
||||
"db/repair.cc",
|
||||
"db/seqno_to_time_mapping.cc",
|
||||
"db/snapshot_impl.cc",
|
||||
"db/table_cache.cc",
|
||||
"db/table_properties_collector.cc",
|
||||
@@ -419,6 +420,7 @@ cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[
|
||||
"db/range_del_aggregator.cc",
|
||||
"db/range_tombstone_fragmenter.cc",
|
||||
"db/repair.cc",
|
||||
"db/seqno_to_time_mapping.cc",
|
||||
"db/snapshot_impl.cc",
|
||||
"db/table_cache.cc",
|
||||
"db/table_properties_collector.cc",
|
||||
@@ -5688,6 +5690,12 @@ cpp_unittest_wrapper(name="ribbon_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="seqno_time_test",
|
||||
srcs=["db/seqno_time_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="sim_cache_test",
|
||||
srcs=["utilities/simulator_cache/sim_cache_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
|
||||
Vendored
+17
-7
@@ -74,6 +74,11 @@ DEFINE_uint32(
|
||||
DEFINE_uint32(gather_stats_entries_per_lock, 256,
|
||||
"For Cache::ApplyToAllEntries");
|
||||
DEFINE_bool(skewed, false, "If true, skew the key access distribution");
|
||||
|
||||
DEFINE_bool(lean, false,
|
||||
"If true, no additional computation is performed besides cache "
|
||||
"operations.");
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
DEFINE_string(secondary_cache_uri, "",
|
||||
"Full URI for creating a custom secondary cache object");
|
||||
@@ -522,7 +527,6 @@ class CacheBench {
|
||||
StopWatchNano timer(clock);
|
||||
|
||||
for (uint64_t i = 0; i < FLAGS_ops_per_thread; i++) {
|
||||
timer.Start();
|
||||
Slice key = gen.GetRand(thread->rnd, max_key_, max_log_);
|
||||
uint64_t random_op = thread->rnd.Next();
|
||||
Cache::CreateCallback create_cb = [](const void* buf, size_t size,
|
||||
@@ -534,6 +538,8 @@ class CacheBench {
|
||||
return Status::OK();
|
||||
};
|
||||
|
||||
timer.Start();
|
||||
|
||||
if (random_op < lookup_insert_threshold_) {
|
||||
if (handle) {
|
||||
cache_->Release(handle);
|
||||
@@ -543,9 +549,11 @@ class CacheBench {
|
||||
handle = cache_->Lookup(key, &helper2, create_cb, Cache::Priority::LOW,
|
||||
true);
|
||||
if (handle) {
|
||||
// do something with the data
|
||||
result += NPHash64(static_cast<char*>(cache_->Value(handle)),
|
||||
FLAGS_value_bytes);
|
||||
if (!FLAGS_lean) {
|
||||
// do something with the data
|
||||
result += NPHash64(static_cast<char*>(cache_->Value(handle)),
|
||||
FLAGS_value_bytes);
|
||||
}
|
||||
} else {
|
||||
// do insert
|
||||
Status s = cache_->Insert(key, createValue(thread->rnd), &helper2,
|
||||
@@ -570,9 +578,11 @@ class CacheBench {
|
||||
handle = cache_->Lookup(key, &helper2, create_cb, Cache::Priority::LOW,
|
||||
true);
|
||||
if (handle) {
|
||||
// do something with the data
|
||||
result += NPHash64(static_cast<char*>(cache_->Value(handle)),
|
||||
FLAGS_value_bytes);
|
||||
if (!FLAGS_lean) {
|
||||
// do something with the data
|
||||
result += NPHash64(static_cast<char*>(cache_->Value(handle)),
|
||||
FLAGS_value_bytes);
|
||||
}
|
||||
}
|
||||
} else if (random_op < erase_threshold_) {
|
||||
// do erase
|
||||
|
||||
Vendored
+273
-183
@@ -37,133 +37,191 @@ ClockHandleTable::ClockHandleTable(int hash_bits)
|
||||
}
|
||||
|
||||
ClockHandleTable::~ClockHandleTable() {
|
||||
ApplyToEntriesRange([](ClockHandle* h) { h->FreeData(); }, 0, GetTableSize());
|
||||
ApplyToEntriesRange([](ClockHandle* h) { h->FreeData(); }, 0, GetTableSize(),
|
||||
true);
|
||||
}
|
||||
|
||||
ClockHandle* ClockHandleTable::Lookup(const Slice& key) {
|
||||
int probe = 0;
|
||||
int slot = FindVisibleElement(key, probe, 0);
|
||||
ClockHandle* ClockHandleTable::Lookup(const Slice& key, uint32_t hash) {
|
||||
uint32_t probe = 0;
|
||||
int slot = FindElement(key, hash, probe);
|
||||
return (slot == -1) ? nullptr : &array_[slot];
|
||||
}
|
||||
|
||||
ClockHandle* ClockHandleTable::Insert(ClockHandle* h, ClockHandle** old) {
|
||||
int probe = 0;
|
||||
int slot =
|
||||
FindVisibleElementOrAvailableSlot(h->key(), probe, 1 /*displacement*/);
|
||||
uint32_t probe = 0;
|
||||
int slot = FindElementOrAvailableSlot(h->key(), h->hash, probe);
|
||||
*old = nullptr;
|
||||
if (slot == -1) {
|
||||
// The key is not already present, and there's no available slot to place
|
||||
// the new copy.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (array_[slot].IsEmpty() || array_[slot].IsTombstone()) {
|
||||
bool empty = array_[slot].IsEmpty();
|
||||
Assign(slot, h);
|
||||
if (!array_[slot].IsElement()) {
|
||||
// The slot is empty or is a tombstone.
|
||||
ClockHandle* new_entry = &array_[slot];
|
||||
if (empty) {
|
||||
// This used to be an empty slot.
|
||||
new_entry->InternalToExclusiveRef();
|
||||
Assign(new_entry, h);
|
||||
if (new_entry->displacements == 0) {
|
||||
// The slot was empty.
|
||||
return new_entry;
|
||||
}
|
||||
// It used to be a tombstone, so there may already be a copy of the
|
||||
// key in the table.
|
||||
slot = FindVisibleElement(h->key(), probe, 0 /*displacement*/);
|
||||
slot = FindElement(h->key(), h->hash, probe);
|
||||
if (slot == -1) {
|
||||
// No existing copy of the key.
|
||||
// Nope, no existing copy of the key.
|
||||
return new_entry;
|
||||
}
|
||||
*old = &array_[slot];
|
||||
ClockHandle* old_entry = &array_[slot];
|
||||
old_entry->ReleaseInternalRef();
|
||||
*old = old_entry;
|
||||
return new_entry;
|
||||
} else {
|
||||
// There is an existing copy of the key.
|
||||
*old = &array_[slot];
|
||||
ClockHandle* old_entry = &array_[slot];
|
||||
old_entry->ReleaseInternalRef();
|
||||
*old = old_entry;
|
||||
// Find an available slot for the new element.
|
||||
array_[slot].displacements++;
|
||||
slot = FindAvailableSlot(h->key(), probe, 1 /*displacement*/);
|
||||
old_entry->displacements++;
|
||||
slot = FindAvailableSlot(h->key(), probe);
|
||||
if (slot == -1) {
|
||||
// No available slots. Roll back displacements.
|
||||
probe = 0;
|
||||
slot = FindVisibleElement(h->key(), probe, -1);
|
||||
array_[slot].displacements--;
|
||||
FindAvailableSlot(h->key(), probe, -1);
|
||||
// No available slots.
|
||||
return nullptr;
|
||||
}
|
||||
Assign(slot, h);
|
||||
return &array_[slot];
|
||||
ClockHandle* new_entry = &array_[slot];
|
||||
new_entry->InternalToExclusiveRef();
|
||||
Assign(new_entry, h);
|
||||
return new_entry;
|
||||
}
|
||||
}
|
||||
|
||||
void ClockHandleTable::Remove(ClockHandle* h) {
|
||||
assert(!h->IsInClockList()); // Already off the clock list.
|
||||
int probe = 0;
|
||||
assert(!h->IsInClock()); // Already off clock.
|
||||
uint32_t probe = 0;
|
||||
FindSlot(
|
||||
h->key(), [&h](ClockHandle* e) { return e == h; }, probe,
|
||||
-1 /*displacement*/);
|
||||
h->SetIsVisible(false);
|
||||
h->key(), [&](ClockHandle* e) { return e == h; },
|
||||
[&](ClockHandle* /*e*/) { return false; },
|
||||
[&](ClockHandle* e) { e->displacements--; }, probe);
|
||||
h->SetWillBeDeleted(false);
|
||||
h->SetIsElement(false);
|
||||
occupancy_--;
|
||||
}
|
||||
|
||||
void ClockHandleTable::Assign(int slot, ClockHandle* h) {
|
||||
ClockHandle* dst = &array_[slot];
|
||||
uint32_t disp = dst->displacements;
|
||||
*dst = *h;
|
||||
dst->displacements = disp;
|
||||
dst->SetIsVisible(true);
|
||||
void ClockHandleTable::Assign(ClockHandle* dst, ClockHandle* src) {
|
||||
// DON'T touch displacements and refs.
|
||||
dst->value = src->value;
|
||||
dst->deleter = src->deleter;
|
||||
dst->hash = src->hash;
|
||||
dst->total_charge = src->total_charge;
|
||||
dst->key_data = src->key_data;
|
||||
dst->flags.store(0);
|
||||
dst->SetIsElement(true);
|
||||
dst->SetClockPriority(ClockHandle::ClockPriority::NONE);
|
||||
dst->SetCachePriority(src->GetCachePriority());
|
||||
occupancy_++;
|
||||
}
|
||||
|
||||
void ClockHandleTable::Exclude(ClockHandle* h) { h->SetIsVisible(false); }
|
||||
|
||||
int ClockHandleTable::FindVisibleElement(const Slice& key, int& probe,
|
||||
int displacement) {
|
||||
return FindSlot(
|
||||
key, [&](ClockHandle* h) { return h->Matches(key) && h->IsVisible(); },
|
||||
probe, displacement);
|
||||
}
|
||||
|
||||
int ClockHandleTable::FindAvailableSlot(const Slice& key, int& probe,
|
||||
int displacement) {
|
||||
return FindSlot(
|
||||
key, [](ClockHandle* h) { return h->IsEmpty() || h->IsTombstone(); },
|
||||
probe, displacement);
|
||||
}
|
||||
|
||||
int ClockHandleTable::FindVisibleElementOrAvailableSlot(const Slice& key,
|
||||
int& probe,
|
||||
int displacement) {
|
||||
int ClockHandleTable::FindElement(const Slice& key, uint32_t hash,
|
||||
uint32_t& probe) {
|
||||
return FindSlot(
|
||||
key,
|
||||
[&](ClockHandle* h) {
|
||||
return h->IsEmpty() || h->IsTombstone() ||
|
||||
(h->Matches(key) && h->IsVisible());
|
||||
if (h->TryInternalRef()) {
|
||||
if (h->Matches(key, hash)) {
|
||||
return true;
|
||||
}
|
||||
h->ReleaseInternalRef();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
probe, displacement);
|
||||
[&](ClockHandle* h) { return h->displacements == 0; },
|
||||
[&](ClockHandle* /*h*/) {}, probe);
|
||||
}
|
||||
|
||||
inline int ClockHandleTable::FindSlot(const Slice& key,
|
||||
std::function<bool(ClockHandle*)> cond,
|
||||
int& probe, int displacement) {
|
||||
int ClockHandleTable::FindAvailableSlot(const Slice& key, uint32_t& probe) {
|
||||
int slot = FindSlot(
|
||||
key,
|
||||
[&](ClockHandle* h) {
|
||||
if (h->TryInternalRef()) {
|
||||
if (!h->IsElement()) {
|
||||
return true;
|
||||
}
|
||||
h->ReleaseInternalRef();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[&](ClockHandle* /*h*/) { return false; },
|
||||
[&](ClockHandle* h) { h->displacements++; }, probe);
|
||||
if (slot == -1) {
|
||||
Rollback(key, probe);
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
int ClockHandleTable::FindElementOrAvailableSlot(const Slice& key,
|
||||
uint32_t hash,
|
||||
uint32_t& probe) {
|
||||
int slot = FindSlot(
|
||||
key,
|
||||
[&](ClockHandle* h) {
|
||||
if (h->TryInternalRef()) {
|
||||
if (!h->IsElement() || h->Matches(key, hash)) {
|
||||
return true;
|
||||
}
|
||||
h->ReleaseInternalRef();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[&](ClockHandle* /*h*/) { return false; },
|
||||
[&](ClockHandle* h) { h->displacements++; }, probe);
|
||||
if (slot == -1) {
|
||||
Rollback(key, probe);
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
int ClockHandleTable::FindSlot(const Slice& key,
|
||||
std::function<bool(ClockHandle*)> match,
|
||||
std::function<bool(ClockHandle*)> abort,
|
||||
std::function<void(ClockHandle*)> update,
|
||||
uint32_t& probe) {
|
||||
// We use double-hashing probing. Every probe in the sequence is a
|
||||
// pseudorandom integer, computed as a linear function of two random hashes,
|
||||
// which we call base and increment. Specifically, the i-th probe is base + i
|
||||
// * increment modulo the table size.
|
||||
uint32_t base = ModTableSize(Hash(key.data(), key.size(), kProbingSeed1));
|
||||
// We use an odd increment, which is relatively prime with the power-of-two
|
||||
// table size. This implies that we cycle back to the first probe only
|
||||
// after probing every slot exactly once.
|
||||
uint32_t increment =
|
||||
ModTableSize((Hash(key.data(), key.size(), kProbingSeed2) << 1) | 1);
|
||||
uint32_t current = ModTableSize(base + probe * increment);
|
||||
while (true) {
|
||||
ClockHandle* h = &array_[current];
|
||||
probe++;
|
||||
if (current == base && probe > 1) {
|
||||
if (current == base && probe > 0) {
|
||||
// We looped back.
|
||||
return -1;
|
||||
}
|
||||
if (cond(h)) {
|
||||
if (match(h)) {
|
||||
probe++;
|
||||
return current;
|
||||
}
|
||||
if (h->IsEmpty()) {
|
||||
// We check emptyness after the condition, because
|
||||
// the condition may be emptyness.
|
||||
if (abort(h)) {
|
||||
return -1;
|
||||
}
|
||||
h->displacements += displacement;
|
||||
probe++;
|
||||
update(h);
|
||||
current = ModTableSize(current + increment);
|
||||
}
|
||||
}
|
||||
|
||||
void ClockHandleTable::Rollback(const Slice& key, uint32_t probe) {
|
||||
uint32_t current = ModTableSize(Hash(key.data(), key.size(), kProbingSeed1));
|
||||
uint32_t increment =
|
||||
ModTableSize((Hash(key.data(), key.size(), kProbingSeed2) << 1) | 1);
|
||||
for (uint32_t i = 0; i < probe; i++) {
|
||||
array_[current].displacements--;
|
||||
current = ModTableSize(current + increment);
|
||||
}
|
||||
}
|
||||
@@ -176,8 +234,7 @@ ClockCacheShard::ClockCacheShard(
|
||||
clock_pointer_(0),
|
||||
table_(
|
||||
CalcHashBits(capacity, estimated_value_size, metadata_charge_policy)),
|
||||
usage_(0),
|
||||
clock_usage_(0) {
|
||||
usage_(0) {
|
||||
set_metadata_charge_policy(metadata_charge_policy);
|
||||
}
|
||||
|
||||
@@ -185,22 +242,16 @@ void ClockCacheShard::EraseUnRefEntries() {
|
||||
autovector<ClockHandle> last_reference_list;
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
uint32_t slot = 0;
|
||||
do {
|
||||
ClockHandle* old = &(table_.array_[slot]);
|
||||
if (!old->IsInClockList()) {
|
||||
continue;
|
||||
}
|
||||
ClockRemove(old);
|
||||
table_.Remove(old);
|
||||
assert(usage_ >= old->total_charge);
|
||||
usage_ -= old->total_charge;
|
||||
last_reference_list.push_back(*old);
|
||||
slot = table_.ModTableSize(slot + 1);
|
||||
} while (slot != 0);
|
||||
table_.ApplyToEntriesRange(
|
||||
[this, &last_reference_list](ClockHandle* h) {
|
||||
// Externally unreferenced element.
|
||||
last_reference_list.push_back(*h);
|
||||
Evict(h);
|
||||
},
|
||||
0, table_.GetTableSize(), true);
|
||||
}
|
||||
|
||||
// Free the entries here outside of mutex for performance reasons.
|
||||
// Free the entry outside of the mutex for performance reasons.
|
||||
for (auto& h : last_reference_list) {
|
||||
h.FreeData();
|
||||
}
|
||||
@@ -238,45 +289,60 @@ void ClockCacheShard::ApplyToSomeEntries(
|
||||
callback(h->key(), h->value, h->GetCharge(metadata_charge_policy),
|
||||
h->deleter);
|
||||
},
|
||||
index_begin, index_end);
|
||||
index_begin, index_end, false);
|
||||
}
|
||||
|
||||
void ClockCacheShard::ClockRemove(ClockHandle* h) {
|
||||
assert(h->IsInClockList());
|
||||
void ClockCacheShard::ClockOff(ClockHandle* h) {
|
||||
h->SetClockPriority(ClockHandle::ClockPriority::NONE);
|
||||
assert(clock_usage_ >= h->total_charge);
|
||||
clock_usage_ -= h->total_charge;
|
||||
}
|
||||
|
||||
void ClockCacheShard::ClockInsert(ClockHandle* h) {
|
||||
assert(!h->IsInClockList());
|
||||
void ClockCacheShard::ClockOn(ClockHandle* h) {
|
||||
assert(!h->IsInClock());
|
||||
bool is_high_priority =
|
||||
h->HasHit() || h->GetCachePriority() == Cache::Priority::HIGH;
|
||||
h->SetClockPriority(static_cast<ClockHandle::ClockPriority>(
|
||||
is_high_priority * ClockHandle::ClockPriority::HIGH +
|
||||
(1 - is_high_priority) * ClockHandle::ClockPriority::MEDIUM));
|
||||
clock_usage_ += h->total_charge;
|
||||
}
|
||||
|
||||
void ClockCacheShard::Evict(ClockHandle* h) {
|
||||
ClockOff(h);
|
||||
table_.Remove(h);
|
||||
assert(usage_ >= h->total_charge);
|
||||
usage_ -= h->total_charge;
|
||||
}
|
||||
|
||||
void ClockCacheShard::EvictFromClock(size_t charge,
|
||||
autovector<ClockHandle>* deleted) {
|
||||
// TODO(Guido) When an element is in the probe sequence of a
|
||||
// hot element, it will be hard to get an exclusive ref.
|
||||
// We may need a mechanism to avoid that an element sits forever
|
||||
// in cache waiting to be evicted.
|
||||
assert(charge <= capacity_);
|
||||
while (clock_usage_ > 0 && (usage_ + charge) > capacity_) {
|
||||
ClockHandle* old = &table_.array_[clock_pointer_];
|
||||
uint32_t max_iterations = table_.GetTableSize();
|
||||
while (usage_ + charge > capacity_ && max_iterations--) {
|
||||
ClockHandle* h = &table_.array_[clock_pointer_];
|
||||
clock_pointer_ = table_.ModTableSize(clock_pointer_ + 1);
|
||||
// Clock list contains only elements which can be evicted.
|
||||
if (!old->IsInClockList()) {
|
||||
continue;
|
||||
|
||||
if (h->TryExclusiveRef()) {
|
||||
if (!h->IsInClock() && h->IsElement()) {
|
||||
// We adjust the clock priority to make the element evictable again.
|
||||
// Why? Elements that are not in clock are either currently
|
||||
// externally referenced or used to be---because we are holding an
|
||||
// exclusive ref, we know we are in the latter case. This can only
|
||||
// happen when the last external reference to an element was released,
|
||||
// and the element was not immediately removed.
|
||||
ClockOn(h);
|
||||
}
|
||||
|
||||
if (h->GetClockPriority() == ClockHandle::ClockPriority::LOW) {
|
||||
deleted->push_back(*h);
|
||||
Evict(h);
|
||||
} else if (h->GetClockPriority() > ClockHandle::ClockPriority::LOW) {
|
||||
h->DecreaseClockPriority();
|
||||
}
|
||||
h->ReleaseExclusiveRef();
|
||||
}
|
||||
if (old->GetClockPriority() == ClockHandle::ClockPriority::LOW) {
|
||||
ClockRemove(old);
|
||||
table_.Remove(old);
|
||||
assert(usage_ >= old->total_charge);
|
||||
usage_ -= old->total_charge;
|
||||
deleted->push_back(*old);
|
||||
return;
|
||||
}
|
||||
old->DecreaseClockPriority();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,13 +375,14 @@ void ClockCacheShard::SetCapacity(size_t capacity) {
|
||||
EvictFromClock(0, &last_reference_list);
|
||||
}
|
||||
|
||||
// Free the entries here outside of mutex for performance reasons.
|
||||
// Free the entry outside of the mutex for performance reasons.
|
||||
for (auto& h : last_reference_list) {
|
||||
h.FreeData();
|
||||
}
|
||||
}
|
||||
|
||||
void ClockCacheShard::SetStrictCapacityLimit(bool strict_capacity_limit) {
|
||||
assert(false); // Not supported. TODO(Guido) Support it?
|
||||
DMutexLock l(mutex_);
|
||||
strict_capacity_limit_ = strict_capacity_limit;
|
||||
}
|
||||
@@ -343,9 +410,10 @@ Status ClockCacheShard::Insert(const Slice& key, uint32_t hash, void* value,
|
||||
autovector<ClockHandle> last_reference_list;
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
|
||||
assert(table_.GetOccupancy() <= table_.GetOccupancyLimit());
|
||||
// Free the space following strict clock policy until enough space
|
||||
// is freed or the clock list is empty.
|
||||
// is freed or there are no evictable elements.
|
||||
EvictFromClock(tmp.total_charge, &last_reference_list);
|
||||
if ((usage_ + tmp.total_charge > capacity_ &&
|
||||
(strict_capacity_limit_ || handle == nullptr)) ||
|
||||
@@ -376,30 +444,29 @@ Status ClockCacheShard::Insert(const Slice& key, uint32_t hash, void* value,
|
||||
usage_ += h->total_charge;
|
||||
if (old != nullptr) {
|
||||
s = Status::OkOverwritten();
|
||||
assert(old->IsVisible());
|
||||
table_.Exclude(old);
|
||||
if (!old->HasRefs()) {
|
||||
// old is in clock because it's in cache and its reference count is 0.
|
||||
ClockRemove(old);
|
||||
table_.Remove(old);
|
||||
assert(usage_ >= old->total_charge);
|
||||
usage_ -= old->total_charge;
|
||||
assert(!old->WillBeDeleted());
|
||||
old->SetWillBeDeleted(true);
|
||||
// Try to evict the old copy of the element.
|
||||
if (old->TryExclusiveRef()) {
|
||||
last_reference_list.push_back(*old);
|
||||
Evict(old);
|
||||
old->ReleaseExclusiveRef();
|
||||
}
|
||||
}
|
||||
if (handle == nullptr) {
|
||||
ClockInsert(h);
|
||||
// If the user didn't provide a handle, no reference is taken,
|
||||
// so we make the element evictable.
|
||||
ClockOn(h);
|
||||
h->ReleaseExclusiveRef();
|
||||
} else {
|
||||
// If caller already holds a ref, no need to take one here.
|
||||
if (!h->HasRefs()) {
|
||||
h->Ref();
|
||||
}
|
||||
// The caller already holds a ref.
|
||||
h->ExclusiveToExternalRef();
|
||||
*handle = reinterpret_cast<Cache::Handle*>(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Free the entries here outside of mutex for performance reasons.
|
||||
// Free the entry outside of the mutex for performance reasons.
|
||||
for (auto& h : last_reference_list) {
|
||||
h.FreeData();
|
||||
}
|
||||
@@ -407,95 +474,102 @@ Status ClockCacheShard::Insert(const Slice& key, uint32_t hash, void* value,
|
||||
return s;
|
||||
}
|
||||
|
||||
Cache::Handle* ClockCacheShard::Lookup(const Slice& key, uint32_t /* hash */) {
|
||||
Cache::Handle* ClockCacheShard::Lookup(const Slice& key, uint32_t hash) {
|
||||
ClockHandle* h = nullptr;
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
h = table_.Lookup(key);
|
||||
if (h != nullptr) {
|
||||
assert(h->IsVisible());
|
||||
if (!h->HasRefs()) {
|
||||
// The entry is in clock since it's in the hash table and has no
|
||||
// external references.
|
||||
ClockRemove(h);
|
||||
}
|
||||
h->Ref();
|
||||
h->SetHit();
|
||||
}
|
||||
h = table_.Lookup(key, hash);
|
||||
if (h != nullptr) {
|
||||
// TODO(Guido) Comment from #10347: Here it looks like we have three atomic
|
||||
// updates where it would be possible to combine into one CAS (more metadata
|
||||
// under one atomic field) or maybe two atomic updates (one arithmetic, one
|
||||
// bitwise). Something to think about optimizing.
|
||||
h->InternalToExternalRef();
|
||||
h->SetHit();
|
||||
// The handle is now referenced, so we take it out of clock.
|
||||
ClockOff(h);
|
||||
}
|
||||
return reinterpret_cast<Cache::Handle*>(h);
|
||||
}
|
||||
|
||||
bool ClockCacheShard::Ref(Cache::Handle* h) {
|
||||
ClockHandle* e = reinterpret_cast<ClockHandle*>(h);
|
||||
DMutexLock l(mutex_);
|
||||
// To create another reference - entry must be already externally referenced.
|
||||
assert(e->HasRefs());
|
||||
e->Ref();
|
||||
return true;
|
||||
assert(e->HasExternalRefs());
|
||||
return e->TryExternalRef();
|
||||
}
|
||||
|
||||
bool ClockCacheShard::Release(Cache::Handle* handle, bool erase_if_last_ref) {
|
||||
// In contrast with LRUCache's Release, this function won't delete the handle
|
||||
// when the reference is the last one and the cache is above capacity. Space
|
||||
// is only freed up by EvictFromClock (called by Insert when space is needed)
|
||||
// and Erase.
|
||||
if (handle == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ClockHandle* h = reinterpret_cast<ClockHandle*>(handle);
|
||||
ClockHandle copy;
|
||||
bool last_reference = false;
|
||||
assert(!h->IsInClockList());
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
last_reference = h->Unref();
|
||||
if (last_reference && h->IsVisible()) {
|
||||
// The item is still in cache, and nobody else holds a reference to it.
|
||||
if (usage_ > capacity_ || erase_if_last_ref) {
|
||||
// The clock list must be empty since the cache is full.
|
||||
assert(clock_usage_ == 0 || erase_if_last_ref);
|
||||
// Take this opportunity and remove the item.
|
||||
table_.Remove(h);
|
||||
uint32_t hash = h->hash;
|
||||
uint32_t refs = h->ReleaseExternalRef();
|
||||
bool last_reference = !(refs & ClockHandle::EXTERNAL_REFS);
|
||||
bool will_be_deleted = refs & ClockHandle::WILL_BE_DELETED;
|
||||
|
||||
if (last_reference && (will_be_deleted || erase_if_last_ref)) {
|
||||
// At this point we want to evict the element, so we need to take
|
||||
// a lock and an exclusive reference. But there's a problem:
|
||||
// as soon as we released the last reference, an Insert or Erase could've
|
||||
// replaced this element, and by the time we take the lock and ref
|
||||
// we could potentially be referencing a different element.
|
||||
// Thus, before evicting the (potentially different) element, we need to
|
||||
// re-check that it's unreferenced and marked as WILL_BE_DELETED, so the
|
||||
// eviction is safe. Additionally, we check that the hash doesn't change,
|
||||
// which will detect, most of the time, whether the element is a different
|
||||
// one. The bottomline is that we only guarantee that the input handle will
|
||||
// be deleted, and occasionally also another handle, but in any case all
|
||||
// deleted handles are safe to delete.
|
||||
// TODO(Guido) With lock-free inserts and deletes we may be able to
|
||||
// "atomically" transition to an exclusive ref, without creating a deadlock.
|
||||
ClockHandle copy;
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
if (h->TrySpinExclusiveRef()) {
|
||||
will_be_deleted = h->refs & ClockHandle::WILL_BE_DELETED;
|
||||
// Check that it's still safe to delete.
|
||||
if (h->IsElement() && (will_be_deleted || erase_if_last_ref) &&
|
||||
h->hash == hash) {
|
||||
copy = *h;
|
||||
Evict(h);
|
||||
}
|
||||
h->ReleaseExclusiveRef();
|
||||
} else {
|
||||
// Put the item back on the clock list, and don't free it.
|
||||
ClockInsert(h);
|
||||
last_reference = false;
|
||||
// An external ref was detected.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// If it was the last reference, then decrement the cache usage.
|
||||
if (last_reference) {
|
||||
assert(usage_ >= h->total_charge);
|
||||
usage_ -= h->total_charge;
|
||||
copy = *h;
|
||||
}
|
||||
|
||||
// Free the entry outside of the mutex for performance reasons.
|
||||
copy.FreeData();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Free the entry here outside of mutex for performance reasons.
|
||||
if (last_reference) {
|
||||
copy.FreeData();
|
||||
}
|
||||
return last_reference;
|
||||
return false;
|
||||
}
|
||||
|
||||
void ClockCacheShard::Erase(const Slice& key, uint32_t /* hash */) {
|
||||
void ClockCacheShard::Erase(const Slice& key, uint32_t hash) {
|
||||
ClockHandle copy;
|
||||
bool last_reference = false;
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
ClockHandle* h = table_.Lookup(key);
|
||||
ClockHandle* h = table_.Lookup(key, hash);
|
||||
if (h != nullptr) {
|
||||
table_.Exclude(h);
|
||||
if (!h->HasRefs()) {
|
||||
// The entry is in Clock since it's in cache and has no external
|
||||
// references.
|
||||
ClockRemove(h);
|
||||
table_.Remove(h);
|
||||
assert(usage_ >= h->total_charge);
|
||||
usage_ -= h->total_charge;
|
||||
last_reference = true;
|
||||
h->SetWillBeDeleted(true);
|
||||
h->ReleaseInternalRef();
|
||||
if (h->TryExclusiveRef()) {
|
||||
copy = *h;
|
||||
Evict(h);
|
||||
last_reference = true;
|
||||
h->ReleaseExclusiveRef();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Free the entry here outside of mutex for performance reasons.
|
||||
// last_reference will only be true if e != nullptr.
|
||||
// Free the entry outside of the mutex for performance reasons.
|
||||
if (last_reference) {
|
||||
copy.FreeData();
|
||||
}
|
||||
@@ -507,9 +581,25 @@ size_t ClockCacheShard::GetUsage() const {
|
||||
}
|
||||
|
||||
size_t ClockCacheShard::GetPinnedUsage() const {
|
||||
// Computes the pinned usage scanning the whole hash table. This
|
||||
// is slow, but avoid keeping an exact counter on the clock usage,
|
||||
// i.e., the number of not externally referenced elements.
|
||||
// Why avoid this? Because Lookup removes elements from the clock
|
||||
// list, so it would need to update the pinned usage every time,
|
||||
// which creates additional synchronization costs.
|
||||
DMutexLock l(mutex_);
|
||||
assert(usage_ >= clock_usage_);
|
||||
return usage_ - clock_usage_;
|
||||
|
||||
size_t clock_usage = 0;
|
||||
|
||||
table_.ConstApplyToEntriesRange(
|
||||
[&clock_usage](ClockHandle* h) {
|
||||
if (h->HasExternalRefs()) {
|
||||
clock_usage += h->total_charge;
|
||||
}
|
||||
},
|
||||
0, table_.GetTableSize(), true);
|
||||
|
||||
return clock_usage;
|
||||
}
|
||||
|
||||
std::string ClockCacheShard::GetPrintableOptions() const {
|
||||
|
||||
Vendored
+422
-134
@@ -10,6 +10,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
@@ -27,116 +29,254 @@ namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace clock_cache {
|
||||
|
||||
// Clock cache implementation. This is based on FastLRUCache's open-addressed
|
||||
// hash table. Importantly, it stores elements in an array, and resolves
|
||||
// collision using a probing strategy. Visibility and referenceability of
|
||||
// elements works as usual. See fast_lru_cache.h for a detailed description.
|
||||
// Block cache implementation using a lock-free open-address hash table
|
||||
// and clock eviction.
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Part 1: Handles
|
||||
//
|
||||
// The main difference with FastLRUCache is, not surprisingly, the eviction
|
||||
// algorithm
|
||||
// ---instead of an LRU list, we maintain a circular list with the elements
|
||||
// available for eviction, which the clock algorithm traverses to pick the next
|
||||
// victim. The clock list is represented using the array of handles, and we
|
||||
// simply mark those elements that are present in the list. This is done using
|
||||
// different clock flags, namely NONE, LOW, MEDIUM, HIGH, that represent
|
||||
// priorities: NONE means that the element is not part of the clock list, and
|
||||
// LOW to HIGH represent how close an element is from being evictable (LOW being
|
||||
// immediately evictable). When the clock pointer steps on an element that is
|
||||
// not immediately evictable, it decreases its priority.
|
||||
// Every slot in the hash table is a ClockHandle. A handle can be in a few
|
||||
// different states, that stem from the fact that handles can be externally
|
||||
// referenced and, thus, can't always be immediately evicted when a delete
|
||||
// operation is executed or when they are replaced by a new version (via an
|
||||
// insert of the same key). Concretely, the state of a handle is defined by the
|
||||
// following two properties:
|
||||
// (R) Externally referenced: A handle can be referenced externally, or not.
|
||||
// Importantly, a handle can be evicted if and only if it's not
|
||||
// referenced. In particular, when an handle becomes referenced, it's
|
||||
// temporarily taken out of clock until all references to it are released.
|
||||
// (M) Marked for deletion (or invisible): An handle is marked for deletion
|
||||
// when an operation attempts to delete it, but the handle is externally
|
||||
// referenced, so it can't be immediately deleted. When this mark is placed,
|
||||
// lookups will no longer be able to find it. Consequently, no more external
|
||||
// references will be taken to the handle. When a handle is marked for
|
||||
// deletion, we also say it's invisible.
|
||||
// These properties induce 4 different states, with transitions defined as
|
||||
// follows:
|
||||
// - Not M --> M: When a handle is deleted or replaced by a new version, but
|
||||
// not immediately evicted.
|
||||
// - M --> not M: This cannot happen. Once a handle is marked for deletion,
|
||||
// there is no can't go back.
|
||||
// - R --> not R: When all references to an handle are released.
|
||||
// - Not R --> R: When an unreferenced handle becomes referenced. This can only
|
||||
// happen if the handle is visible, since references to an handle can only be
|
||||
// created when it's visible.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Part 2: Hash table structure
|
||||
//
|
||||
// Internally, the cache uses an open-addressed hash table to index the handles.
|
||||
// We use tombstone counters to keep track of displacements. Probes are
|
||||
// generated with double-hashing (but the code can be easily modified to use
|
||||
// other probing schemes, like linear hashing). Because of the tombstones and
|
||||
// the two possible visibility states of a handle, the table slots (we use the
|
||||
// word "slot" to refer to handles that are not necessary valid key-value
|
||||
// elements) can be in 4 different states:
|
||||
// 1. Visible element: The slot contains an element in not M state.
|
||||
// 2. To-be-deleted element: The slot contains an element in M state.
|
||||
// 3. Tombstone: The slot doesn't contain an element, but there is some other
|
||||
// element that probed this slot during its insertion.
|
||||
// 4. Empty: The slot is unused.
|
||||
// When a ghost is removed from the table, it can either transition to being a
|
||||
// tombstone or an empty slot, depending on the number of displacements of the
|
||||
// slot. In any case, the slot becomes available. When a handle is inserted
|
||||
// into that slot, it becomes a visible element again.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Part 3: The clock algorithm
|
||||
//
|
||||
// We maintain a circular buffer with the handles available for eviction,
|
||||
// which the clock algorithm traverses (using a "clock pointer") to pick the
|
||||
// next victim. We use the hash table array as the circular buffer, and mark
|
||||
// the handles that are evictable. For this we use different clock flags, namely
|
||||
// NONE, LOW, MEDIUM, HIGH, that represent priorities: LOW, MEDIUM and HIGH
|
||||
// represent how close an element is from being evictable, LOW being immediately
|
||||
// evictable. NONE means the slot is not evictable. This is due to one of the
|
||||
// following reasons:
|
||||
// (i) the slot doesn't contain an element, or
|
||||
// (ii) the slot contains an element that is in R state, or
|
||||
// (iii) the slot contains an element that was in R state but it's
|
||||
// not any more, and the clock pointer has not swept through the
|
||||
// slot since the element stopped being referenced.
|
||||
//
|
||||
// The priority NONE is really only important for case (iii), as in the other
|
||||
// two cases there are other metadata fields that already capture the state.
|
||||
// When an element stops being referenced (and is not deleted), the clock
|
||||
// algorithm must acknowledge this, and assign a non-NONE priority to make
|
||||
// the element evictable again.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Part 4: Synchronization
|
||||
//
|
||||
// We provide the following synchronization guarantees:
|
||||
// - Lookup is lock-free.
|
||||
// - Release is lock-free, unless (i) no references to the element are left,
|
||||
// and (ii) it was marked for deletion or the user wishes to delete if
|
||||
// releasing the last reference.
|
||||
// - Insert and Erase still use a per-shard lock.
|
||||
//
|
||||
// Our hash table is lock-free, in the sense that system-wide progress is
|
||||
// guaranteed, i.e., some thread is always able to make progress.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
constexpr double kLoadFactor = 0.35; // See fast_lru_cache.h.
|
||||
// The load factor p is a real number in (0, 1) such that at all
|
||||
// times at most a fraction p of all slots, without counting tombstones,
|
||||
// are occupied by elements. This means that the probability that a
|
||||
// random probe hits an empty slot is at most p, and thus at most 1/p probes
|
||||
// are required on average. For example, p = 70% implies that between 1 and 2
|
||||
// probes are needed on average (bear in mind that this reasoning doesn't
|
||||
// consider the effects of clustering over time).
|
||||
// Because the size of the hash table is always rounded up to the next
|
||||
// power of 2, p is really an upper bound on the actual load factor---the
|
||||
// actual load factor is anywhere between p/2 and p. This is a bit wasteful,
|
||||
// but bear in mind that slots only hold metadata, not actual values.
|
||||
// Since space cost is dominated by the values (the LSM blocks),
|
||||
// overprovisioning the table with metadata only increases the total cache space
|
||||
// usage by a tiny fraction.
|
||||
constexpr double kLoadFactor = 0.35;
|
||||
|
||||
constexpr double kStrictLoadFactor = 0.7; // See fast_lru_cache.h.
|
||||
// The user can exceed kLoadFactor if the sizes of the inserted values don't
|
||||
// match estimated_value_size, or if strict_capacity_limit == false. To
|
||||
// avoid performance to plunge, we set a strict upper bound on the load factor.
|
||||
constexpr double kStrictLoadFactor = 0.7;
|
||||
|
||||
// Arbitrary seeds.
|
||||
constexpr uint32_t kProbingSeed1 = 0xbc9f1d34;
|
||||
constexpr uint32_t kProbingSeed2 = 0x7a2bb9d5;
|
||||
|
||||
// An experimental (under development!) alternative to LRUCache
|
||||
// An experimental (under development!) alternative to LRUCache.
|
||||
|
||||
struct ClockHandle {
|
||||
void* value;
|
||||
Cache::DeleterFn deleter;
|
||||
uint32_t hash;
|
||||
size_t total_charge; // TODO(opt): Only allow uint32_t?
|
||||
// The number of external refs to this entry.
|
||||
uint32_t refs;
|
||||
|
||||
static constexpr int kIsVisibleOffset = 0;
|
||||
static constexpr int kIsElementOffset = 1;
|
||||
static constexpr int kClockPriorityOffset = 2;
|
||||
static constexpr int kIsHitOffset = 4;
|
||||
static constexpr int kCachePriorityOffset = 5;
|
||||
|
||||
enum Flags : uint8_t {
|
||||
// Whether the handle is visible to Lookups.
|
||||
IS_VISIBLE = (1 << kIsVisibleOffset),
|
||||
// Whether the slot is in use by an element.
|
||||
IS_ELEMENT = (1 << kIsElementOffset),
|
||||
// Clock priorities. Represents how close a handle is from
|
||||
// being evictable.
|
||||
CLOCK_PRIORITY = (3 << kClockPriorityOffset),
|
||||
// Whether the handle has been looked up after its insertion.
|
||||
HAS_HIT = (1 << kIsHitOffset),
|
||||
CACHE_PRIORITY = (1 << kCachePriorityOffset),
|
||||
};
|
||||
uint8_t flags;
|
||||
|
||||
enum ClockPriority : uint8_t {
|
||||
NONE = (0 << kClockPriorityOffset), // Not an element in the eyes of clock.
|
||||
LOW = (1 << kClockPriorityOffset), // Immediately evictable.
|
||||
MEDIUM = (2 << kClockPriorityOffset),
|
||||
HIGH = (3 << kClockPriorityOffset)
|
||||
// Priority is NONE if and only if
|
||||
// (i) the handle is not an element, or
|
||||
// (ii) the handle is an element but it is being referenced.
|
||||
};
|
||||
|
||||
// The number of elements that hash to this slot or a lower one,
|
||||
// but wind up in a higher slot.
|
||||
uint32_t displacements;
|
||||
|
||||
size_t total_charge;
|
||||
std::array<char, kCacheKeySize> key_data;
|
||||
|
||||
ClockHandle() {
|
||||
value = nullptr;
|
||||
deleter = nullptr;
|
||||
hash = 0;
|
||||
total_charge = 0;
|
||||
refs = 0;
|
||||
flags = 0;
|
||||
SetIsVisible(false);
|
||||
static constexpr uint8_t kExternalRefsOffset = 0;
|
||||
static constexpr uint8_t kSharedRefsOffset = 15;
|
||||
static constexpr uint8_t kExclusiveRefOffset = 30;
|
||||
static constexpr uint8_t kWillBeDeletedOffset = 31;
|
||||
|
||||
enum Refs : uint32_t {
|
||||
// Number of external references to the slot.
|
||||
EXTERNAL_REFS = ((uint32_t{1} << 15) - 1)
|
||||
<< kExternalRefsOffset, // Bits 0, ..., 14
|
||||
// Number of internal references plus external references to the slot.
|
||||
SHARED_REFS = ((uint32_t{1} << 15) - 1)
|
||||
<< kSharedRefsOffset, // Bits 15, ..., 29
|
||||
// Whether a thread has an exclusive reference to the slot.
|
||||
EXCLUSIVE_REF = uint32_t{1} << kExclusiveRefOffset, // Bit 30
|
||||
// Whether the handle will be deleted soon. When this bit is set, new
|
||||
// internal
|
||||
// or external references to this handle stop being accepted.
|
||||
// There is an exception: external references can be created from
|
||||
// existing external references, or converting from existing internal
|
||||
// references.
|
||||
WILL_BE_DELETED = uint32_t{1} << kWillBeDeletedOffset // Bit 31
|
||||
|
||||
// Shared references (i.e., external and internal references) and exclusive
|
||||
// references are our custom implementation of RW locks---external and
|
||||
// internal references are read locks, and exclusive references are write
|
||||
// locks. We prioritize readers, which never block; in fact, they don't even
|
||||
// use compare-and-swap operations. Using our own implementation of RW locks
|
||||
// allows us to save many atomic operations by packing data more carefully.
|
||||
// In particular:
|
||||
// - Combining EXTERNAL_REFS and SHARED_REFS allows us to convert an
|
||||
// internal
|
||||
// reference into an external reference in a single atomic arithmetic
|
||||
// operation.
|
||||
// - Combining SHARED_REFS and WILL_BE_DELETED allows us to attempt to take
|
||||
// a shared reference and check whether the entry is marked for deletion
|
||||
// in a single atomic arithmetic operation.
|
||||
};
|
||||
|
||||
static constexpr uint32_t kOneInternalRef = 0x8000;
|
||||
static constexpr uint32_t kOneExternalRef = 0x8001;
|
||||
|
||||
std::atomic<uint32_t> refs;
|
||||
|
||||
static constexpr uint8_t kIsElementOffset = 1;
|
||||
static constexpr uint8_t kClockPriorityOffset = 2;
|
||||
static constexpr uint8_t kIsHitOffset = 4;
|
||||
static constexpr uint8_t kCachePriorityOffset = 5;
|
||||
|
||||
enum Flags : uint8_t {
|
||||
// Whether the slot is in use by an element.
|
||||
IS_ELEMENT = 1 << kIsElementOffset,
|
||||
// Clock priorities. Represents how close a handle is from being evictable.
|
||||
CLOCK_PRIORITY = 3 << kClockPriorityOffset,
|
||||
// Whether the handle has been looked up after its insertion.
|
||||
HAS_HIT = 1 << kIsHitOffset,
|
||||
// The value of Cache::Priority for the handle.
|
||||
CACHE_PRIORITY = 1 << kCachePriorityOffset,
|
||||
};
|
||||
|
||||
std::atomic<uint8_t> flags;
|
||||
|
||||
enum ClockPriority : uint8_t {
|
||||
NONE = (0 << kClockPriorityOffset),
|
||||
LOW = (1 << kClockPriorityOffset),
|
||||
MEDIUM = (2 << kClockPriorityOffset),
|
||||
HIGH = (3 << kClockPriorityOffset)
|
||||
};
|
||||
|
||||
// The number of elements that hash to this slot or a lower one, but wind
|
||||
// up in this slot or a higher one.
|
||||
std::atomic<uint32_t> displacements;
|
||||
|
||||
// Synchronization rules:
|
||||
// - Use a shared reference when we want the handle's identity
|
||||
// members (key_data, hash, value and IS_ELEMENT flag) to
|
||||
// remain untouched, but not modify them. The only updates
|
||||
// that a shared reference allows are:
|
||||
// * set CLOCK_PRIORITY to NONE;
|
||||
// * set the HAS_HIT bit.
|
||||
// Notice that these two types of updates are idempotent, so
|
||||
// they don't require synchronization across shared references.
|
||||
// - Use an exclusive reference when we want identity members
|
||||
// to remain untouched, as well as modify any identity member
|
||||
// or flag.
|
||||
// - displacements can be modified without holding a reference.
|
||||
// - refs is only modified through appropriate functions to
|
||||
// take or release references.
|
||||
|
||||
ClockHandle()
|
||||
: value(nullptr),
|
||||
deleter(nullptr),
|
||||
hash(0),
|
||||
total_charge(0),
|
||||
refs(0),
|
||||
flags(0),
|
||||
displacements(0) {
|
||||
SetWillBeDeleted(false);
|
||||
SetIsElement(false);
|
||||
SetClockPriority(ClockPriority::NONE);
|
||||
SetCachePriority(Cache::Priority::LOW);
|
||||
displacements = 0;
|
||||
key_data.fill(0);
|
||||
}
|
||||
|
||||
ClockHandle(const ClockHandle& other) { *this = other; }
|
||||
|
||||
void operator=(const ClockHandle& other) {
|
||||
value = other.value;
|
||||
deleter = other.deleter;
|
||||
hash = other.hash;
|
||||
total_charge = other.total_charge;
|
||||
refs.store(other.refs);
|
||||
key_data = other.key_data;
|
||||
flags.store(other.flags);
|
||||
SetWillBeDeleted(other.WillBeDeleted());
|
||||
SetIsElement(other.IsElement());
|
||||
SetClockPriority(other.GetClockPriority());
|
||||
SetCachePriority(other.GetCachePriority());
|
||||
displacements.store(other.displacements);
|
||||
}
|
||||
|
||||
Slice key() const { return Slice(key_data.data(), kCacheKeySize); }
|
||||
|
||||
// Increase the reference count by 1.
|
||||
void Ref() { refs++; }
|
||||
|
||||
// Just reduce the reference count by 1. Return true if it was last reference.
|
||||
bool Unref() {
|
||||
assert(refs > 0);
|
||||
refs--;
|
||||
return refs == 0;
|
||||
}
|
||||
|
||||
// Return true if there are external refs, false otherwise.
|
||||
bool HasRefs() const { return refs > 0; }
|
||||
|
||||
bool IsVisible() const { return flags & IS_VISIBLE; }
|
||||
|
||||
void SetIsVisible(bool is_visible) {
|
||||
if (is_visible) {
|
||||
flags |= IS_VISIBLE;
|
||||
} else {
|
||||
flags &= ~IS_VISIBLE;
|
||||
}
|
||||
}
|
||||
bool HasExternalRefs() const { return (refs & EXTERNAL_REFS) > 0; }
|
||||
|
||||
bool IsElement() const { return flags & IS_ELEMENT; }
|
||||
|
||||
@@ -144,7 +284,7 @@ struct ClockHandle {
|
||||
if (is_element) {
|
||||
flags |= IS_ELEMENT;
|
||||
} else {
|
||||
flags &= ~IS_ELEMENT;
|
||||
flags &= static_cast<uint8_t>(~IS_ELEMENT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +292,7 @@ struct ClockHandle {
|
||||
|
||||
void SetHit() { flags |= HAS_HIT; }
|
||||
|
||||
bool IsInClockList() const {
|
||||
bool IsInClock() const {
|
||||
return GetClockPriority() != ClockHandle::ClockPriority::NONE;
|
||||
}
|
||||
|
||||
@@ -164,7 +304,7 @@ struct ClockHandle {
|
||||
if (priority == Cache::Priority::HIGH) {
|
||||
flags |= Flags::CACHE_PRIORITY;
|
||||
} else {
|
||||
flags &= ~Flags::CACHE_PRIORITY;
|
||||
flags &= static_cast<uint8_t>(~Flags::CACHE_PRIORITY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +313,7 @@ struct ClockHandle {
|
||||
}
|
||||
|
||||
void SetClockPriority(ClockPriority priority) {
|
||||
flags &= ~Flags::CLOCK_PRIORITY;
|
||||
flags &= static_cast<uint8_t>(~Flags::CLOCK_PRIORITY);
|
||||
flags |= priority;
|
||||
}
|
||||
|
||||
@@ -182,14 +322,13 @@ struct ClockHandle {
|
||||
kClockPriorityOffset;
|
||||
assert(p > 0);
|
||||
p--;
|
||||
flags &= ~Flags::CLOCK_PRIORITY;
|
||||
flags &= static_cast<uint8_t>(~Flags::CLOCK_PRIORITY);
|
||||
ClockPriority new_priority =
|
||||
static_cast<ClockPriority>(p << kClockPriorityOffset);
|
||||
flags |= new_priority;
|
||||
}
|
||||
|
||||
void FreeData() {
|
||||
assert(refs == 0);
|
||||
if (deleter) {
|
||||
(*deleter)(key(), value);
|
||||
}
|
||||
@@ -232,17 +371,131 @@ struct ClockHandle {
|
||||
return total_charge - meta_charge;
|
||||
}
|
||||
|
||||
inline bool IsEmpty() {
|
||||
inline bool IsEmpty() const {
|
||||
return !this->IsElement() && this->displacements == 0;
|
||||
}
|
||||
|
||||
inline bool IsTombstone() {
|
||||
inline bool IsTombstone() const {
|
||||
return !this->IsElement() && this->displacements > 0;
|
||||
}
|
||||
|
||||
inline bool Matches(const Slice& some_key) {
|
||||
return this->IsElement() && this->key() == some_key;
|
||||
inline bool Matches(const Slice& some_key, uint32_t some_hash) const {
|
||||
return this->IsElement() && this->hash == some_hash &&
|
||||
this->key() == some_key;
|
||||
}
|
||||
|
||||
bool WillBeDeleted() const { return refs & WILL_BE_DELETED; }
|
||||
|
||||
void SetWillBeDeleted(bool will_be_deleted) {
|
||||
if (will_be_deleted) {
|
||||
refs |= WILL_BE_DELETED;
|
||||
} else {
|
||||
refs &= ~WILL_BE_DELETED;
|
||||
}
|
||||
}
|
||||
|
||||
// The following functions are for taking and releasing refs.
|
||||
|
||||
// Tries to take an external ref. Returns true iff it succeeds.
|
||||
inline bool TryExternalRef() {
|
||||
if (!((refs += kOneExternalRef) & (EXCLUSIVE_REF | WILL_BE_DELETED))) {
|
||||
return true;
|
||||
}
|
||||
refs -= kOneExternalRef;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Releases an external ref. Returns the new value (this is useful to
|
||||
// avoid an extra atomic read).
|
||||
inline uint32_t ReleaseExternalRef() { return refs -= kOneExternalRef; }
|
||||
|
||||
// Take an external ref, assuming there is already one external ref
|
||||
// to the handle.
|
||||
void Ref() {
|
||||
// TODO(Guido) Is it okay to assume that the existing external reference
|
||||
// survives until this function returns?
|
||||
refs += kOneExternalRef;
|
||||
}
|
||||
|
||||
// Tries to take an internal ref. Returns true iff it succeeds.
|
||||
inline bool TryInternalRef() {
|
||||
if (!((refs += kOneInternalRef) & (EXCLUSIVE_REF | WILL_BE_DELETED))) {
|
||||
return true;
|
||||
}
|
||||
refs -= kOneInternalRef;
|
||||
return false;
|
||||
}
|
||||
|
||||
inline void ReleaseInternalRef() { refs -= kOneInternalRef; }
|
||||
|
||||
// Tries to take an exclusive ref. Returns true iff it succeeds.
|
||||
inline bool TryExclusiveRef() {
|
||||
uint32_t will_be_deleted = refs & WILL_BE_DELETED;
|
||||
uint32_t expected = will_be_deleted;
|
||||
return refs.compare_exchange_strong(expected,
|
||||
EXCLUSIVE_REF | will_be_deleted);
|
||||
}
|
||||
|
||||
// Repeatedly tries to take an exclusive reference, but stops as soon
|
||||
// as an external reference is detected (in this case the wait would
|
||||
// presumably be too long).
|
||||
inline bool TrySpinExclusiveRef() {
|
||||
uint32_t expected = 0;
|
||||
uint32_t will_be_deleted = 0;
|
||||
while (!refs.compare_exchange_strong(expected,
|
||||
EXCLUSIVE_REF | will_be_deleted)) {
|
||||
if (expected & EXTERNAL_REFS) {
|
||||
return false;
|
||||
}
|
||||
will_be_deleted = expected & WILL_BE_DELETED;
|
||||
expected = will_be_deleted;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void ReleaseExclusiveRef() { refs.fetch_and(~EXCLUSIVE_REF); }
|
||||
|
||||
// The following functions are for upgrading and downgrading refs.
|
||||
// They guarantee atomicity, i.e., no exclusive refs to the handle
|
||||
// can be taken by a different thread during the conversion.
|
||||
|
||||
inline void ExclusiveToInternalRef() {
|
||||
refs += kOneInternalRef;
|
||||
ReleaseExclusiveRef();
|
||||
}
|
||||
|
||||
inline void ExclusiveToExternalRef() {
|
||||
refs += kOneExternalRef;
|
||||
ReleaseExclusiveRef();
|
||||
}
|
||||
|
||||
// TODO(Guido) Do we want to bound the loop and prepare the
|
||||
// algorithms to react to a failure?
|
||||
inline void InternalToExclusiveRef() {
|
||||
uint32_t expected = kOneInternalRef;
|
||||
uint32_t will_be_deleted = 0;
|
||||
while (!refs.compare_exchange_strong(expected,
|
||||
EXCLUSIVE_REF | will_be_deleted)) {
|
||||
will_be_deleted = expected & WILL_BE_DELETED;
|
||||
expected = kOneInternalRef | will_be_deleted;
|
||||
}
|
||||
}
|
||||
|
||||
inline void InternalToExternalRef() {
|
||||
refs += kOneExternalRef - kOneInternalRef;
|
||||
}
|
||||
|
||||
// TODO(Guido) Same concern.
|
||||
inline void ExternalToExclusiveRef() {
|
||||
uint32_t expected = kOneExternalRef;
|
||||
uint32_t will_be_deleted = 0;
|
||||
while (!refs.compare_exchange_strong(expected,
|
||||
EXCLUSIVE_REF | will_be_deleted)) {
|
||||
will_be_deleted = expected & WILL_BE_DELETED;
|
||||
expected = kOneExternalRef | will_be_deleted;
|
||||
}
|
||||
}
|
||||
|
||||
}; // struct ClockHandle
|
||||
|
||||
class ClockHandleTable {
|
||||
@@ -252,31 +505,54 @@ class ClockHandleTable {
|
||||
|
||||
// Returns a pointer to a visible element matching the key/hash, or
|
||||
// nullptr if not present.
|
||||
ClockHandle* Lookup(const Slice& key);
|
||||
ClockHandle* Lookup(const Slice& key, uint32_t hash);
|
||||
|
||||
// Inserts a copy of h into the hash table.
|
||||
// Returns a pointer to the inserted handle, or nullptr if no slot
|
||||
// available was found. If an existing visible element matching the
|
||||
// key/hash is already present in the hash table, the argument old
|
||||
// is set to pointe to it; otherwise, it's set to nullptr.
|
||||
// is set to point to it; otherwise, it's set to nullptr.
|
||||
// Returns an exclusive reference to h, and no references to old.
|
||||
ClockHandle* Insert(ClockHandle* h, ClockHandle** old);
|
||||
|
||||
// Removes h from the hash table. The handle must already be off
|
||||
// the clock list.
|
||||
// Removes h from the hash table. The handle must already be off clock.
|
||||
void Remove(ClockHandle* h);
|
||||
|
||||
// Turns a visible element h into a ghost (i.e., not visible).
|
||||
void Exclude(ClockHandle* h);
|
||||
|
||||
// Assigns a copy of h to the given slot.
|
||||
void Assign(int slot, ClockHandle* h);
|
||||
// Extracts the element information from a handle (src), and assigns it
|
||||
// to a hash table slot (dst). Doesn't touch displacements and refs,
|
||||
// which are maintained by the hash table algorithm.
|
||||
void Assign(ClockHandle* dst, ClockHandle* src);
|
||||
|
||||
template <typename T>
|
||||
void ApplyToEntriesRange(T func, uint32_t index_begin, uint32_t index_end) {
|
||||
void ApplyToEntriesRange(T func, uint32_t index_begin, uint32_t index_end,
|
||||
bool apply_if_will_be_deleted) {
|
||||
for (uint32_t i = index_begin; i < index_end; i++) {
|
||||
ClockHandle* h = &array_[i];
|
||||
if (h->IsVisible()) {
|
||||
func(h);
|
||||
if (h->TryExclusiveRef()) {
|
||||
if (h->IsElement() &&
|
||||
(apply_if_will_be_deleted || !h->WillBeDeleted())) {
|
||||
// Hand the internal ref over to func, which is now responsible
|
||||
// to release it.
|
||||
func(h);
|
||||
} else {
|
||||
h->ReleaseExclusiveRef();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void ConstApplyToEntriesRange(T func, uint32_t index_begin,
|
||||
uint32_t index_end,
|
||||
bool apply_if_will_be_deleted) const {
|
||||
for (uint32_t i = index_begin; i < index_end; i++) {
|
||||
ClockHandle* h = &array_[i];
|
||||
if (h->TryExclusiveRef()) {
|
||||
if (h->IsElement() &&
|
||||
(apply_if_will_be_deleted || !h->WillBeDeleted())) {
|
||||
func(h);
|
||||
}
|
||||
h->ReleaseExclusiveRef();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,28 +571,38 @@ class ClockHandleTable {
|
||||
private:
|
||||
friend class ClockCacheShard;
|
||||
|
||||
int FindVisibleElement(const Slice& key, int& probe, int displacement);
|
||||
int FindElement(const Slice& key, uint32_t hash, uint32_t& probe);
|
||||
|
||||
int FindAvailableSlot(const Slice& key, int& probe, int displacement);
|
||||
int FindAvailableSlot(const Slice& key, uint32_t& probe);
|
||||
|
||||
int FindVisibleElementOrAvailableSlot(const Slice& key, int& probe,
|
||||
int displacement);
|
||||
int FindElementOrAvailableSlot(const Slice& key, uint32_t hash,
|
||||
uint32_t& probe);
|
||||
|
||||
// Returns the index of the first slot probed (hashing with
|
||||
// the given key) with a handle e such that cond(e) is true.
|
||||
// Otherwise, if no match is found, returns -1.
|
||||
// For every handle e probed except the final slot, updates
|
||||
// e->displacements += displacement.
|
||||
// The argument probe is modified such that consecutive calls
|
||||
// to FindSlot continue probing right after where the previous
|
||||
// call left.
|
||||
int FindSlot(const Slice& key, std::function<bool(ClockHandle*)> cond,
|
||||
int& probe, int displacement);
|
||||
// the given key) with a handle e such that match(e) is true.
|
||||
// At every step, the function first tests whether match(e) holds.
|
||||
// If it's false, it evaluates abort(e) to decide whether the
|
||||
// search should be aborted, and in the affirmative returns -1.
|
||||
// For every handle e probed except the last one, the function runs
|
||||
// update(e). We say a probe to a handle e is aborting if match(e) is
|
||||
// false and abort(e) is true. The argument probe is one more than the
|
||||
// last non-aborting probe during the call. This is so that that the
|
||||
// variable can be used to keep track of progress across consecutive
|
||||
// calls to FindSlot.
|
||||
inline int FindSlot(const Slice& key, std::function<bool(ClockHandle*)> match,
|
||||
std::function<bool(ClockHandle*)> stop,
|
||||
std::function<void(ClockHandle*)> update,
|
||||
uint32_t& probe);
|
||||
|
||||
// After a failed FindSlot call (i.e., with answer -1), this function
|
||||
// decrements all displacements, starting from the 0-th probe.
|
||||
void Rollback(const Slice& key, uint32_t probe);
|
||||
|
||||
// Number of hash bits used for table index.
|
||||
// The size of the table is 1 << length_bits_.
|
||||
int length_bits_;
|
||||
|
||||
// For faster computation of ModTableSize.
|
||||
const uint32_t length_bits_mask_;
|
||||
|
||||
// Number of elements in the table.
|
||||
@@ -345,10 +631,10 @@ class ALIGN_AS(CACHE_LINE_SIZE) ClockCacheShard final : public CacheShard {
|
||||
void SetStrictCapacityLimit(bool strict_capacity_limit) override;
|
||||
|
||||
// Like Cache methods, but with an extra "hash" parameter.
|
||||
// Insert an item into the hash table and, if handle is null, insert into
|
||||
// the clock list. Older items are evicted as necessary. If the cache is full
|
||||
// and free_handle_on_fail is true, the item is deleted and handle is set to
|
||||
// nullptr.
|
||||
// Insert an item into the hash table and, if handle is null, make it
|
||||
// evictable by the clock algorithm. Older items are evicted as necessary.
|
||||
// If the cache is full and free_handle_on_fail is true, the item is deleted
|
||||
// and handle is set to nullptr.
|
||||
Status Insert(const Slice& key, uint32_t hash, void* value, size_t charge,
|
||||
Cache::DeleterFn deleter, Cache::Handle** handle,
|
||||
Cache::Priority priority) override;
|
||||
@@ -393,13 +679,18 @@ class ALIGN_AS(CACHE_LINE_SIZE) ClockCacheShard final : public CacheShard {
|
||||
|
||||
private:
|
||||
friend class ClockCache;
|
||||
void ClockRemove(ClockHandle* e);
|
||||
void ClockInsert(ClockHandle* e);
|
||||
|
||||
// Makes an element evictable by clock.
|
||||
void ClockOn(ClockHandle* h);
|
||||
|
||||
// Makes an element non-evictable.
|
||||
void ClockOff(ClockHandle* h);
|
||||
|
||||
// Requires an exclusive ref on h.
|
||||
void Evict(ClockHandle* h);
|
||||
|
||||
// Free some space following strict clock policy until enough space
|
||||
// to hold (usage_ + charge) is freed or the clock list is empty
|
||||
// This function is not thread safe - it needs to be executed while
|
||||
// holding the mutex_.
|
||||
// to hold (usage_ + charge) is freed or there are no evictable elements.
|
||||
void EvictFromClock(size_t charge, autovector<ClockHandle>* deleted);
|
||||
|
||||
// Returns the charge of a single handle.
|
||||
@@ -436,9 +727,6 @@ class ALIGN_AS(CACHE_LINE_SIZE) ClockCacheShard final : public CacheShard {
|
||||
// Memory size for entries residing in the cache.
|
||||
size_t usage_;
|
||||
|
||||
// Memory size for unpinned entries in the clock list.
|
||||
size_t clock_usage_;
|
||||
|
||||
// mutex_ protects the following state.
|
||||
// We don't count mutex_ as the cache's internal state so semantically we
|
||||
// don't mind mutex_ invoking the non-const actions.
|
||||
|
||||
Vendored
+1
@@ -52,6 +52,7 @@ LRUHandle* LRUHandleTable::Insert(LRUHandle* h, LRUHandle** old) {
|
||||
1 /*displacement*/);
|
||||
*old = nullptr;
|
||||
if (slot == -1) {
|
||||
// TODO(Guido) Don't we need to roll back displacements here?
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -798,6 +798,42 @@ TEST_F(DBBlobCompactionTest, CompactionReadaheadMerge) {
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBBlobCompactionTest, CompactionDoNotFillCache) {
|
||||
Options options = GetDefaultOptions();
|
||||
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 0;
|
||||
options.enable_blob_garbage_collection = true;
|
||||
options.blob_garbage_collection_age_cutoff = 1.0;
|
||||
options.disable_auto_compactions = true;
|
||||
options.statistics = CreateDBStatistics();
|
||||
|
||||
LRUCacheOptions cache_options;
|
||||
cache_options.capacity = 1 << 20;
|
||||
cache_options.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
|
||||
options.blob_cache = NewLRUCache(cache_options);
|
||||
|
||||
Reopen(options);
|
||||
|
||||
ASSERT_OK(Put("key", "lime"));
|
||||
ASSERT_OK(Put("foo", "bar"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(Put("key", "pie"));
|
||||
ASSERT_OK(Put("foo", "baz"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
constexpr Slice* begin = nullptr;
|
||||
constexpr Slice* end = nullptr;
|
||||
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), begin, end));
|
||||
|
||||
ASSERT_EQ(options.statistics->getTickerCount(BLOB_DB_CACHE_ADD), 0);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+11
-1
@@ -66,7 +66,8 @@ Status BuildTable(
|
||||
SequenceNumber job_snapshot, SnapshotChecker* snapshot_checker,
|
||||
bool paranoid_file_checks, InternalStats* internal_stats,
|
||||
IOStatus* io_status, const std::shared_ptr<IOTracer>& io_tracer,
|
||||
BlobFileCreationReason blob_creation_reason, EventLogger* event_logger,
|
||||
BlobFileCreationReason blob_creation_reason,
|
||||
const SeqnoToTimeMapping& seqno_to_time_mapping, EventLogger* event_logger,
|
||||
int job_id, const Env::IOPriority io_priority,
|
||||
TableProperties* table_properties, Env::WriteLifeTimeHint write_hint,
|
||||
const std::string* full_history_ts_low,
|
||||
@@ -260,6 +261,15 @@ Status BuildTable(
|
||||
if (!s.ok() || empty) {
|
||||
builder->Abandon();
|
||||
} else {
|
||||
std::string seqno_time_mapping_str;
|
||||
seqno_to_time_mapping.Encode(
|
||||
seqno_time_mapping_str, meta->fd.smallest_seqno,
|
||||
meta->fd.largest_seqno, meta->file_creation_time);
|
||||
builder->SetSeqnoTimeTableProperties(
|
||||
seqno_time_mapping_str,
|
||||
ioptions.compaction_style == CompactionStyle::kCompactionStyleFIFO
|
||||
? meta->file_creation_time
|
||||
: meta->oldest_ancester_time);
|
||||
s = builder->Finish();
|
||||
}
|
||||
if (io_status->ok()) {
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "db/range_tombstone_fragmenter.h"
|
||||
#include "db/seqno_to_time_mapping.h"
|
||||
#include "db/table_properties_collector.h"
|
||||
#include "logging/event_logger.h"
|
||||
#include "options/cf_options.h"
|
||||
@@ -61,6 +63,7 @@ extern Status BuildTable(
|
||||
bool paranoid_file_checks, InternalStats* internal_stats,
|
||||
IOStatus* io_status, const std::shared_ptr<IOTracer>& io_tracer,
|
||||
BlobFileCreationReason blob_creation_reason,
|
||||
const SeqnoToTimeMapping& seqno_to_time_mapping,
|
||||
EventLogger* event_logger = nullptr, int job_id = 0,
|
||||
const Env::IOPriority io_priority = Env::IO_HIGH,
|
||||
TableProperties* table_properties = nullptr,
|
||||
|
||||
@@ -699,19 +699,6 @@ ColumnFamilyData::~ColumnFamilyData() {
|
||||
id_, name_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (data_dirs_.size()) { // Explicitly close data directories
|
||||
Status s = Status::OK();
|
||||
for (auto& data_dir_ptr : data_dirs_) {
|
||||
if (data_dir_ptr) {
|
||||
s = data_dir_ptr->Close(IOOptions(), nullptr);
|
||||
if (!s.ok()) {
|
||||
// TODO(zichen): add `Status Close()` and `CloseDirectories()
|
||||
s.PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ColumnFamilyData::UnrefAndTryDelete() {
|
||||
|
||||
@@ -524,6 +524,8 @@ class ColumnFamilyData {
|
||||
return file_metadata_cache_res_mgr_;
|
||||
}
|
||||
|
||||
SequenceNumber GetFirstMemtableSequenceNumber() const;
|
||||
|
||||
static const uint32_t kDummyColumnFamilyDataId;
|
||||
|
||||
// Keep track of whether the mempurge feature was ever used.
|
||||
|
||||
@@ -440,6 +440,11 @@ bool Compaction::IsTrivialMove() const {
|
||||
}
|
||||
}
|
||||
|
||||
// PerKeyPlacement compaction should never be trivial move.
|
||||
if (SupportsPerKeyPlacement()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -741,10 +746,10 @@ int Compaction::EvaluatePenultimateLevel(
|
||||
return kInvalidLevel;
|
||||
}
|
||||
|
||||
// TODO: will add public like `options.preclude_last_level_data_seconds` for
|
||||
// per_key_placement feature, will check that option here. Currently, only
|
||||
// set by unittest
|
||||
bool supports_per_key_placement = false;
|
||||
bool supports_per_key_placement =
|
||||
immutable_options.preclude_last_level_data_seconds > 0;
|
||||
|
||||
// it could be overridden by unittest
|
||||
TEST_SYNC_POINT_CALLBACK("Compaction::SupportsPerKeyPlacement:Enabled",
|
||||
&supports_per_key_placement);
|
||||
if (!supports_per_key_placement) {
|
||||
|
||||
@@ -33,7 +33,8 @@ CompactionIterator::CompactionIterator(
|
||||
const Compaction* compaction, const CompactionFilter* compaction_filter,
|
||||
const std::atomic<bool>* shutting_down,
|
||||
const std::shared_ptr<Logger> info_log,
|
||||
const std::string* full_history_ts_low)
|
||||
const std::string* full_history_ts_low,
|
||||
const SequenceNumber penultimate_level_cutoff_seqno)
|
||||
: CompactionIterator(
|
||||
input, cmp, merge_helper, last_sequence, snapshots,
|
||||
earliest_write_conflict_snapshot, job_snapshot, snapshot_checker, env,
|
||||
@@ -42,7 +43,8 @@ CompactionIterator::CompactionIterator(
|
||||
manual_compaction_canceled,
|
||||
std::unique_ptr<CompactionProxy>(
|
||||
compaction ? new RealCompaction(compaction) : nullptr),
|
||||
compaction_filter, shutting_down, info_log, full_history_ts_low) {}
|
||||
compaction_filter, shutting_down, info_log, full_history_ts_low,
|
||||
penultimate_level_cutoff_seqno) {}
|
||||
|
||||
CompactionIterator::CompactionIterator(
|
||||
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
|
||||
@@ -58,7 +60,8 @@ CompactionIterator::CompactionIterator(
|
||||
const CompactionFilter* compaction_filter,
|
||||
const std::atomic<bool>* shutting_down,
|
||||
const std::shared_ptr<Logger> info_log,
|
||||
const std::string* full_history_ts_low)
|
||||
const std::string* full_history_ts_low,
|
||||
const SequenceNumber penultimate_level_cutoff_seqno)
|
||||
: input_(input, cmp,
|
||||
!compaction || compaction->DoesInputReferenceBlobFiles()),
|
||||
cmp_(cmp),
|
||||
@@ -92,7 +95,8 @@ CompactionIterator::CompactionIterator(
|
||||
CreatePrefetchBufferCollectionIfNeeded(compaction_.get())),
|
||||
current_key_committed_(false),
|
||||
cmp_with_history_ts_low_(0),
|
||||
level_(compaction_ == nullptr ? 0 : compaction_->level()) {
|
||||
level_(compaction_ == nullptr ? 0 : compaction_->level()),
|
||||
penultimate_level_cutoff_seqno_(penultimate_level_cutoff_seqno) {
|
||||
assert(snapshots_ != nullptr);
|
||||
bottommost_level_ = compaction_ == nullptr
|
||||
? false
|
||||
@@ -1077,8 +1081,7 @@ void CompactionIterator::GarbageCollectBlobIfNeeded() {
|
||||
|
||||
void CompactionIterator::DecideOutputLevel() {
|
||||
#ifndef NDEBUG
|
||||
// TODO: will be set by sequence number or key range, for now, it will only be
|
||||
// set by unittest
|
||||
// Could be overridden by unittest
|
||||
PerKeyPlacementContext context(level_, ikey_.user_key, value_,
|
||||
ikey_.sequence);
|
||||
TEST_SYNC_POINT_CALLBACK("CompactionIterator::PrepareOutput.context",
|
||||
@@ -1086,9 +1089,10 @@ void CompactionIterator::DecideOutputLevel() {
|
||||
output_to_penultimate_level_ = context.output_to_penultimate_level;
|
||||
#endif /* !NDEBUG */
|
||||
|
||||
// if the key is within the earliest snapshot, it has to output to the
|
||||
// penultimate level.
|
||||
if (ikey_.sequence > earliest_snapshot_) {
|
||||
// if the key is newer than the cutoff sequence or within the earliest
|
||||
// snapshot, it should output to the penultimate level.
|
||||
if (ikey_.sequence > penultimate_level_cutoff_seqno_ ||
|
||||
ikey_.sequence > earliest_snapshot_) {
|
||||
output_to_penultimate_level_ = true;
|
||||
}
|
||||
|
||||
@@ -1148,7 +1152,8 @@ void CompactionIterator::PrepareOutput() {
|
||||
!compaction_->allow_ingest_behind() && bottommost_level_ &&
|
||||
DefinitelyInSnapshot(ikey_.sequence, earliest_snapshot_) &&
|
||||
ikey_.type != kTypeMerge && current_key_committed_ &&
|
||||
!output_to_penultimate_level_) {
|
||||
!output_to_penultimate_level_ &&
|
||||
ikey_.sequence < penultimate_level_cutoff_seqno_) {
|
||||
if (ikey_.type == kTypeDeletion ||
|
||||
(ikey_.type == kTypeSingleDeletion && timestamp_size_ == 0)) {
|
||||
ROCKS_LOG_FATAL(
|
||||
@@ -1279,7 +1284,10 @@ std::unique_ptr<BlobFetcher> CompactionIterator::CreateBlobFetcherIfNeeded(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::unique_ptr<BlobFetcher>(new BlobFetcher(version, ReadOptions()));
|
||||
ReadOptions read_options;
|
||||
read_options.fill_cache = false;
|
||||
|
||||
return std::unique_ptr<BlobFetcher>(new BlobFetcher(version, read_options));
|
||||
}
|
||||
|
||||
std::unique_ptr<PrefetchBufferCollection>
|
||||
|
||||
@@ -181,42 +181,40 @@ class CompactionIterator {
|
||||
const Compaction* compaction_;
|
||||
};
|
||||
|
||||
CompactionIterator(InternalIterator* input, const Comparator* cmp,
|
||||
MergeHelper* merge_helper, SequenceNumber last_sequence,
|
||||
std::vector<SequenceNumber>* snapshots,
|
||||
SequenceNumber earliest_write_conflict_snapshot,
|
||||
SequenceNumber job_snapshot,
|
||||
const SnapshotChecker* snapshot_checker, Env* env,
|
||||
bool report_detailed_time, bool expect_valid_internal_key,
|
||||
CompactionRangeDelAggregator* range_del_agg,
|
||||
BlobFileBuilder* blob_file_builder,
|
||||
bool allow_data_in_errors,
|
||||
bool enforce_single_del_contracts,
|
||||
const std::atomic<bool>& manual_compaction_canceled,
|
||||
const Compaction* compaction = nullptr,
|
||||
const CompactionFilter* compaction_filter = nullptr,
|
||||
const std::atomic<bool>* shutting_down = nullptr,
|
||||
const std::shared_ptr<Logger> info_log = nullptr,
|
||||
const std::string* full_history_ts_low = nullptr);
|
||||
CompactionIterator(
|
||||
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
|
||||
SequenceNumber last_sequence, std::vector<SequenceNumber>* snapshots,
|
||||
SequenceNumber earliest_write_conflict_snapshot,
|
||||
SequenceNumber job_snapshot, const SnapshotChecker* snapshot_checker,
|
||||
Env* env, bool report_detailed_time, bool expect_valid_internal_key,
|
||||
CompactionRangeDelAggregator* range_del_agg,
|
||||
BlobFileBuilder* blob_file_builder, bool allow_data_in_errors,
|
||||
bool enforce_single_del_contracts,
|
||||
const std::atomic<bool>& manual_compaction_canceled,
|
||||
const Compaction* compaction = nullptr,
|
||||
const CompactionFilter* compaction_filter = nullptr,
|
||||
const std::atomic<bool>* shutting_down = nullptr,
|
||||
const std::shared_ptr<Logger> info_log = nullptr,
|
||||
const std::string* full_history_ts_low = nullptr,
|
||||
const SequenceNumber penultimate_level_cutoff_seqno = kMaxSequenceNumber);
|
||||
|
||||
// Constructor with custom CompactionProxy, used for tests.
|
||||
CompactionIterator(InternalIterator* input, const Comparator* cmp,
|
||||
MergeHelper* merge_helper, SequenceNumber last_sequence,
|
||||
std::vector<SequenceNumber>* snapshots,
|
||||
SequenceNumber earliest_write_conflict_snapshot,
|
||||
SequenceNumber job_snapshot,
|
||||
const SnapshotChecker* snapshot_checker, Env* env,
|
||||
bool report_detailed_time, bool expect_valid_internal_key,
|
||||
CompactionRangeDelAggregator* range_del_agg,
|
||||
BlobFileBuilder* blob_file_builder,
|
||||
bool allow_data_in_errors,
|
||||
bool enforce_single_del_contracts,
|
||||
const std::atomic<bool>& manual_compaction_canceled,
|
||||
std::unique_ptr<CompactionProxy> compaction,
|
||||
const CompactionFilter* compaction_filter = nullptr,
|
||||
const std::atomic<bool>* shutting_down = nullptr,
|
||||
const std::shared_ptr<Logger> info_log = nullptr,
|
||||
const std::string* full_history_ts_low = nullptr);
|
||||
CompactionIterator(
|
||||
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
|
||||
SequenceNumber last_sequence, std::vector<SequenceNumber>* snapshots,
|
||||
SequenceNumber earliest_write_conflict_snapshot,
|
||||
SequenceNumber job_snapshot, const SnapshotChecker* snapshot_checker,
|
||||
Env* env, bool report_detailed_time, bool expect_valid_internal_key,
|
||||
CompactionRangeDelAggregator* range_del_agg,
|
||||
BlobFileBuilder* blob_file_builder, bool allow_data_in_errors,
|
||||
bool enforce_single_del_contracts,
|
||||
const std::atomic<bool>& manual_compaction_canceled,
|
||||
std::unique_ptr<CompactionProxy> compaction,
|
||||
const CompactionFilter* compaction_filter = nullptr,
|
||||
const std::atomic<bool>* shutting_down = nullptr,
|
||||
const std::shared_ptr<Logger> info_log = nullptr,
|
||||
const std::string* full_history_ts_low = nullptr,
|
||||
const SequenceNumber penultimate_level_cutoff_seqno = kMaxSequenceNumber);
|
||||
|
||||
~CompactionIterator();
|
||||
|
||||
@@ -446,6 +444,10 @@ class CompactionIterator {
|
||||
// output to.
|
||||
bool output_to_penultimate_level_{false};
|
||||
|
||||
// any key later than this sequence number should have
|
||||
// output_to_penultimate_level_ set to true
|
||||
const SequenceNumber penultimate_level_cutoff_seqno_ = kMaxSequenceNumber;
|
||||
|
||||
void AdvanceInputIter() { input_.Next(); }
|
||||
|
||||
void SkipUntil(const Slice& skip_until) { input_.Seek(skip_until); }
|
||||
|
||||
@@ -223,12 +223,12 @@ void CompactionJob::Prepare() {
|
||||
|
||||
// Generate file_levels_ for compaction before making Iterator
|
||||
auto* c = compact_->compaction;
|
||||
assert(c->column_family_data() != nullptr);
|
||||
assert(c->column_family_data()->current()->storage_info()->NumLevelFiles(
|
||||
ColumnFamilyData* cfd = c->column_family_data();
|
||||
assert(cfd != nullptr);
|
||||
assert(cfd->current()->storage_info()->NumLevelFiles(
|
||||
compact_->compaction->level()) > 0);
|
||||
|
||||
write_hint_ =
|
||||
c->column_family_data()->CalculateSSTWriteHint(c->output_level());
|
||||
write_hint_ = cfd->CalculateSSTWriteHint(c->output_level());
|
||||
bottommost_level_ = c->bottommost_level();
|
||||
|
||||
if (c->ShouldFormSubcompactions()) {
|
||||
@@ -251,6 +251,43 @@ void CompactionJob::Prepare() {
|
||||
|
||||
compact_->sub_compact_states.emplace_back(c, start, end, /*sub_job_id*/ 0);
|
||||
}
|
||||
|
||||
if (c->immutable_options()->preclude_last_level_data_seconds > 0) {
|
||||
// TODO(zjay): move to a function
|
||||
seqno_time_mapping_.SetMaxTimeDuration(
|
||||
c->immutable_options()->preclude_last_level_data_seconds);
|
||||
// setup seqno_time_mapping_
|
||||
for (const auto& each_level : *c->inputs()) {
|
||||
for (const auto& fmd : each_level.files) {
|
||||
std::shared_ptr<const TableProperties> tp;
|
||||
Status s = cfd->current()->GetTableProperties(&tp, fmd, nullptr);
|
||||
if (s.ok()) {
|
||||
seqno_time_mapping_.Add(tp->seqno_to_time_mapping)
|
||||
.PermitUncheckedError();
|
||||
seqno_time_mapping_.Add(fmd->fd.smallest_seqno,
|
||||
fmd->oldest_ancester_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto status = seqno_time_mapping_.Sort();
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"Invalid sequence number to time mapping: Status: %s",
|
||||
status.ToString().c_str());
|
||||
}
|
||||
int64_t _current_time = 0;
|
||||
status = db_options_.clock->GetCurrentTime(&_current_time);
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"Failed to get current time in compaction: Status: %s",
|
||||
status.ToString().c_str());
|
||||
penultimate_level_cutoff_seqno_ = 0;
|
||||
} else {
|
||||
penultimate_level_cutoff_seqno_ =
|
||||
seqno_time_mapping_.TruncateOldEntries(_current_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RangeWithSize {
|
||||
@@ -989,7 +1026,8 @@ void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
|
||||
blob_file_builder.get(), db_options_.allow_data_in_errors,
|
||||
db_options_.enforce_single_del_contracts, manual_compaction_canceled_,
|
||||
sub_compact->compaction, compaction_filter, shutting_down_,
|
||||
db_options_.info_log, full_history_ts_low);
|
||||
db_options_.info_log, full_history_ts_low,
|
||||
penultimate_level_cutoff_seqno_);
|
||||
c_iter->SeekToFirst();
|
||||
|
||||
// Assign range delete aggregator to the target output level, which makes sure
|
||||
@@ -1253,7 +1291,7 @@ Status CompactionJob::FinishCompactionOutputFile(
|
||||
|
||||
const uint64_t current_entries = outputs.NumEntries();
|
||||
|
||||
s = outputs.Finish(s);
|
||||
s = outputs.Finish(s, seqno_time_mapping_);
|
||||
|
||||
if (s.ok()) {
|
||||
// With accurate smallest and largest key, we can get a slightly more
|
||||
@@ -1617,9 +1655,8 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
sub_compact->compaction->output_compression_opts(), cfd->GetID(),
|
||||
cfd->GetName(), sub_compact->compaction->output_level(),
|
||||
bottommost_level_, TableFileCreationReason::kCompaction,
|
||||
oldest_ancester_time, 0 /* oldest_key_time */, current_time, db_id_,
|
||||
db_session_id_, sub_compact->compaction->max_output_file_size(),
|
||||
file_number);
|
||||
0 /* oldest_key_time */, current_time, db_id_, db_session_id_,
|
||||
sub_compact->compaction->max_output_file_size(), file_number);
|
||||
|
||||
outputs.NewBuilder(tboptions);
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "db/log_writer.h"
|
||||
#include "db/memtable_list.h"
|
||||
#include "db/range_del_aggregator.h"
|
||||
#include "db/seqno_to_time_mapping.h"
|
||||
#include "db/version_edit.h"
|
||||
#include "db/write_controller.h"
|
||||
#include "db/write_thread.h"
|
||||
@@ -299,6 +300,17 @@ class CompactionJob {
|
||||
|
||||
uint64_t GetCompactionId(SubcompactionState* sub_compact) const;
|
||||
|
||||
// Stores the sequence number to time mapping gathered from all input files
|
||||
// it also collects the smallest_seqno -> oldest_ancester_time from the SST.
|
||||
SeqnoToTimeMapping seqno_time_mapping_;
|
||||
|
||||
// cutoff sequence number for penultimate level, only set when
|
||||
// per_key_placement feature is enabled.
|
||||
// If a key with sequence number larger than penultimate_level_cutoff_seqno_,
|
||||
// it will be placed on the penultimate_level and seqnuence number won't be
|
||||
// zeroed out.
|
||||
SequenceNumber penultimate_level_cutoff_seqno_ = kMaxSequenceNumber;
|
||||
|
||||
// Get table file name in where it's outputting to, which should also be in
|
||||
// `output_directory_`.
|
||||
virtual std::string GetTableFileName(uint64_t file_number);
|
||||
|
||||
@@ -18,12 +18,19 @@ void CompactionOutputs::NewBuilder(const TableBuilderOptions& tboptions) {
|
||||
builder_.reset(NewTableBuilder(tboptions, file_writer_.get()));
|
||||
}
|
||||
|
||||
Status CompactionOutputs::Finish(const Status& intput_status) {
|
||||
Status CompactionOutputs::Finish(const Status& intput_status,
|
||||
const SeqnoToTimeMapping& seqno_time_mapping) {
|
||||
FileMetaData* meta = GetMetaData();
|
||||
assert(meta != nullptr);
|
||||
Status s = intput_status;
|
||||
if (s.ok()) {
|
||||
std::string seqno_time_mapping_str;
|
||||
seqno_time_mapping.Encode(seqno_time_mapping_str, meta->fd.smallest_seqno,
|
||||
meta->fd.largest_seqno, meta->file_creation_time);
|
||||
builder_->SetSeqnoTimeTableProperties(seqno_time_mapping_str,
|
||||
meta->oldest_ancester_time);
|
||||
s = builder_->Finish();
|
||||
|
||||
} else {
|
||||
builder_->Abandon();
|
||||
}
|
||||
|
||||
@@ -111,7 +111,8 @@ class CompactionOutputs {
|
||||
}
|
||||
|
||||
// Finish the current output file
|
||||
Status Finish(const Status& intput_status);
|
||||
Status Finish(const Status& intput_status,
|
||||
const SeqnoToTimeMapping& seqno_time_mapping);
|
||||
|
||||
// Update output table properties from table builder
|
||||
void UpdateTableProperties() {
|
||||
|
||||
@@ -53,26 +53,17 @@ class TieredCompactionTest : public DBTestBase {
|
||||
InternalStats::CompactionOutputsStats kBasicPerLevelStats;
|
||||
InternalStats::CompactionStats kBasicFlushStats;
|
||||
|
||||
std::atomic_bool enable_per_key_placement = true;
|
||||
|
||||
void SetUp() override {
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Compaction::SupportsPerKeyPlacement:Enabled", [&](void* arg) {
|
||||
auto supports_per_key_placement = static_cast<bool*>(arg);
|
||||
*supports_per_key_placement = true;
|
||||
*supports_per_key_placement = enable_per_key_placement;
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
uint64_t GetSstSizeHelper(Temperature temperature) {
|
||||
std::string prop;
|
||||
EXPECT_TRUE(dbfull()->GetProperty(
|
||||
DB::Properties::kLiveSstFilesSizeAtTemperature +
|
||||
std::to_string(static_cast<uint8_t>(temperature)),
|
||||
&prop));
|
||||
return static_cast<uint64_t>(std::atoi(prop.c_str()));
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
const std::vector<InternalStats::CompactionStats>& GetCompactionStats() {
|
||||
VersionSet* const versions = dbfull()->GetVersionSet();
|
||||
assert(versions);
|
||||
@@ -1054,12 +1045,14 @@ TEST_F(TieredCompactionTest, SequenceBasedTieredStorageLevel) {
|
||||
ASSERT_GT(GetSstSizeHelper(Temperature::kUnknown), 0);
|
||||
ASSERT_EQ(GetSstSizeHelper(Temperature::kCold), 0);
|
||||
|
||||
latest_cold_seq = seq_history[2];
|
||||
|
||||
MoveFilesToLevel(kLastLevel);
|
||||
|
||||
// move forward the cold_seq again with range delete, take a snapshot to keep
|
||||
// the range dels in bottommost
|
||||
auto snap = db_->GetSnapshot();
|
||||
latest_cold_seq = seq_history[2];
|
||||
|
||||
std::string start = Key(25), end = Key(35);
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), start, end));
|
||||
@@ -1104,9 +1097,12 @@ TEST_F(TieredCompactionTest, SequenceBasedTieredStorageLevel) {
|
||||
|
||||
db_->ReleaseSnapshot(snap);
|
||||
|
||||
// TODO: it should push the data to last level, but penultimate level file is
|
||||
// already bottommost, it's a conflict between bottommost_temperature and
|
||||
// tiered compaction which only applies to last level compaction.
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
ASSERT_EQ("0,0,0,0,0,0,1", FilesPerLevel());
|
||||
ASSERT_EQ(GetSstSizeHelper(Temperature::kUnknown), 0);
|
||||
ASSERT_EQ("0,0,0,0,0,1,1", FilesPerLevel());
|
||||
ASSERT_GT(GetSstSizeHelper(Temperature::kUnknown), 0);
|
||||
ASSERT_GT(GetSstSizeHelper(Temperature::kCold), 0);
|
||||
|
||||
// 3 range dels dropped, the first one is double counted as expected, which is
|
||||
@@ -1123,8 +1119,8 @@ TEST_F(TieredCompactionTest, SequenceBasedTieredStorageLevel) {
|
||||
// input range
|
||||
latest_cold_seq = seq_history[1];
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
ASSERT_EQ("0,0,0,0,0,0,1", FilesPerLevel());
|
||||
ASSERT_EQ(GetSstSizeHelper(Temperature::kUnknown), 0);
|
||||
ASSERT_EQ("0,0,0,0,0,1,1", FilesPerLevel());
|
||||
ASSERT_GT(GetSstSizeHelper(Temperature::kUnknown), 0);
|
||||
ASSERT_GT(GetSstSizeHelper(Temperature::kCold), 0);
|
||||
}
|
||||
|
||||
|
||||
+29
-12
@@ -1209,9 +1209,9 @@ TEST_F(DBBasicTest, DBCloseAllDirectoryFDs) {
|
||||
s = db->Close();
|
||||
auto* counted_fs =
|
||||
options.env->GetFileSystem()->CheckedCast<CountedFileSystem>();
|
||||
assert(counted_fs);
|
||||
ASSERT_TRUE(counted_fs->counters()->dir_opens ==
|
||||
counted_fs->counters()->dir_closes);
|
||||
ASSERT_TRUE(counted_fs != nullptr);
|
||||
ASSERT_EQ(counted_fs->counters()->dir_opens,
|
||||
counted_fs->counters()->dir_closes);
|
||||
ASSERT_OK(s);
|
||||
delete db;
|
||||
}
|
||||
@@ -2618,13 +2618,20 @@ TEST_F(DBBasicTest, GetAllKeyVersions) {
|
||||
ASSERT_OK(Delete(std::to_string(i)));
|
||||
}
|
||||
std::vector<KeyVersion> key_versions;
|
||||
ASSERT_OK(ROCKSDB_NAMESPACE::GetAllKeyVersions(
|
||||
db_, Slice(), Slice(), std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
ASSERT_OK(GetAllKeyVersions(db_, Slice(), Slice(),
|
||||
std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
ASSERT_EQ(kNumInserts + kNumDeletes + kNumUpdates, key_versions.size());
|
||||
ASSERT_OK(ROCKSDB_NAMESPACE::GetAllKeyVersions(
|
||||
db_, handles_[0], Slice(), Slice(), std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
for (size_t i = 0; i < kNumInserts + kNumDeletes + kNumUpdates; i++) {
|
||||
if (i % 3 == 0) {
|
||||
ASSERT_EQ(key_versions[i].GetTypeName(), "TypeDeletion");
|
||||
} else {
|
||||
ASSERT_EQ(key_versions[i].GetTypeName(), "TypeValue");
|
||||
}
|
||||
}
|
||||
ASSERT_OK(GetAllKeyVersions(db_, handles_[0], Slice(), Slice(),
|
||||
std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
ASSERT_EQ(kNumInserts + kNumDeletes + kNumUpdates, key_versions.size());
|
||||
|
||||
// Check non-default column family
|
||||
@@ -2637,11 +2644,21 @@ TEST_F(DBBasicTest, GetAllKeyVersions) {
|
||||
for (size_t i = 0; i + 1 != kNumDeletes; ++i) {
|
||||
ASSERT_OK(Delete(1, std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(ROCKSDB_NAMESPACE::GetAllKeyVersions(
|
||||
db_, handles_[1], Slice(), Slice(), std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
ASSERT_OK(GetAllKeyVersions(db_, handles_[1], Slice(), Slice(),
|
||||
std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
ASSERT_EQ(kNumInserts + kNumDeletes + kNumUpdates - 3, key_versions.size());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, ValueTypeString) {
|
||||
KeyVersion key_version;
|
||||
// when adding new type, please also update `value_type_string_map`
|
||||
for (unsigned char i = ValueType::kTypeDeletion; i < ValueType::kTypeMaxValid;
|
||||
i++) {
|
||||
key_version.type = i;
|
||||
ASSERT_TRUE(key_version.GetTypeName() != "Invalid");
|
||||
}
|
||||
}
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetIOBufferOverrun) {
|
||||
|
||||
@@ -78,17 +78,6 @@ class DBCompactionTest : public DBTestBase {
|
||||
: DBTestBase("db_compaction_test", /*env_do_fsync=*/true) {}
|
||||
|
||||
protected:
|
||||
#ifndef ROCKSDB_LITE
|
||||
uint64_t GetSstSizeHelper(Temperature temperature) {
|
||||
std::string prop;
|
||||
EXPECT_TRUE(dbfull()->GetProperty(
|
||||
DB::Properties::kLiveSstFilesSizeAtTemperature +
|
||||
std::to_string(static_cast<uint8_t>(temperature)),
|
||||
&prop));
|
||||
return static_cast<uint64_t>(std::atoi(prop.c_str()));
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
/*
|
||||
* Verifies compaction stats of cfd are valid.
|
||||
*
|
||||
|
||||
@@ -480,6 +480,7 @@ void DBImpl::CancelAllBackgroundWork(bool wait) {
|
||||
#ifndef ROCKSDB_LITE
|
||||
if (periodic_work_scheduler_ != nullptr) {
|
||||
periodic_work_scheduler_->Unregister(this);
|
||||
periodic_work_scheduler_->UnregisterRecordSeqnoTimeWorker(this);
|
||||
}
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
@@ -791,6 +792,66 @@ Status DBImpl::StartPeriodicWorkScheduler() {
|
||||
#endif // !ROCKSDB_LITE
|
||||
}
|
||||
|
||||
Status DBImpl::RegisterRecordSeqnoTimeWorker() {
|
||||
#ifndef ROCKSDB_LITE
|
||||
if (!periodic_work_scheduler_) {
|
||||
return Status::OK();
|
||||
}
|
||||
uint64_t min_time_duration = std::numeric_limits<uint64_t>::max();
|
||||
uint64_t max_time_duration = std::numeric_limits<uint64_t>::min();
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
|
||||
for (auto cfd : *versions_->GetColumnFamilySet()) {
|
||||
uint64_t preclude_last_option =
|
||||
cfd->ioptions()->preclude_last_level_data_seconds;
|
||||
if (!cfd->IsDropped() && preclude_last_option > 0) {
|
||||
min_time_duration = std::min(preclude_last_option, min_time_duration);
|
||||
max_time_duration = std::max(preclude_last_option, max_time_duration);
|
||||
}
|
||||
}
|
||||
if (min_time_duration == std::numeric_limits<uint64_t>::max()) {
|
||||
seqno_time_mapping_.Resize(0, 0);
|
||||
} else {
|
||||
seqno_time_mapping_.Resize(min_time_duration, max_time_duration);
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t seqno_time_cadence = 0;
|
||||
if (min_time_duration != std::numeric_limits<uint64_t>::max()) {
|
||||
seqno_time_cadence =
|
||||
min_time_duration / SeqnoToTimeMapping::kMaxSeqnoTimePairsPerCF;
|
||||
}
|
||||
|
||||
Status s;
|
||||
if (seqno_time_cadence != record_seqno_time_cadence_) {
|
||||
if (seqno_time_cadence == 0) {
|
||||
periodic_work_scheduler_->UnregisterRecordSeqnoTimeWorker(this);
|
||||
} else {
|
||||
s = periodic_work_scheduler_->RegisterRecordSeqnoTimeWorker(
|
||||
this, seqno_time_cadence);
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
record_seqno_time_cadence_ = seqno_time_cadence;
|
||||
}
|
||||
|
||||
if (s.IsNotSupported()) {
|
||||
// TODO: Fix the timer cannot cancel and re-add the same task
|
||||
ROCKS_LOG_WARN(
|
||||
immutable_db_options_.info_log,
|
||||
"Updating seqno to time worker cadence is not supported yet, to make "
|
||||
"the change effective, please reopen the DB instance.");
|
||||
s = Status::OK();
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
#else
|
||||
return Status::OK();
|
||||
#endif // !ROCKSDB_LITE
|
||||
}
|
||||
|
||||
// esitmate the total size of stats_history_
|
||||
size_t DBImpl::EstimateInMemoryStatsHistorySize() const {
|
||||
size_t size_total =
|
||||
@@ -2805,6 +2866,14 @@ Status DBImpl::CreateColumnFamilyImpl(const ColumnFamilyOptions& cf_options,
|
||||
}
|
||||
} // InstrumentedMutexLock l(&mutex_)
|
||||
|
||||
if (cf_options.preclude_last_level_data_seconds > 0) {
|
||||
// TODO(zjay): Fix the timer issue and re-enable this.
|
||||
ROCKS_LOG_ERROR(
|
||||
immutable_db_options_.info_log,
|
||||
"Creating column family with `preclude_last_level_data_seconds` needs "
|
||||
"to restart DB to take effect");
|
||||
// s = RegisterRecordSeqnoTimeWorker();
|
||||
}
|
||||
sv_context.Clean();
|
||||
// this is outside the mutex
|
||||
if (s.ok()) {
|
||||
@@ -2893,6 +2962,10 @@ Status DBImpl::DropColumnFamilyImpl(ColumnFamilyHandle* column_family) {
|
||||
bg_cv_.SignalAll();
|
||||
}
|
||||
|
||||
if (cfd->ioptions()->preclude_last_level_data_seconds > 0) {
|
||||
s = RegisterRecordSeqnoTimeWorker();
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
// Note that here we erase the associated cf_info of the to-be-dropped
|
||||
// cfd before its ref-count goes to zero to avoid having to erase cf_info
|
||||
@@ -5536,6 +5609,26 @@ Status DBImpl::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
|
||||
return Status::NotSupported("This API only works if max_open_files = -1");
|
||||
}
|
||||
}
|
||||
|
||||
void DBImpl::RecordSeqnoToTimeMapping() {
|
||||
// Get time first then sequence number, so the actual time of seqno is <=
|
||||
// unix_time recorded
|
||||
int64_t unix_time = 0;
|
||||
immutable_db_options_.clock->GetCurrentTime(&unix_time)
|
||||
.PermitUncheckedError(); // Ignore error
|
||||
SequenceNumber seqno = GetLatestSequenceNumber();
|
||||
bool appended = false;
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
appended = seqno_time_mapping_.Append(seqno, unix_time);
|
||||
}
|
||||
if (!appended) {
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"Failed to insert sequence number to time entry: %" PRIu64
|
||||
" -> %" PRIu64,
|
||||
seqno, unix_time);
|
||||
}
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+36
-40
@@ -36,6 +36,7 @@
|
||||
#include "db/pre_release_callback.h"
|
||||
#include "db/range_del_aggregator.h"
|
||||
#include "db/read_callback.h"
|
||||
#include "db/seqno_to_time_mapping.h"
|
||||
#include "db/snapshot_checker.h"
|
||||
#include "db/snapshot_impl.h"
|
||||
#include "db/trim_history_scheduler.h"
|
||||
@@ -117,7 +118,6 @@ class Directories {
|
||||
IOStatus Close(const IOOptions& options, IODebugContext* dbg) {
|
||||
// close all directories for all database paths
|
||||
IOStatus s = IOStatus::OK();
|
||||
IOStatus temp_s = IOStatus::OK();
|
||||
|
||||
// The default implementation for Close() in Directory/FSDirectory class
|
||||
// "NotSupported" status, the upper level interface should be able to
|
||||
@@ -126,53 +126,35 @@ class Directories {
|
||||
// `FSDirectory::Close()` yet
|
||||
|
||||
if (db_dir_) {
|
||||
temp_s = db_dir_->Close(options, dbg);
|
||||
if (!temp_s.ok()) {
|
||||
if (temp_s.IsNotSupported()) {
|
||||
temp_s.PermitUncheckedError();
|
||||
} else {
|
||||
s = temp_s;
|
||||
}
|
||||
IOStatus temp_s = db_dir_->Close(options, dbg);
|
||||
if (!temp_s.ok() && !temp_s.IsNotSupported() && s.ok()) {
|
||||
s = std::move(temp_s);
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
// Attempt to close everything even if one fails
|
||||
s.PermitUncheckedError();
|
||||
|
||||
if (wal_dir_) {
|
||||
s = wal_dir_->Close(options, dbg);
|
||||
if (!temp_s.ok()) {
|
||||
if (temp_s.IsNotSupported()) {
|
||||
temp_s.PermitUncheckedError();
|
||||
} else {
|
||||
s = temp_s;
|
||||
IOStatus temp_s = wal_dir_->Close(options, dbg);
|
||||
if (!temp_s.ok() && !temp_s.IsNotSupported() && s.ok()) {
|
||||
s = std::move(temp_s);
|
||||
}
|
||||
}
|
||||
|
||||
s.PermitUncheckedError();
|
||||
|
||||
for (auto& data_dir_ptr : data_dirs_) {
|
||||
if (data_dir_ptr) {
|
||||
IOStatus temp_s = data_dir_ptr->Close(options, dbg);
|
||||
if (!temp_s.ok() && !temp_s.IsNotSupported() && s.ok()) {
|
||||
s = std::move(temp_s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (data_dirs_.size() > 0 && s.ok()) {
|
||||
for (auto& data_dir_ptr : data_dirs_) {
|
||||
if (data_dir_ptr) {
|
||||
temp_s = data_dir_ptr->Close(options, dbg);
|
||||
if (!temp_s.ok()) {
|
||||
if (temp_s.IsNotSupported()) {
|
||||
temp_s.PermitUncheckedError();
|
||||
} else {
|
||||
return temp_s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark temp_s as checked when temp_s is still the initial status
|
||||
// (IOStatus::OK(), not checked yet)
|
||||
temp_s.PermitUncheckedError();
|
||||
// Ready for caller
|
||||
s.MustCheck();
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -1158,7 +1140,8 @@ class DBImpl : public DB {
|
||||
int TEST_BGCompactionsAllowed() const;
|
||||
int TEST_BGFlushesAllowed() const;
|
||||
size_t TEST_GetWalPreallocateBlockSize(uint64_t write_buffer_size) const;
|
||||
void TEST_WaitForStatsDumpRun(std::function<void()> callback) const;
|
||||
void TEST_WaitForPeridicWorkerRun(std::function<void()> callback) const;
|
||||
SeqnoToTimeMapping TEST_GetSeqnoToTimeMapping() const;
|
||||
size_t TEST_EstimateInMemoryStatsHistorySize() const;
|
||||
|
||||
uint64_t TEST_GetCurrentLogNumber() const {
|
||||
@@ -1186,6 +1169,9 @@ class DBImpl : public DB {
|
||||
// flush LOG out of application buffer
|
||||
void FlushInfoLog();
|
||||
|
||||
// record current sequence number to time mapping
|
||||
void RecordSeqnoToTimeMapping();
|
||||
|
||||
// Interface to block and signal the DB in case of stalling writes by
|
||||
// WriteBufferManager. Each DBImpl object contains ptr to WBMStallInterface.
|
||||
// When DB needs to be blocked or signalled by WriteBufferManager,
|
||||
@@ -2069,6 +2055,8 @@ class DBImpl : public DB {
|
||||
// Schedule background tasks
|
||||
Status StartPeriodicWorkScheduler();
|
||||
|
||||
Status RegisterRecordSeqnoTimeWorker();
|
||||
|
||||
void PrintStatistics();
|
||||
|
||||
size_t EstimateInMemoryStatsHistorySize() const;
|
||||
@@ -2537,6 +2525,10 @@ class DBImpl : public DB {
|
||||
// PeriodicWorkScheduler::Default(). Only in unittest, it can be overrided by
|
||||
// PeriodicWorkTestScheduler.
|
||||
PeriodicWorkScheduler* periodic_work_scheduler_;
|
||||
|
||||
// Current cadence of the periodic worker for recording sequence number to
|
||||
// time.
|
||||
uint64_t record_seqno_time_cadence_ = 0;
|
||||
#endif
|
||||
|
||||
// When set, we use a separate queue for writes that don't write to memtable.
|
||||
@@ -2586,6 +2578,10 @@ class DBImpl : public DB {
|
||||
|
||||
// Pointer to WriteBufferManager stalling interface.
|
||||
std::unique_ptr<StallInterface> wbm_stall_;
|
||||
|
||||
// seqno_time_mapping_ stores the sequence number to time mapping, it's not
|
||||
// thread safe, both read and write need db mutex hold.
|
||||
SeqnoToTimeMapping seqno_time_mapping_;
|
||||
};
|
||||
|
||||
class GetWithTimestampReadCallback : public ReadCallback {
|
||||
|
||||
@@ -212,8 +212,8 @@ Status DBImpl::FlushMemTableToOutputFile(
|
||||
GetCompressionFlush(*cfd->ioptions(), mutable_cf_options), stats_,
|
||||
&event_logger_, mutable_cf_options.report_bg_io_stats,
|
||||
true /* sync_output_directory */, true /* write_manifest */, thread_pri,
|
||||
io_tracer_, db_id_, db_session_id_, cfd->GetFullHistoryTsLow(),
|
||||
&blob_callback_);
|
||||
io_tracer_, seqno_time_mapping_, db_id_, db_session_id_,
|
||||
cfd->GetFullHistoryTsLow(), &blob_callback_);
|
||||
FileMetaData file_meta;
|
||||
|
||||
Status s;
|
||||
@@ -450,7 +450,7 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
|
||||
data_dir, GetCompressionFlush(*cfd->ioptions(), mutable_cf_options),
|
||||
stats_, &event_logger_, mutable_cf_options.report_bg_io_stats,
|
||||
false /* sync_output_directory */, false /* write_manifest */,
|
||||
thread_pri, io_tracer_, db_id_, db_session_id_,
|
||||
thread_pri, io_tracer_, seqno_time_mapping_, db_id_, db_session_id_,
|
||||
cfd->GetFullHistoryTsLow(), &blob_callback_));
|
||||
}
|
||||
|
||||
@@ -3802,11 +3802,6 @@ void DBImpl::GetSnapshotContext(
|
||||
|
||||
Status DBImpl::WaitForCompact(bool wait_unscheduled) {
|
||||
// Wait until the compaction completes
|
||||
|
||||
// TODO: a bug here. This function actually does not necessarily
|
||||
// wait for compact. It actually waits for scheduled compaction
|
||||
// OR flush to finish.
|
||||
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
while ((bg_bottom_compaction_scheduled_ || bg_compaction_scheduled_ ||
|
||||
bg_flush_scheduled_ ||
|
||||
|
||||
@@ -302,7 +302,8 @@ size_t DBImpl::TEST_GetWalPreallocateBlockSize(
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
void DBImpl::TEST_WaitForStatsDumpRun(std::function<void()> callback) const {
|
||||
void DBImpl::TEST_WaitForPeridicWorkerRun(
|
||||
std::function<void()> callback) const {
|
||||
if (periodic_work_scheduler_ != nullptr) {
|
||||
static_cast<PeriodicWorkTestScheduler*>(periodic_work_scheduler_)
|
||||
->TEST_WaitForRun(callback);
|
||||
@@ -312,6 +313,12 @@ void DBImpl::TEST_WaitForStatsDumpRun(std::function<void()> callback) const {
|
||||
PeriodicWorkTestScheduler* DBImpl::TEST_GetPeriodicWorkScheduler() const {
|
||||
return static_cast<PeriodicWorkTestScheduler*>(periodic_work_scheduler_);
|
||||
}
|
||||
|
||||
SeqnoToTimeMapping DBImpl::TEST_GetSeqnoToTimeMapping() const {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
return seqno_time_mapping_;
|
||||
}
|
||||
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
size_t DBImpl::TEST_EstimateInMemoryStatsHistorySize() const {
|
||||
|
||||
@@ -538,6 +538,7 @@ Status DBImpl::Recover(
|
||||
s = CheckConsistency();
|
||||
}
|
||||
if (s.ok() && !read_only) {
|
||||
// TODO: share file descriptors (FSDirectory) with SetDirectories above
|
||||
std::map<std::string, std::shared_ptr<FSDirectory>> created_dirs;
|
||||
for (auto cfd : *versions_->GetColumnFamilySet()) {
|
||||
s = cfd->AddDirectories(&created_dirs);
|
||||
@@ -718,6 +719,12 @@ Status DBImpl::VerifySstUniqueIdInManifest() {
|
||||
status = version->VerifySstUniqueIds();
|
||||
mutex_.Lock();
|
||||
version->Unref();
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"SST unique id mismatch in column family \"%s\": %s",
|
||||
cfd->GetName().c_str(), status.ToString().c_str());
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
return status;
|
||||
@@ -1554,17 +1561,19 @@ Status DBImpl::WriteLevel0TableForRecovery(int job_id, ColumnFamilyData* cfd,
|
||||
GetCompressionFlush(*cfd->ioptions(), mutable_cf_options),
|
||||
mutable_cf_options.compression_opts, cfd->GetID(), cfd->GetName(),
|
||||
0 /* level */, false /* is_bottommost */,
|
||||
TableFileCreationReason::kRecovery, current_time,
|
||||
0 /* oldest_key_time */, 0 /* file_creation_time */, db_id_,
|
||||
db_session_id_, 0 /* target_file_size */, meta.fd.GetNumber());
|
||||
TableFileCreationReason::kRecovery, 0 /* oldest_key_time */,
|
||||
0 /* file_creation_time */, db_id_, db_session_id_,
|
||||
0 /* target_file_size */, meta.fd.GetNumber());
|
||||
SeqnoToTimeMapping empty_seqno_time_mapping;
|
||||
s = BuildTable(
|
||||
dbname_, versions_.get(), immutable_db_options_, tboptions,
|
||||
file_options_for_compaction_, cfd->table_cache(), iter.get(),
|
||||
std::move(range_del_iters), &meta, &blob_file_additions,
|
||||
snapshot_seqs, earliest_write_conflict_snapshot, kMaxSequenceNumber,
|
||||
snapshot_checker, paranoid_file_checks, cfd->internal_stats(), &io_s,
|
||||
io_tracer_, BlobFileCreationReason::kRecovery, &event_logger_, job_id,
|
||||
Env::IO_HIGH, nullptr /* table_properties */, write_hint,
|
||||
io_tracer_, BlobFileCreationReason::kRecovery,
|
||||
empty_seqno_time_mapping, &event_logger_, job_id, Env::IO_HIGH,
|
||||
nullptr /* table_properties */, write_hint,
|
||||
nullptr /*full_history_ts_low*/, &blob_callback_);
|
||||
LogFlush(immutable_db_options_.info_log);
|
||||
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
|
||||
@@ -2106,6 +2115,10 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
if (s.ok()) {
|
||||
s = impl->StartPeriodicWorkScheduler();
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
s = impl->RegisterRecordSeqnoTimeWorker();
|
||||
}
|
||||
if (!s.ok()) {
|
||||
for (auto* h : *handles) {
|
||||
delete h;
|
||||
|
||||
@@ -4101,9 +4101,6 @@ class MockedRateLimiterWithNoOptionalAPIImpl : public RateLimiter {
|
||||
|
||||
~MockedRateLimiterWithNoOptionalAPIImpl() override {}
|
||||
|
||||
const char* Name() const override {
|
||||
return "MockedRateLimiterWithNoOptionalAPI";
|
||||
}
|
||||
void SetBytesPerSecond(int64_t bytes_per_second) override {
|
||||
(void)bytes_per_second;
|
||||
}
|
||||
|
||||
+39
-12
@@ -33,18 +33,6 @@ namespace ROCKSDB_NAMESPACE {
|
||||
class DBTest2 : public DBTestBase {
|
||||
public:
|
||||
DBTest2() : DBTestBase("db_test2", /*env_do_fsync=*/true) {}
|
||||
|
||||
protected:
|
||||
#ifndef ROCKSDB_LITE
|
||||
uint64_t GetSstSizeHelper(Temperature temperature) {
|
||||
std::string prop;
|
||||
EXPECT_TRUE(dbfull()->GetProperty(
|
||||
DB::Properties::kLiveSstFilesSizeAtTemperature +
|
||||
std::to_string(static_cast<uint8_t>(temperature)),
|
||||
&prop));
|
||||
return static_cast<uint64_t>(std::atoi(prop.c_str()));
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
};
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
@@ -7436,6 +7424,45 @@ TEST_F(DBTest2, SstUniqueIdVerify) {
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, SstUniqueIdVerifyMultiCFs) {
|
||||
const int kNumSst = 3;
|
||||
const int kLevel0Trigger = 4;
|
||||
auto options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kLevel0Trigger;
|
||||
|
||||
CreateAndReopenWithCF({"one", "two"}, options);
|
||||
|
||||
// generate good SSTs
|
||||
for (int cf_num : {0, 2}) {
|
||||
for (int i = 0; i < kNumSst; i++) {
|
||||
for (int j = 0; j < 100; j++) {
|
||||
ASSERT_OK(Put(cf_num, Key(i * 10 + j), "value"));
|
||||
}
|
||||
ASSERT_OK(Flush(cf_num));
|
||||
}
|
||||
}
|
||||
|
||||
// generate SSTs with bad unique id
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"PropertyBlockBuilder::AddTableProperty:Start", [&](void* props_vs) {
|
||||
auto props = static_cast<TableProperties*>(props_vs);
|
||||
// update table property session_id to a different one
|
||||
props->db_session_id = DBImpl::GenerateDbSessionId(nullptr);
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
for (int i = 0; i < kNumSst; i++) {
|
||||
for (int j = 0; j < 100; j++) {
|
||||
ASSERT_OK(Put(1, Key(i * 10 + j), "value"));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
|
||||
// Reopen with verification should report corruption
|
||||
options.verify_sst_unique_id_in_manifest = true;
|
||||
auto s = TryReopenWithColumnFamilies({"default", "one", "two"}, options);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
TEST_F(DBTest2, GetLatestSeqAndTsForKey) {
|
||||
Destroy(last_options_);
|
||||
|
||||
@@ -1676,6 +1676,15 @@ uint64_t DBTestBase::GetNumberOfSstFilesForColumnFamily(
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
uint64_t DBTestBase::GetSstSizeHelper(Temperature temperature) {
|
||||
std::string prop;
|
||||
EXPECT_TRUE(dbfull()->GetProperty(
|
||||
DB::Properties::kLiveSstFilesSizeAtTemperature +
|
||||
std::to_string(static_cast<uint8_t>(temperature)),
|
||||
&prop));
|
||||
return static_cast<uint64_t>(std::atoi(prop.c_str()));
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
void VerifySstUniqueIds(const TablePropertiesCollection& props) {
|
||||
|
||||
@@ -1345,6 +1345,8 @@ class DBTestBase : public testing::Test {
|
||||
#ifndef ROCKSDB_LITE
|
||||
uint64_t GetNumberOfSstFilesForColumnFamily(DB* db,
|
||||
std::string column_family_name);
|
||||
|
||||
uint64_t GetSstSizeHelper(Temperature temperature);
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
uint64_t TestGetTickerCount(const Options& options, Tickers ticker_type) {
|
||||
|
||||
+3
-1
@@ -68,7 +68,9 @@ enum ValueType : unsigned char {
|
||||
kTypeCommitXIDAndTimestamp = 0x15, // WAL only
|
||||
kTypeWideColumnEntity = 0x16,
|
||||
kTypeColumnFamilyWideColumnEntity = 0x17, // WAL only
|
||||
kMaxValue = 0x7F // Not used for storing records.
|
||||
kTypeMaxValid, // Should be after the last valid type, only used for
|
||||
// validation
|
||||
kMaxValue = 0x7F // Not used for storing records.
|
||||
};
|
||||
|
||||
// Defined in dbformat.cc
|
||||
|
||||
+13
-1
@@ -148,7 +148,19 @@ void EventHelpers::LogAndNotifyTableFileCreationFinished(
|
||||
<< table_properties.fast_compression_estimated_data_size
|
||||
<< "db_id" << table_properties.db_id << "db_session_id"
|
||||
<< table_properties.db_session_id << "orig_file_number"
|
||||
<< table_properties.orig_file_number;
|
||||
<< table_properties.orig_file_number << "seqno_to_time_mapping";
|
||||
|
||||
if (table_properties.seqno_to_time_mapping.empty()) {
|
||||
jwriter << "N/A";
|
||||
} else {
|
||||
SeqnoToTimeMapping tmp;
|
||||
Status status = tmp.Add(table_properties.seqno_to_time_mapping);
|
||||
if (status.ok()) {
|
||||
jwriter << tmp.ToHumanString();
|
||||
} else {
|
||||
jwriter << "Invalid";
|
||||
}
|
||||
}
|
||||
|
||||
// user collected properties
|
||||
for (const auto& prop : table_properties.readable_properties) {
|
||||
|
||||
@@ -187,16 +187,6 @@ class ExternalSSTFileBasicTest
|
||||
std::string sst_files_dir_;
|
||||
std::unique_ptr<FaultInjectionTestEnv> fault_injection_test_env_;
|
||||
bool random_rwfile_supported_;
|
||||
#ifndef ROCKSDB_LITE
|
||||
uint64_t GetSstSizeHelper(Temperature temperature) {
|
||||
std::string prop;
|
||||
EXPECT_TRUE(dbfull()->GetProperty(
|
||||
DB::Properties::kLiveSstFilesSizeAtTemperature +
|
||||
std::to_string(static_cast<uint8_t>(temperature)),
|
||||
&prop));
|
||||
return static_cast<uint64_t>(std::atoi(prop.c_str()));
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
};
|
||||
|
||||
TEST_F(ExternalSSTFileBasicTest, Basic) {
|
||||
|
||||
+19
-15
@@ -95,8 +95,9 @@ FlushJob::FlushJob(
|
||||
Statistics* stats, EventLogger* event_logger, bool measure_io_stats,
|
||||
const bool sync_output_directory, const bool write_manifest,
|
||||
Env::Priority thread_pri, const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::string& db_id, const std::string& db_session_id,
|
||||
std::string full_history_ts_low, BlobFileCompletionCallback* blob_callback)
|
||||
const SeqnoToTimeMapping& seqno_time_mapping, const std::string& db_id,
|
||||
const std::string& db_session_id, std::string full_history_ts_low,
|
||||
BlobFileCompletionCallback* blob_callback)
|
||||
: dbname_(dbname),
|
||||
db_id_(db_id),
|
||||
db_session_id_(db_session_id),
|
||||
@@ -128,7 +129,8 @@ FlushJob::FlushJob(
|
||||
io_tracer_(io_tracer),
|
||||
clock_(db_options_.clock),
|
||||
full_history_ts_low_(std::move(full_history_ts_low)),
|
||||
blob_callback_(blob_callback) {
|
||||
blob_callback_(blob_callback),
|
||||
db_impl_seqno_time_mapping_(seqno_time_mapping) {
|
||||
// Update the thread status to indicate flush.
|
||||
ReportStartedFlush();
|
||||
TEST_SYNC_POINT("FlushJob::FlushJob()");
|
||||
@@ -814,6 +816,13 @@ Status FlushJob::WriteLevel0Table() {
|
||||
const uint64_t start_cpu_micros = clock_->CPUMicros();
|
||||
Status s;
|
||||
|
||||
SequenceNumber smallest_seqno = mems_.front()->GetEarliestSequenceNumber();
|
||||
if (!db_impl_seqno_time_mapping_.Empty()) {
|
||||
// make a local copy, as the seqno_time_mapping from db_impl is not thread
|
||||
// safe, which will be used while not holding the db_mutex.
|
||||
seqno_to_time_mapping_ = db_impl_seqno_time_mapping_.Copy(smallest_seqno);
|
||||
}
|
||||
|
||||
std::vector<BlobFileAddition> blob_file_additions;
|
||||
|
||||
{
|
||||
@@ -902,18 +911,13 @@ Status FlushJob::WriteLevel0Table() {
|
||||
"FlushJob::WriteLevel0Table:oldest_ancester_time",
|
||||
&oldest_ancester_time);
|
||||
meta_.oldest_ancester_time = oldest_ancester_time;
|
||||
|
||||
meta_.file_creation_time = current_time;
|
||||
|
||||
uint64_t creation_time = (cfd_->ioptions()->compaction_style ==
|
||||
CompactionStyle::kCompactionStyleFIFO)
|
||||
? current_time
|
||||
: meta_.oldest_ancester_time;
|
||||
|
||||
uint64_t num_input_entries = 0;
|
||||
uint64_t memtable_payload_bytes = 0;
|
||||
uint64_t memtable_garbage_bytes = 0;
|
||||
IOStatus io_s;
|
||||
|
||||
const std::string* const full_history_ts_low =
|
||||
(full_history_ts_low_.empty()) ? nullptr : &full_history_ts_low_;
|
||||
TableBuilderOptions tboptions(
|
||||
@@ -921,8 +925,8 @@ Status FlushJob::WriteLevel0Table() {
|
||||
cfd_->int_tbl_prop_collector_factories(), output_compression_,
|
||||
mutable_cf_options_.compression_opts, cfd_->GetID(), cfd_->GetName(),
|
||||
0 /* level */, false /* is_bottommost */,
|
||||
TableFileCreationReason::kFlush, creation_time, oldest_key_time,
|
||||
current_time, db_id_, db_session_id_, 0 /* target_file_size */,
|
||||
TableFileCreationReason::kFlush, oldest_key_time, current_time,
|
||||
db_id_, db_session_id_, 0 /* target_file_size */,
|
||||
meta_.fd.GetNumber());
|
||||
const SequenceNumber job_snapshot_seq =
|
||||
job_context_->GetJobSnapshotSequence();
|
||||
@@ -933,10 +937,10 @@ Status FlushJob::WriteLevel0Table() {
|
||||
earliest_write_conflict_snapshot_, job_snapshot_seq,
|
||||
snapshot_checker_, mutable_cf_options_.paranoid_file_checks,
|
||||
cfd_->internal_stats(), &io_s, io_tracer_,
|
||||
BlobFileCreationReason::kFlush, event_logger_, job_context_->job_id,
|
||||
io_priority, &table_properties_, write_hint, full_history_ts_low,
|
||||
blob_callback_, &num_input_entries, &memtable_payload_bytes,
|
||||
&memtable_garbage_bytes);
|
||||
BlobFileCreationReason::kFlush, seqno_to_time_mapping_, event_logger_,
|
||||
job_context_->job_id, io_priority, &table_properties_, write_hint,
|
||||
full_history_ts_low, blob_callback_, &num_input_entries,
|
||||
&memtable_payload_bytes, &memtable_garbage_bytes);
|
||||
// TODO: Cleanup io_status in BuildTable and table builders
|
||||
assert(!s.ok() || io_s.ok());
|
||||
io_s.PermitUncheckedError();
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "db/log_writer.h"
|
||||
#include "db/logs_with_prep_tracker.h"
|
||||
#include "db/memtable_list.h"
|
||||
#include "db/seqno_to_time_mapping.h"
|
||||
#include "db/snapshot_impl.h"
|
||||
#include "db/version_edit.h"
|
||||
#include "db/write_controller.h"
|
||||
@@ -72,6 +73,7 @@ class FlushJob {
|
||||
EventLogger* event_logger, bool measure_io_stats,
|
||||
const bool sync_output_directory, const bool write_manifest,
|
||||
Env::Priority thread_pri, const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const SeqnoToTimeMapping& seq_time_mapping,
|
||||
const std::string& db_id = "", const std::string& db_session_id = "",
|
||||
std::string full_history_ts_low = "",
|
||||
BlobFileCompletionCallback* blob_callback = nullptr);
|
||||
@@ -191,6 +193,11 @@ class FlushJob {
|
||||
|
||||
const std::string full_history_ts_low_;
|
||||
BlobFileCompletionCallback* blob_callback_;
|
||||
|
||||
// reference to the seqno_time_mapping_ in db_impl.h, not safe to read without
|
||||
// db mutex
|
||||
const SeqnoToTimeMapping& db_impl_seqno_time_mapping_;
|
||||
SeqnoToTimeMapping seqno_to_time_mapping_;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+21
-15
@@ -148,6 +148,8 @@ class FlushJobTestBase : public testing::Test {
|
||||
InstrumentedMutex mutex_;
|
||||
std::atomic<bool> shutting_down_;
|
||||
std::shared_ptr<mock::MockTableFactory> mock_table_factory_;
|
||||
|
||||
SeqnoToTimeMapping empty_seqno_to_time_mapping_;
|
||||
};
|
||||
|
||||
class FlushJobTest : public FlushJobTestBase {
|
||||
@@ -162,14 +164,15 @@ TEST_F(FlushJobTest, Empty) {
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
EventLogger event_logger(db_options_.info_log.get());
|
||||
SnapshotChecker* snapshot_checker = nullptr; // not relavant
|
||||
FlushJob flush_job(
|
||||
dbname_, versions_->GetColumnFamilySet()->GetDefault(), db_options_,
|
||||
*cfd->GetLatestMutableCFOptions(),
|
||||
std::numeric_limits<uint64_t>::max() /* memtable_id */, env_options_,
|
||||
versions_.get(), &mutex_, &shutting_down_, {}, kMaxSequenceNumber,
|
||||
snapshot_checker, &job_context, nullptr, nullptr, nullptr, kNoCompression,
|
||||
nullptr, &event_logger, false, true /* sync_output_directory */,
|
||||
true /* write_manifest */, Env::Priority::USER, nullptr /*IOTracer*/);
|
||||
FlushJob flush_job(dbname_, versions_->GetColumnFamilySet()->GetDefault(),
|
||||
db_options_, *cfd->GetLatestMutableCFOptions(),
|
||||
std::numeric_limits<uint64_t>::max() /* memtable_id */,
|
||||
env_options_, versions_.get(), &mutex_, &shutting_down_,
|
||||
{}, kMaxSequenceNumber, snapshot_checker, &job_context,
|
||||
nullptr, nullptr, nullptr, kNoCompression, nullptr,
|
||||
&event_logger, false, true /* sync_output_directory */,
|
||||
true /* write_manifest */, Env::Priority::USER,
|
||||
nullptr /*IOTracer*/, empty_seqno_to_time_mapping_);
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
flush_job.PickMemTable();
|
||||
@@ -254,7 +257,7 @@ TEST_F(FlushJobTest, NonEmpty) {
|
||||
snapshot_checker, &job_context, nullptr, nullptr, nullptr, kNoCompression,
|
||||
db_options_.statistics.get(), &event_logger, true,
|
||||
true /* sync_output_directory */, true /* write_manifest */,
|
||||
Env::Priority::USER, nullptr /*IOTracer*/);
|
||||
Env::Priority::USER, nullptr /*IOTracer*/, empty_seqno_to_time_mapping_);
|
||||
|
||||
HistogramData hist;
|
||||
FileMetaData file_meta;
|
||||
@@ -316,7 +319,7 @@ TEST_F(FlushJobTest, FlushMemTablesSingleColumnFamily) {
|
||||
snapshot_checker, &job_context, nullptr, nullptr, nullptr, kNoCompression,
|
||||
db_options_.statistics.get(), &event_logger, true,
|
||||
true /* sync_output_directory */, true /* write_manifest */,
|
||||
Env::Priority::USER, nullptr /*IOTracer*/);
|
||||
Env::Priority::USER, nullptr /*IOTracer*/, empty_seqno_to_time_mapping_);
|
||||
HistogramData hist;
|
||||
FileMetaData file_meta;
|
||||
mutex_.Lock();
|
||||
@@ -389,7 +392,8 @@ TEST_F(FlushJobTest, FlushMemtablesMultipleColumnFamilies) {
|
||||
&job_context, nullptr, nullptr, nullptr, kNoCompression,
|
||||
db_options_.statistics.get(), &event_logger, true,
|
||||
false /* sync_output_directory */, false /* write_manifest */,
|
||||
Env::Priority::USER, nullptr /*IOTracer*/));
|
||||
Env::Priority::USER, nullptr /*IOTracer*/,
|
||||
empty_seqno_to_time_mapping_));
|
||||
k++;
|
||||
}
|
||||
HistogramData hist;
|
||||
@@ -516,7 +520,7 @@ TEST_F(FlushJobTest, Snapshots) {
|
||||
snapshot_checker, &job_context, nullptr, nullptr, nullptr, kNoCompression,
|
||||
db_options_.statistics.get(), &event_logger, true,
|
||||
true /* sync_output_directory */, true /* write_manifest */,
|
||||
Env::Priority::USER, nullptr /*IOTracer*/);
|
||||
Env::Priority::USER, nullptr /*IOTracer*/, empty_seqno_to_time_mapping_);
|
||||
mutex_.Lock();
|
||||
flush_job.PickMemTable();
|
||||
ASSERT_OK(flush_job.Run());
|
||||
@@ -571,7 +575,7 @@ TEST_F(FlushJobTest, GetRateLimiterPriorityForWrite) {
|
||||
snapshot_checker, &job_context, nullptr, nullptr, nullptr, kNoCompression,
|
||||
db_options_.statistics.get(), &event_logger, true,
|
||||
true /* sync_output_directory */, true /* write_manifest */,
|
||||
Env::Priority::USER, nullptr /*IOTracer*/);
|
||||
Env::Priority::USER, nullptr /*IOTracer*/, empty_seqno_to_time_mapping_);
|
||||
|
||||
// When the state from WriteController is normal.
|
||||
ASSERT_EQ(flush_job.GetRateLimiterPriorityForWrite(), Env::IO_HIGH);
|
||||
@@ -650,7 +654,8 @@ TEST_F(FlushJobTimestampTest, AllKeysExpired) {
|
||||
snapshot_checker, &job_context, nullptr, nullptr, nullptr, kNoCompression,
|
||||
db_options_.statistics.get(), &event_logger, true,
|
||||
true /* sync_output_directory */, true /* write_manifest */,
|
||||
Env::Priority::USER, nullptr /*IOTracer*/, /*db_id=*/"",
|
||||
Env::Priority::USER, nullptr /*IOTracer*/, empty_seqno_to_time_mapping_,
|
||||
/*db_id=*/"",
|
||||
/*db_session_id=*/"", full_history_ts_low);
|
||||
|
||||
FileMetaData fmeta;
|
||||
@@ -701,7 +706,8 @@ TEST_F(FlushJobTimestampTest, NoKeyExpired) {
|
||||
snapshot_checker, &job_context, nullptr, nullptr, nullptr, kNoCompression,
|
||||
db_options_.statistics.get(), &event_logger, true,
|
||||
true /* sync_output_directory */, true /* write_manifest */,
|
||||
Env::Priority::USER, nullptr /*IOTracer*/, /*db_id=*/"",
|
||||
Env::Priority::USER, nullptr /*IOTracer*/, empty_seqno_to_time_mapping_,
|
||||
/*db_id=*/"",
|
||||
/*db_session_id=*/"", full_history_ts_low);
|
||||
|
||||
FileMetaData fmeta;
|
||||
|
||||
@@ -255,6 +255,15 @@ SequenceNumber MemTableListVersion::GetEarliestSequenceNumber(
|
||||
}
|
||||
}
|
||||
|
||||
SequenceNumber MemTableListVersion::GetFirstSequenceNumber() const {
|
||||
SequenceNumber min_first_seqno = kMaxSequenceNumber;
|
||||
// The first memtable in the list might not be the oldest one with mempurge
|
||||
for (const auto& m : memlist_) {
|
||||
min_first_seqno = std::min(m->GetFirstSequenceNumber(), min_first_seqno);
|
||||
}
|
||||
return min_first_seqno;
|
||||
}
|
||||
|
||||
// caller is responsible for referencing m
|
||||
void MemTableListVersion::Add(MemTable* m, autovector<MemTable*>* to_delete) {
|
||||
assert(refs_ == 1); // only when refs_ == 1 is MemTableListVersion mutable
|
||||
|
||||
@@ -129,6 +129,11 @@ class MemTableListVersion {
|
||||
// History.
|
||||
SequenceNumber GetEarliestSequenceNumber(bool include_history = false) const;
|
||||
|
||||
// Return the first sequence number from the memtable list, which is the
|
||||
// smallest sequence number of all FirstSequenceNumber.
|
||||
// Return kMaxSequenceNumber if the list is empty.
|
||||
SequenceNumber GetFirstSequenceNumber() const;
|
||||
|
||||
private:
|
||||
friend class MemTableList;
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
#ifndef ROCKSDB_LITE
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
const std::string PeriodicWorkTaskNames::kDumpStats = "dump_st";
|
||||
const std::string PeriodicWorkTaskNames::kPersistStats = "pst_st";
|
||||
const std::string PeriodicWorkTaskNames::kFlushInfoLog = "flush_info_log";
|
||||
const std::string PeriodicWorkTaskNames::kRecordSeqnoTime = "record_seq_time";
|
||||
|
||||
PeriodicWorkScheduler::PeriodicWorkScheduler(
|
||||
const std::shared_ptr<SystemClock>& clock) {
|
||||
timer = std::unique_ptr<Timer>(new Timer(clock.get()));
|
||||
@@ -24,7 +29,8 @@ Status PeriodicWorkScheduler::Register(DBImpl* dbi,
|
||||
timer->Start();
|
||||
if (stats_dump_period_sec > 0) {
|
||||
bool succeeded = timer->Add(
|
||||
[dbi]() { dbi->DumpStats(); }, GetTaskName(dbi, "dump_st"),
|
||||
[dbi]() { dbi->DumpStats(); },
|
||||
GetTaskName(dbi, PeriodicWorkTaskNames::kDumpStats),
|
||||
initial_delay.fetch_add(1) %
|
||||
static_cast<uint64_t>(stats_dump_period_sec) * kMicrosInSecond,
|
||||
static_cast<uint64_t>(stats_dump_period_sec) * kMicrosInSecond);
|
||||
@@ -34,7 +40,8 @@ Status PeriodicWorkScheduler::Register(DBImpl* dbi,
|
||||
}
|
||||
if (stats_persist_period_sec > 0) {
|
||||
bool succeeded = timer->Add(
|
||||
[dbi]() { dbi->PersistStats(); }, GetTaskName(dbi, "pst_st"),
|
||||
[dbi]() { dbi->PersistStats(); },
|
||||
GetTaskName(dbi, PeriodicWorkTaskNames::kPersistStats),
|
||||
initial_delay.fetch_add(1) %
|
||||
static_cast<uint64_t>(stats_persist_period_sec) * kMicrosInSecond,
|
||||
static_cast<uint64_t>(stats_persist_period_sec) * kMicrosInSecond);
|
||||
@@ -42,22 +49,48 @@ Status PeriodicWorkScheduler::Register(DBImpl* dbi,
|
||||
return Status::Aborted("Unable to add periodic task PersistStats");
|
||||
}
|
||||
}
|
||||
bool succeeded = timer->Add(
|
||||
[dbi]() { dbi->FlushInfoLog(); }, GetTaskName(dbi, "flush_info_log"),
|
||||
initial_delay.fetch_add(1) % kDefaultFlushInfoLogPeriodSec *
|
||||
kMicrosInSecond,
|
||||
kDefaultFlushInfoLogPeriodSec * kMicrosInSecond);
|
||||
bool succeeded =
|
||||
timer->Add([dbi]() { dbi->FlushInfoLog(); },
|
||||
GetTaskName(dbi, PeriodicWorkTaskNames::kFlushInfoLog),
|
||||
initial_delay.fetch_add(1) % kDefaultFlushInfoLogPeriodSec *
|
||||
kMicrosInSecond,
|
||||
kDefaultFlushInfoLogPeriodSec * kMicrosInSecond);
|
||||
if (!succeeded) {
|
||||
return Status::Aborted("Unable to add periodic task PersistStats");
|
||||
return Status::Aborted("Unable to add periodic task FlushInfoLog");
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status PeriodicWorkScheduler::RegisterRecordSeqnoTimeWorker(
|
||||
DBImpl* dbi, uint64_t record_cadence_sec) {
|
||||
MutexLock l(&timer_mu_);
|
||||
timer->Start();
|
||||
static std::atomic_uint64_t initial_delay(0);
|
||||
bool succeeded = timer->Add(
|
||||
[dbi]() { dbi->RecordSeqnoToTimeMapping(); },
|
||||
GetTaskName(dbi, PeriodicWorkTaskNames::kRecordSeqnoTime),
|
||||
initial_delay.fetch_add(1) % record_cadence_sec * kMicrosInSecond,
|
||||
record_cadence_sec * kMicrosInSecond);
|
||||
if (!succeeded) {
|
||||
return Status::NotSupported(
|
||||
"Updating seqno to time worker cadence is not supported yet");
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void PeriodicWorkScheduler::UnregisterRecordSeqnoTimeWorker(DBImpl* dbi) {
|
||||
MutexLock l(&timer_mu_);
|
||||
timer->Cancel(GetTaskName(dbi, PeriodicWorkTaskNames::kRecordSeqnoTime));
|
||||
if (!timer->HasPendingTask()) {
|
||||
timer->Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
void PeriodicWorkScheduler::Unregister(DBImpl* dbi) {
|
||||
MutexLock l(&timer_mu_);
|
||||
timer->Cancel(GetTaskName(dbi, "dump_st"));
|
||||
timer->Cancel(GetTaskName(dbi, "pst_st"));
|
||||
timer->Cancel(GetTaskName(dbi, "flush_info_log"));
|
||||
timer->Cancel(GetTaskName(dbi, PeriodicWorkTaskNames::kDumpStats));
|
||||
timer->Cancel(GetTaskName(dbi, PeriodicWorkTaskNames::kPersistStats));
|
||||
timer->Cancel(GetTaskName(dbi, PeriodicWorkTaskNames::kFlushInfoLog));
|
||||
if (!timer->HasPendingTask()) {
|
||||
timer->Shutdown();
|
||||
}
|
||||
@@ -71,8 +104,8 @@ PeriodicWorkScheduler* PeriodicWorkScheduler::Default() {
|
||||
return &scheduler;
|
||||
}
|
||||
|
||||
std::string PeriodicWorkScheduler::GetTaskName(DBImpl* dbi,
|
||||
const std::string& func_name) {
|
||||
std::string PeriodicWorkScheduler::GetTaskName(
|
||||
const DBImpl* dbi, const std::string& func_name) const {
|
||||
std::string db_session_id;
|
||||
// TODO: Should this error be ignored?
|
||||
dbi->GetDbSessionId(db_session_id).PermitUncheckedError();
|
||||
@@ -117,6 +150,14 @@ size_t PeriodicWorkTestScheduler::TEST_GetValidTaskNum() const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool PeriodicWorkTestScheduler::TEST_HasValidTask(
|
||||
const DBImpl* dbi, const std::string& func_name) const {
|
||||
if (timer == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return timer->TEST_HasVaildTask(GetTaskName(dbi, func_name));
|
||||
}
|
||||
|
||||
PeriodicWorkTestScheduler::PeriodicWorkTestScheduler(
|
||||
const std::shared_ptr<SystemClock>& clock)
|
||||
: PeriodicWorkScheduler(clock) {}
|
||||
|
||||
@@ -32,8 +32,10 @@ class PeriodicWorkScheduler {
|
||||
|
||||
Status Register(DBImpl* dbi, unsigned int stats_dump_period_sec,
|
||||
unsigned int stats_persist_period_sec);
|
||||
Status RegisterRecordSeqnoTimeWorker(DBImpl* dbi, uint64_t record_cadence);
|
||||
|
||||
void Unregister(DBImpl* dbi);
|
||||
void UnregisterRecordSeqnoTimeWorker(DBImpl* dbi);
|
||||
|
||||
// Periodically flush info log out of application buffer at a low frequency.
|
||||
// This improves debuggability in case of RocksDB hanging since it ensures the
|
||||
@@ -52,8 +54,9 @@ class PeriodicWorkScheduler {
|
||||
|
||||
explicit PeriodicWorkScheduler(const std::shared_ptr<SystemClock>& clock);
|
||||
|
||||
private:
|
||||
std::string GetTaskName(DBImpl* dbi, const std::string& func_name);
|
||||
// Get the unique task name (prefix with db session id)
|
||||
std::string GetTaskName(const DBImpl* dbi,
|
||||
const std::string& func_name) const;
|
||||
};
|
||||
|
||||
#ifndef NDEBUG
|
||||
@@ -68,11 +71,20 @@ class PeriodicWorkTestScheduler : public PeriodicWorkScheduler {
|
||||
|
||||
size_t TEST_GetValidTaskNum() const;
|
||||
|
||||
bool TEST_HasValidTask(const DBImpl* dbi, const std::string& func_name) const;
|
||||
|
||||
private:
|
||||
explicit PeriodicWorkTestScheduler(const std::shared_ptr<SystemClock>& clock);
|
||||
};
|
||||
#endif // !NDEBUG
|
||||
|
||||
struct PeriodicWorkTaskNames {
|
||||
static const std::string kDumpStats;
|
||||
static const std::string kPersistStats;
|
||||
static const std::string kFlushInfoLog;
|
||||
static const std::string kRecordSeqnoTime;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
#endif // ROCKSDB_LITE
|
||||
@@ -66,7 +66,7 @@ TEST_F(PeriodicWorkSchedulerTest, Basic) {
|
||||
ASSERT_EQ(kPeriodSec, dbfull()->GetDBOptions().stats_persist_period_sec);
|
||||
|
||||
ASSERT_GT(kPeriodSec, 1u);
|
||||
dbfull()->TEST_WaitForStatsDumpRun([&] {
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun([&] {
|
||||
mock_clock_->MockSleepForSeconds(static_cast<int>(kPeriodSec) - 1);
|
||||
});
|
||||
|
||||
@@ -78,14 +78,14 @@ TEST_F(PeriodicWorkSchedulerTest, Basic) {
|
||||
ASSERT_EQ(1, pst_st_counter);
|
||||
ASSERT_EQ(1, flush_info_log_counter);
|
||||
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(kPeriodSec)); });
|
||||
|
||||
ASSERT_EQ(2, dump_st_counter);
|
||||
ASSERT_EQ(2, pst_st_counter);
|
||||
ASSERT_EQ(2, flush_info_log_counter);
|
||||
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(kPeriodSec)); });
|
||||
|
||||
ASSERT_EQ(3, dump_st_counter);
|
||||
@@ -99,7 +99,7 @@ TEST_F(PeriodicWorkSchedulerTest, Basic) {
|
||||
ASSERT_EQ(0u, dbfull()->GetDBOptions().stats_persist_period_sec);
|
||||
|
||||
// Info log flush should still run.
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(kPeriodSec)); });
|
||||
ASSERT_EQ(3, dump_st_counter);
|
||||
ASSERT_EQ(3, pst_st_counter);
|
||||
@@ -117,7 +117,7 @@ TEST_F(PeriodicWorkSchedulerTest, Basic) {
|
||||
ASSERT_NE(nullptr, scheduler);
|
||||
ASSERT_EQ(2, scheduler->TEST_GetValidTaskNum());
|
||||
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(kPeriodSec)); });
|
||||
ASSERT_EQ(4, dump_st_counter);
|
||||
ASSERT_EQ(3, pst_st_counter);
|
||||
@@ -157,19 +157,19 @@ TEST_F(PeriodicWorkSchedulerTest, MultiInstances) {
|
||||
ASSERT_EQ(kInstanceNum * 3, scheduler->TEST_GetValidTaskNum());
|
||||
|
||||
int expected_run = kInstanceNum;
|
||||
dbi->TEST_WaitForStatsDumpRun(
|
||||
dbi->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec - 1); });
|
||||
ASSERT_EQ(expected_run, dump_st_counter);
|
||||
ASSERT_EQ(expected_run, pst_st_counter);
|
||||
|
||||
expected_run += kInstanceNum;
|
||||
dbi->TEST_WaitForStatsDumpRun(
|
||||
dbi->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
ASSERT_EQ(expected_run, dump_st_counter);
|
||||
ASSERT_EQ(expected_run, pst_st_counter);
|
||||
|
||||
expected_run += kInstanceNum;
|
||||
dbi->TEST_WaitForStatsDumpRun(
|
||||
dbi->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
ASSERT_EQ(expected_run, dump_st_counter);
|
||||
ASSERT_EQ(expected_run, pst_st_counter);
|
||||
@@ -181,9 +181,9 @@ TEST_F(PeriodicWorkSchedulerTest, MultiInstances) {
|
||||
|
||||
expected_run += (kInstanceNum - half) * 2;
|
||||
|
||||
dbi->TEST_WaitForStatsDumpRun(
|
||||
dbi->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
dbi->TEST_WaitForStatsDumpRun(
|
||||
dbi->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
ASSERT_EQ(expected_run, dump_st_counter);
|
||||
ASSERT_EQ(expected_run, pst_st_counter);
|
||||
|
||||
+8
-6
@@ -425,6 +425,7 @@ class Repairer {
|
||||
immutable_db_options_.clock->GetCurrentTime(&_current_time)
|
||||
.PermitUncheckedError(); // ignore error
|
||||
const uint64_t current_time = static_cast<uint64_t>(_current_time);
|
||||
meta.file_creation_time = current_time;
|
||||
SnapshotChecker* snapshot_checker = DisableGCSnapshotChecker::Instance();
|
||||
|
||||
auto write_hint = cfd->CalculateSSTWriteHint(0);
|
||||
@@ -443,10 +444,11 @@ class Repairer {
|
||||
cfd->internal_comparator(), cfd->int_tbl_prop_collector_factories(),
|
||||
kNoCompression, default_compression, cfd->GetID(), cfd->GetName(),
|
||||
-1 /* level */, false /* is_bottommost */,
|
||||
TableFileCreationReason::kRecovery, current_time,
|
||||
0 /* oldest_key_time */, 0 /* file_creation_time */,
|
||||
"DB Repairer" /* db_id */, db_session_id_, 0 /*target_file_size*/,
|
||||
meta.fd.GetNumber());
|
||||
TableFileCreationReason::kRecovery, 0 /* oldest_key_time */,
|
||||
0 /* file_creation_time */, "DB Repairer" /* db_id */, db_session_id_,
|
||||
0 /*target_file_size*/, meta.fd.GetNumber());
|
||||
|
||||
SeqnoToTimeMapping empty_seqno_time_mapping;
|
||||
status = BuildTable(
|
||||
dbname_, /* versions */ nullptr, immutable_db_options_, tboptions,
|
||||
file_options_, table_cache_.get(), iter.get(),
|
||||
@@ -454,8 +456,8 @@ class Repairer {
|
||||
{}, kMaxSequenceNumber, kMaxSequenceNumber, snapshot_checker,
|
||||
false /* paranoid_file_checks*/, nullptr /* internal_stats */, &io_s,
|
||||
nullptr /*IOTracer*/, BlobFileCreationReason::kRecovery,
|
||||
nullptr /* event_logger */, 0 /* job_id */, Env::IO_HIGH,
|
||||
nullptr /* table_properties */, write_hint);
|
||||
empty_seqno_time_mapping, nullptr /* event_logger */, 0 /* job_id */,
|
||||
Env::IO_HIGH, nullptr /* table_properties */, write_hint);
|
||||
ROCKS_LOG_INFO(db_options_.info_log,
|
||||
"Log #%" PRIu64 ": %d ops saved to Table #%" PRIu64 " %s",
|
||||
log, counter, meta.fd.GetNumber(),
|
||||
|
||||
@@ -0,0 +1,890 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
//
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/periodic_work_scheduler.h"
|
||||
#include "db/seqno_to_time_mapping.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/iostats_context.h"
|
||||
#include "test_util/mock_time_env.h"
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class SeqnoTimeTest : public DBTestBase {
|
||||
public:
|
||||
SeqnoTimeTest() : DBTestBase("seqno_time_test", /*env_do_fsync=*/false) {
|
||||
mock_clock_ = std::make_shared<MockSystemClock>(env_->GetSystemClock());
|
||||
mock_env_ = std::make_unique<CompositeEnvWrapper>(env_, mock_clock_);
|
||||
}
|
||||
|
||||
protected:
|
||||
std::unique_ptr<Env> mock_env_;
|
||||
std::shared_ptr<MockSystemClock> mock_clock_;
|
||||
|
||||
void SetUp() override {
|
||||
mock_clock_->InstallTimedWaitFixCallback();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::StartPeriodicWorkScheduler:Init", [&](void* arg) {
|
||||
auto* periodic_work_scheduler_ptr =
|
||||
reinterpret_cast<PeriodicWorkScheduler**>(arg);
|
||||
*periodic_work_scheduler_ptr =
|
||||
PeriodicWorkTestScheduler::Default(mock_clock_);
|
||||
});
|
||||
}
|
||||
|
||||
// make sure the file is not in cache, otherwise it won't have IO info
|
||||
void AssertKetTemperature(int key_id, Temperature expected_temperature) {
|
||||
get_iostats_context()->Reset();
|
||||
IOStatsContext* iostats = get_iostats_context();
|
||||
std::string result = Get(Key(key_id));
|
||||
ASSERT_FALSE(result.empty());
|
||||
ASSERT_GT(iostats->bytes_read, 0);
|
||||
switch (expected_temperature) {
|
||||
case Temperature::kUnknown:
|
||||
ASSERT_EQ(iostats->file_io_stats_by_temperature.cold_file_read_count,
|
||||
0);
|
||||
ASSERT_EQ(iostats->file_io_stats_by_temperature.cold_file_bytes_read,
|
||||
0);
|
||||
break;
|
||||
case Temperature::kCold:
|
||||
ASSERT_GT(iostats->file_io_stats_by_temperature.cold_file_read_count,
|
||||
0);
|
||||
ASSERT_GT(iostats->file_io_stats_by_temperature.cold_file_bytes_read,
|
||||
0);
|
||||
break;
|
||||
default:
|
||||
// the test only support kCold now for the bottommost temperature
|
||||
FAIL();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SeqnoTimeTest, TemperatureBasicUniversal) {
|
||||
const int kNumTrigger = 4;
|
||||
const int kNumLevels = 7;
|
||||
const int kNumKeys = 100;
|
||||
const int kKeyPerSec = 10;
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleUniversal;
|
||||
options.preclude_last_level_data_seconds = 10000;
|
||||
options.env = mock_env_.get();
|
||||
options.bottommost_temperature = Temperature::kCold;
|
||||
options.num_levels = kNumLevels;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// pass some time first, otherwise the first a few keys write time are going
|
||||
// to be zero, and internally zero has special meaning: kUnknownSeqnoTime
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(kKeyPerSec)); });
|
||||
|
||||
int sst_num = 0;
|
||||
// Write files that are overlap and enough to trigger compaction
|
||||
for (; sst_num < kNumTrigger; sst_num++) {
|
||||
for (int i = 0; i < kNumKeys; i++) {
|
||||
ASSERT_OK(Put(Key(sst_num * (kNumKeys - 1) + i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun([&] {
|
||||
mock_clock_->MockSleepForSeconds(static_cast<int>(kKeyPerSec));
|
||||
});
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
ASSERT_OK(dbfull()->WaitForCompact(true));
|
||||
|
||||
// All data is hot, only output to penultimate level
|
||||
ASSERT_EQ("0,0,0,0,0,1", FilesPerLevel());
|
||||
ASSERT_GT(GetSstSizeHelper(Temperature::kUnknown), 0);
|
||||
ASSERT_EQ(GetSstSizeHelper(Temperature::kCold), 0);
|
||||
|
||||
// read a random key, which should be hot (kUnknown)
|
||||
AssertKetTemperature(20, Temperature::kUnknown);
|
||||
|
||||
// Write more data, but still all hot until the 10th SST, as:
|
||||
// write a key every 10 seconds, 100 keys per SST, each SST takes 1000 seconds
|
||||
// The preclude_last_level_data_seconds is 10k
|
||||
for (; sst_num < kNumTrigger * 2; sst_num++) {
|
||||
for (int i = 0; i < kNumKeys; i++) {
|
||||
ASSERT_OK(Put(Key(sst_num * (kNumKeys - 1) + i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun([&] {
|
||||
mock_clock_->MockSleepForSeconds(static_cast<int>(kKeyPerSec));
|
||||
});
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->WaitForCompact(true));
|
||||
ASSERT_GT(GetSstSizeHelper(Temperature::kUnknown), 0);
|
||||
ASSERT_EQ(GetSstSizeHelper(Temperature::kCold), 0);
|
||||
}
|
||||
|
||||
// Now we have both hot data and cold data
|
||||
for (; sst_num < kNumTrigger * 3; sst_num++) {
|
||||
for (int i = 0; i < kNumKeys; i++) {
|
||||
ASSERT_OK(Put(Key(sst_num * (kNumKeys - 1) + i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun([&] {
|
||||
mock_clock_->MockSleepForSeconds(static_cast<int>(kKeyPerSec));
|
||||
});
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->WaitForCompact(true));
|
||||
}
|
||||
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
uint64_t hot_data_size = GetSstSizeHelper(Temperature::kUnknown);
|
||||
uint64_t cold_data_size = GetSstSizeHelper(Temperature::kCold);
|
||||
ASSERT_GT(hot_data_size, 0);
|
||||
ASSERT_GT(cold_data_size, 0);
|
||||
// the first a few key should be cold
|
||||
AssertKetTemperature(20, Temperature::kCold);
|
||||
|
||||
// Wait some time, each time after compaction, the cold data size is
|
||||
// increasing and hot data size is decreasing
|
||||
for (int i = 0; i < 30; i++) {
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun([&] {
|
||||
mock_clock_->MockSleepForSeconds(static_cast<int>(20 * kKeyPerSec));
|
||||
});
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
uint64_t pre_hot = hot_data_size;
|
||||
uint64_t pre_cold = cold_data_size;
|
||||
hot_data_size = GetSstSizeHelper(Temperature::kUnknown);
|
||||
cold_data_size = GetSstSizeHelper(Temperature::kCold);
|
||||
ASSERT_LT(hot_data_size, pre_hot);
|
||||
ASSERT_GT(cold_data_size, pre_cold);
|
||||
|
||||
// the hot/cold data cut off range should be between i * 20 + 200 -> 250
|
||||
AssertKetTemperature(i * 20 + 250, Temperature::kUnknown);
|
||||
AssertKetTemperature(i * 20 + 200, Temperature::kCold);
|
||||
}
|
||||
|
||||
// Wait again, all data should be cold after that
|
||||
for (int i = 0; i < 5; i++) {
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(1000)); });
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
}
|
||||
|
||||
ASSERT_EQ(GetSstSizeHelper(Temperature::kUnknown), 0);
|
||||
ASSERT_GT(GetSstSizeHelper(Temperature::kCold), 0);
|
||||
|
||||
// any random data should be cold
|
||||
AssertKetTemperature(1000, Temperature::kCold);
|
||||
|
||||
// close explicitly, because the env is local variable which will be released
|
||||
// first.
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(SeqnoTimeTest, TemperatureBasicLevel) {
|
||||
const int kNumLevels = 7;
|
||||
const int kNumKeys = 100;
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.preclude_last_level_data_seconds = 10000;
|
||||
options.env = mock_env_.get();
|
||||
options.bottommost_temperature = Temperature::kCold;
|
||||
options.num_levels = kNumLevels;
|
||||
options.level_compaction_dynamic_level_bytes = true;
|
||||
// TODO(zjay): for level compaction, auto-compaction may stuck in deadloop, if
|
||||
// the penultimate level score > 1, but the hot is not cold enough to compact
|
||||
// to last level, which will keep triggering compaction.
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// pass some time first, otherwise the first a few keys write time are going
|
||||
// to be zero, and internally zero has special meaning: kUnknownSeqnoTime
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(10)); });
|
||||
|
||||
int sst_num = 0;
|
||||
// Write files that are overlap
|
||||
for (; sst_num < 4; sst_num++) {
|
||||
for (int i = 0; i < kNumKeys; i++) {
|
||||
ASSERT_OK(Put(Key(sst_num * (kNumKeys - 1) + i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(10)); });
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
// All data is hot, only output to penultimate level
|
||||
ASSERT_EQ("0,0,0,0,0,1", FilesPerLevel());
|
||||
ASSERT_GT(GetSstSizeHelper(Temperature::kUnknown), 0);
|
||||
ASSERT_EQ(GetSstSizeHelper(Temperature::kCold), 0);
|
||||
|
||||
// read a random key, which should be hot (kUnknown)
|
||||
AssertKetTemperature(20, Temperature::kUnknown);
|
||||
|
||||
// Adding more data to have mixed hot and cold data
|
||||
for (; sst_num < 14; sst_num++) {
|
||||
for (int i = 0; i < kNumKeys; i++) {
|
||||
ASSERT_OK(Put(Key(sst_num * (kNumKeys - 1) + i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(10)); });
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
// TODO(zjay): all data become cold because of level 5 (penultimate level) is
|
||||
// the bottommost level, which converts the data to cold. PerKeyPlacement is
|
||||
// for the last level (level 6). Will be fixed by change the
|
||||
// bottommost_temperature to the last_level_temperature
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
ASSERT_EQ(GetSstSizeHelper(Temperature::kUnknown), 0);
|
||||
ASSERT_GT(GetSstSizeHelper(Temperature::kCold), 0);
|
||||
|
||||
// Compact the files to the last level which should split the hot/cold data
|
||||
MoveFilesToLevel(6);
|
||||
uint64_t hot_data_size = GetSstSizeHelper(Temperature::kUnknown);
|
||||
uint64_t cold_data_size = GetSstSizeHelper(Temperature::kCold);
|
||||
ASSERT_GT(hot_data_size, 0);
|
||||
ASSERT_GT(cold_data_size, 0);
|
||||
// the first a few key should be cold
|
||||
AssertKetTemperature(20, Temperature::kCold);
|
||||
|
||||
// Wait some time, each it wait, the cold data is increasing and hot data is
|
||||
// decreasing
|
||||
for (int i = 0; i < 30; i++) {
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(200)); });
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
uint64_t pre_hot = hot_data_size;
|
||||
uint64_t pre_cold = cold_data_size;
|
||||
hot_data_size = GetSstSizeHelper(Temperature::kUnknown);
|
||||
cold_data_size = GetSstSizeHelper(Temperature::kCold);
|
||||
ASSERT_LT(hot_data_size, pre_hot);
|
||||
ASSERT_GT(cold_data_size, pre_cold);
|
||||
|
||||
// the hot/cold cut_off key should be around i * 20 + 400 -> 450
|
||||
AssertKetTemperature(i * 20 + 450, Temperature::kUnknown);
|
||||
AssertKetTemperature(i * 20 + 400, Temperature::kCold);
|
||||
}
|
||||
|
||||
// Wait again, all data should be cold after that
|
||||
for (int i = 0; i < 5; i++) {
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(1000)); });
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
}
|
||||
|
||||
ASSERT_EQ(GetSstSizeHelper(Temperature::kUnknown), 0);
|
||||
ASSERT_GT(GetSstSizeHelper(Temperature::kCold), 0);
|
||||
|
||||
// any random data should be cold
|
||||
AssertKetTemperature(1000, Temperature::kCold);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(SeqnoTimeTest, BasicSeqnoToTimeMapping) {
|
||||
Options options = CurrentOptions();
|
||||
options.preclude_last_level_data_seconds = 10000;
|
||||
options.env = mock_env_.get();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
std::set<uint64_t> checked_file_nums;
|
||||
SequenceNumber start_seq = dbfull()->GetLatestSequenceNumber();
|
||||
// Write a key every 10 seconds
|
||||
for (int i = 0; i < 200; i++) {
|
||||
ASSERT_OK(Put(Key(i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(10)); });
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
TablePropertiesCollection tables_props;
|
||||
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(&tables_props));
|
||||
ASSERT_EQ(tables_props.size(), 1);
|
||||
auto it = tables_props.begin();
|
||||
SeqnoToTimeMapping tp_mapping;
|
||||
ASSERT_OK(tp_mapping.Add(it->second->seqno_to_time_mapping));
|
||||
ASSERT_OK(tp_mapping.Sort());
|
||||
ASSERT_FALSE(tp_mapping.Empty());
|
||||
auto seqs = tp_mapping.TEST_GetInternalMapping();
|
||||
ASSERT_GE(seqs.size(), 19);
|
||||
ASSERT_LE(seqs.size(), 21);
|
||||
SequenceNumber seq_end = dbfull()->GetLatestSequenceNumber();
|
||||
for (auto i = start_seq; i < start_seq + 10; i++) {
|
||||
ASSERT_LE(tp_mapping.GetOldestApproximateTime(i), (i + 1) * 10);
|
||||
}
|
||||
start_seq += 10;
|
||||
for (auto i = start_seq; i < seq_end; i++) {
|
||||
// The result is within the range
|
||||
ASSERT_GE(tp_mapping.GetOldestApproximateTime(i), (i - 10) * 10);
|
||||
ASSERT_LE(tp_mapping.GetOldestApproximateTime(i), (i + 10) * 10);
|
||||
}
|
||||
checked_file_nums.insert(it->second->orig_file_number);
|
||||
start_seq = seq_end;
|
||||
|
||||
// Write a key every 1 seconds
|
||||
for (int i = 0; i < 200; i++) {
|
||||
ASSERT_OK(Put(Key(i + 190), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(1)); });
|
||||
}
|
||||
seq_end = dbfull()->GetLatestSequenceNumber();
|
||||
ASSERT_OK(Flush());
|
||||
tables_props.clear();
|
||||
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(&tables_props));
|
||||
ASSERT_EQ(tables_props.size(), 2);
|
||||
it = tables_props.begin();
|
||||
while (it != tables_props.end()) {
|
||||
if (!checked_file_nums.count(it->second->orig_file_number)) {
|
||||
break;
|
||||
}
|
||||
it++;
|
||||
}
|
||||
ASSERT_TRUE(it != tables_props.end());
|
||||
|
||||
tp_mapping.Clear();
|
||||
ASSERT_OK(tp_mapping.Add(it->second->seqno_to_time_mapping));
|
||||
ASSERT_OK(tp_mapping.Sort());
|
||||
seqs = tp_mapping.TEST_GetInternalMapping();
|
||||
// There only a few time sample
|
||||
ASSERT_GE(seqs.size(), 1);
|
||||
ASSERT_LE(seqs.size(), 3);
|
||||
for (auto i = start_seq; i < seq_end; i++) {
|
||||
// The result is not very accurate, as there is more data write within small
|
||||
// range of time
|
||||
ASSERT_GE(tp_mapping.GetOldestApproximateTime(i), (i - start_seq) + 1000);
|
||||
ASSERT_LE(tp_mapping.GetOldestApproximateTime(i), (i - start_seq) + 3000);
|
||||
}
|
||||
checked_file_nums.insert(it->second->orig_file_number);
|
||||
start_seq = seq_end;
|
||||
|
||||
// Write a key every 200 seconds
|
||||
for (int i = 0; i < 200; i++) {
|
||||
ASSERT_OK(Put(Key(i + 380), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(200)); });
|
||||
}
|
||||
seq_end = dbfull()->GetLatestSequenceNumber();
|
||||
ASSERT_OK(Flush());
|
||||
tables_props.clear();
|
||||
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(&tables_props));
|
||||
ASSERT_EQ(tables_props.size(), 3);
|
||||
it = tables_props.begin();
|
||||
while (it != tables_props.end()) {
|
||||
if (!checked_file_nums.count(it->second->orig_file_number)) {
|
||||
break;
|
||||
}
|
||||
it++;
|
||||
}
|
||||
ASSERT_TRUE(it != tables_props.end());
|
||||
|
||||
tp_mapping.Clear();
|
||||
ASSERT_OK(tp_mapping.Add(it->second->seqno_to_time_mapping));
|
||||
ASSERT_OK(tp_mapping.Sort());
|
||||
seqs = tp_mapping.TEST_GetInternalMapping();
|
||||
// The sequence number -> time entries should be maxed
|
||||
ASSERT_GE(seqs.size(), 99);
|
||||
ASSERT_LE(seqs.size(), 101);
|
||||
for (auto i = start_seq; i < seq_end - 99; i++) {
|
||||
// likely the first 100 entries reports 0
|
||||
ASSERT_LE(tp_mapping.GetOldestApproximateTime(i), (i - start_seq) + 3000);
|
||||
}
|
||||
start_seq += 101;
|
||||
|
||||
for (auto i = start_seq; i < seq_end; i++) {
|
||||
ASSERT_GE(tp_mapping.GetOldestApproximateTime(i),
|
||||
(i - start_seq) * 200 + 22200);
|
||||
ASSERT_LE(tp_mapping.GetOldestApproximateTime(i),
|
||||
(i - start_seq) * 200 + 22600);
|
||||
}
|
||||
checked_file_nums.insert(it->second->orig_file_number);
|
||||
start_seq = seq_end;
|
||||
|
||||
// Write a key every 100 seconds
|
||||
for (int i = 0; i < 200; i++) {
|
||||
ASSERT_OK(Put(Key(i + 570), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(100)); });
|
||||
}
|
||||
seq_end = dbfull()->GetLatestSequenceNumber();
|
||||
ASSERT_OK(Flush());
|
||||
tables_props.clear();
|
||||
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(&tables_props));
|
||||
ASSERT_EQ(tables_props.size(), 4);
|
||||
it = tables_props.begin();
|
||||
while (it != tables_props.end()) {
|
||||
if (!checked_file_nums.count(it->second->orig_file_number)) {
|
||||
break;
|
||||
}
|
||||
it++;
|
||||
}
|
||||
ASSERT_TRUE(it != tables_props.end());
|
||||
tp_mapping.Clear();
|
||||
ASSERT_OK(tp_mapping.Add(it->second->seqno_to_time_mapping));
|
||||
ASSERT_OK(tp_mapping.Sort());
|
||||
seqs = tp_mapping.TEST_GetInternalMapping();
|
||||
ASSERT_GE(seqs.size(), 99);
|
||||
ASSERT_LE(seqs.size(), 101);
|
||||
|
||||
checked_file_nums.insert(it->second->orig_file_number);
|
||||
|
||||
// re-enable compaction
|
||||
ASSERT_OK(dbfull()->SetOptions({
|
||||
{"disable_auto_compactions", "false"},
|
||||
}));
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
tables_props.clear();
|
||||
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(&tables_props));
|
||||
ASSERT_GE(tables_props.size(), 1);
|
||||
it = tables_props.begin();
|
||||
while (it != tables_props.end()) {
|
||||
if (!checked_file_nums.count(it->second->orig_file_number)) {
|
||||
break;
|
||||
}
|
||||
it++;
|
||||
}
|
||||
ASSERT_TRUE(it != tables_props.end());
|
||||
tp_mapping.Clear();
|
||||
ASSERT_OK(tp_mapping.Add(it->second->seqno_to_time_mapping));
|
||||
ASSERT_OK(tp_mapping.Sort());
|
||||
seqs = tp_mapping.TEST_GetInternalMapping();
|
||||
ASSERT_GE(seqs.size(), 99);
|
||||
ASSERT_LE(seqs.size(), 101);
|
||||
for (auto i = start_seq; i < seq_end - 99; i++) {
|
||||
// likely the first 100 entries reports 0
|
||||
ASSERT_LE(tp_mapping.GetOldestApproximateTime(i), (i - start_seq) + 3000);
|
||||
}
|
||||
start_seq += 101;
|
||||
|
||||
for (auto i = start_seq; i < seq_end; i++) {
|
||||
ASSERT_GE(tp_mapping.GetOldestApproximateTime(i),
|
||||
(i - start_seq) * 100 + 52200);
|
||||
ASSERT_LE(tp_mapping.GetOldestApproximateTime(i),
|
||||
(i - start_seq) * 100 + 52400);
|
||||
}
|
||||
ASSERT_OK(db_->Close());
|
||||
}
|
||||
|
||||
// TODO(zjay): Disabled, until New CF bug with preclude_last_level_data_seconds
|
||||
// is fixed
|
||||
TEST_F(SeqnoTimeTest, DISABLED_MultiCFs) {
|
||||
Options options = CurrentOptions();
|
||||
options.preclude_last_level_data_seconds = 0;
|
||||
options.env = mock_env_.get();
|
||||
options.stats_dump_period_sec = 0;
|
||||
options.stats_persist_period_sec = 0;
|
||||
ReopenWithColumnFamilies({"default"}, options);
|
||||
|
||||
auto scheduler = dbfull()->TEST_GetPeriodicWorkScheduler();
|
||||
ASSERT_FALSE(scheduler->TEST_HasValidTask(
|
||||
dbfull(), PeriodicWorkTaskNames::kRecordSeqnoTime));
|
||||
|
||||
// Write some data and increase the current time
|
||||
for (int i = 0; i < 200; i++) {
|
||||
ASSERT_OK(Put(Key(i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(100)); });
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
TablePropertiesCollection tables_props;
|
||||
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(&tables_props));
|
||||
ASSERT_EQ(tables_props.size(), 1);
|
||||
auto it = tables_props.begin();
|
||||
ASSERT_TRUE(it->second->seqno_to_time_mapping.empty());
|
||||
|
||||
ASSERT_TRUE(dbfull()->TEST_GetSeqnoToTimeMapping().Empty());
|
||||
|
||||
Options options_1 = options;
|
||||
options_1.preclude_last_level_data_seconds = 10000; // 10k
|
||||
CreateColumnFamilies({"one"}, options_1);
|
||||
ASSERT_TRUE(scheduler->TEST_HasValidTask(
|
||||
dbfull(), PeriodicWorkTaskNames::kRecordSeqnoTime));
|
||||
|
||||
// Write some data to the default CF (without preclude_last_level feature)
|
||||
for (int i = 0; i < 200; i++) {
|
||||
ASSERT_OK(Put(Key(i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(100)); });
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// in memory mapping won't increase because CFs with preclude_last_level
|
||||
// feature doesn't have memtable
|
||||
auto queue = dbfull()->TEST_GetSeqnoToTimeMapping().TEST_GetInternalMapping();
|
||||
ASSERT_LT(queue.size(), 5);
|
||||
|
||||
// Write some data to the CF one
|
||||
for (int i = 0; i < 20; i++) {
|
||||
ASSERT_OK(Put(1, Key(i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(10)); });
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
tables_props.clear();
|
||||
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(handles_[1], &tables_props));
|
||||
ASSERT_EQ(tables_props.size(), 1);
|
||||
it = tables_props.begin();
|
||||
SeqnoToTimeMapping tp_mapping;
|
||||
ASSERT_OK(tp_mapping.Add(it->second->seqno_to_time_mapping));
|
||||
ASSERT_OK(tp_mapping.Sort());
|
||||
ASSERT_FALSE(tp_mapping.Empty());
|
||||
auto seqs = tp_mapping.TEST_GetInternalMapping();
|
||||
ASSERT_GE(seqs.size(), 1);
|
||||
ASSERT_LE(seqs.size(), 3);
|
||||
|
||||
// Create one more CF with larger preclude_last_level time
|
||||
Options options_2 = options;
|
||||
options_2.preclude_last_level_data_seconds = 1000000; // 1m
|
||||
CreateColumnFamilies({"two"}, options_2);
|
||||
|
||||
// Add more data to CF "two" to fill the in memory mapping
|
||||
for (int i = 0; i < 2000; i++) {
|
||||
ASSERT_OK(Put(2, Key(i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(100)); });
|
||||
}
|
||||
seqs = dbfull()->TEST_GetSeqnoToTimeMapping().TEST_GetInternalMapping();
|
||||
ASSERT_GE(seqs.size(), 1000 - 1);
|
||||
ASSERT_LE(seqs.size(), 1000 + 1);
|
||||
|
||||
ASSERT_OK(Flush(2));
|
||||
tables_props.clear();
|
||||
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(handles_[2], &tables_props));
|
||||
ASSERT_EQ(tables_props.size(), 1);
|
||||
it = tables_props.begin();
|
||||
tp_mapping.Clear();
|
||||
ASSERT_OK(tp_mapping.Add(it->second->seqno_to_time_mapping));
|
||||
ASSERT_OK(tp_mapping.Sort());
|
||||
seqs = tp_mapping.TEST_GetInternalMapping();
|
||||
// the max encoded entries is 100
|
||||
ASSERT_GE(seqs.size(), 100 - 1);
|
||||
ASSERT_LE(seqs.size(), 100 + 1);
|
||||
|
||||
// Write some data to default CF, as all memtable with preclude_last_level
|
||||
// enabled have flushed, the in-memory seqno->time mapping should be cleared
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ASSERT_OK(Put(0, Key(i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(100)); });
|
||||
}
|
||||
seqs = dbfull()->TEST_GetSeqnoToTimeMapping().TEST_GetInternalMapping();
|
||||
ASSERT_LE(seqs.size(), 5);
|
||||
ASSERT_OK(Flush(0));
|
||||
|
||||
// trigger compaction for CF "two" and make sure the compaction output has
|
||||
// seqno_to_time_mapping
|
||||
for (int j = 0; j < 3; j++) {
|
||||
for (int i = 0; i < 200; i++) {
|
||||
ASSERT_OK(Put(2, Key(i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(100)); });
|
||||
}
|
||||
ASSERT_OK(Flush(2));
|
||||
}
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
tables_props.clear();
|
||||
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(handles_[2], &tables_props));
|
||||
ASSERT_EQ(tables_props.size(), 1);
|
||||
it = tables_props.begin();
|
||||
tp_mapping.Clear();
|
||||
ASSERT_OK(tp_mapping.Add(it->second->seqno_to_time_mapping));
|
||||
ASSERT_OK(tp_mapping.Sort());
|
||||
seqs = tp_mapping.TEST_GetInternalMapping();
|
||||
ASSERT_GE(seqs.size(), 99);
|
||||
ASSERT_LE(seqs.size(), 101);
|
||||
|
||||
for (int j = 0; j < 2; j++) {
|
||||
for (int i = 0; i < 200; i++) {
|
||||
ASSERT_OK(Put(0, Key(i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(100)); });
|
||||
}
|
||||
ASSERT_OK(Flush(0));
|
||||
}
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
tables_props.clear();
|
||||
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(handles_[0], &tables_props));
|
||||
ASSERT_EQ(tables_props.size(), 1);
|
||||
it = tables_props.begin();
|
||||
ASSERT_TRUE(it->second->seqno_to_time_mapping.empty());
|
||||
|
||||
// Write some data to CF "two", but don't flush to accumulate
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
ASSERT_OK(Put(2, Key(i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(100)); });
|
||||
}
|
||||
ASSERT_GE(
|
||||
dbfull()->TEST_GetSeqnoToTimeMapping().TEST_GetInternalMapping().size(),
|
||||
500);
|
||||
// After dropping CF "one", the in-memory mapping will be change to only
|
||||
// follow CF "two" options.
|
||||
ASSERT_OK(db_->DropColumnFamily(handles_[1]));
|
||||
ASSERT_LE(
|
||||
dbfull()->TEST_GetSeqnoToTimeMapping().TEST_GetInternalMapping().size(),
|
||||
100 + 5);
|
||||
|
||||
// After dropping CF "two", the in-memory mapping is also clear.
|
||||
ASSERT_OK(db_->DropColumnFamily(handles_[2]));
|
||||
ASSERT_EQ(
|
||||
dbfull()->TEST_GetSeqnoToTimeMapping().TEST_GetInternalMapping().size(),
|
||||
0);
|
||||
|
||||
// And the timer worker is stopped
|
||||
ASSERT_FALSE(scheduler->TEST_HasValidTask(
|
||||
dbfull(), PeriodicWorkTaskNames::kRecordSeqnoTime));
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(SeqnoTimeTest, MultiInstancesBasic) {
|
||||
const int kInstanceNum = 2;
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.preclude_last_level_data_seconds = 10000;
|
||||
options.env = mock_env_.get();
|
||||
options.stats_dump_period_sec = 0;
|
||||
options.stats_persist_period_sec = 0;
|
||||
|
||||
auto dbs = std::vector<DB*>(kInstanceNum);
|
||||
for (int i = 0; i < kInstanceNum; i++) {
|
||||
ASSERT_OK(
|
||||
DB::Open(options, test::PerThreadDBPath(std::to_string(i)), &(dbs[i])));
|
||||
}
|
||||
|
||||
// Make sure the second instance has the worker enabled
|
||||
auto dbi = static_cast_with_check<DBImpl>(dbs[1]);
|
||||
WriteOptions wo;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
ASSERT_OK(dbi->Put(wo, Key(i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(100)); });
|
||||
}
|
||||
SeqnoToTimeMapping seqno_to_time_mapping = dbi->TEST_GetSeqnoToTimeMapping();
|
||||
ASSERT_GT(seqno_to_time_mapping.Size(), 10);
|
||||
|
||||
for (int i = 0; i < kInstanceNum; i++) {
|
||||
ASSERT_OK(dbs[i]->Close());
|
||||
delete dbs[i];
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(SeqnoTimeTest, SeqnoToTimeMappingUniversal) {
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleUniversal;
|
||||
options.preclude_last_level_data_seconds = 10000;
|
||||
options.env = mock_env_.get();
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
for (int j = 0; j < 3; j++) {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ASSERT_OK(Put(Key(i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(10)); });
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
TablePropertiesCollection tables_props;
|
||||
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(&tables_props));
|
||||
ASSERT_EQ(tables_props.size(), 3);
|
||||
for (const auto& props : tables_props) {
|
||||
ASSERT_FALSE(props.second->seqno_to_time_mapping.empty());
|
||||
SeqnoToTimeMapping tp_mapping;
|
||||
ASSERT_OK(tp_mapping.Add(props.second->seqno_to_time_mapping));
|
||||
ASSERT_OK(tp_mapping.Sort());
|
||||
ASSERT_FALSE(tp_mapping.Empty());
|
||||
auto seqs = tp_mapping.TEST_GetInternalMapping();
|
||||
ASSERT_GE(seqs.size(), 10 - 1);
|
||||
ASSERT_LE(seqs.size(), 10 + 1);
|
||||
}
|
||||
|
||||
// Trigger a compaction
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ASSERT_OK(Put(Key(i), "value"));
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(10)); });
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
tables_props.clear();
|
||||
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(&tables_props));
|
||||
ASSERT_EQ(tables_props.size(), 1);
|
||||
|
||||
auto it = tables_props.begin();
|
||||
SeqnoToTimeMapping tp_mapping;
|
||||
ASSERT_FALSE(it->second->seqno_to_time_mapping.empty());
|
||||
ASSERT_OK(tp_mapping.Add(it->second->seqno_to_time_mapping));
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(SeqnoTimeTest, MappingAppend) {
|
||||
SeqnoToTimeMapping test(/*max_time_duration=*/100, /*max_capacity=*/10);
|
||||
|
||||
// ignore seqno == 0, as it may mean the seqno is zeroed out
|
||||
ASSERT_FALSE(test.Append(0, 9));
|
||||
|
||||
ASSERT_TRUE(test.Append(3, 10));
|
||||
auto size = test.Size();
|
||||
// normal add
|
||||
ASSERT_TRUE(test.Append(10, 11));
|
||||
size++;
|
||||
ASSERT_EQ(size, test.Size());
|
||||
|
||||
// Append unsorted
|
||||
ASSERT_FALSE(test.Append(8, 12));
|
||||
ASSERT_EQ(size, test.Size());
|
||||
|
||||
// Append with the same seqno, newer time will be accepted
|
||||
ASSERT_TRUE(test.Append(10, 12));
|
||||
ASSERT_EQ(size, test.Size());
|
||||
// older time will be ignored
|
||||
ASSERT_FALSE(test.Append(10, 9));
|
||||
ASSERT_EQ(size, test.Size());
|
||||
|
||||
// new seqno with old time will be ignored
|
||||
ASSERT_FALSE(test.Append(12, 8));
|
||||
ASSERT_EQ(size, test.Size());
|
||||
}
|
||||
|
||||
TEST_F(SeqnoTimeTest, GetOldestApproximateTime) {
|
||||
SeqnoToTimeMapping test(/*max_time_duration=*/100, /*max_capacity=*/10);
|
||||
|
||||
ASSERT_EQ(test.GetOldestApproximateTime(10), kUnknownSeqnoTime);
|
||||
|
||||
test.Append(3, 10);
|
||||
|
||||
ASSERT_EQ(test.GetOldestApproximateTime(2), kUnknownSeqnoTime);
|
||||
ASSERT_EQ(test.GetOldestApproximateTime(3), 10);
|
||||
ASSERT_EQ(test.GetOldestApproximateTime(10), 10);
|
||||
|
||||
test.Append(10, 100);
|
||||
|
||||
test.Append(100, 1000);
|
||||
ASSERT_EQ(test.GetOldestApproximateTime(10), 100);
|
||||
ASSERT_EQ(test.GetOldestApproximateTime(40), 100);
|
||||
ASSERT_EQ(test.GetOldestApproximateTime(111), 1000);
|
||||
}
|
||||
|
||||
TEST_F(SeqnoTimeTest, Sort) {
|
||||
SeqnoToTimeMapping test;
|
||||
|
||||
// single entry
|
||||
test.Add(10, 11);
|
||||
ASSERT_OK(test.Sort());
|
||||
ASSERT_EQ(test.Size(), 1);
|
||||
|
||||
// duplicate, should be removed by sort
|
||||
test.Add(10, 11);
|
||||
// same seqno, but older time, should be removed
|
||||
test.Add(10, 9);
|
||||
|
||||
// unuseful ones, should be removed by sort
|
||||
test.Add(11, 9);
|
||||
test.Add(9, 8);
|
||||
|
||||
// Good ones
|
||||
test.Add(1, 10);
|
||||
test.Add(100, 100);
|
||||
|
||||
ASSERT_OK(test.Sort());
|
||||
|
||||
auto seqs = test.TEST_GetInternalMapping();
|
||||
|
||||
std::deque<SeqnoToTimeMapping::SeqnoTimePair> expected;
|
||||
expected.emplace_back(1, 10);
|
||||
expected.emplace_back(10, 11);
|
||||
expected.emplace_back(100, 100);
|
||||
|
||||
ASSERT_EQ(expected, seqs);
|
||||
}
|
||||
|
||||
TEST_F(SeqnoTimeTest, EncodeDecodeBasic) {
|
||||
SeqnoToTimeMapping test(0, 1000);
|
||||
|
||||
std::string output;
|
||||
test.Encode(output, 0, 1000, 100);
|
||||
ASSERT_TRUE(output.empty());
|
||||
|
||||
for (int i = 1; i <= 1000; i++) {
|
||||
ASSERT_TRUE(test.Append(i, i * 10));
|
||||
}
|
||||
test.Encode(output, 0, 1000, 100);
|
||||
|
||||
ASSERT_FALSE(output.empty());
|
||||
|
||||
SeqnoToTimeMapping decoded;
|
||||
ASSERT_OK(decoded.Add(output));
|
||||
ASSERT_OK(decoded.Sort());
|
||||
ASSERT_EQ(decoded.Size(), SeqnoToTimeMapping::kMaxSeqnoTimePairsPerSST);
|
||||
ASSERT_EQ(test.Size(), 1000);
|
||||
|
||||
for (SequenceNumber seq = 0; seq <= 1000; seq++) {
|
||||
// test has the more accurate time mapping, encode only pick
|
||||
// kMaxSeqnoTimePairsPerSST number of entries, which is less accurate
|
||||
uint64_t target_time = test.GetOldestApproximateTime(seq);
|
||||
ASSERT_GE(decoded.GetOldestApproximateTime(seq),
|
||||
target_time < 200 ? 0 : target_time - 200);
|
||||
ASSERT_LE(decoded.GetOldestApproximateTime(seq), target_time);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(SeqnoTimeTest, EncodeDecodePerferNewTime) {
|
||||
SeqnoToTimeMapping test(0, 10);
|
||||
|
||||
test.Append(1, 10);
|
||||
test.Append(5, 17);
|
||||
test.Append(6, 25);
|
||||
test.Append(8, 30);
|
||||
|
||||
std::string output;
|
||||
test.Encode(output, 1, 10, 0, 3);
|
||||
|
||||
SeqnoToTimeMapping decoded;
|
||||
ASSERT_OK(decoded.Add(output));
|
||||
ASSERT_OK(decoded.Sort());
|
||||
|
||||
ASSERT_EQ(decoded.Size(), 3);
|
||||
|
||||
auto seqs = decoded.TEST_GetInternalMapping();
|
||||
std::deque<SeqnoToTimeMapping::SeqnoTimePair> expected;
|
||||
expected.emplace_back(1, 10);
|
||||
expected.emplace_back(6, 25);
|
||||
expected.emplace_back(8, 30);
|
||||
ASSERT_EQ(expected, seqs);
|
||||
|
||||
// Add a few large time number
|
||||
test.Append(10, 100);
|
||||
test.Append(13, 200);
|
||||
test.Append(16, 300);
|
||||
|
||||
output.clear();
|
||||
test.Encode(output, 1, 20, 0, 4);
|
||||
decoded.Clear();
|
||||
ASSERT_OK(decoded.Add(output));
|
||||
ASSERT_OK(decoded.Sort());
|
||||
ASSERT_EQ(decoded.Size(), 4);
|
||||
|
||||
expected.clear();
|
||||
expected.emplace_back(1, 10);
|
||||
// entry #6, #8 are skipped as they are too close to #1.
|
||||
// entry #100 is also within skip range, but if it's skipped, there not enough
|
||||
// number to fill 4 entries, so select it.
|
||||
expected.emplace_back(10, 100);
|
||||
expected.emplace_back(13, 200);
|
||||
expected.emplace_back(16, 300);
|
||||
seqs = decoded.TEST_GetInternalMapping();
|
||||
ASSERT_EQ(expected, seqs);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
//
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "db/seqno_to_time_mapping.h"
|
||||
|
||||
#include "db/version_edit.h"
|
||||
#include "util/string_util.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
uint64_t SeqnoToTimeMapping::GetOldestApproximateTime(
|
||||
const SequenceNumber seqno) const {
|
||||
assert(is_sorted_);
|
||||
auto it = std::upper_bound(seqno_time_mapping_.begin(),
|
||||
seqno_time_mapping_.end(), seqno);
|
||||
if (it == seqno_time_mapping_.begin()) {
|
||||
return 0;
|
||||
}
|
||||
it--;
|
||||
return it->time;
|
||||
}
|
||||
|
||||
void SeqnoToTimeMapping::Add(SequenceNumber seqno, uint64_t time) {
|
||||
if (seqno == 0) {
|
||||
return;
|
||||
}
|
||||
is_sorted_ = false;
|
||||
seqno_time_mapping_.emplace_back(seqno, time);
|
||||
}
|
||||
|
||||
SequenceNumber SeqnoToTimeMapping::TruncateOldEntries(const uint64_t now) {
|
||||
assert(is_sorted_);
|
||||
|
||||
if (max_time_duration_ == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint64_t cut_off_time =
|
||||
now > max_time_duration_ ? now - max_time_duration_ : 0;
|
||||
assert(cut_off_time <= now); // no overflow
|
||||
|
||||
auto it = std::upper_bound(
|
||||
seqno_time_mapping_.begin(), seqno_time_mapping_.end(), cut_off_time,
|
||||
[](uint64_t target, const SeqnoTimePair& other) -> bool {
|
||||
return target < other.time;
|
||||
});
|
||||
if (it == seqno_time_mapping_.begin()) {
|
||||
return 0;
|
||||
}
|
||||
it--;
|
||||
seqno_time_mapping_.erase(seqno_time_mapping_.begin(), it);
|
||||
|
||||
return seqno_time_mapping_.front().seqno;
|
||||
}
|
||||
|
||||
// The encoded format is:
|
||||
// [num_of_entries][[seqno][time],[seqno][time],...]
|
||||
// ^ ^
|
||||
// var_int delta_encoded (var_int)
|
||||
void SeqnoToTimeMapping::Encode(std::string& dest, const SequenceNumber start,
|
||||
const SequenceNumber end, const uint64_t now,
|
||||
const uint64_t output_size) const {
|
||||
assert(is_sorted_);
|
||||
if (start > end) {
|
||||
// It could happen when the SST file is empty, the initial value of min
|
||||
// sequence number is kMaxSequenceNumber and max is 0.
|
||||
// The empty output file will be removed in the final step of compaction.
|
||||
return;
|
||||
}
|
||||
|
||||
auto start_it = std::upper_bound(seqno_time_mapping_.begin(),
|
||||
seqno_time_mapping_.end(), start);
|
||||
if (start_it != seqno_time_mapping_.begin()) {
|
||||
start_it--;
|
||||
}
|
||||
|
||||
auto end_it = std::upper_bound(seqno_time_mapping_.begin(),
|
||||
seqno_time_mapping_.end(), end);
|
||||
if (end_it == seqno_time_mapping_.begin()) {
|
||||
return;
|
||||
}
|
||||
if (start_it >= end_it) {
|
||||
return;
|
||||
}
|
||||
|
||||
// truncate old entries that are not needed
|
||||
if (max_time_duration_ > 0) {
|
||||
const uint64_t cut_off_time =
|
||||
now > max_time_duration_ ? now - max_time_duration_ : 0;
|
||||
while (start_it < end_it && start_it->time < cut_off_time) {
|
||||
start_it++;
|
||||
}
|
||||
}
|
||||
|
||||
// If there are more data than needed, pick the entries for encoding.
|
||||
// It's not the most optimized algorithm for selecting the best representative
|
||||
// entries over the time.
|
||||
// It starts from the beginning and makes sure the distance is larger than
|
||||
// `(end - start) / size` before selecting the number. For example, for the
|
||||
// following list, pick 3 entries (it will pick seqno #1, #6, #8):
|
||||
// 1 -> 10
|
||||
// 5 -> 17
|
||||
// 6 -> 25
|
||||
// 8 -> 30
|
||||
// first, it always picks the first one, then there are 2 num_entries_to_fill
|
||||
// and the time difference between current one vs. the last one is
|
||||
// (30 - 10) = 20. 20/2 = 10. So it will skip until 10+10 = 20. => it skips
|
||||
// #5 and pick #6.
|
||||
// But the most optimized solution is picking #1 #5 #8, as it will be more
|
||||
// evenly distributed for time. Anyway the following algorithm is simple and
|
||||
// may over-select new data, which is good. We do want more accurate time
|
||||
// information for recent data.
|
||||
std::deque<SeqnoTimePair> output_copy;
|
||||
if (std::distance(start_it, end_it) > static_cast<int64_t>(output_size)) {
|
||||
int64_t num_entries_to_fill = static_cast<int64_t>(output_size);
|
||||
auto last_it = end_it;
|
||||
last_it--;
|
||||
uint64_t end_time = last_it->time;
|
||||
uint64_t skip_until_time = 0;
|
||||
for (auto it = start_it; it < end_it; it++) {
|
||||
// skip if it's not reach the skip_until_time yet
|
||||
if (std::distance(it, end_it) > num_entries_to_fill &&
|
||||
it->time < skip_until_time) {
|
||||
continue;
|
||||
}
|
||||
output_copy.push_back(*it);
|
||||
num_entries_to_fill--;
|
||||
if (std::distance(it, end_it) > num_entries_to_fill &&
|
||||
num_entries_to_fill > 0) {
|
||||
// If there are more entries than we need, re-calculate the
|
||||
// skip_until_time, which means skip until that time
|
||||
skip_until_time =
|
||||
it->time + ((end_time - it->time) / num_entries_to_fill);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure all entries are filled
|
||||
assert(num_entries_to_fill == 0);
|
||||
start_it = output_copy.begin();
|
||||
end_it = output_copy.end();
|
||||
}
|
||||
|
||||
// Delta encode the data
|
||||
uint64_t size = std::distance(start_it, end_it);
|
||||
PutVarint64(&dest, size);
|
||||
SeqnoTimePair base;
|
||||
for (auto it = start_it; it < end_it; it++) {
|
||||
assert(base < *it);
|
||||
SeqnoTimePair val = *it - base;
|
||||
base = *it;
|
||||
val.Encode(dest);
|
||||
}
|
||||
}
|
||||
|
||||
Status SeqnoToTimeMapping::Add(const std::string& seqno_time_mapping_str) {
|
||||
Slice input(seqno_time_mapping_str);
|
||||
if (input.empty()) {
|
||||
return Status::OK();
|
||||
}
|
||||
uint64_t size;
|
||||
if (!GetVarint64(&input, &size)) {
|
||||
return Status::Corruption("Invalid sequence number time size");
|
||||
}
|
||||
is_sorted_ = false;
|
||||
SeqnoTimePair base;
|
||||
for (uint64_t i = 0; i < size; i++) {
|
||||
SeqnoTimePair val;
|
||||
Status s = val.Decode(input);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
val.Add(base);
|
||||
seqno_time_mapping_.emplace_back(val);
|
||||
base = val;
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void SeqnoToTimeMapping::SeqnoTimePair::Encode(std::string& dest) const {
|
||||
PutVarint64Varint64(&dest, seqno, time);
|
||||
}
|
||||
|
||||
Status SeqnoToTimeMapping::SeqnoTimePair::Decode(Slice& input) {
|
||||
if (!GetVarint64(&input, &seqno)) {
|
||||
return Status::Corruption("Invalid sequence number");
|
||||
}
|
||||
if (!GetVarint64(&input, &time)) {
|
||||
return Status::Corruption("Invalid time");
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
bool SeqnoToTimeMapping::Append(SequenceNumber seqno, uint64_t time) {
|
||||
assert(is_sorted_);
|
||||
|
||||
// skip seq number 0, which may have special meaning, like zeroed out data
|
||||
if (seqno == 0) {
|
||||
return false;
|
||||
}
|
||||
if (!Empty()) {
|
||||
if (seqno < Last().seqno || time < Last().time) {
|
||||
return false;
|
||||
}
|
||||
if (seqno == Last().seqno) {
|
||||
Last().time = time;
|
||||
return true;
|
||||
}
|
||||
if (time == Last().time) {
|
||||
// new sequence has the same time as old one, no need to add new mapping
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
seqno_time_mapping_.emplace_back(seqno, time);
|
||||
|
||||
if (seqno_time_mapping_.size() > max_capacity_) {
|
||||
seqno_time_mapping_.pop_front();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SeqnoToTimeMapping::Resize(uint64_t min_time_duration,
|
||||
uint64_t max_time_duration) {
|
||||
uint64_t new_max_capacity =
|
||||
CalculateMaxCapacity(min_time_duration, max_time_duration);
|
||||
if (new_max_capacity == max_capacity_) {
|
||||
return false;
|
||||
} else if (new_max_capacity < seqno_time_mapping_.size()) {
|
||||
uint64_t delta = seqno_time_mapping_.size() - new_max_capacity;
|
||||
seqno_time_mapping_.erase(seqno_time_mapping_.begin(),
|
||||
seqno_time_mapping_.begin() + delta);
|
||||
}
|
||||
max_capacity_ = new_max_capacity;
|
||||
return true;
|
||||
}
|
||||
|
||||
Status SeqnoToTimeMapping::Sort() {
|
||||
if (is_sorted_) {
|
||||
return Status::OK();
|
||||
}
|
||||
if (seqno_time_mapping_.empty()) {
|
||||
is_sorted_ = true;
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
std::deque<SeqnoTimePair> copy = std::move(seqno_time_mapping_);
|
||||
|
||||
std::sort(copy.begin(), copy.end());
|
||||
|
||||
seqno_time_mapping_.clear();
|
||||
|
||||
// remove seqno = 0, which may have special meaning, like zeroed out data
|
||||
while (copy.front().seqno == 0) {
|
||||
copy.pop_front();
|
||||
}
|
||||
|
||||
SeqnoTimePair prev = copy.front();
|
||||
for (const auto& it : copy) {
|
||||
// If sequence number is the same, pick the one with larger time, which is
|
||||
// more accurate than the older time.
|
||||
if (it.seqno == prev.seqno) {
|
||||
assert(it.time >= prev.time);
|
||||
prev.time = it.time;
|
||||
} else {
|
||||
assert(it.seqno > prev.seqno);
|
||||
// If a larger sequence number has an older time which is not useful, skip
|
||||
if (it.time > prev.time) {
|
||||
seqno_time_mapping_.push_back(prev);
|
||||
prev = it;
|
||||
}
|
||||
}
|
||||
}
|
||||
seqno_time_mapping_.emplace_back(prev);
|
||||
|
||||
is_sorted_ = true;
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
std::string SeqnoToTimeMapping::ToHumanString() const {
|
||||
std::string ret;
|
||||
for (const auto& seq_time : seqno_time_mapping_) {
|
||||
AppendNumberTo(&ret, seq_time.seqno);
|
||||
ret.append("->");
|
||||
AppendNumberTo(&ret, seq_time.time);
|
||||
ret.append(",");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
SeqnoToTimeMapping SeqnoToTimeMapping::Copy(
|
||||
SequenceNumber smallest_seqno) const {
|
||||
SeqnoToTimeMapping ret;
|
||||
auto it = std::upper_bound(seqno_time_mapping_.begin(),
|
||||
seqno_time_mapping_.end(), smallest_seqno);
|
||||
if (it != seqno_time_mapping_.begin()) {
|
||||
it--;
|
||||
}
|
||||
std::copy(it, seqno_time_mapping_.end(),
|
||||
std::back_inserter(ret.seqno_time_mapping_));
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint64_t SeqnoToTimeMapping::CalculateMaxCapacity(uint64_t min_time_duration,
|
||||
uint64_t max_time_duration) {
|
||||
if (min_time_duration == 0) {
|
||||
return 0;
|
||||
}
|
||||
return std::min(
|
||||
kMaxSeqnoToTimeEntries,
|
||||
max_time_duration * kMaxSeqnoTimePairsPerCF / min_time_duration);
|
||||
}
|
||||
|
||||
SeqnoToTimeMapping::SeqnoTimePair SeqnoToTimeMapping::SeqnoTimePair::operator-(
|
||||
const SeqnoTimePair& other) const {
|
||||
SeqnoTimePair res;
|
||||
res.seqno = seqno - other.seqno;
|
||||
res.time = time - other.time;
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,186 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
//
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
|
||||
#include "rocksdb/status.h"
|
||||
#include "rocksdb/types.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
constexpr uint64_t kUnknownSeqnoTime = 0;
|
||||
|
||||
// SeqnoToTimeMapping stores the sequence number to time mapping, so given a
|
||||
// sequence number it can estimate the oldest possible time for that sequence
|
||||
// number. For example:
|
||||
// 10 -> 100
|
||||
// 50 -> 300
|
||||
// then if a key has seqno 19, the OldestApproximateTime would be 100, for 51 it
|
||||
// would be 300.
|
||||
// As it's a sorted list, the new entry is inserted from the back. The old data
|
||||
// will be popped from the front if they're no longer used.
|
||||
//
|
||||
// Note: the data struct is not thread safe, both read and write need to be
|
||||
// synchronized by caller.
|
||||
class SeqnoToTimeMapping {
|
||||
public:
|
||||
// Maximum number of entries can be encoded into SST. The data is delta encode
|
||||
// so the maximum data usage for each SST is < 0.3K
|
||||
static constexpr uint64_t kMaxSeqnoTimePairsPerSST = 100;
|
||||
|
||||
// Maximum number of entries per CF. If there's only CF with this feature on,
|
||||
// the max duration divided by this number, so for example, if
|
||||
// preclude_last_level_data_seconds = 100000 (~1day), then it will sample the
|
||||
// seqno -> time every 1000 seconds (~17minutes). Then the maximum entry it
|
||||
// needs is 100.
|
||||
// When there are multiple CFs having this feature on, the sampling cadence is
|
||||
// determined by the smallest setting, the capacity is determined the largest
|
||||
// setting, also it's caped by kMaxSeqnoTimePairsPerCF * 10.
|
||||
static constexpr uint64_t kMaxSeqnoTimePairsPerCF = 100;
|
||||
|
||||
// A simple struct for sequence number to time pair
|
||||
struct SeqnoTimePair {
|
||||
SequenceNumber seqno = 0;
|
||||
uint64_t time = 0;
|
||||
|
||||
SeqnoTimePair() = default;
|
||||
SeqnoTimePair(SequenceNumber _seqno, uint64_t _time)
|
||||
: seqno(_seqno), time(_time) {}
|
||||
|
||||
// Encode to dest string
|
||||
void Encode(std::string& dest) const;
|
||||
|
||||
// Decode the value from input Slice and remove it from the input
|
||||
Status Decode(Slice& input);
|
||||
|
||||
// subtraction of 2 SeqnoTimePair
|
||||
SeqnoTimePair operator-(const SeqnoTimePair& other) const;
|
||||
|
||||
// Add 2 values together
|
||||
void Add(const SeqnoTimePair& obj) {
|
||||
seqno += obj.seqno;
|
||||
time += obj.time;
|
||||
}
|
||||
|
||||
// Compare SeqnoTimePair with a sequence number, used for binary search a
|
||||
// sequence number in a list of SeqnoTimePair
|
||||
bool operator<(const SequenceNumber& other) const { return seqno < other; }
|
||||
|
||||
// Compare 2 SeqnoTimePair
|
||||
bool operator<(const SeqnoTimePair& other) const {
|
||||
return std::tie(seqno, time) < std::tie(other.seqno, other.time);
|
||||
}
|
||||
|
||||
// Check if 2 SeqnoTimePair is the same
|
||||
bool operator==(const SeqnoTimePair& other) const {
|
||||
return std::tie(seqno, time) == std::tie(other.seqno, other.time);
|
||||
}
|
||||
};
|
||||
|
||||
// constractor of SeqnoToTimeMapping
|
||||
// max_time_duration is the maximum time it should track. For example, if
|
||||
// preclude_last_level_data_seconds is 1 day, then if an entry is older than 1
|
||||
// day, then it can be removed.
|
||||
// max_capacity is the maximum number of entry it can hold. For single CF,
|
||||
// it's caped at 100 (kMaxSeqnoTimePairsPerCF), otherwise
|
||||
// kMaxSeqnoTimePairsPerCF * 10.
|
||||
// If it's set to 0, means it won't truncate any old data.
|
||||
explicit SeqnoToTimeMapping(uint64_t max_time_duration = 0,
|
||||
uint64_t max_capacity = 0)
|
||||
: max_time_duration_(max_time_duration), max_capacity_(max_capacity) {}
|
||||
|
||||
// Append a new entry to the list. The new entry should be newer than the
|
||||
// existing ones. It maintains the internal sorted status.
|
||||
bool Append(SequenceNumber seqno, uint64_t time);
|
||||
|
||||
// Given a sequence number, estimate it's oldest time
|
||||
uint64_t GetOldestApproximateTime(SequenceNumber seqno) const;
|
||||
|
||||
// Truncate the old entries based on the current time and max_time_duration_
|
||||
SequenceNumber TruncateOldEntries(uint64_t now);
|
||||
|
||||
// Encode to a binary string
|
||||
void Encode(std::string& des, SequenceNumber start, SequenceNumber end,
|
||||
uint64_t now,
|
||||
uint64_t output_size = kMaxSeqnoTimePairsPerSST) const;
|
||||
|
||||
// Add a new random entry, unlike Append(), it can be any data, but also makes
|
||||
// the list un-sorted.
|
||||
void Add(SequenceNumber seqno, uint64_t time);
|
||||
|
||||
// Decode and add the entries to the current obj. The list will be unsorted
|
||||
Status Add(const std::string& seqno_time_mapping_str);
|
||||
|
||||
// Return the number of entries
|
||||
size_t Size() const { return seqno_time_mapping_.size(); }
|
||||
|
||||
// Reduce the size of internal list
|
||||
bool Resize(uint64_t min_time_duration, uint64_t max_time_duration);
|
||||
|
||||
// Override the max_time_duration_
|
||||
void SetMaxTimeDuration(uint64_t max_time_duration) {
|
||||
max_time_duration_ = max_time_duration;
|
||||
}
|
||||
|
||||
uint64_t GetCapacity() const { return max_capacity_; }
|
||||
|
||||
// Sort the list, which also remove the redundant entries, useless entries,
|
||||
// which makes sure the seqno is sorted, but also the time
|
||||
Status Sort();
|
||||
|
||||
// copy the current obj from the given smallest_seqno.
|
||||
SeqnoToTimeMapping Copy(SequenceNumber smallest_seqno) const;
|
||||
|
||||
// If the internal list is empty
|
||||
bool Empty() const { return seqno_time_mapping_.empty(); }
|
||||
|
||||
// clear all entries
|
||||
void Clear() { seqno_time_mapping_.clear(); }
|
||||
|
||||
// return the string for user message
|
||||
// Note: Not efficient, okay for print
|
||||
std::string ToHumanString() const;
|
||||
|
||||
#ifndef NDEBUG
|
||||
const std::deque<SeqnoTimePair>& TEST_GetInternalMapping() const {
|
||||
return seqno_time_mapping_;
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
static constexpr uint64_t kMaxSeqnoToTimeEntries =
|
||||
kMaxSeqnoTimePairsPerCF * 10;
|
||||
|
||||
uint64_t max_time_duration_;
|
||||
uint64_t max_capacity_;
|
||||
|
||||
std::deque<SeqnoTimePair> seqno_time_mapping_;
|
||||
|
||||
bool is_sorted_ = true;
|
||||
|
||||
static uint64_t CalculateMaxCapacity(uint64_t min_time_duration,
|
||||
uint64_t max_time_duration);
|
||||
|
||||
SeqnoTimePair& Last() {
|
||||
assert(!Empty());
|
||||
return seqno_time_mapping_.back();
|
||||
}
|
||||
};
|
||||
|
||||
// for searching the sequence number from SeqnoToTimeMapping
|
||||
inline bool operator<(const SequenceNumber& seqno,
|
||||
const SeqnoToTimeMapping::SeqnoTimePair& other) {
|
||||
return seqno < other.seqno;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+37
-18
@@ -2733,24 +2733,43 @@ void VersionStorageInfo::ComputeCompactionScore(
|
||||
// Level-based involves L0->L0 compactions that can lead to oversized
|
||||
// L0 files. Take into account size as well to avoid later giant
|
||||
// compactions to the base level.
|
||||
uint64_t l0_target_size = mutable_cf_options.max_bytes_for_level_base;
|
||||
if (immutable_options.level_compaction_dynamic_level_bytes &&
|
||||
level_multiplier_ != 0.0) {
|
||||
// Prevent L0 to Lbase fanout from growing larger than
|
||||
// `level_multiplier_`. This prevents us from getting stuck picking
|
||||
// L0 forever even when it is hurting write-amp. That could happen
|
||||
// in dynamic level compaction's write-burst mode where the base
|
||||
// level's target size can grow to be enormous.
|
||||
l0_target_size =
|
||||
std::max(l0_target_size,
|
||||
static_cast<uint64_t>(level_max_bytes_[base_level_] /
|
||||
level_multiplier_));
|
||||
}
|
||||
score =
|
||||
std::max(score, static_cast<double>(total_size) / l0_target_size);
|
||||
if (immutable_options.level_compaction_dynamic_level_bytes &&
|
||||
score > 1.0) {
|
||||
score *= kScoreScale;
|
||||
// If score in L0 is always too high, L0->L1 will always be
|
||||
// prioritized over L1->L2 compaction and L1 will accumulate to
|
||||
// too large. But if L0 score isn't high enough, L0 will accumulate
|
||||
// and data is not moved to L1 fast enough. With potential L0->L0
|
||||
// compaction, number of L0 files aren't always an indication of
|
||||
// L0 oversizing, and we also need to consider total size of L0.
|
||||
if (immutable_options.level_compaction_dynamic_level_bytes) {
|
||||
if (total_size >= mutable_cf_options.max_bytes_for_level_base) {
|
||||
// When calculating estimated_compaction_needed_bytes, we assume
|
||||
// L0 is qualified as pending compactions. We will need to make
|
||||
// sure that it qualifies for compaction.
|
||||
// It might be guafanteed by logic below anyway, but we are
|
||||
// explicit here to make sure we don't stop writes with no
|
||||
// compaction scheduled.
|
||||
score = std::max(score, 1.01);
|
||||
}
|
||||
if (total_size > level_max_bytes_[base_level_]) {
|
||||
// In this case, we compare L0 size with actual L1 size and make
|
||||
// sure score is more than 1.0 (10.0 after scaled) if L0 is larger
|
||||
// than L1. Since in this case L1 score is lower than 10.0, L0->L1
|
||||
// is prioritized over L1->L2.
|
||||
uint64_t base_level_size = 0;
|
||||
for (auto f : files_[base_level_]) {
|
||||
base_level_size += f->compensated_file_size;
|
||||
}
|
||||
score = std::max(score, static_cast<double>(total_size) /
|
||||
static_cast<double>(std::max(
|
||||
base_level_size,
|
||||
level_max_bytes_[base_level_])));
|
||||
}
|
||||
if (score > 1.0) {
|
||||
score *= kScoreScale;
|
||||
}
|
||||
} else {
|
||||
score = std::max(score,
|
||||
static_cast<double>(total_size) /
|
||||
mutable_cf_options.max_bytes_for_level_base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+74
-1
@@ -1401,7 +1401,7 @@ TEST_P(EnvPosixTestWithParam, MultiRead) {
|
||||
}
|
||||
});
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
std::unique_ptr<RandomAccessFile> file;
|
||||
std::vector<ReadRequest> reqs(3);
|
||||
std::vector<std::unique_ptr<char, Deleter>> data;
|
||||
@@ -1522,6 +1522,79 @@ TEST_F(EnvPosixTest, MultiReadNonAlignedLargeNum) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
TEST_F(EnvPosixTest, NonAlignedDirectIOMultiReadBeyondFileSize) {
|
||||
EnvOptions soptions;
|
||||
soptions.use_direct_reads = true;
|
||||
soptions.use_direct_writes = false;
|
||||
std::string fname = test::PerThreadDBPath(env_, "testfile");
|
||||
|
||||
Random rnd(301);
|
||||
std::unique_ptr<WritableFile> wfile;
|
||||
size_t alignment = 0;
|
||||
// Create file.
|
||||
{
|
||||
ASSERT_OK(env_->NewWritableFile(fname, &wfile, soptions));
|
||||
auto data_ptr = NewAligned(4095, 'b');
|
||||
Slice data_b(data_ptr.get(), 4095);
|
||||
ASSERT_OK(wfile->PositionedAppend(data_b, 0U));
|
||||
ASSERT_OK(wfile->Close());
|
||||
}
|
||||
|
||||
#if !defined(OS_MACOSX) && !defined(OS_WIN) && !defined(OS_SOLARIS) && \
|
||||
!defined(OS_AIX) && !defined(OS_OPENBSD) && !defined(OS_FREEBSD)
|
||||
if (soptions.use_direct_reads) {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"NewRandomAccessFile:O_DIRECT", [&](void* arg) {
|
||||
int* val = static_cast<int*>(arg);
|
||||
*val &= ~O_DIRECT;
|
||||
});
|
||||
}
|
||||
#endif
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
const int num_reads = 2;
|
||||
// Create requests
|
||||
std::vector<std::string> scratches;
|
||||
scratches.reserve(num_reads);
|
||||
std::vector<ReadRequest> reqs(num_reads);
|
||||
|
||||
std::unique_ptr<RandomAccessFile> file;
|
||||
ASSERT_OK(env_->NewRandomAccessFile(fname, &file, soptions));
|
||||
alignment = file->GetRequiredBufferAlignment();
|
||||
ASSERT_EQ(num_reads, reqs.size());
|
||||
|
||||
std::vector<std::unique_ptr<char, Deleter>> data;
|
||||
|
||||
std::vector<size_t> offsets = {0, 2047};
|
||||
std::vector<size_t> lens = {2047, 4096 - 2047};
|
||||
|
||||
for (size_t i = 0; i < num_reads; i++) {
|
||||
// Do alignment
|
||||
reqs[i].offset = static_cast<uint64_t>(TruncateToPageBoundary(
|
||||
alignment, static_cast<size_t>(/*offset=*/offsets[i])));
|
||||
reqs[i].len =
|
||||
Roundup(static_cast<size_t>(/*offset=*/offsets[i]) + /*length=*/lens[i],
|
||||
alignment) -
|
||||
reqs[i].offset;
|
||||
|
||||
size_t new_capacity = Roundup(reqs[i].len, alignment);
|
||||
data.emplace_back(NewAligned(new_capacity, 0));
|
||||
reqs[i].scratch = data.back().get();
|
||||
}
|
||||
|
||||
// Query the data
|
||||
ASSERT_OK(file->MultiRead(reqs.data(), reqs.size()));
|
||||
|
||||
// Validate results
|
||||
for (size_t i = 0; i < num_reads; ++i) {
|
||||
ASSERT_OK(reqs[i].status);
|
||||
}
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
void GenerateFilesAndRequest(Env* env, const std::string& fname,
|
||||
std::vector<ReadRequest>* ret_reqs,
|
||||
|
||||
Vendored
+12
-8
@@ -750,14 +750,17 @@ IOStatus PosixRandomAccessFile::MultiRead(FSReadRequest* reqs,
|
||||
bytes_read, read_again);
|
||||
int32_t res = cqe->res;
|
||||
if (res >= 0) {
|
||||
if (bytes_read == 0 && read_again) {
|
||||
Slice tmp_slice;
|
||||
req->status =
|
||||
Read(req->offset + req_wrap->finished_len,
|
||||
req->len - req_wrap->finished_len, options, &tmp_slice,
|
||||
req->scratch + req_wrap->finished_len, dbg);
|
||||
req->result =
|
||||
Slice(req->scratch, req_wrap->finished_len + tmp_slice.size());
|
||||
if (bytes_read == 0) {
|
||||
if (read_again) {
|
||||
Slice tmp_slice;
|
||||
req->status =
|
||||
Read(req->offset + req_wrap->finished_len,
|
||||
req->len - req_wrap->finished_len, options, &tmp_slice,
|
||||
req->scratch + req_wrap->finished_len, dbg);
|
||||
req->result =
|
||||
Slice(req->scratch, req_wrap->finished_len + tmp_slice.size());
|
||||
}
|
||||
// else It means EOF so no need to do anything.
|
||||
} else if (bytes_read < req_wrap->iov.iov_len) {
|
||||
incomplete_rq_list.push_back(req_wrap);
|
||||
}
|
||||
@@ -1681,6 +1684,7 @@ IOStatus PosixDirectory::Close(const IOOptions& /*opts*/,
|
||||
IOStatus PosixDirectory::FsyncWithDirOptions(
|
||||
const IOOptions& /*opts*/, IODebugContext* /*dbg*/,
|
||||
const DirFsyncOptions& dir_fsync_options) {
|
||||
assert(fd_ >= 0); // Check use after close
|
||||
IOStatus s = IOStatus::OK();
|
||||
#ifndef OS_AIX
|
||||
if (is_btrfs_) {
|
||||
|
||||
@@ -874,6 +874,15 @@ struct AdvancedColumnFamilyOptions {
|
||||
// Dynamically changeable through the SetOptions() API
|
||||
Temperature bottommost_temperature = Temperature::kUnknown;
|
||||
|
||||
// EXPERIMENTAL
|
||||
// The feature is still in development and is incomplete.
|
||||
// If this option is set, when data insert time is within this time range, it
|
||||
// will be precluded from the last level.
|
||||
// 0 means no key will be precluded from the last level.
|
||||
//
|
||||
// Default: 0 (disable the feature)
|
||||
uint64_t preclude_last_level_data_seconds = 0;
|
||||
|
||||
// When set, large values (blobs) are written to separate blob files, and
|
||||
// only pointers to them are stored in SST files. This can reduce write
|
||||
// amplification for large-value use cases at the cost of introducing a level
|
||||
|
||||
@@ -410,7 +410,7 @@ class DB {
|
||||
// UNDER CONSTRUCTION -- DO NOT USE
|
||||
virtual Status PutEntity(const WriteOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
const WideColumns& columns) = 0;
|
||||
const WideColumns& columns);
|
||||
|
||||
// Remove the database entry (if any) for "key". Returns OK on
|
||||
// success, and a non-OK status on error. It is not an error if "key"
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "rocksdb/customizable.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/statistics.h"
|
||||
#include "rocksdb/status.h"
|
||||
@@ -19,7 +18,7 @@ namespace ROCKSDB_NAMESPACE {
|
||||
// Exceptions MUST NOT propagate out of overridden functions into RocksDB,
|
||||
// because RocksDB is not exception-safe. This could cause undefined behavior
|
||||
// including data loss, unreported corruption, deadlocks, and more.
|
||||
class RateLimiter : public Customizable {
|
||||
class RateLimiter {
|
||||
public:
|
||||
enum class OpType {
|
||||
kRead,
|
||||
@@ -32,20 +31,11 @@ class RateLimiter : public Customizable {
|
||||
kAllIo,
|
||||
};
|
||||
|
||||
static const char* Type() { return "RateLimiter"; }
|
||||
static Status CreateFromString(const ConfigOptions& options,
|
||||
const std::string& value,
|
||||
std::shared_ptr<RateLimiter>* result);
|
||||
|
||||
// For API compatibility, default to rate-limiting writes only.
|
||||
explicit RateLimiter(Mode mode = Mode::kWritesOnly);
|
||||
explicit RateLimiter(Mode mode = Mode::kWritesOnly) : mode_(mode) {}
|
||||
|
||||
virtual ~RateLimiter() {}
|
||||
|
||||
// Deprecated. Will be removed in a major release. Derived classes
|
||||
// should implement this method.
|
||||
virtual const char* Name() const override { return ""; }
|
||||
|
||||
// This API allows user to dynamically change rate limiter's bytes per second.
|
||||
// REQUIRED: bytes_per_second > 0
|
||||
virtual void SetBytesPerSecond(int64_t bytes_per_second) = 0;
|
||||
@@ -135,7 +125,7 @@ class RateLimiter : public Customizable {
|
||||
Mode GetMode() { return mode_; }
|
||||
|
||||
private:
|
||||
Mode mode_;
|
||||
const Mode mode_;
|
||||
};
|
||||
|
||||
// Create a RateLimiter object, which can be shared among RocksDB instances to
|
||||
|
||||
@@ -69,6 +69,7 @@ struct TablePropertiesNames {
|
||||
static const std::string kFileCreationTime;
|
||||
static const std::string kSlowCompressionEstimatedDataSize;
|
||||
static const std::string kFastCompressionEstimatedDataSize;
|
||||
static const std::string kSequenceNumberTimeMapping;
|
||||
};
|
||||
|
||||
// `TablePropertiesCollector` provides the mechanism for users to collect
|
||||
@@ -220,6 +221,7 @@ struct TableProperties {
|
||||
// TODO(sagar0): Should be changed to latest_key_time ... but don't know the
|
||||
// full implications of backward compatibility. Hence retaining for now.
|
||||
uint64_t creation_time = 0;
|
||||
|
||||
// Timestamp of the earliest key. 0 means unknown.
|
||||
uint64_t oldest_key_time = 0;
|
||||
// Actual SST file creation time. 0 means unknown.
|
||||
@@ -284,6 +286,9 @@ struct TableProperties {
|
||||
// Compression options used to compress the SST files.
|
||||
std::string compression_options;
|
||||
|
||||
// Sequence number to time mapping, delta encoded.
|
||||
std::string seqno_to_time_mapping;
|
||||
|
||||
// user collected properties
|
||||
UserCollectedProperties user_collected_properties;
|
||||
UserCollectedProperties readable_properties;
|
||||
|
||||
@@ -25,9 +25,8 @@ struct KeyVersion {
|
||||
std::string user_key;
|
||||
std::string value;
|
||||
SequenceNumber sequence;
|
||||
// TODO(ajkr): we should provide a helper function that converts the int to a
|
||||
// string describing the type for easier debugging.
|
||||
int type;
|
||||
std::string GetTypeName() const;
|
||||
};
|
||||
|
||||
// Returns listing of all versions of keys in the provided user key range.
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// minor or major version number planned for release.
|
||||
#define ROCKSDB_MAJOR 7
|
||||
#define ROCKSDB_MINOR 5
|
||||
#define ROCKSDB_PATCH 0
|
||||
#define ROCKSDB_PATCH 4
|
||||
|
||||
// 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
|
||||
|
||||
@@ -67,10 +67,10 @@ TEST_F(StatsHistoryTest, RunStatsDumpPeriodSec) {
|
||||
|
||||
// Wait for the first stats persist to finish, as the initial delay could be
|
||||
// different.
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec - 1); });
|
||||
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
ASSERT_GE(counter, 1);
|
||||
|
||||
@@ -99,17 +99,17 @@ TEST_F(StatsHistoryTest, StatsPersistScheduling) {
|
||||
|
||||
// Wait for the first stats persist to finish, as the initial delay could be
|
||||
// different.
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec - 1); });
|
||||
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
ASSERT_GE(counter, 1);
|
||||
|
||||
// Test cancel job through SetOptions
|
||||
ASSERT_OK(dbfull()->SetDBOptions({{"stats_persist_period_sec", "0"}}));
|
||||
int old_val = counter;
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec * 2); });
|
||||
ASSERT_EQ(counter, old_val);
|
||||
|
||||
@@ -131,7 +131,7 @@ TEST_F(StatsHistoryTest, PersistentStatsFreshInstall) {
|
||||
{{"stats_persist_period_sec", std::to_string(kPeriodSec)}}));
|
||||
ASSERT_EQ(kPeriodSec, dbfull()->GetDBOptions().stats_persist_period_sec);
|
||||
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
ASSERT_GE(counter, 1);
|
||||
Close();
|
||||
@@ -150,11 +150,11 @@ TEST_F(StatsHistoryTest, GetStatsHistoryInMemory) {
|
||||
ReopenWithColumnFamilies({"default", "pikachu"}, options);
|
||||
|
||||
// make sure the first stats persist to finish
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec - 1); });
|
||||
|
||||
// Wait for stats persist to finish
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
|
||||
std::unique_ptr<StatsHistoryIterator> stats_iter;
|
||||
@@ -172,7 +172,7 @@ TEST_F(StatsHistoryTest, GetStatsHistoryInMemory) {
|
||||
ASSERT_GT(stats_count, 0);
|
||||
// Wait a bit and verify no more stats are found
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(1); });
|
||||
}
|
||||
ASSERT_OK(db_->GetStatsHistory(0, mock_clock_->NowSeconds(), &stats_iter));
|
||||
@@ -227,7 +227,7 @@ TEST_F(StatsHistoryTest, InMemoryStatsHistoryPurging) {
|
||||
|
||||
const int kIterations = 10;
|
||||
for (int i = 0; i < kIterations; ++i) {
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ TEST_F(StatsHistoryTest, InMemoryStatsHistoryPurging) {
|
||||
|
||||
// Wait for stats persist to finish
|
||||
for (int i = 0; i < kIterations; ++i) {
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
}
|
||||
|
||||
@@ -300,11 +300,11 @@ TEST_F(StatsHistoryTest, GetStatsHistoryFromDisk) {
|
||||
|
||||
// Wait for the first stats persist to finish, as the initial delay could be
|
||||
// different.
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec - 1); });
|
||||
|
||||
// Wait for stats persist to finish
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
|
||||
auto iter =
|
||||
@@ -312,14 +312,14 @@ TEST_F(StatsHistoryTest, GetStatsHistoryFromDisk) {
|
||||
int key_count1 = countkeys(iter);
|
||||
delete iter;
|
||||
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
iter =
|
||||
db_->NewIterator(ReadOptions(), dbfull()->PersistentStatsColumnFamily());
|
||||
int key_count2 = countkeys(iter);
|
||||
delete iter;
|
||||
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
iter =
|
||||
db_->NewIterator(ReadOptions(), dbfull()->PersistentStatsColumnFamily());
|
||||
@@ -393,32 +393,32 @@ TEST_F(StatsHistoryTest, PersitentStatsVerifyValue) {
|
||||
|
||||
// Wait for the first stats persist to finish, as the initial delay could be
|
||||
// different.
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec - 1); });
|
||||
|
||||
// Wait for stats persist to finish
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
auto iter =
|
||||
db_->NewIterator(ReadOptions(), dbfull()->PersistentStatsColumnFamily());
|
||||
countkeys(iter);
|
||||
delete iter;
|
||||
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
iter =
|
||||
db_->NewIterator(ReadOptions(), dbfull()->PersistentStatsColumnFamily());
|
||||
countkeys(iter);
|
||||
delete iter;
|
||||
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
iter =
|
||||
db_->NewIterator(ReadOptions(), dbfull()->PersistentStatsColumnFamily());
|
||||
countkeys(iter);
|
||||
delete iter;
|
||||
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
|
||||
std::map<std::string, uint64_t> stats_map_after;
|
||||
@@ -482,10 +482,10 @@ TEST_F(StatsHistoryTest, PersistentStatsCreateColumnFamilies) {
|
||||
ASSERT_EQ(Get(2, "foo"), "bar");
|
||||
|
||||
// make sure the first stats persist to finish
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec - 1); });
|
||||
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
auto iter =
|
||||
db_->NewIterator(ReadOptions(), dbfull()->PersistentStatsColumnFamily());
|
||||
@@ -582,7 +582,7 @@ TEST_F(StatsHistoryTest, ForceManualFlushStatsCF) {
|
||||
|
||||
// Wait for the first stats persist to finish, as the initial delay could be
|
||||
// different.
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec - 1); });
|
||||
|
||||
ColumnFamilyData* cfd_default =
|
||||
@@ -601,7 +601,7 @@ TEST_F(StatsHistoryTest, ForceManualFlushStatsCF) {
|
||||
ASSERT_OK(Put(1, "Eevee", "v0"));
|
||||
ASSERT_EQ("v0", Get(1, "Eevee"));
|
||||
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
// writing to all three cf, flush default cf
|
||||
// LogNumbers: default: 16, stats: 10, pikachu: 5
|
||||
@@ -630,7 +630,7 @@ TEST_F(StatsHistoryTest, ForceManualFlushStatsCF) {
|
||||
ASSERT_EQ("v2", Get("bar2"));
|
||||
ASSERT_EQ("v2", Get("foo2"));
|
||||
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
// writing to default and stats cf, flushing default cf
|
||||
// LogNumbers: default: 19, stats: 19, pikachu: 19
|
||||
@@ -645,7 +645,7 @@ TEST_F(StatsHistoryTest, ForceManualFlushStatsCF) {
|
||||
ASSERT_OK(Put(1, "Jolteon", "v3"));
|
||||
ASSERT_EQ("v3", Get(1, "Jolteon"));
|
||||
|
||||
dbfull()->TEST_WaitForStatsDumpRun(
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
// writing to all three cf, flushing test cf
|
||||
// LogNumbers: default: 19, stats: 19, pikachu: 22
|
||||
|
||||
@@ -547,6 +547,10 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
||||
{offsetof(struct ImmutableCFOptions, force_consistency_checks),
|
||||
OptionType::kBoolean, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"preclude_last_level_data_seconds",
|
||||
{offsetof(struct ImmutableCFOptions, preclude_last_level_data_seconds),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
// Need to keep this around to be able to read old OPTIONS files.
|
||||
{"max_mem_compaction_level",
|
||||
{0, OptionType::kInt, OptionVerificationType::kDeprecated,
|
||||
@@ -880,6 +884,8 @@ ImmutableCFOptions::ImmutableCFOptions(const ColumnFamilyOptions& cf_options)
|
||||
num_levels(cf_options.num_levels),
|
||||
optimize_filters_for_hits(cf_options.optimize_filters_for_hits),
|
||||
force_consistency_checks(cf_options.force_consistency_checks),
|
||||
preclude_last_level_data_seconds(
|
||||
cf_options.preclude_last_level_data_seconds),
|
||||
memtable_insert_with_hint_prefix_extractor(
|
||||
cf_options.memtable_insert_with_hint_prefix_extractor),
|
||||
cf_paths(cf_options.cf_paths),
|
||||
|
||||
@@ -70,6 +70,8 @@ struct ImmutableCFOptions {
|
||||
|
||||
bool force_consistency_checks;
|
||||
|
||||
uint64_t preclude_last_level_data_seconds;
|
||||
|
||||
std::shared_ptr<const SliceTransform>
|
||||
memtable_insert_with_hint_prefix_extractor;
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
#include "rocksdb/filter_policy.h"
|
||||
#include "rocksdb/flush_block_policy.h"
|
||||
#include "rocksdb/memory_allocator.h"
|
||||
#include "rocksdb/rate_limiter.h"
|
||||
#include "rocksdb/secondary_cache.h"
|
||||
#include "rocksdb/slice_transform.h"
|
||||
#include "rocksdb/sst_partitioner.h"
|
||||
@@ -42,7 +41,6 @@
|
||||
#include "test_util/testharness.h"
|
||||
#include "test_util/testutil.h"
|
||||
#include "util/file_checksum_helper.h"
|
||||
#include "util/rate_limiter.h"
|
||||
#include "util/string_util.h"
|
||||
#include "utilities/compaction_filters/remove_emptyvalue_compactionfilter.h"
|
||||
#include "utilities/memory_allocators.h"
|
||||
@@ -1472,21 +1470,6 @@ class MockFileChecksumGenFactory : public FileChecksumGenFactory {
|
||||
}
|
||||
};
|
||||
|
||||
class MockRateLimiter : public RateLimiter {
|
||||
public:
|
||||
static const char* kClassName() { return "MockRateLimiter"; }
|
||||
const char* Name() const override { return kClassName(); }
|
||||
void SetBytesPerSecond(int64_t /*bytes_per_second*/) override {}
|
||||
int64_t GetBytesPerSecond() const override { return 0; }
|
||||
int64_t GetSingleBurstBytes() const override { return 0; }
|
||||
int64_t GetTotalBytesThrough(const Env::IOPriority /*pri*/) const override {
|
||||
return 0;
|
||||
}
|
||||
int64_t GetTotalRequests(const Env::IOPriority /*pri*/) const override {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
class MockFilterPolicy : public FilterPolicy {
|
||||
public:
|
||||
static const char* kClassName() { return "MockFilterPolicy"; }
|
||||
@@ -1618,14 +1601,6 @@ static int RegisterLocalObjects(ObjectLibrary& library,
|
||||
return guard->get();
|
||||
});
|
||||
|
||||
library.AddFactory<RateLimiter>(
|
||||
MockRateLimiter::kClassName(),
|
||||
[](const std::string& /*uri*/, std::unique_ptr<RateLimiter>* guard,
|
||||
std::string* /* errmsg */) {
|
||||
guard->reset(new MockRateLimiter());
|
||||
return guard->get();
|
||||
});
|
||||
|
||||
library.AddFactory<const FilterPolicy>(
|
||||
MockFilterPolicy::kClassName(),
|
||||
[](const std::string& /*uri*/, std::unique_ptr<const FilterPolicy>* guard,
|
||||
@@ -2149,37 +2124,6 @@ TEST_F(LoadCustomizableTest, LoadMemoryAllocatorTest) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LoadCustomizableTest, LoadRateLimiterTest) {
|
||||
#ifndef ROCKSDB_LITE
|
||||
ASSERT_OK(TestSharedBuiltins<RateLimiter>(MockRateLimiter::kClassName(),
|
||||
GenericRateLimiter::kClassName()));
|
||||
#else
|
||||
ASSERT_OK(TestSharedBuiltins<RateLimiter>(MockRateLimiter::kClassName(), ""));
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
std::shared_ptr<RateLimiter> result;
|
||||
ASSERT_OK(RateLimiter::CreateFromString(
|
||||
config_options_, std::string(GenericRateLimiter::kClassName()) + ":1234",
|
||||
&result));
|
||||
ASSERT_NE(result, nullptr);
|
||||
ASSERT_TRUE(result->IsInstanceOf(GenericRateLimiter::kClassName()));
|
||||
#ifndef ROCKSDB_LITE
|
||||
ASSERT_OK(GetDBOptionsFromString(
|
||||
config_options_, db_opts_,
|
||||
std::string("rate_limiter=") + GenericRateLimiter::kClassName(),
|
||||
&db_opts_));
|
||||
ASSERT_NE(db_opts_.rate_limiter, nullptr);
|
||||
if (RegisterTests("Test")) {
|
||||
ExpectCreateShared<RateLimiter>(MockRateLimiter::kClassName());
|
||||
ASSERT_OK(GetDBOptionsFromString(
|
||||
config_options_, db_opts_,
|
||||
std::string("rate_limiter=") + MockRateLimiter::kClassName(),
|
||||
&db_opts_));
|
||||
ASSERT_NE(db_opts_.rate_limiter, nullptr);
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
}
|
||||
|
||||
TEST_F(LoadCustomizableTest, LoadFilterPolicyTest) {
|
||||
const std::string kAutoBloom = BloomFilterPolicy::kClassName();
|
||||
const std::string kAutoRibbon = RibbonFilterPolicy::kClassName();
|
||||
|
||||
@@ -421,12 +421,11 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
||||
{"db_host_id",
|
||||
{offsetof(struct ImmutableDBOptions, db_host_id), OptionType::kString,
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kCompareNever}},
|
||||
// Temporarily deprecated due to race conditions (examples in PR 10375).
|
||||
{"rate_limiter",
|
||||
OptionTypeInfo::AsCustomSharedPtr<RateLimiter>(
|
||||
offsetof(struct ImmutableDBOptions, rate_limiter),
|
||||
OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kCompareNever | OptionTypeFlags::kAllowNull)},
|
||||
|
||||
{offsetof(struct ImmutableDBOptions, rate_limiter),
|
||||
OptionType::kUnknown, OptionVerificationType::kDeprecated,
|
||||
OptionTypeFlags::kDontSerialize | OptionTypeFlags::kCompareNever}},
|
||||
// The following properties were handled as special cases in ParseOption
|
||||
// This means that the properties could be read from the options file
|
||||
// but never written to the file or compared to each other.
|
||||
|
||||
@@ -92,6 +92,8 @@ AdvancedColumnFamilyOptions::AdvancedColumnFamilyOptions(const Options& options)
|
||||
ttl(options.ttl),
|
||||
periodic_compaction_seconds(options.periodic_compaction_seconds),
|
||||
sample_for_compression(options.sample_for_compression),
|
||||
preclude_last_level_data_seconds(
|
||||
options.preclude_last_level_data_seconds),
|
||||
enable_blob_files(options.enable_blob_files),
|
||||
min_blob_size(options.min_blob_size),
|
||||
blob_file_size(options.blob_file_size),
|
||||
@@ -398,6 +400,8 @@ void ColumnFamilyOptions::Dump(Logger* log) const {
|
||||
ROCKS_LOG_HEADER(log,
|
||||
" Options.periodic_compaction_seconds: %" PRIu64,
|
||||
periodic_compaction_seconds);
|
||||
ROCKS_LOG_HEADER(log, " Options.preclude_last_level_data_seconds: %" PRIu64,
|
||||
preclude_last_level_data_seconds);
|
||||
ROCKS_LOG_HEADER(log, " Options.enable_blob_files: %s",
|
||||
enable_blob_files ? "true" : "false");
|
||||
ROCKS_LOG_HEADER(
|
||||
|
||||
@@ -306,6 +306,8 @@ void UpdateColumnFamilyOptions(const ImmutableCFOptions& ioptions,
|
||||
cf_opts->compaction_thread_limiter = ioptions.compaction_thread_limiter;
|
||||
cf_opts->sst_partitioner_factory = ioptions.sst_partitioner_factory;
|
||||
cf_opts->blob_cache = ioptions.blob_cache;
|
||||
cf_opts->preclude_last_level_data_seconds =
|
||||
ioptions.preclude_last_level_data_seconds;
|
||||
|
||||
// TODO(yhchiang): find some way to handle the following derived options
|
||||
// * max_file_size
|
||||
|
||||
@@ -400,6 +400,8 @@ TEST_F(OptionsSettableTest, ColumnFamilyOptionsAllFieldsSettable) {
|
||||
{offsetof(struct ColumnFamilyOptions,
|
||||
table_properties_collector_factories),
|
||||
sizeof(ColumnFamilyOptions::TablePropertiesCollectorFactories)},
|
||||
{offsetof(struct ColumnFamilyOptions, preclude_last_level_data_seconds),
|
||||
sizeof(uint64_t)},
|
||||
{offsetof(struct ColumnFamilyOptions, blob_cache),
|
||||
sizeof(std::shared_ptr<Cache>)},
|
||||
{offsetof(struct ColumnFamilyOptions, comparator), sizeof(Comparator*)},
|
||||
@@ -525,6 +527,7 @@ TEST_F(OptionsSettableTest, ColumnFamilyOptionsAllFieldsSettable) {
|
||||
"blob_compaction_readahead_size=262144;"
|
||||
"blob_file_starting_level=1;"
|
||||
"bottommost_temperature=kWarm;"
|
||||
"preclude_last_level_data_seconds=86400;"
|
||||
"compaction_options_fifo={max_table_files_size=3;allow_"
|
||||
"compaction=false;age_for_warm=1;};"
|
||||
"blob_cache=1M;",
|
||||
|
||||
@@ -76,6 +76,7 @@ LIB_SOURCES = \
|
||||
db/range_del_aggregator.cc \
|
||||
db/range_tombstone_fragmenter.cc \
|
||||
db/repair.cc \
|
||||
db/seqno_to_time_mapping.cc \
|
||||
db/snapshot_impl.cc \
|
||||
db/table_cache.cc \
|
||||
db/table_properties_collector.cc \
|
||||
@@ -503,6 +504,7 @@ TEST_MAIN_SOURCES = \
|
||||
db/repair_test.cc \
|
||||
db/range_del_aggregator_test.cc \
|
||||
db/range_tombstone_fragmenter_test.cc \
|
||||
db/seqno_time_test.cc \
|
||||
db/table_properties_collector_test.cc \
|
||||
db/version_builder_test.cc \
|
||||
db/version_edit_test.cc \
|
||||
|
||||
@@ -541,7 +541,6 @@ struct BlockBasedTableBuilder::Rep {
|
||||
// These are only needed for populating table properties
|
||||
props.column_family_id = tbo.column_family_id;
|
||||
props.column_family_name = tbo.column_family_name;
|
||||
props.creation_time = tbo.creation_time;
|
||||
props.oldest_key_time = tbo.oldest_key_time;
|
||||
props.file_creation_time = tbo.file_creation_time;
|
||||
props.orig_file_number = tbo.cur_file_num;
|
||||
@@ -2084,6 +2083,12 @@ const char* BlockBasedTableBuilder::GetFileChecksumFuncName() const {
|
||||
return kUnknownFileChecksumFuncName;
|
||||
}
|
||||
}
|
||||
void BlockBasedTableBuilder::SetSeqnoTimeTableProperties(
|
||||
const std::string& encoded_seqno_to_time_mapping,
|
||||
uint64_t oldest_ancestor_time) {
|
||||
rep_->props.seqno_to_time_mapping = encoded_seqno_to_time_mapping;
|
||||
rep_->props.creation_time = oldest_ancestor_time;
|
||||
}
|
||||
|
||||
const std::string BlockBasedTable::kObsoleteFilterBlockPrefix = "filter.";
|
||||
const std::string BlockBasedTable::kFullFilterBlockPrefix = "fullfilter.";
|
||||
|
||||
@@ -100,6 +100,10 @@ class BlockBasedTableBuilder : public TableBuilder {
|
||||
// Get file checksum function name
|
||||
const char* GetFileChecksumFuncName() const override;
|
||||
|
||||
void SetSeqnoTimeTableProperties(
|
||||
const std::string& encoded_seqno_to_time_mapping,
|
||||
uint64_t oldest_ancestor_time) override;
|
||||
|
||||
private:
|
||||
bool ok() const { return status().ok(); }
|
||||
|
||||
|
||||
@@ -288,6 +288,7 @@ Status PartitionedFilterBlockReader::GetFilterPartitionBlock(
|
||||
FilePrefetchBuffer* prefetch_buffer, const BlockHandle& fltr_blk_handle,
|
||||
bool no_io, GetContext* get_context,
|
||||
BlockCacheLookupContext* lookup_context,
|
||||
Env::IOPriority rate_limiter_priority,
|
||||
CachableEntry<ParsedFullFilterBlock>* filter_block) const {
|
||||
assert(table());
|
||||
assert(filter_block);
|
||||
@@ -304,6 +305,7 @@ Status PartitionedFilterBlockReader::GetFilterPartitionBlock(
|
||||
}
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.rate_limiter_priority = rate_limiter_priority;
|
||||
if (no_io) {
|
||||
read_options.read_tier = kBlockCacheTier;
|
||||
}
|
||||
@@ -344,7 +346,7 @@ bool PartitionedFilterBlockReader::MayMatch(
|
||||
CachableEntry<ParsedFullFilterBlock> filter_partition_block;
|
||||
s = GetFilterPartitionBlock(nullptr /* prefetch_buffer */, filter_handle,
|
||||
no_io, get_context, lookup_context,
|
||||
&filter_partition_block);
|
||||
rate_limiter_priority, &filter_partition_block);
|
||||
if (UNLIKELY(!s.ok())) {
|
||||
IGNORE_STATUS_IF_ERROR(s);
|
||||
return true;
|
||||
@@ -419,7 +421,8 @@ void PartitionedFilterBlockReader::MayMatchPartition(
|
||||
CachableEntry<ParsedFullFilterBlock> filter_partition_block;
|
||||
Status s = GetFilterPartitionBlock(
|
||||
nullptr /* prefetch_buffer */, filter_handle, no_io,
|
||||
range->begin()->get_context, lookup_context, &filter_partition_block);
|
||||
range->begin()->get_context, lookup_context, rate_limiter_priority,
|
||||
&filter_partition_block);
|
||||
if (UNLIKELY(!s.ok())) {
|
||||
IGNORE_STATUS_IF_ERROR(s);
|
||||
return; // Any/all may match
|
||||
|
||||
@@ -137,6 +137,7 @@ class PartitionedFilterBlockReader : public FilterBlockReaderCommon<Block> {
|
||||
FilePrefetchBuffer* prefetch_buffer, const BlockHandle& handle,
|
||||
bool no_io, GetContext* get_context,
|
||||
BlockCacheLookupContext* lookup_context,
|
||||
Env::IOPriority rate_limiter_priority,
|
||||
CachableEntry<ParsedFullFilterBlock>* filter_block) const;
|
||||
|
||||
using FilterFunction = bool (FullFilterBlockReader::*)(
|
||||
|
||||
@@ -154,6 +154,10 @@ void PropertyBlockBuilder::AddTableProperty(const TableProperties& props) {
|
||||
if (!props.compression_options.empty()) {
|
||||
Add(TablePropertiesNames::kCompressionOptions, props.compression_options);
|
||||
}
|
||||
if (!props.seqno_to_time_mapping.empty()) {
|
||||
Add(TablePropertiesNames::kSequenceNumberTimeMapping,
|
||||
props.seqno_to_time_mapping);
|
||||
}
|
||||
}
|
||||
|
||||
Slice PropertyBlockBuilder::Finish() {
|
||||
@@ -369,6 +373,8 @@ Status ReadTablePropertiesHelper(
|
||||
new_table_properties->compression_name = raw_val.ToString();
|
||||
} else if (key == TablePropertiesNames::kCompressionOptions) {
|
||||
new_table_properties->compression_options = raw_val.ToString();
|
||||
} else if (key == TablePropertiesNames::kSequenceNumberTimeMapping) {
|
||||
new_table_properties->seqno_to_time_mapping = raw_val.ToString();
|
||||
} else {
|
||||
// handle user-collected properties
|
||||
new_table_properties->user_collected_properties.insert(
|
||||
|
||||
@@ -330,6 +330,11 @@ const char* PlainTableBuilder::GetFileChecksumFuncName() const {
|
||||
return kUnknownFileChecksumFuncName;
|
||||
}
|
||||
}
|
||||
void PlainTableBuilder::SetSeqnoTimeTableProperties(const std::string& string,
|
||||
uint64_t uint_64) {
|
||||
// TODO: storing seqno to time mapping is not yet support for plain table.
|
||||
TableBuilder::SetSeqnoTimeTableProperties(string, uint_64);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
#endif // ROCKSDB_LITE
|
||||
@@ -94,6 +94,9 @@ class PlainTableBuilder: public TableBuilder {
|
||||
// Get file checksum function name
|
||||
const char* GetFileChecksumFuncName() const override;
|
||||
|
||||
void SetSeqnoTimeTableProperties(const std::string& string,
|
||||
uint64_t uint_64) override;
|
||||
|
||||
private:
|
||||
Arena arena_;
|
||||
const ImmutableOptions& ioptions_;
|
||||
|
||||
@@ -279,14 +279,16 @@ Status SstFileWriter::Open(const std::string& file_path) {
|
||||
r->column_family_name = "";
|
||||
cf_id = TablePropertiesCollectorFactory::Context::kUnknownColumnFamily;
|
||||
}
|
||||
|
||||
// TODO: it would be better to set oldest_key_time to be used for getting the
|
||||
// approximate time of ingested keys.
|
||||
TableBuilderOptions table_builder_options(
|
||||
r->ioptions, r->mutable_cf_options, r->internal_comparator,
|
||||
&int_tbl_prop_collector_factories, compression_type, compression_opts,
|
||||
cf_id, r->column_family_name, unknown_level, false /* is_bottommost */,
|
||||
TableFileCreationReason::kMisc, 0 /* creation_time */,
|
||||
0 /* oldest_key_time */, 0 /* file_creation_time */,
|
||||
"SST Writer" /* db_id */, r->db_session_id, 0 /* target_file_size */,
|
||||
r->next_file_number);
|
||||
TableFileCreationReason::kMisc, 0 /* oldest_key_time */,
|
||||
0 /* file_creation_time */, "SST Writer" /* db_id */, r->db_session_id,
|
||||
0 /* target_file_size */, r->next_file_number);
|
||||
// External SST files used to each get a unique session id. Now for
|
||||
// slightly better uniqueness probability in constructing cache keys, we
|
||||
// assign fake file numbers to each file (into table properties) and keep
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "db/dbformat.h"
|
||||
#include "db/seqno_to_time_mapping.h"
|
||||
#include "db/table_properties_collector.h"
|
||||
#include "file/writable_file_writer.h"
|
||||
#include "options/cf_options.h"
|
||||
@@ -109,7 +110,7 @@ struct TableBuilderOptions {
|
||||
const std::string& _column_family_name, int _level,
|
||||
bool _is_bottommost = false,
|
||||
TableFileCreationReason _reason = TableFileCreationReason::kMisc,
|
||||
const uint64_t _creation_time = 0, const int64_t _oldest_key_time = 0,
|
||||
const int64_t _oldest_key_time = 0,
|
||||
const uint64_t _file_creation_time = 0, const std::string& _db_id = "",
|
||||
const std::string& _db_session_id = "",
|
||||
const uint64_t _target_file_size = 0, const uint64_t _cur_file_num = 0)
|
||||
@@ -121,7 +122,6 @@ struct TableBuilderOptions {
|
||||
compression_opts(_compression_opts),
|
||||
column_family_id(_column_family_id),
|
||||
column_family_name(_column_family_name),
|
||||
creation_time(_creation_time),
|
||||
oldest_key_time(_oldest_key_time),
|
||||
target_file_size(_target_file_size),
|
||||
file_creation_time(_file_creation_time),
|
||||
@@ -140,7 +140,6 @@ struct TableBuilderOptions {
|
||||
const CompressionOptions& compression_opts;
|
||||
const uint32_t column_family_id;
|
||||
const std::string& column_family_name;
|
||||
const uint64_t creation_time;
|
||||
const int64_t oldest_key_time;
|
||||
const uint64_t target_file_size;
|
||||
const uint64_t file_creation_time;
|
||||
@@ -222,6 +221,11 @@ class TableBuilder {
|
||||
|
||||
// Return file checksum function name
|
||||
virtual const char* GetFileChecksumFuncName() const = 0;
|
||||
|
||||
// Set the sequence number to time mapping
|
||||
virtual void SetSeqnoTimeTableProperties(
|
||||
const std::string& /*encoded_seqno_to_time_mapping*/,
|
||||
uint64_t /*oldest_ancestor_time*/){};
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "rocksdb/table_properties.h"
|
||||
|
||||
#include "db/seqno_to_time_mapping.h"
|
||||
#include "port/malloc.h"
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/env.h"
|
||||
@@ -164,6 +165,12 @@ std::string TableProperties::ToString(
|
||||
s.ok() ? UniqueIdToHumanString(id) : "N/A", prop_delim,
|
||||
kv_delim);
|
||||
|
||||
SeqnoToTimeMapping seq_time_mapping;
|
||||
s = seq_time_mapping.Add(seqno_to_time_mapping);
|
||||
AppendProperty(result, "Sequence number to time mapping",
|
||||
s.ok() ? seq_time_mapping.ToHumanString() : "N/A", prop_delim,
|
||||
kv_delim);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -307,6 +314,8 @@ const std::string TablePropertiesNames::kSlowCompressionEstimatedDataSize =
|
||||
"rocksdb.sample_for_compression.slow.data.size";
|
||||
const std::string TablePropertiesNames::kFastCompressionEstimatedDataSize =
|
||||
"rocksdb.sample_for_compression.fast.data.size";
|
||||
const std::string TablePropertiesNames::kSequenceNumberTimeMapping =
|
||||
"rocksdb.seqno.time.map";
|
||||
|
||||
#ifndef NDEBUG
|
||||
// WARNING: TEST_SetRandomTableProperties assumes the following layout of
|
||||
|
||||
@@ -60,8 +60,9 @@ class MockSystemClock : public SystemClockWrapper {
|
||||
|
||||
void MockSleepForSeconds(int seconds) {
|
||||
assert(seconds >= 0);
|
||||
int micros = seconds * kMicrosInSecond;
|
||||
SleepForMicroseconds(micros);
|
||||
uint64_t micros = static_cast<uint64_t>(seconds) * kMicrosInSecond;
|
||||
assert(current_time_us_ + micros >= current_time_us_);
|
||||
current_time_us_.fetch_add(micros);
|
||||
}
|
||||
|
||||
// TODO: this is a workaround for the different behavior on different platform
|
||||
|
||||
+29
-154
@@ -13,14 +13,9 @@
|
||||
|
||||
#include "monitoring/statistics.h"
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
#include "rocksdb/system_clock.h"
|
||||
#include "rocksdb/utilities/customizable_util.h"
|
||||
#include "rocksdb/utilities/object_registry.h"
|
||||
#include "rocksdb/utilities/options_type.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/aligned_buffer.h"
|
||||
#include "util/string_util.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
size_t RateLimiter::RequestToken(size_t bytes, size_t alignment,
|
||||
@@ -50,68 +45,33 @@ struct GenericRateLimiter::Req {
|
||||
bool granted;
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo>
|
||||
generic_rate_limiter_type_info = {
|
||||
#ifndef ROCKSDB_LITE
|
||||
{"rate_bytes_per_sec",
|
||||
{offsetof(struct GenericRateLimiter::GenericRateLimiterOptions,
|
||||
max_bytes_per_sec),
|
||||
OptionType::kInt64T}},
|
||||
{"refill_period_us",
|
||||
{offsetof(struct GenericRateLimiter::GenericRateLimiterOptions,
|
||||
refill_period_us),
|
||||
OptionType::kInt64T}},
|
||||
{"fairness",
|
||||
{offsetof(struct GenericRateLimiter::GenericRateLimiterOptions,
|
||||
fairness),
|
||||
OptionType::kInt32T}},
|
||||
{"auto_tuned",
|
||||
{offsetof(struct GenericRateLimiter::GenericRateLimiterOptions,
|
||||
auto_tuned),
|
||||
OptionType::kBoolean}},
|
||||
{"clock",
|
||||
OptionTypeInfo::AsCustomSharedPtr<SystemClock>(
|
||||
offsetof(struct GenericRateLimiter::GenericRateLimiterOptions,
|
||||
clock),
|
||||
OptionVerificationType::kByNameAllowFromNull,
|
||||
OptionTypeFlags::kAllowNull)},
|
||||
#endif // ROCKSDB_LITE
|
||||
};
|
||||
|
||||
GenericRateLimiter::GenericRateLimiter(
|
||||
int64_t rate_bytes_per_sec, int64_t refill_period_us, int32_t fairness,
|
||||
RateLimiter::Mode mode, const std::shared_ptr<SystemClock>& clock,
|
||||
bool auto_tuned)
|
||||
: RateLimiter(mode),
|
||||
options_(rate_bytes_per_sec, refill_period_us, fairness, clock,
|
||||
auto_tuned),
|
||||
refill_period_us_(refill_period_us),
|
||||
rate_bytes_per_sec_(auto_tuned ? rate_bytes_per_sec / 2
|
||||
: rate_bytes_per_sec),
|
||||
refill_bytes_per_period_(
|
||||
CalculateRefillBytesPerPeriod(rate_bytes_per_sec_)),
|
||||
clock_(clock),
|
||||
stop_(false),
|
||||
exit_cv_(&request_mutex_),
|
||||
requests_to_wait_(0),
|
||||
available_bytes_(0),
|
||||
next_refill_us_(NowMicrosMonotonic()),
|
||||
fairness_(fairness > 100 ? 100 : fairness),
|
||||
rnd_((uint32_t)time(nullptr)),
|
||||
wait_until_refill_pending_(false),
|
||||
num_drains_(0) {
|
||||
RegisterOptions(&options_, &generic_rate_limiter_type_info);
|
||||
auto_tuned_(auto_tuned),
|
||||
num_drains_(0),
|
||||
max_bytes_per_sec_(rate_bytes_per_sec),
|
||||
tuned_time_(NowMicrosMonotonic()) {
|
||||
for (int i = Env::IO_LOW; i < Env::IO_TOTAL; ++i) {
|
||||
total_requests_[i] = 0;
|
||||
total_bytes_through_[i] = 0;
|
||||
}
|
||||
Initialize();
|
||||
}
|
||||
void GenericRateLimiter::Initialize() {
|
||||
if (options_.clock == nullptr) {
|
||||
options_.clock = SystemClock::Default();
|
||||
}
|
||||
options_.fairness = std::min(options_.fairness, 100);
|
||||
next_refill_us_ = NowMicrosMonotonic();
|
||||
tuned_time_ = std::chrono::microseconds(NowMicrosMonotonic());
|
||||
if (options_.auto_tuned) {
|
||||
rate_bytes_per_sec_ = options_.max_bytes_per_sec / 2;
|
||||
} else {
|
||||
rate_bytes_per_sec_ = options_.max_bytes_per_sec;
|
||||
}
|
||||
refill_bytes_per_period_ = CalculateRefillBytesPerPeriod(rate_bytes_per_sec_);
|
||||
}
|
||||
|
||||
GenericRateLimiter::~GenericRateLimiter() {
|
||||
@@ -135,18 +95,6 @@ GenericRateLimiter::~GenericRateLimiter() {
|
||||
}
|
||||
}
|
||||
|
||||
Status GenericRateLimiter::PrepareOptions(const ConfigOptions& options) {
|
||||
if (options_.fairness <= 0) {
|
||||
return Status::InvalidArgument("Fairness must be > 0");
|
||||
} else if (options_.max_bytes_per_sec <= 0) {
|
||||
return Status::InvalidArgument("max_bytes_per_sec must be > 0");
|
||||
} else if (options_.refill_period_us <= 0) {
|
||||
return Status::InvalidArgument("Refill_period_us must be > 0");
|
||||
}
|
||||
Initialize();
|
||||
return RateLimiter::PrepareOptions(options);
|
||||
}
|
||||
|
||||
// This API allows user to dynamically change rate limiter's bytes per second.
|
||||
void GenericRateLimiter::SetBytesPerSecond(int64_t bytes_per_second) {
|
||||
assert(bytes_per_second > 0);
|
||||
@@ -165,11 +113,11 @@ void GenericRateLimiter::Request(int64_t bytes, const Env::IOPriority pri,
|
||||
&rate_bytes_per_sec_);
|
||||
MutexLock g(&request_mutex_);
|
||||
|
||||
if (options_.auto_tuned) {
|
||||
if (auto_tuned_) {
|
||||
static const int kRefillsPerTune = 100;
|
||||
std::chrono::microseconds now(NowMicrosMonotonic());
|
||||
if (now - tuned_time_ >= kRefillsPerTune * std::chrono::microseconds(
|
||||
options_.refill_period_us)) {
|
||||
if (now - tuned_time_ >=
|
||||
kRefillsPerTune * std::chrono::microseconds(refill_period_us_)) {
|
||||
Status s = Tune();
|
||||
s.PermitUncheckedError(); //**TODO: What to do on error?
|
||||
}
|
||||
@@ -213,7 +161,7 @@ void GenericRateLimiter::Request(int64_t bytes, const Env::IOPriority pri,
|
||||
} else {
|
||||
// Whichever thread reaches here first performs duty (1) as described
|
||||
// above.
|
||||
int64_t wait_until = options_.clock->NowMicros() + time_until_refill_us;
|
||||
int64_t wait_until = clock_->NowMicros() + time_until_refill_us;
|
||||
RecordTick(stats, NUMBER_RATE_LIMITER_DRAINS);
|
||||
++num_drains_;
|
||||
wait_until_refill_pending_ = true;
|
||||
@@ -273,12 +221,12 @@ GenericRateLimiter::GeneratePriorityIterationOrder() {
|
||||
// first
|
||||
pri_iteration_order[0] = Env::IO_USER;
|
||||
|
||||
bool high_pri_iterated_after_mid_low_pri = rnd_.OneIn(options_.fairness);
|
||||
bool high_pri_iterated_after_mid_low_pri = rnd_.OneIn(fairness_);
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"GenericRateLimiter::GeneratePriorityIterationOrder::"
|
||||
"PostRandomOneInFairnessForHighPri",
|
||||
&high_pri_iterated_after_mid_low_pri);
|
||||
bool mid_pri_itereated_after_low_pri = rnd_.OneIn(options_.fairness);
|
||||
bool mid_pri_itereated_after_low_pri = rnd_.OneIn(fairness_);
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"GenericRateLimiter::GeneratePriorityIterationOrder::"
|
||||
"PostRandomOneInFairnessForMidPri",
|
||||
@@ -307,7 +255,7 @@ GenericRateLimiter::GeneratePriorityIterationOrder() {
|
||||
|
||||
void GenericRateLimiter::RefillBytesAndGrantRequests() {
|
||||
TEST_SYNC_POINT("GenericRateLimiter::RefillBytesAndGrantRequests");
|
||||
next_refill_us_ = NowMicrosMonotonic() + options_.refill_period_us;
|
||||
next_refill_us_ = NowMicrosMonotonic() + refill_period_us_;
|
||||
// Carry over the left over quota from the last period
|
||||
auto refill_bytes_per_period =
|
||||
refill_bytes_per_period_.load(std::memory_order_relaxed);
|
||||
@@ -348,12 +296,12 @@ void GenericRateLimiter::RefillBytesAndGrantRequests() {
|
||||
int64_t GenericRateLimiter::CalculateRefillBytesPerPeriod(
|
||||
int64_t rate_bytes_per_sec) {
|
||||
if (std::numeric_limits<int64_t>::max() / rate_bytes_per_sec <
|
||||
options_.refill_period_us) {
|
||||
refill_period_us_) {
|
||||
// Avoid unexpected result in the overflow case. The result now is still
|
||||
// inaccurate but is a number that is large enough.
|
||||
return std::numeric_limits<int64_t>::max() / 1000000;
|
||||
} else {
|
||||
return rate_bytes_per_sec * options_.refill_period_us / 1000000;
|
||||
return rate_bytes_per_sec * refill_period_us_ / 1000000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,11 +316,10 @@ Status GenericRateLimiter::Tune() {
|
||||
std::chrono::microseconds prev_tuned_time = tuned_time_;
|
||||
tuned_time_ = std::chrono::microseconds(NowMicrosMonotonic());
|
||||
|
||||
int64_t elapsed_intervals =
|
||||
(tuned_time_ - prev_tuned_time +
|
||||
std::chrono::microseconds(options_.refill_period_us) -
|
||||
std::chrono::microseconds(1)) /
|
||||
std::chrono::microseconds(options_.refill_period_us);
|
||||
int64_t elapsed_intervals = (tuned_time_ - prev_tuned_time +
|
||||
std::chrono::microseconds(refill_period_us_) -
|
||||
std::chrono::microseconds(1)) /
|
||||
std::chrono::microseconds(refill_period_us_);
|
||||
// We tune every kRefillsPerTune intervals, so the overflow and division-by-
|
||||
// zero conditions should never happen.
|
||||
assert(num_drains_ <= std::numeric_limits<int64_t>::max() / 100);
|
||||
@@ -382,13 +329,13 @@ Status GenericRateLimiter::Tune() {
|
||||
int64_t prev_bytes_per_sec = GetBytesPerSecond();
|
||||
int64_t new_bytes_per_sec;
|
||||
if (drained_pct == 0) {
|
||||
new_bytes_per_sec = options_.max_bytes_per_sec / kAllowedRangeFactor;
|
||||
new_bytes_per_sec = max_bytes_per_sec_ / kAllowedRangeFactor;
|
||||
} else if (drained_pct < kLowWatermarkPct) {
|
||||
// sanitize to prevent overflow
|
||||
int64_t sanitized_prev_bytes_per_sec =
|
||||
std::min(prev_bytes_per_sec, std::numeric_limits<int64_t>::max() / 100);
|
||||
new_bytes_per_sec =
|
||||
std::max(options_.max_bytes_per_sec / kAllowedRangeFactor,
|
||||
std::max(max_bytes_per_sec_ / kAllowedRangeFactor,
|
||||
sanitized_prev_bytes_per_sec * 100 / (100 + kAdjustFactorPct));
|
||||
} else if (drained_pct > kHighWatermarkPct) {
|
||||
// sanitize to prevent overflow
|
||||
@@ -396,7 +343,7 @@ Status GenericRateLimiter::Tune() {
|
||||
std::min(prev_bytes_per_sec, std::numeric_limits<int64_t>::max() /
|
||||
(100 + kAdjustFactorPct));
|
||||
new_bytes_per_sec =
|
||||
std::min(options_.max_bytes_per_sec,
|
||||
std::min(max_bytes_per_sec_,
|
||||
sanitized_prev_bytes_per_sec * (100 + kAdjustFactorPct) / 100);
|
||||
} else {
|
||||
new_bytes_per_sec = prev_bytes_per_sec;
|
||||
@@ -419,79 +366,7 @@ RateLimiter* NewGenericRateLimiter(
|
||||
std::unique_ptr<RateLimiter> limiter(
|
||||
new GenericRateLimiter(rate_bytes_per_sec, refill_period_us, fairness,
|
||||
mode, SystemClock::Default(), auto_tuned));
|
||||
Status s = limiter->PrepareOptions(ConfigOptions());
|
||||
if (s.ok()) {
|
||||
return limiter.release();
|
||||
} else {
|
||||
assert(false);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
namespace {
|
||||
#ifndef ROCKSDB_LITE
|
||||
static int RegisterBuiltinRateLimiters(ObjectLibrary& library,
|
||||
const std::string& /*arg*/) {
|
||||
library.AddFactory<RateLimiter>(
|
||||
GenericRateLimiter::kClassName(),
|
||||
[](const std::string& /*uri*/, std::unique_ptr<RateLimiter>* guard,
|
||||
std::string* /*errmsg*/) {
|
||||
guard->reset(
|
||||
new GenericRateLimiter(std::numeric_limits<int64_t>::max()));
|
||||
return guard->get();
|
||||
});
|
||||
size_t num_types;
|
||||
return static_cast<int>(library.GetFactoryCount(&num_types));
|
||||
}
|
||||
|
||||
static std::unordered_map<std::string, RateLimiter::Mode>
|
||||
rate_limiter_mode_map = {
|
||||
{"kReadsOnly", RateLimiter::Mode::kReadsOnly},
|
||||
{"kWritesOnly", RateLimiter::Mode::kWritesOnly},
|
||||
{"kAllIo", RateLimiter::Mode::kAllIo},
|
||||
};
|
||||
#endif // ROCKSDB_LITE
|
||||
static bool LoadRateLimiter(const std::string& name,
|
||||
std::shared_ptr<RateLimiter>* limiter) {
|
||||
auto plen = strlen(GenericRateLimiter::kClassName());
|
||||
if (name.size() > plen + 2 && name[plen] == ':' &&
|
||||
StartsWith(name, GenericRateLimiter::kClassName())) {
|
||||
auto rate = ParseInt64(name.substr(plen + 1));
|
||||
limiter->reset(new GenericRateLimiter(rate));
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo> rate_limiter_type_info =
|
||||
{
|
||||
#ifndef ROCKSDB_LITE
|
||||
{"mode",
|
||||
OptionTypeInfo::Enum<RateLimiter::Mode>(0, &rate_limiter_mode_map)},
|
||||
#endif // ROCKSDB_LITE
|
||||
};
|
||||
} // namespace
|
||||
|
||||
RateLimiter::RateLimiter(Mode mode) : mode_(mode) {
|
||||
RegisterOptions("", &mode_, &rate_limiter_type_info);
|
||||
}
|
||||
|
||||
Status RateLimiter::CreateFromString(const ConfigOptions& config_options,
|
||||
const std::string& value,
|
||||
std::shared_ptr<RateLimiter>* result) {
|
||||
if (value.empty()) {
|
||||
result->reset();
|
||||
return Status::OK();
|
||||
} else {
|
||||
#ifndef ROCKSDB_LITE
|
||||
static std::once_flag once;
|
||||
std::call_once(once, [&]() {
|
||||
RegisterBuiltinRateLimiters(*(ObjectLibrary::Default().get()), "");
|
||||
});
|
||||
#endif // ROCKSDB_LITE
|
||||
return LoadSharedObject<RateLimiter>(config_options, value, LoadRateLimiter,
|
||||
result);
|
||||
}
|
||||
return limiter.release();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+16
-34
@@ -26,38 +26,13 @@ namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class GenericRateLimiter : public RateLimiter {
|
||||
public:
|
||||
struct GenericRateLimiterOptions {
|
||||
static const char* kName() { return "GenericRateLimiterOptions"; }
|
||||
GenericRateLimiterOptions(int64_t _rate_bytes_per_sec,
|
||||
int64_t _refill_period_us, int32_t _fairness,
|
||||
const std::shared_ptr<SystemClock>& _clock,
|
||||
bool _auto_tuned)
|
||||
: max_bytes_per_sec(_rate_bytes_per_sec),
|
||||
refill_period_us(_refill_period_us),
|
||||
clock(_clock),
|
||||
fairness(_fairness > 100 ? 100 : _fairness),
|
||||
auto_tuned(_auto_tuned) {}
|
||||
int64_t max_bytes_per_sec;
|
||||
int64_t refill_period_us;
|
||||
std::shared_ptr<SystemClock> clock;
|
||||
int32_t fairness;
|
||||
bool auto_tuned;
|
||||
};
|
||||
|
||||
public:
|
||||
explicit GenericRateLimiter(
|
||||
int64_t refill_bytes, int64_t refill_period_us = 100 * 1000,
|
||||
int32_t fairness = 10,
|
||||
RateLimiter::Mode mode = RateLimiter::Mode::kWritesOnly,
|
||||
const std::shared_ptr<SystemClock>& clock = nullptr,
|
||||
bool auto_tuned = false);
|
||||
GenericRateLimiter(int64_t refill_bytes, int64_t refill_period_us,
|
||||
int32_t fairness, RateLimiter::Mode mode,
|
||||
const std::shared_ptr<SystemClock>& clock,
|
||||
bool auto_tuned);
|
||||
|
||||
virtual ~GenericRateLimiter();
|
||||
|
||||
static const char* kClassName() { return "GenericRateLimiter"; }
|
||||
const char* Name() const override { return kClassName(); }
|
||||
Status PrepareOptions(const ConfigOptions& options) override;
|
||||
|
||||
// This API allows user to dynamically change rate limiter's bytes per second.
|
||||
virtual void SetBytesPerSecond(int64_t bytes_per_second) override;
|
||||
|
||||
@@ -120,25 +95,29 @@ class GenericRateLimiter : public RateLimiter {
|
||||
return rate_bytes_per_sec_;
|
||||
}
|
||||
|
||||
virtual void TEST_SetClock(std::shared_ptr<SystemClock> clock) {
|
||||
MutexLock g(&request_mutex_);
|
||||
clock_ = std::move(clock);
|
||||
next_refill_us_ = NowMicrosMonotonic();
|
||||
}
|
||||
|
||||
private:
|
||||
void Initialize();
|
||||
void RefillBytesAndGrantRequests();
|
||||
std::vector<Env::IOPriority> GeneratePriorityIterationOrder();
|
||||
int64_t CalculateRefillBytesPerPeriod(int64_t rate_bytes_per_sec);
|
||||
Status Tune();
|
||||
|
||||
uint64_t NowMicrosMonotonic() {
|
||||
return options_.clock->NowNanos() / std::milli::den;
|
||||
}
|
||||
uint64_t NowMicrosMonotonic() { return clock_->NowNanos() / std::milli::den; }
|
||||
|
||||
// This mutex guard all internal states
|
||||
mutable port::Mutex request_mutex_;
|
||||
|
||||
GenericRateLimiterOptions options_;
|
||||
const int64_t refill_period_us_;
|
||||
|
||||
int64_t rate_bytes_per_sec_;
|
||||
// This variable can be changed dynamically.
|
||||
std::atomic<int64_t> refill_bytes_per_period_;
|
||||
std::shared_ptr<SystemClock> clock_;
|
||||
|
||||
bool stop_;
|
||||
port::CondVar exit_cv_;
|
||||
@@ -149,13 +128,16 @@ class GenericRateLimiter : public RateLimiter {
|
||||
int64_t available_bytes_;
|
||||
int64_t next_refill_us_;
|
||||
|
||||
int32_t fairness_;
|
||||
Random rnd_;
|
||||
|
||||
struct Req;
|
||||
std::deque<Req*> queue_[Env::IO_TOTAL];
|
||||
bool wait_until_refill_pending_;
|
||||
|
||||
bool auto_tuned_;
|
||||
int64_t num_drains_;
|
||||
const int64_t max_bytes_per_sec_;
|
||||
std::chrono::microseconds tuned_time_;
|
||||
};
|
||||
|
||||
|
||||
@@ -15,11 +15,8 @@
|
||||
#include <limits>
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "options/options_parser.h"
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
#include "rocksdb/system_clock.h"
|
||||
#include "rocksdb/utilities/options_type.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "util/random.h"
|
||||
@@ -466,95 +463,6 @@ TEST_F(RateLimiterTest, AutoTuneIncreaseWhenFull) {
|
||||
ASSERT_LT(new_bytes_per_sec, orig_bytes_per_sec);
|
||||
}
|
||||
|
||||
TEST_F(RateLimiterTest, CreateGenericRateLimiterFromString) {
|
||||
std::shared_ptr<RateLimiter> limiter;
|
||||
ConfigOptions config_options;
|
||||
std::string limiter_id = GenericRateLimiter::kClassName();
|
||||
ASSERT_OK(RateLimiter::CreateFromString(config_options, limiter_id + ":1024",
|
||||
&limiter));
|
||||
ASSERT_NE(limiter, nullptr);
|
||||
ASSERT_EQ(limiter->GetBytesPerSecond(), 1024U);
|
||||
#ifndef ROCKSDB_LITE
|
||||
ASSERT_OK(RateLimiter::CreateFromString(
|
||||
config_options, "rate_bytes_per_sec=2048;id=" + limiter_id, &limiter));
|
||||
ASSERT_NE(limiter, nullptr);
|
||||
ASSERT_EQ(limiter->GetBytesPerSecond(), 2048U);
|
||||
ASSERT_NOK(RateLimiter::CreateFromString(
|
||||
config_options, "rate_bytes_per_sec=0;id=" + limiter_id, &limiter));
|
||||
ASSERT_NOK(RateLimiter::CreateFromString(
|
||||
config_options, "rate_bytes_per_sec=2048;fairness=0;id=" + limiter_id,
|
||||
&limiter));
|
||||
|
||||
ASSERT_OK(
|
||||
RateLimiter::CreateFromString(config_options,
|
||||
"rate_bytes_per_sec=2048;refill_period_us="
|
||||
"1024;fairness=42;auto_tuned=true;"
|
||||
"mode=kReadsOnly;id=" +
|
||||
limiter_id,
|
||||
&limiter));
|
||||
ASSERT_NE(limiter, nullptr);
|
||||
auto opts =
|
||||
limiter->GetOptions<GenericRateLimiter::GenericRateLimiterOptions>();
|
||||
ASSERT_NE(opts, nullptr);
|
||||
ASSERT_EQ(opts->max_bytes_per_sec, 2048);
|
||||
ASSERT_EQ(opts->refill_period_us, 1024);
|
||||
ASSERT_EQ(opts->fairness, 42);
|
||||
ASSERT_EQ(opts->auto_tuned, true);
|
||||
ASSERT_TRUE(limiter->IsRateLimited(RateLimiter::OpType::kRead));
|
||||
ASSERT_FALSE(limiter->IsRateLimited(RateLimiter::OpType::kWrite));
|
||||
#endif // ROCKSDB_LITE
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
// This test is for a rate limiter that has no name (Name() returns "").
|
||||
// When the default Name() method is deprecated, this test should be removed.
|
||||
TEST_F(RateLimiterTest, NoNameRateLimiter) {
|
||||
static std::unordered_map<std::string, OptionTypeInfo> dummy_limiter_options =
|
||||
{
|
||||
{"dummy",
|
||||
{0, OptionType::kInt, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
};
|
||||
class NoNameRateLimiter : public RateLimiter {
|
||||
public:
|
||||
explicit NoNameRateLimiter(bool do_register) {
|
||||
if (do_register) {
|
||||
RegisterOptions("", &dummy, &dummy_limiter_options);
|
||||
}
|
||||
}
|
||||
void SetBytesPerSecond(int64_t /*bytes_per_second*/) override {}
|
||||
int64_t GetSingleBurstBytes() const override { return 0; }
|
||||
int64_t GetTotalBytesThrough(const Env::IOPriority /*pri*/) const override {
|
||||
return 0;
|
||||
}
|
||||
int64_t GetTotalRequests(const Env::IOPriority /*pri*/) const override {
|
||||
return 0;
|
||||
}
|
||||
int64_t GetBytesPerSecond() const override { return 0; }
|
||||
|
||||
private:
|
||||
int dummy;
|
||||
};
|
||||
|
||||
ConfigOptions config_options;
|
||||
DBOptions db_opts, copy;
|
||||
db_opts.rate_limiter.reset(new NoNameRateLimiter(false));
|
||||
ASSERT_EQ(db_opts.rate_limiter->GetId(), "");
|
||||
ASSERT_EQ(db_opts.rate_limiter->ToString(config_options), "");
|
||||
db_opts.rate_limiter.reset(new NoNameRateLimiter(true));
|
||||
ASSERT_EQ(db_opts.rate_limiter->GetId(), "");
|
||||
ASSERT_EQ(db_opts.rate_limiter->ToString(config_options), "");
|
||||
std::string opt_str;
|
||||
ASSERT_OK(GetStringFromDBOptions(config_options, db_opts, &opt_str));
|
||||
ASSERT_OK(
|
||||
GetDBOptionsFromString(config_options, DBOptions(), opt_str, ©));
|
||||
ASSERT_OK(
|
||||
RocksDBOptionsParser::VerifyDBOptions(config_options, db_opts, copy));
|
||||
ASSERT_EQ(copy.rate_limiter, nullptr);
|
||||
ASSERT_NE(copy.rate_limiter, db_opts.rate_limiter);
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -186,6 +186,12 @@ class Timer {
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool TEST_HasVaildTask(const std::string& func_name) const {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
auto it = map_.find(func_name);
|
||||
return it != map_.end() && it->second->IsValid();
|
||||
}
|
||||
#endif // NDEBUG
|
||||
|
||||
private:
|
||||
|
||||
@@ -186,54 +186,13 @@ class BackupEngineImpl {
|
||||
const std::shared_ptr<SystemClock>& backup_rate_limiter_clock,
|
||||
const std::shared_ptr<SystemClock>& restore_rate_limiter_clock) {
|
||||
if (backup_rate_limiter_clock) {
|
||||
assert(options_.backup_rate_limiter->IsInstanceOf(
|
||||
GenericRateLimiter::kClassName()));
|
||||
auto* backup_rate_limiter_options =
|
||||
options_.backup_rate_limiter
|
||||
->GetOptions<GenericRateLimiter::GenericRateLimiterOptions>();
|
||||
|
||||
assert(backup_rate_limiter_options);
|
||||
RateLimiter::Mode backup_rate_limiter_mode;
|
||||
if (!options_.backup_rate_limiter->IsRateLimited(
|
||||
RateLimiter::OpType::kRead)) {
|
||||
backup_rate_limiter_mode = RateLimiter::Mode::kWritesOnly;
|
||||
} else if (!options_.backup_rate_limiter->IsRateLimited(
|
||||
RateLimiter::OpType::kWrite)) {
|
||||
backup_rate_limiter_mode = RateLimiter::Mode::kReadsOnly;
|
||||
} else {
|
||||
backup_rate_limiter_mode = RateLimiter::Mode::kAllIo;
|
||||
}
|
||||
options_.backup_rate_limiter.reset(new GenericRateLimiter(
|
||||
backup_rate_limiter_options->max_bytes_per_sec,
|
||||
backup_rate_limiter_options->refill_period_us,
|
||||
backup_rate_limiter_options->fairness, backup_rate_limiter_mode,
|
||||
backup_rate_limiter_clock, backup_rate_limiter_options->auto_tuned));
|
||||
static_cast<GenericRateLimiter*>(options_.backup_rate_limiter.get())
|
||||
->TEST_SetClock(backup_rate_limiter_clock);
|
||||
}
|
||||
|
||||
if (restore_rate_limiter_clock) {
|
||||
assert(options_.restore_rate_limiter->IsInstanceOf(
|
||||
GenericRateLimiter::kClassName()));
|
||||
auto* restore_rate_limiter_options =
|
||||
options_.restore_rate_limiter
|
||||
->GetOptions<GenericRateLimiter::GenericRateLimiterOptions>();
|
||||
assert(restore_rate_limiter_options);
|
||||
|
||||
RateLimiter::Mode restore_rate_limiter_mode;
|
||||
if (!options_.restore_rate_limiter->IsRateLimited(
|
||||
RateLimiter::OpType::kRead)) {
|
||||
restore_rate_limiter_mode = RateLimiter::Mode::kWritesOnly;
|
||||
} else if (!options_.restore_rate_limiter->IsRateLimited(
|
||||
RateLimiter::OpType::kWrite)) {
|
||||
restore_rate_limiter_mode = RateLimiter::Mode::kReadsOnly;
|
||||
} else {
|
||||
restore_rate_limiter_mode = RateLimiter::Mode::kAllIo;
|
||||
}
|
||||
options_.restore_rate_limiter.reset(new GenericRateLimiter(
|
||||
restore_rate_limiter_options->max_bytes_per_sec,
|
||||
restore_rate_limiter_options->refill_period_us,
|
||||
restore_rate_limiter_options->fairness, restore_rate_limiter_mode,
|
||||
restore_rate_limiter_clock,
|
||||
restore_rate_limiter_options->auto_tuned));
|
||||
static_cast<GenericRateLimiter*>(options_.restore_rate_limiter.get())
|
||||
->TEST_SetClock(restore_rate_limiter_clock);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -211,6 +211,7 @@ class CountedRandomRWFile : public FSRandomRWFileOwnerWrapper {
|
||||
class CountedDirectory : public FSDirectoryWrapper {
|
||||
private:
|
||||
mutable CountedFileSystem* fs_;
|
||||
bool closed_ = false;
|
||||
|
||||
public:
|
||||
CountedDirectory(std::unique_ptr<FSDirectory>&& f, CountedFileSystem* fs)
|
||||
@@ -229,6 +230,7 @@ class CountedDirectory : public FSDirectoryWrapper {
|
||||
if (rv.ok()) {
|
||||
fs_->counters()->closes++;
|
||||
fs_->counters()->dir_closes++;
|
||||
closed_ = true;
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
@@ -242,6 +244,14 @@ class CountedDirectory : public FSDirectoryWrapper {
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
~CountedDirectory() {
|
||||
if (!closed_) {
|
||||
// TODO: fix DB+CF code to use explicit Close, not rely on destructor
|
||||
fs_->counters()->closes++;
|
||||
fs_->counters()->dir_closes++;
|
||||
}
|
||||
}
|
||||
};
|
||||
} // anonymous namespace
|
||||
|
||||
|
||||
@@ -8,9 +8,49 @@
|
||||
#include "rocksdb/utilities/debug.h"
|
||||
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "rocksdb/utilities/options_type.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
static std::unordered_map<std::string, ValueType> value_type_string_map = {
|
||||
{"TypeDeletion", ValueType::kTypeDeletion},
|
||||
{"TypeValue", ValueType::kTypeValue},
|
||||
{"TypeMerge", ValueType::kTypeMerge},
|
||||
{"TypeLogData", ValueType::kTypeLogData},
|
||||
{"TypeColumnFamilyDeletion", ValueType::kTypeColumnFamilyDeletion},
|
||||
{"TypeColumnFamilyValue", ValueType::kTypeColumnFamilyValue},
|
||||
{"TypeColumnFamilyMerge", ValueType::kTypeColumnFamilyMerge},
|
||||
{"TypeSingleDeletion", ValueType::kTypeSingleDeletion},
|
||||
{"TypeColumnFamilySingleDeletion",
|
||||
ValueType::kTypeColumnFamilySingleDeletion},
|
||||
{"TypeBeginPrepareXID", ValueType::kTypeBeginPrepareXID},
|
||||
{"TypeEndPrepareXID", ValueType::kTypeEndPrepareXID},
|
||||
{"TypeCommitXID", ValueType::kTypeCommitXID},
|
||||
{"TypeRollbackXID", ValueType::kTypeRollbackXID},
|
||||
{"TypeNoop", ValueType::kTypeNoop},
|
||||
{"TypeColumnFamilyRangeDeletion",
|
||||
ValueType::kTypeColumnFamilyRangeDeletion},
|
||||
{"TypeRangeDeletion", ValueType::kTypeRangeDeletion},
|
||||
{"TypeColumnFamilyBlobIndex", ValueType::kTypeColumnFamilyBlobIndex},
|
||||
{"TypeBlobIndex", ValueType::kTypeBlobIndex},
|
||||
{"TypeBeginPersistedPrepareXID", ValueType::kTypeBeginPersistedPrepareXID},
|
||||
{"TypeBeginUnprepareXID", ValueType::kTypeBeginUnprepareXID},
|
||||
{"TypeDeletionWithTimestamp", ValueType::kTypeDeletionWithTimestamp},
|
||||
{"TypeCommitXIDAndTimestamp", ValueType::kTypeCommitXIDAndTimestamp},
|
||||
{"TypeWideColumnEntity", ValueType::kTypeWideColumnEntity},
|
||||
{"TypeColumnFamilyWideColumnEntity",
|
||||
ValueType::kTypeColumnFamilyWideColumnEntity}};
|
||||
|
||||
std::string KeyVersion::GetTypeName() const {
|
||||
std::string type_name;
|
||||
if (SerializeEnum<ValueType>(value_type_string_map,
|
||||
static_cast<ValueType>(type), &type_name)) {
|
||||
return type_name;
|
||||
} else {
|
||||
return "Invalid";
|
||||
}
|
||||
}
|
||||
|
||||
Status GetAllKeyVersions(DB* db, Slice begin_key, Slice end_key,
|
||||
size_t max_num_ikeys,
|
||||
std::vector<KeyVersion>* key_versions) {
|
||||
|
||||
Reference in new issue
Block a user